@mosaicoo/form-angular 0.6.0 → 0.8.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.
@@ -5,27 +5,42 @@ import { collectInputs, generateKey, validateSchema, createFormEngine, getNodeAt
5
5
  import { importForm, createLegacyImporter } from '@mosaicoo/form-core/interop';
6
6
  import { FormNodeComponent, FormRendererComponent } from '@mosaicoo/form-angular';
7
7
 
8
+ function dropId(target) {
9
+ return target.column === undefined
10
+ ? target.path.join('.')
11
+ : `${target.path.join('.')}@${target.column}`;
12
+ }
13
+
8
14
  /**
9
15
  * Canvas node: draws the selection frame and structure chrome, delegating
10
16
  * leaf rendering to the RUNTIME `<mform-node>` so what you see is what the
11
17
  * renderer will produce. Canvas controls are non-interactive — testing
12
18
  * happens in Preview mode.
19
+ *
20
+ * Drop zones sit between children (and inside each column), so fields can be
21
+ * dragged INTO containers, not just reordered at the root.
13
22
  */
14
23
  class DesignerNodeComponent {
15
24
  node = input$1.required(...(ngDevMode ? [{ debugName: "node" }] : /* istanbul ignore next */ []));
16
25
  path = input$1.required(...(ngDevMode ? [{ debugName: "path" }] : /* istanbul ignore next */ []));
17
26
  engine = input$1.required(...(ngDevMode ? [{ debugName: "engine" }] : /* istanbul ignore next */ []));
18
27
  selected = input$1(null, ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
28
+ /** True while something is being dragged (reveals the drop zones). */
29
+ dragging = input$1(false, ...(ngDevMode ? [{ debugName: "dragging" }] : /* istanbul ignore next */ []));
30
+ /** Id of the drop zone currently hovered. */
31
+ activeDrop = input$1(null, ...(ngDevMode ? [{ debugName: "activeDrop" }] : /* istanbul ignore next */ []));
19
32
  selectNode = output();
33
+ nodeDragStart = output();
34
+ dragOverTarget = output();
35
+ dropOnTarget = output();
36
+ dragFinished = output();
20
37
  activeTab = signal(0, ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
21
38
  container() {
22
39
  return this.node();
23
40
  }
24
41
  chip() {
25
42
  const node = this.node();
26
- const label = node.kind === 'container' || node.kind === 'static'
27
- ? node.label ?? node.key
28
- : node.label ?? node.key;
43
+ const label = node.label ?? node.key;
29
44
  return `${label} · ${node.type}`;
30
45
  }
31
46
  isSelected() {
@@ -48,8 +63,28 @@ class DesignerNodeComponent {
48
63
  event.stopPropagation();
49
64
  this.activeTab.set(index);
50
65
  }
66
+ // -- drag and drop --------------------------------------------------------
67
+ isOver(path, column) {
68
+ return this.activeDrop() === dropId({ path, column });
69
+ }
70
+ onDragStart(path, event) {
71
+ event.stopPropagation();
72
+ this.nodeDragStart.emit(path);
73
+ }
74
+ onDragOver(path, column, event) {
75
+ if (!this.dragging())
76
+ return;
77
+ event.preventDefault();
78
+ event.stopPropagation();
79
+ this.dragOverTarget.emit({ path, column });
80
+ }
81
+ onDrop(path, column, event) {
82
+ event.preventDefault();
83
+ event.stopPropagation();
84
+ this.dropOnTarget.emit({ path, column });
85
+ }
51
86
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: DesignerNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
52
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: DesignerNodeComponent, isStandalone: true, selector: "mform-designer-node", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null }, path: { classPropertyName: "path", publicName: "path", isSignal: true, isRequired: true, transformFunction: null }, engine: { classPropertyName: "engine", publicName: "engine", isSignal: true, isRequired: true, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectNode: "selectNode" }, ngImport: i0, template: `
87
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: DesignerNodeComponent, isStandalone: true, selector: "mform-designer-node", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null }, path: { classPropertyName: "path", publicName: "path", isSignal: true, isRequired: true, transformFunction: null }, engine: { classPropertyName: "engine", publicName: "engine", isSignal: true, isRequired: true, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, dragging: { classPropertyName: "dragging", publicName: "dragging", isSignal: true, isRequired: false, transformFunction: null }, activeDrop: { classPropertyName: "activeDrop", publicName: "activeDrop", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectNode: "selectNode", nodeDragStart: "nodeDragStart", dragOverTarget: "dragOverTarget", dropOnTarget: "dropOnTarget", dragFinished: "dragFinished" }, ngImport: i0, template: `
53
88
  @let n = node();
54
89
  <div
55
90
  class="dz-node"
@@ -85,24 +120,51 @@ class DesignerNodeComponent {
85
120
  [path]="childPath(i)"
86
121
  [engine]="engine()"
87
122
  [selected]="selected()"
123
+ [dragging]="dragging()"
124
+ [activeDrop]="activeDrop()"
88
125
  (selectNode)="selectNode.emit($event)"
126
+ (nodeDragStart)="nodeDragStart.emit($event)"
127
+ (dragOverTarget)="dragOverTarget.emit($event)"
128
+ (dropOnTarget)="dropOnTarget.emit($event)"
129
+ (dragFinished)="dragFinished.emit()"
89
130
  />
90
131
  </div>
91
132
  }
92
133
  }
93
134
  } @else if (n.type === 'columns' && container().columns?.length) {
94
135
  <div class="dz-columns" [style.grid-template-columns]="columnsTemplate()">
95
- @for (column of container().columns; track $index) {
136
+ @for (column of container().columns; track $index; let c = $index) {
96
137
  <div class="dz-column">
97
138
  @for (childIndex of column.children; track childIndex) {
139
+ <div
140
+ class="dz-dropzone"
141
+ [class.is-over]="isOver(childPath(childIndex), c)"
142
+ (dragover)="onDragOver(childPath(childIndex), c, $event)"
143
+ (drop)="onDrop(childPath(childIndex), c, $event)"
144
+ ></div>
98
145
  <mform-designer-node
99
146
  [node]="container().children[childIndex]!"
100
147
  [path]="childPath(childIndex)"
101
148
  [engine]="engine()"
102
149
  [selected]="selected()"
150
+ [dragging]="dragging()"
151
+ [activeDrop]="activeDrop()"
152
+ draggable="true"
153
+ (dragstart)="onDragStart(childPath(childIndex), $event)"
154
+ (dragend)="dragFinished.emit()"
103
155
  (selectNode)="selectNode.emit($event)"
156
+ (nodeDragStart)="nodeDragStart.emit($event)"
157
+ (dragOverTarget)="dragOverTarget.emit($event)"
158
+ (dropOnTarget)="dropOnTarget.emit($event)"
159
+ (dragFinished)="dragFinished.emit()"
104
160
  />
105
161
  }
162
+ <div
163
+ class="dz-dropzone is-last"
164
+ [class.is-over]="isOver(childPath(container().children.length), c)"
165
+ (dragover)="onDragOver(childPath(container().children.length), c, $event)"
166
+ (drop)="onDrop(childPath(container().children.length), c, $event)"
167
+ ></div>
106
168
  @if (column.children.length === 0) {
107
169
  <div class="dz-empty">empty column</div>
108
170
  }
@@ -112,16 +174,37 @@ class DesignerNodeComponent {
112
174
  } @else {
113
175
  <div class="dz-children">
114
176
  @for (child of container().children; track child.key; let i = $index) {
177
+ <div
178
+ class="dz-dropzone"
179
+ [class.is-over]="isOver(childPath(i))"
180
+ (dragover)="onDragOver(childPath(i), undefined, $event)"
181
+ (drop)="onDrop(childPath(i), undefined, $event)"
182
+ ></div>
115
183
  <mform-designer-node
116
184
  [node]="child"
117
185
  [path]="childPath(i)"
118
186
  [engine]="engine()"
119
187
  [selected]="selected()"
188
+ [dragging]="dragging()"
189
+ [activeDrop]="activeDrop()"
190
+ draggable="true"
191
+ (dragstart)="onDragStart(childPath(i), $event)"
192
+ (dragend)="dragFinished.emit()"
120
193
  (selectNode)="selectNode.emit($event)"
194
+ (nodeDragStart)="nodeDragStart.emit($event)"
195
+ (dragOverTarget)="dragOverTarget.emit($event)"
196
+ (dropOnTarget)="dropOnTarget.emit($event)"
197
+ (dragFinished)="dragFinished.emit()"
121
198
  />
122
199
  } @empty {
123
- <div class="dz-empty">empty — select and add fields</div>
200
+ <div class="dz-empty">empty — drop fields here</div>
124
201
  }
202
+ <div
203
+ class="dz-dropzone is-last"
204
+ [class.is-over]="isOver(childPath(container().children.length))"
205
+ (dragover)="onDragOver(childPath(container().children.length), undefined, $event)"
206
+ (drop)="onDrop(childPath(container().children.length), undefined, $event)"
207
+ ></div>
125
208
  </div>
126
209
  }
127
210
  } @else {
@@ -130,7 +213,7 @@ class DesignerNodeComponent {
130
213
  </div>
131
214
  }
132
215
  </div>
133
- `, isInline: true, dependencies: [{ kind: "component", type: DesignerNodeComponent, selector: "mform-designer-node", inputs: ["node", "path", "engine", "selected"], outputs: ["selectNode"] }, { kind: "component", type: FormNodeComponent, selector: "mform-node", inputs: ["node", "engine", "tick", "scope", "bare", "providers", "mode", "notify"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
216
+ `, isInline: true, dependencies: [{ kind: "component", type: DesignerNodeComponent, selector: "mform-designer-node", inputs: ["node", "path", "engine", "selected", "dragging", "activeDrop"], outputs: ["selectNode", "nodeDragStart", "dragOverTarget", "dropOnTarget", "dragFinished"] }, { kind: "component", type: FormNodeComponent, selector: "mform-node", inputs: ["node", "engine", "tick", "scope", "bare", "providers", "mode", "notify"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
134
217
  }
135
218
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: DesignerNodeComponent, decorators: [{
136
219
  type: Component,
@@ -175,24 +258,51 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
175
258
  [path]="childPath(i)"
176
259
  [engine]="engine()"
177
260
  [selected]="selected()"
261
+ [dragging]="dragging()"
262
+ [activeDrop]="activeDrop()"
178
263
  (selectNode)="selectNode.emit($event)"
264
+ (nodeDragStart)="nodeDragStart.emit($event)"
265
+ (dragOverTarget)="dragOverTarget.emit($event)"
266
+ (dropOnTarget)="dropOnTarget.emit($event)"
267
+ (dragFinished)="dragFinished.emit()"
179
268
  />
180
269
  </div>
181
270
  }
182
271
  }
183
272
  } @else if (n.type === 'columns' && container().columns?.length) {
184
273
  <div class="dz-columns" [style.grid-template-columns]="columnsTemplate()">
185
- @for (column of container().columns; track $index) {
274
+ @for (column of container().columns; track $index; let c = $index) {
186
275
  <div class="dz-column">
187
276
  @for (childIndex of column.children; track childIndex) {
277
+ <div
278
+ class="dz-dropzone"
279
+ [class.is-over]="isOver(childPath(childIndex), c)"
280
+ (dragover)="onDragOver(childPath(childIndex), c, $event)"
281
+ (drop)="onDrop(childPath(childIndex), c, $event)"
282
+ ></div>
188
283
  <mform-designer-node
189
284
  [node]="container().children[childIndex]!"
190
285
  [path]="childPath(childIndex)"
191
286
  [engine]="engine()"
192
287
  [selected]="selected()"
288
+ [dragging]="dragging()"
289
+ [activeDrop]="activeDrop()"
290
+ draggable="true"
291
+ (dragstart)="onDragStart(childPath(childIndex), $event)"
292
+ (dragend)="dragFinished.emit()"
193
293
  (selectNode)="selectNode.emit($event)"
294
+ (nodeDragStart)="nodeDragStart.emit($event)"
295
+ (dragOverTarget)="dragOverTarget.emit($event)"
296
+ (dropOnTarget)="dropOnTarget.emit($event)"
297
+ (dragFinished)="dragFinished.emit()"
194
298
  />
195
299
  }
300
+ <div
301
+ class="dz-dropzone is-last"
302
+ [class.is-over]="isOver(childPath(container().children.length), c)"
303
+ (dragover)="onDragOver(childPath(container().children.length), c, $event)"
304
+ (drop)="onDrop(childPath(container().children.length), c, $event)"
305
+ ></div>
196
306
  @if (column.children.length === 0) {
197
307
  <div class="dz-empty">empty column</div>
198
308
  }
@@ -202,16 +312,37 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
202
312
  } @else {
203
313
  <div class="dz-children">
204
314
  @for (child of container().children; track child.key; let i = $index) {
315
+ <div
316
+ class="dz-dropzone"
317
+ [class.is-over]="isOver(childPath(i))"
318
+ (dragover)="onDragOver(childPath(i), undefined, $event)"
319
+ (drop)="onDrop(childPath(i), undefined, $event)"
320
+ ></div>
205
321
  <mform-designer-node
206
322
  [node]="child"
207
323
  [path]="childPath(i)"
208
324
  [engine]="engine()"
209
325
  [selected]="selected()"
326
+ [dragging]="dragging()"
327
+ [activeDrop]="activeDrop()"
328
+ draggable="true"
329
+ (dragstart)="onDragStart(childPath(i), $event)"
330
+ (dragend)="dragFinished.emit()"
210
331
  (selectNode)="selectNode.emit($event)"
332
+ (nodeDragStart)="nodeDragStart.emit($event)"
333
+ (dragOverTarget)="dragOverTarget.emit($event)"
334
+ (dropOnTarget)="dropOnTarget.emit($event)"
335
+ (dragFinished)="dragFinished.emit()"
211
336
  />
212
337
  } @empty {
213
- <div class="dz-empty">empty — select and add fields</div>
338
+ <div class="dz-empty">empty — drop fields here</div>
214
339
  }
340
+ <div
341
+ class="dz-dropzone is-last"
342
+ [class.is-over]="isOver(childPath(container().children.length))"
343
+ (dragover)="onDragOver(childPath(container().children.length), undefined, $event)"
344
+ (drop)="onDrop(childPath(container().children.length), undefined, $event)"
345
+ ></div>
215
346
  </div>
216
347
  }
217
348
  } @else {
@@ -222,7 +353,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
222
353
  </div>
223
354
  `,
224
355
  }]
225
- }], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }], path: [{ type: i0.Input, args: [{ isSignal: true, alias: "path", required: true }] }], engine: [{ type: i0.Input, args: [{ isSignal: true, alias: "engine", required: true }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], selectNode: [{ type: i0.Output, args: ["selectNode"] }] } });
356
+ }], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }], path: [{ type: i0.Input, args: [{ isSignal: true, alias: "path", required: true }] }], engine: [{ type: i0.Input, args: [{ isSignal: true, alias: "engine", required: true }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], dragging: [{ type: i0.Input, args: [{ isSignal: true, alias: "dragging", required: false }] }], activeDrop: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeDrop", required: false }] }], selectNode: [{ type: i0.Output, args: ["selectNode"] }], nodeDragStart: [{ type: i0.Output, args: ["nodeDragStart"] }], dragOverTarget: [{ type: i0.Output, args: ["dragOverTarget"] }], dropOnTarget: [{ type: i0.Output, args: ["dropOnTarget"] }], dragFinished: [{ type: i0.Output, args: ["dragFinished"] }] } });
226
357
 
227
358
  /**
228
359
  * Property panel of the selected node. Emits immutable patches — the shell
@@ -976,6 +1107,16 @@ const DEFAULT_CATALOG = [
976
1107
  ] };
977
1108
  },
978
1109
  },
1110
+ {
1111
+ type: 'accordion', label: 'Accordion', icon: '☰',
1112
+ create: (s) => {
1113
+ const key = generateKey(s, 'accordion');
1114
+ return { kind: 'container', key, type: 'accordion', dataScope: 'inherit', children: [
1115
+ { kind: 'container', key: `${key}-section-1`, type: 'panel', dataScope: 'inherit', label: 'Section 1', children: [] },
1116
+ { kind: 'container', key: `${key}-section-2`, type: 'panel', dataScope: 'inherit', label: 'Section 2', children: [] },
1117
+ ] };
1118
+ },
1119
+ },
979
1120
  {
980
1121
  type: 'group', label: 'Data group', icon: '⧈',
981
1122
  create: (s) => ({ kind: 'container', key: generateKey(s, 'group'), type: 'group',
@@ -1005,6 +1146,62 @@ const DEFAULT_CATALOG = [
1005
1146
  },
1006
1147
  ];
1007
1148
 
1149
+ const STORAGE_KEY$1 = 'mform-designer-blocks';
1150
+ function loadBlocks(storage) {
1151
+ if (!storage)
1152
+ return [];
1153
+ try {
1154
+ const raw = storage.getItem(STORAGE_KEY$1);
1155
+ const parsed = raw ? JSON.parse(raw) : [];
1156
+ return Array.isArray(parsed) ? parsed : [];
1157
+ }
1158
+ catch {
1159
+ return [];
1160
+ }
1161
+ }
1162
+ function saveBlocks(storage, blocks) {
1163
+ try {
1164
+ storage?.setItem(STORAGE_KEY$1, JSON.stringify(blocks));
1165
+ }
1166
+ catch {
1167
+ /* storage unavailable — blocks stay in memory for this session */
1168
+ }
1169
+ }
1170
+
1171
+ const DESIGNER_LABELS_EN = {
1172
+ undo: 'Undo (Ctrl+Z)',
1173
+ redo: 'Redo (Ctrl+Y)',
1174
+ moveUp: 'Move up',
1175
+ moveDown: 'Move down',
1176
+ duplicate: 'Duplicate (Ctrl+D)',
1177
+ delete: 'Delete (Del)',
1178
+ saveBlock: 'Save as reusable block',
1179
+ workspace: 'Workspace',
1180
+ panels: 'Panels',
1181
+ presets: 'Presets',
1182
+ resetLayout: 'Reset layout',
1183
+ moveLeft: 'Move left',
1184
+ moveRight: 'Move right',
1185
+ hidePanel: 'Hide panel',
1186
+ import: 'Import',
1187
+ export: 'Export',
1188
+ preview: 'Preview',
1189
+ design: 'Design',
1190
+ cancel: 'Cancel',
1191
+ close: 'Close',
1192
+ importTitle: 'Import definition (native schema or supported formats)',
1193
+ importPlaceholder: '{"version":1,"fields":[…]} or a legacy definition',
1194
+ importError: 'Could not import: ',
1195
+ exportTitle: 'Schema JSON',
1196
+ canvasEmpty: 'Click a field in the palette — or drag it here — to start building.',
1197
+ selectField: 'Select a field to edit its properties.',
1198
+ testData: 'Test data',
1199
+ panelFields: 'Fields',
1200
+ panelStructure: 'Structure',
1201
+ panelProperties: 'Properties',
1202
+ noFields: 'no fields yet',
1203
+ };
1204
+
1008
1205
  /**
1009
1206
  * Designer workspace layout.
1010
1207
  *
@@ -1070,6 +1267,37 @@ function saveLayout(storage, layout) {
1070
1267
 
1071
1268
  const EMPTY_SCHEMA = { version: 1, fields: [] };
1072
1269
  const HISTORY_LIMIT = 50;
1270
+ /** True when `target` is `from` itself or somewhere inside it. */
1271
+ function isSameOrInside(from, target) {
1272
+ return from.every((segment, index) => target[index] === segment);
1273
+ }
1274
+ /**
1275
+ * A move removes before it inserts: when the source sits before the target
1276
+ * under the same parent, every index after it shifts down by one.
1277
+ */
1278
+ function adjustAfterRemoval(from, target) {
1279
+ const parentDepth = from.length - 1;
1280
+ const sameParent = from.slice(0, parentDepth).every((s, i) => target[i] === s);
1281
+ if (!sameParent || target.length < from.length)
1282
+ return target;
1283
+ const adjusted = [...target];
1284
+ const fromIndex = from[parentDepth];
1285
+ const targetIndex = adjusted[parentDepth];
1286
+ if (targetIndex > fromIndex)
1287
+ adjusted[parentDepth] = targetIndex - 1;
1288
+ return adjusted;
1289
+ }
1290
+ /** Deep copy of a block with fresh keys, so it can be inserted repeatedly. */
1291
+ function rekeyBlock(node, schema) {
1292
+ const copy = JSON.parse(JSON.stringify(node));
1293
+ const assign = (target) => {
1294
+ target.key = generateKey(schema, target.key);
1295
+ if (target.kind === 'container')
1296
+ target.children.forEach(assign);
1297
+ };
1298
+ assign(copy);
1299
+ return copy;
1300
+ }
1073
1301
  /**
1074
1302
  * `<mform-designer>` — visual authoring of form schemas.
1075
1303
  *
@@ -1084,6 +1312,10 @@ class FormDesignerComponent {
1084
1312
  source = input$1(null, ...(ngDevMode ? [{ debugName: "source" }] : /* istanbul ignore next */ []));
1085
1313
  /** Extra palette groups contributed by the host. */
1086
1314
  extraCatalog = input$1([], ...(ngDevMode ? [{ debugName: "extraCatalog" }] : /* istanbul ignore next */ []));
1315
+ /** Replaces the built-in palette (e.g. a translated DEFAULT_CATALOG). */
1316
+ catalogOverride = input$1(null, { ...(ngDevMode ? { debugName: "catalogOverride" } : /* istanbul ignore next */ {}), alias: 'catalog' });
1317
+ /** Designer texts; partial overrides fall back to English. */
1318
+ labels = input$1(DESIGNER_LABELS_EN, { ...(ngDevMode ? { debugName: "labels" } : /* istanbul ignore next */ {}), transform: (value) => ({ ...DESIGNER_LABELS_EN, ...value }) });
1087
1319
  /** Emitted after every committed edit (undo/redo included). */
1088
1320
  schemaChanged = output();
1089
1321
  draft = signal(EMPTY_SCHEMA, ...(ngDevMode ? [{ debugName: "draft" }] : /* istanbul ignore next */ []));
@@ -1115,7 +1347,12 @@ class FormDesignerComponent {
1115
1347
  }
1116
1348
  })();
1117
1349
  panelLabel(panel) {
1118
- return PANEL_LABELS[panel];
1350
+ const labels = this.labels();
1351
+ if (panel === 'palette')
1352
+ return labels.panelFields;
1353
+ if (panel === 'structure')
1354
+ return labels.panelStructure;
1355
+ return labels.panelProperties;
1119
1356
  }
1120
1357
  panelSide(panel) {
1121
1358
  return this.layout()[panel];
@@ -1147,6 +1384,34 @@ class FormDesignerComponent {
1147
1384
  saveLayout(this.storage, this.layout());
1148
1385
  this.workspaceMenu.set(false);
1149
1386
  }
1387
+ /** Live drag state of a dock splitter. */
1388
+ resizing = null;
1389
+ startResize(side, event) {
1390
+ event.preventDefault();
1391
+ const layout = this.layout();
1392
+ this.resizing = {
1393
+ side,
1394
+ startX: event.clientX,
1395
+ startWidth: side === 'left' ? layout.leftWidth : layout.rightWidth,
1396
+ };
1397
+ event.target.setPointerCapture?.(event.pointerId);
1398
+ }
1399
+ onResizeMove(event) {
1400
+ const resizing = this.resizing;
1401
+ if (!resizing)
1402
+ return;
1403
+ // dragging right widens a left dock and narrows a right one
1404
+ const delta = event.clientX - resizing.startX;
1405
+ const raw = resizing.startWidth + (resizing.side === 'left' ? delta : -delta);
1406
+ const width = Math.max(160, Math.min(520, Math.round(raw)));
1407
+ this.layout.update((current) => resizing.side === 'left' ? { ...current, leftWidth: width } : { ...current, rightWidth: width });
1408
+ }
1409
+ endResize() {
1410
+ if (!this.resizing)
1411
+ return;
1412
+ this.resizing = null;
1413
+ saveLayout(this.storage, this.layout());
1414
+ }
1150
1415
  isGroupOpen(label) {
1151
1416
  return this.openGroups().has(label);
1152
1417
  }
@@ -1160,7 +1425,28 @@ class FormDesignerComponent {
1160
1425
  return next;
1161
1426
  });
1162
1427
  }
1163
- catalog = computed(() => [...DEFAULT_CATALOG, ...this.extraCatalog()], ...(ngDevMode ? [{ debugName: "catalog" }] : /* istanbul ignore next */ []));
1428
+ /** Saved blocks (host-controlled when `[blocks]` is bound). */
1429
+ blocks = input$1(null, ...(ngDevMode ? [{ debugName: "blocks" }] : /* istanbul ignore next */ []));
1430
+ blocksChanged = output();
1431
+ localBlocks = signal([], ...(ngDevMode ? [{ debugName: "localBlocks" }] : /* istanbul ignore next */ []));
1432
+ effectiveBlocks = computed(() => this.blocks() ?? this.localBlocks(), ...(ngDevMode ? [{ debugName: "effectiveBlocks" }] : /* istanbul ignore next */ []));
1433
+ /** Palette = built-ins + host extras + saved blocks as a group. */
1434
+ catalog = computed(() => {
1435
+ const groups = [...(this.catalogOverride() ?? DEFAULT_CATALOG), ...this.extraCatalog()];
1436
+ const blocks = this.effectiveBlocks();
1437
+ if (blocks.length > 0) {
1438
+ groups.push({
1439
+ label: 'Blocks',
1440
+ entries: blocks.map((block) => ({
1441
+ type: `block:${block.id}`,
1442
+ label: block.label,
1443
+ icon: '⊞',
1444
+ create: (schema) => rekeyBlock(block.node, schema),
1445
+ })),
1446
+ });
1447
+ }
1448
+ return groups;
1449
+ }, ...(ngDevMode ? [{ debugName: "catalog" }] : /* istanbul ignore next */ []));
1164
1450
  diagnostics = computed(() => validateSchema(this.draft()), ...(ngDevMode ? [{ debugName: "diagnostics" }] : /* istanbul ignore next */ []));
1165
1451
  exportedJson = computed(() => JSON.stringify(this.draft(), null, 2), ...(ngDevMode ? [{ debugName: "exportedJson" }] : /* istanbul ignore next */ []));
1166
1452
  /** Disposable engine driving the canvas leaves (defaults visible). */
@@ -1190,6 +1476,7 @@ class FormDesignerComponent {
1190
1476
  }, ...(ngDevMode ? [{ debugName: "tree" }] : /* istanbul ignore next */ []));
1191
1477
  constructor() {
1192
1478
  this.layout.set(loadLayout(this.storage));
1479
+ this.localBlocks.set(loadBlocks(this.storage));
1193
1480
  // First palette group starts expanded; the rest opens on demand.
1194
1481
  const first = this.catalog()[0]?.label;
1195
1482
  if (first)
@@ -1300,6 +1587,96 @@ class FormDesignerComponent {
1300
1587
  return;
1301
1588
  this.commit(updateNode(this.draft(), selected, event.patch));
1302
1589
  }
1590
+ // -- reusable blocks ------------------------------------------------------
1591
+ /** Saves the selected node (with its subtree) as a reusable block. */
1592
+ saveAsBlock() {
1593
+ const selected = this.selected();
1594
+ const node = selected ? getNodeAt(this.draft(), selected) : null;
1595
+ if (!node)
1596
+ return;
1597
+ const label = node.label ?? node.key;
1598
+ const block = {
1599
+ id: `${node.key}-${this.effectiveBlocks().length + 1}`,
1600
+ label,
1601
+ node: JSON.parse(JSON.stringify(node)),
1602
+ };
1603
+ const next = [...this.effectiveBlocks(), block];
1604
+ if (this.blocks() === null) {
1605
+ this.localBlocks.set(next);
1606
+ saveBlocks(this.storage, next);
1607
+ }
1608
+ this.blocksChanged.emit(next);
1609
+ }
1610
+ removeBlock(id) {
1611
+ const next = this.effectiveBlocks().filter((block) => block.id !== id);
1612
+ if (this.blocks() === null) {
1613
+ this.localBlocks.set(next);
1614
+ saveBlocks(this.storage, next);
1615
+ }
1616
+ this.blocksChanged.emit(next);
1617
+ }
1618
+ isBlockEntry(entry) {
1619
+ return entry.type.startsWith('block:');
1620
+ }
1621
+ blockIdOf(entry) {
1622
+ return entry.type.slice('block:'.length);
1623
+ }
1624
+ // -- drag and drop --------------------------------------------------------
1625
+ /** What is being dragged: a palette entry or an existing node. */
1626
+ dragging = signal(null, ...(ngDevMode ? [{ debugName: "dragging" }] : /* istanbul ignore next */ []));
1627
+ /** Drop zone highlighted while hovering (see `dropId`). */
1628
+ dropTarget = signal(null, ...(ngDevMode ? [{ debugName: "dropTarget" }] : /* istanbul ignore next */ []));
1629
+ startEntryDrag(entry, event) {
1630
+ this.dragging.set({ kind: 'entry', entry });
1631
+ event.dataTransfer?.setData('text/plain', entry.type);
1632
+ if (event.dataTransfer)
1633
+ event.dataTransfer.effectAllowed = 'copy';
1634
+ }
1635
+ startNodeDrag(path, event) {
1636
+ this.dragging.set({ kind: 'node', path });
1637
+ event?.dataTransfer?.setData('text/plain', path.join('.'));
1638
+ if (event?.dataTransfer)
1639
+ event.dataTransfer.effectAllowed = 'move';
1640
+ }
1641
+ endDrag() {
1642
+ this.dragging.set(null);
1643
+ this.dropTarget.set(null);
1644
+ }
1645
+ allowDrop(target, event) {
1646
+ if (!this.dragging())
1647
+ return;
1648
+ event?.preventDefault();
1649
+ event?.stopPropagation();
1650
+ this.dropTarget.set(dropId(target));
1651
+ }
1652
+ /** Drops onto `target` — the position (and column) the node should occupy. */
1653
+ drop(target, event) {
1654
+ event?.preventDefault();
1655
+ event?.stopPropagation();
1656
+ const dragging = this.dragging();
1657
+ this.endDrag();
1658
+ if (!dragging)
1659
+ return;
1660
+ const options = target.column === undefined ? undefined : { column: target.column };
1661
+ if (dragging.kind === 'entry') {
1662
+ const node = dragging.entry.create(this.draft());
1663
+ this.commit(insertNode(this.draft(), target.path, node, options));
1664
+ this.select(target.path);
1665
+ return;
1666
+ }
1667
+ const from = dragging.path;
1668
+ if (isSameOrInside(from, target.path))
1669
+ return; // never drop a node into itself
1670
+ const adjusted = adjustAfterRemoval(from, target.path);
1671
+ this.commit(moveNode(this.draft(), from, adjusted, options));
1672
+ this.select(adjusted);
1673
+ }
1674
+ isDropTarget(target) {
1675
+ return this.dropTarget() === dropId(target);
1676
+ }
1677
+ isDragging() {
1678
+ return this.dragging() !== null;
1679
+ }
1303
1680
  // -- selection ------------------------------------------------------------
1304
1681
  select(path) {
1305
1682
  this.selected.set(path);
@@ -1321,7 +1698,7 @@ class FormDesignerComponent {
1321
1698
  this.importError.set('');
1322
1699
  }
1323
1700
  catch (error) {
1324
- this.importError.set('Could not import: ' + String(error));
1701
+ this.importError.set(this.labels().importError + String(error));
1325
1702
  }
1326
1703
  }
1327
1704
  // -- keyboard -------------------------------------------------------------
@@ -1348,17 +1725,18 @@ class FormDesignerComponent {
1348
1725
  }
1349
1726
  }
1350
1727
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FormDesignerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1351
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FormDesignerComponent, isStandalone: true, selector: "mform-designer", inputs: { schema: { classPropertyName: "schema", publicName: "schema", isSignal: true, isRequired: false, transformFunction: null }, source: { classPropertyName: "source", publicName: "source", isSignal: true, isRequired: false, transformFunction: null }, extraCatalog: { classPropertyName: "extraCatalog", publicName: "extraCatalog", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { schemaChanged: "schemaChanged" }, ngImport: i0, template: `
1728
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FormDesignerComponent, isStandalone: true, selector: "mform-designer", inputs: { schema: { classPropertyName: "schema", publicName: "schema", isSignal: true, isRequired: false, transformFunction: null }, source: { classPropertyName: "source", publicName: "source", isSignal: true, isRequired: false, transformFunction: null }, extraCatalog: { classPropertyName: "extraCatalog", publicName: "extraCatalog", isSignal: true, isRequired: false, transformFunction: null }, catalogOverride: { classPropertyName: "catalogOverride", publicName: "catalog", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null }, blocks: { classPropertyName: "blocks", publicName: "blocks", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { schemaChanged: "schemaChanged", blocksChanged: "blocksChanged" }, ngImport: i0, template: `
1352
1729
  <div class="dz-shell" (keydown)="onKeydown($event)">
1353
1730
  <header class="dz-toolbar">
1354
1731
  <div class="dz-tools">
1355
- <button type="button" (click)="undo()" [disabled]="past().length === 0" title="Undo (Ctrl+Z)">↶</button>
1356
- <button type="button" (click)="redo()" [disabled]="future().length === 0" title="Redo (Ctrl+Y)">↷</button>
1732
+ <button type="button" (click)="undo()" [disabled]="past().length === 0" [title]="labels().undo">↶</button>
1733
+ <button type="button" (click)="redo()" [disabled]="future().length === 0" [title]="labels().redo">↷</button>
1357
1734
  <span class="dz-sep"></span>
1358
- <button type="button" (click)="moveSelected(-1)" [disabled]="!selected()" title="Move up">↑</button>
1359
- <button type="button" (click)="moveSelected(1)" [disabled]="!selected()" title="Move down">↓</button>
1360
- <button type="button" (click)="duplicateSelected()" [disabled]="!selected()" title="Duplicate (Ctrl+D)">⧉</button>
1361
- <button type="button" (click)="removeSelected()" [disabled]="!selected()" title="Delete (Del)">🗑</button>
1735
+ <button type="button" (click)="moveSelected(-1)" [disabled]="!selected()" [title]="labels().moveUp">↑</button>
1736
+ <button type="button" (click)="moveSelected(1)" [disabled]="!selected()" [title]="labels().moveDown">↓</button>
1737
+ <button type="button" (click)="duplicateSelected()" [disabled]="!selected()" [title]="labels().duplicate">⧉</button>
1738
+ <button type="button" (click)="removeSelected()" [disabled]="!selected()" [title]="labels().delete">🗑</button>
1739
+ <button type="button" (click)="saveAsBlock()" [disabled]="!selected()" [title]="labels().saveBlock">⊞</button>
1362
1740
  </div>
1363
1741
  <div class="dz-tools">
1364
1742
  @if (diagnostics().length > 0) {
@@ -1367,12 +1745,12 @@ class FormDesignerComponent {
1367
1745
  </button>
1368
1746
  }
1369
1747
  <div class="dz-menu">
1370
- <button type="button" (click)="workspaceMenu.set(!workspaceMenu())" title="Workspace layout">
1371
- Workspace
1748
+ <button type="button" (click)="workspaceMenu.set(!workspaceMenu())" [title]="labels().workspace">
1749
+ {{ labels().workspace }}
1372
1750
  </button>
1373
1751
  @if (workspaceMenu()) {
1374
1752
  <div class="dz-menu-panel" role="menu">
1375
- <span class="dz-menu-title">Panels</span>
1753
+ <span class="dz-menu-title">{{ labels().panels }}</span>
1376
1754
  @for (panel of panelIds; track panel) {
1377
1755
  <div class="dz-menu-row">
1378
1756
  <span>{{ panelLabel(panel) }}</span>
@@ -1387,20 +1765,20 @@ class FormDesignerComponent {
1387
1765
  </span>
1388
1766
  </div>
1389
1767
  }
1390
- <span class="dz-menu-title">Presets</span>
1768
+ <span class="dz-menu-title">{{ labels().presets }}</span>
1391
1769
  @for (preset of presets; track preset.id) {
1392
1770
  <button type="button" class="dz-menu-item" (click)="applyPreset(preset.id)">
1393
1771
  {{ preset.label }}
1394
1772
  </button>
1395
1773
  }
1396
- <button type="button" class="dz-menu-item" (click)="resetLayout()">Reset layout</button>
1774
+ <button type="button" class="dz-menu-item" (click)="resetLayout()">{{ labels().resetLayout }}</button>
1397
1775
  </div>
1398
1776
  }
1399
1777
  </div>
1400
- <button type="button" (click)="dialog.set(dialog() === 'import' ? null : 'import')">Import</button>
1401
- <button type="button" (click)="openExport()">Export</button>
1778
+ <button type="button" (click)="dialog.set(dialog() === 'import' ? null : 'import')">{{ labels().import }}</button>
1779
+ <button type="button" (click)="openExport()">{{ labels().export }}</button>
1402
1780
  <button type="button" class="dz-primary" (click)="preview.set(!preview())">
1403
- {{ preview() ? 'Design' : 'Preview' }}
1781
+ {{ preview() ? labels().design : labels().preview }}
1404
1782
  </button>
1405
1783
  </div>
1406
1784
  </header>
@@ -1417,23 +1795,23 @@ class FormDesignerComponent {
1417
1795
 
1418
1796
  @if (dialog() === 'export') {
1419
1797
  <div class="dz-dialog">
1420
- <h4>Schema JSON</h4>
1798
+ <h4>{{ labels().exportTitle }}</h4>
1421
1799
  <textarea readonly rows="12">{{ exportedJson() }}</textarea>
1422
1800
  <div class="dz-dialog-actions">
1423
- <button type="button" (click)="dialog.set(null)">Close</button>
1801
+ <button type="button" (click)="dialog.set(null)">{{ labels().close }}</button>
1424
1802
  </div>
1425
1803
  </div>
1426
1804
  }
1427
1805
  @if (dialog() === 'import') {
1428
1806
  <div class="dz-dialog">
1429
- <h4>Import definition (native schema or supported formats)</h4>
1430
- <textarea rows="12" #importArea placeholder='{"version":1,"fields":[…]} or a legacy definition'></textarea>
1807
+ <h4>{{ labels().importTitle }}</h4>
1808
+ <textarea rows="12" #importArea [placeholder]="labels().importPlaceholder"></textarea>
1431
1809
  @if (importError()) {
1432
1810
  <p class="dz-import-error" role="alert">{{ importError() }}</p>
1433
1811
  }
1434
1812
  <div class="dz-dialog-actions">
1435
- <button type="button" (click)="dialog.set(null)">Cancel</button>
1436
- <button type="button" class="dz-primary" (click)="doImport(importArea.value)">Import</button>
1813
+ <button type="button" (click)="dialog.set(null)">{{ labels().cancel }}</button>
1814
+ <button type="button" class="dz-primary" (click)="doImport(importArea.value)">{{ labels().import }}</button>
1437
1815
  </div>
1438
1816
  </div>
1439
1817
  }
@@ -1442,38 +1820,77 @@ class FormDesignerComponent {
1442
1820
  <div class="dz-preview">
1443
1821
  <mform-renderer [schema]="draft()" (valueChanged)="previewData.set($event)" />
1444
1822
  <details class="dz-preview-data">
1445
- <summary>Test data</summary>
1823
+ <summary>{{ labels().testData }}</summary>
1446
1824
  <pre>{{ previewData() | json }}</pre>
1447
1825
  </details>
1448
1826
  </div>
1449
1827
  } @else {
1450
- <div class="dz-body" [style.grid-template-columns]="bodyColumns()">
1828
+ <div
1829
+ class="dz-body"
1830
+ [style.grid-template-columns]="bodyColumns()"
1831
+ (pointermove)="onResizeMove($event)"
1832
+ (pointerup)="endResize()"
1833
+ (pointercancel)="endResize()"
1834
+ >
1451
1835
  @if (dockedPanels('left').length > 0) {
1452
1836
  <aside class="dz-dock dz-dock-left">
1453
1837
  @for (panel of dockedPanels('left'); track panel) {
1454
1838
  <ng-container *ngTemplateOutlet="panelTemplate; context: { $implicit: panel }" />
1455
1839
  }
1840
+ <div
1841
+ class="dz-splitter dz-splitter-left"
1842
+ role="separator"
1843
+ aria-orientation="vertical"
1844
+ (pointerdown)="startResize('left', $event)"
1845
+ ></div>
1456
1846
  </aside>
1457
1847
  }
1458
1848
 
1459
- <main class="dz-canvas" (click)="select(null)">
1849
+ <main class="dz-canvas" [class.is-dragging]="isDragging()" (click)="select(null)">
1460
1850
  @for (node of draft().fields; track node.key; let i = $index) {
1851
+ <div
1852
+ class="dz-dropzone"
1853
+ [class.is-over]="isDropTarget({ path: [i] })"
1854
+ (dragover)="allowDrop({ path: [i] }, $event)"
1855
+ (drop)="drop({ path: [i] }, $event)"
1856
+ ></div>
1461
1857
  <mform-designer-node
1462
1858
  [node]="node"
1463
1859
  [path]="[i]"
1464
1860
  [engine]="canvasEngine()"
1465
1861
  [selected]="selected()"
1862
+ [dragging]="isDragging()"
1863
+ [activeDrop]="dropTarget()"
1864
+ draggable="true"
1865
+ (dragstart)="startNodeDrag([i], $event)"
1866
+ (dragend)="endDrag()"
1466
1867
  (selectNode)="select($event)"
1868
+ (nodeDragStart)="startNodeDrag($event)"
1869
+ (dragOverTarget)="allowDrop($event)"
1870
+ (dropOnTarget)="drop($event)"
1871
+ (dragFinished)="endDrag()"
1467
1872
  />
1468
1873
  } @empty {
1469
1874
  <div class="dz-canvas-empty">
1470
- Click a field in the palette to start building.
1875
+ {{ labels().canvasEmpty }}
1471
1876
  </div>
1472
1877
  }
1878
+ <div
1879
+ class="dz-dropzone is-last"
1880
+ [class.is-over]="isDropTarget({ path: [draft().fields.length] })"
1881
+ (dragover)="allowDrop({ path: [draft().fields.length] }, $event)"
1882
+ (drop)="drop({ path: [draft().fields.length] }, $event)"
1883
+ ></div>
1473
1884
  </main>
1474
1885
 
1475
1886
  @if (dockedPanels('right').length > 0) {
1476
1887
  <aside class="dz-dock dz-dock-right">
1888
+ <div
1889
+ class="dz-splitter dz-splitter-right"
1890
+ role="separator"
1891
+ aria-orientation="vertical"
1892
+ (pointerdown)="startResize('right', $event)"
1893
+ ></div>
1477
1894
  @for (panel of dockedPanels('right'); track panel) {
1478
1895
  <ng-container *ngTemplateOutlet="panelTemplate; context: { $implicit: panel }" />
1479
1896
  }
@@ -1491,10 +1908,10 @@ class FormDesignerComponent {
1491
1908
  <span class="dz-panel-actions">
1492
1909
  <button
1493
1910
  type="button"
1494
- [title]="panelSide(panel) === 'left' ? 'Move right' : 'Move left'"
1911
+ [title]="panelSide(panel) === 'left' ? labels().moveRight : labels().moveLeft"
1495
1912
  (click)="dock(panel, panelSide(panel) === 'left' ? 'right' : 'left')"
1496
1913
  >{{ panelSide(panel) === 'left' ? '⇥' : '⇤' }}</button>
1497
- <button type="button" title="Hide panel" (click)="dock(panel, 'hidden')">✕</button>
1914
+ <button type="button" [title]="labels().hidePanel" (click)="dock(panel, 'hidden')">✕</button>
1498
1915
  </span>
1499
1916
  </header>
1500
1917
  <div class="dz-panel-body">
@@ -1516,10 +1933,27 @@ class FormDesignerComponent {
1516
1933
  @if (isGroupOpen(group.label)) {
1517
1934
  <div class="dz-group-entries">
1518
1935
  @for (entry of group.entries; track entry.type) {
1519
- <button type="button" class="dz-palette-item" (click)="add(entry)"
1520
- [title]="'Add ' + entry.label">
1521
- <span class="dz-icon">{{ entry.icon }}</span>{{ entry.label }}
1522
- </button>
1936
+ <div class="dz-palette-row">
1937
+ <button
1938
+ type="button"
1939
+ class="dz-palette-item"
1940
+ draggable="true"
1941
+ (dragstart)="startEntryDrag(entry, $event)"
1942
+ (dragend)="endDrag()"
1943
+ (click)="add(entry)"
1944
+ [title]="'Add ' + entry.label + ' (or drag onto the canvas)'"
1945
+ >
1946
+ <span class="dz-icon">{{ entry.icon }}</span>{{ entry.label }}
1947
+ </button>
1948
+ @if (isBlockEntry(entry)) {
1949
+ <button
1950
+ type="button"
1951
+ class="dz-block-remove"
1952
+ (click)="removeBlock(blockIdOf(entry))"
1953
+ [attr.aria-label]="'Remove block ' + entry.label"
1954
+ >✕</button>
1955
+ }
1956
+ </div>
1523
1957
  }
1524
1958
  </div>
1525
1959
  }
@@ -1540,7 +1974,7 @@ class FormDesignerComponent {
1540
1974
  {{ item.label }}
1541
1975
  </button>
1542
1976
  } @empty {
1543
- <span class="dz-empty">no fields yet</span>
1977
+ <span class="dz-empty">{{ labels().noFields }}</span>
1544
1978
  }
1545
1979
  </div>
1546
1980
  }
@@ -1552,14 +1986,14 @@ class FormDesignerComponent {
1552
1986
  (nodePatch)="applyPatch($event)"
1553
1987
  />
1554
1988
  } @else {
1555
- <div class="dz-empty dz-props-empty">Select a field to edit its properties.</div>
1989
+ <div class="dz-empty dz-props-empty">{{ labels().selectField }}</div>
1556
1990
  }
1557
1991
  }
1558
1992
  }
1559
1993
  </div>
1560
1994
  </section>
1561
1995
  </ng-template>
1562
- `, isInline: true, styles: [".dz-shell{display:flex;flex-direction:column;height:100%;min-height:480px;font-family:var(--mform-font, system-ui, \"Segoe UI\", sans-serif);color:var(--mform-text, #1f1b2e);background:var(--mform-bg, #f6f5fa);border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);overflow:hidden}.dz-toolbar{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:8px 12px;background:var(--mform-surface, #fff);border-bottom:1px solid var(--mform-border, #d9d5e6)}.dz-tools{display:flex;align-items:center;gap:6px}.dz-toolbar button{font:inherit;font-size:13px;padding:6px 10px;border:1px solid transparent;border-radius:8px;background:none;color:inherit;cursor:pointer}.dz-toolbar button:hover:not(:disabled){background:var(--mform-bg, #f6f5fa)}.dz-toolbar button:disabled{opacity:.35;cursor:default}.dz-toolbar .dz-primary{background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff)}.dz-sep{width:1px;height:18px;background:var(--mform-border, #d9d5e6)}.dz-diag-badge{color:#b45309;font-weight:600}.dz-diagnostics{max-height:140px;overflow-y:auto;padding:8px 14px;background:#fdf5ef;border-bottom:1px solid var(--mform-border, #d9d5e6);font-size:12px}.dz-diag{padding:2px 0;color:#9a3412}.dz-diag.is-warning{color:#92400e}.dz-diag code{font-weight:600;margin-right:6px}.dz-diag em{color:var(--mform-muted, #6b6580);margin-left:6px}.dz-dialog{padding:14px;background:var(--mform-surface, #fff);border-bottom:1px solid var(--mform-border, #d9d5e6)}.dz-dialog h4{margin:0 0 8px;font-size:13px}.dz-dialog textarea{width:100%;font-family:ui-monospace,Consolas,monospace;font-size:12px;border:1px solid var(--mform-border, #d9d5e6);border-radius:8px;padding:10px;box-sizing:border-box;resize:vertical}.dz-dialog-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}.dz-dialog-actions button{font:inherit;font-size:13px;padding:7px 14px;border:1px solid var(--mform-border, #d9d5e6);border-radius:8px;background:none;cursor:pointer}.dz-dialog-actions .dz-primary{background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff);border-color:transparent}.dz-import-error{color:var(--mform-danger, #c62842);font-size:12px}.dz-body{display:grid;flex:1;min-height:0}.dz-dock{display:flex;flex-direction:column;min-height:0;overflow-y:auto;background:var(--mform-surface, #fff)}.dz-dock-left{border-right:1px solid var(--mform-border, #d9d5e6)}.dz-dock-right{border-left:1px solid var(--mform-border, #d9d5e6)}.dz-panel{display:flex;flex-direction:column;border-bottom:1px solid var(--mform-border, #d9d5e6)}.dz-panel:last-child{border-bottom:none;flex:1}.dz-panel-head{display:flex;align-items:center;justify-content:space-between;padding:8px 10px;font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--mform-muted, #6b6580);background:var(--mform-bg, #f6f5fa);position:sticky;top:0;z-index:1}.dz-panel-actions{display:flex;gap:2px;opacity:0;transition:opacity .12s}.dz-panel:hover .dz-panel-actions,.dz-panel:focus-within .dz-panel-actions{opacity:1}.dz-panel-actions button{border:none;background:none;color:inherit;cursor:pointer;font-size:11px;padding:2px 4px;border-radius:4px}.dz-panel-actions button:hover{background:var(--mform-surface, #fff)}.dz-panel-body{padding:10px}.dz-menu{position:relative}.dz-menu-panel{position:absolute;top:calc(100% + 6px);right:0;z-index:20;min-width:260px;padding:8px;background:var(--mform-surface, #fff);border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);box-shadow:0 10px 30px #1f1b2e26}.dz-menu-title{display:block;font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--mform-muted, #6b6580);padding:6px 6px 4px}.dz-menu-row{display:flex;align-items:center;justify-content:space-between;padding:4px 6px;font-size:13px}.dz-seg{display:flex;gap:2px}.dz-seg button{border:1px solid var(--mform-border, #d9d5e6);background:none;color:var(--mform-muted, #6b6580);border-radius:6px;font-size:11px;padding:3px 8px;cursor:pointer}.dz-seg button.is-on{background:var(--mform-primary, #6d4bd0);border-color:transparent;color:var(--mform-primary-contrast, #fff)}.dz-menu-item{display:block;width:100%;text-align:left;font:inherit;font-size:13px;padding:6px;border:none;border-radius:6px;background:none;color:inherit;cursor:pointer}.dz-menu-item:hover{background:var(--mform-bg, #f6f5fa)}.dz-group{margin-bottom:4px}.dz-group-toggle{display:flex;align-items:center;gap:6px;width:100%;font:inherit;font-size:12px;font-weight:600;text-align:left;padding:6px;border:none;border-radius:6px;background:none;color:inherit;cursor:pointer}.dz-group-toggle:hover{background:var(--mform-bg, #f6f5fa)}.dz-caret{font-size:10px;color:var(--mform-muted, #6b6580);width:10px}.dz-group-count{margin-left:auto;font-size:10px;font-weight:400;color:var(--mform-muted, #6b6580);background:var(--mform-bg, #f6f5fa);border-radius:999px;padding:1px 7px}.dz-group-entries{display:flex;flex-direction:column;gap:2px;margin:2px 0 6px 10px}.dz-palette-item{display:flex;align-items:center;gap:8px;font:inherit;font-size:13px;text-align:left;padding:6px 8px;border:none;border-radius:8px;background:none;color:inherit;cursor:pointer}.dz-palette-item:hover{background:var(--mform-bg, #f6f5fa)}.dz-icon{width:22px;height:22px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--mform-border, #d9d5e6);border-radius:6px;font-size:11px}.dz-tree{display:flex;flex-direction:column;gap:1px}.dz-tree-item{font:inherit;font-size:12px;text-align:left;padding:4px 8px;border:none;border-radius:6px;background:none;color:inherit;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dz-tree-item:hover{background:var(--mform-bg, #f6f5fa)}.dz-tree-item.is-selected{background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff)}.dz-canvas{padding:20px;overflow-y:auto;display:flex;flex-direction:column;gap:10px}.dz-canvas-empty,.dz-empty{color:var(--mform-muted, #6b6580);font-size:13px;border:1px dashed var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);padding:18px;text-align:center}.dz-node{position:relative;border:1px solid transparent;border-radius:var(--mform-radius, 10px);padding:20px 10px 10px;cursor:pointer;background:var(--mform-surface, #fff)}.dz-node:hover{border-color:var(--mform-border, #d9d5e6)}.dz-node.is-selected{border-color:var(--mform-primary, #6d4bd0);box-shadow:0 0 0 1px var(--mform-primary, #6d4bd0)}.dz-chip{position:absolute;top:2px;left:10px;font-size:10px;color:var(--mform-muted, #6b6580);pointer-events:none}.dz-node.is-selected>.dz-chip{color:var(--mform-primary, #6d4bd0);font-weight:600}.dz-children{display:flex;flex-direction:column;gap:8px}.dz-columns{display:grid;gap:10px}.dz-column{display:flex;flex-direction:column;gap:8px;border:1px dashed var(--mform-border, #d9d5e6);border-radius:8px;padding:8px;min-height:40px}.dz-tabbar{display:flex;gap:4px;margin-bottom:8px;border-bottom:2px solid var(--mform-border, #d9d5e6)}.dz-tab{font:inherit;font-size:12px;padding:5px 12px;border:none;background:none;color:var(--mform-muted, #6b6580);cursor:pointer}.dz-tab.is-active{color:var(--mform-primary, #6d4bd0);font-weight:600;border-bottom:2px solid var(--mform-primary, #6d4bd0);margin-bottom:-2px}.dz-leaf{pointer-events:none}.dz-props-empty{margin:4px 0}.dz-inspector{display:flex;flex-direction:column;gap:10px}.dz-inspector h3{margin:0;font-size:13px}.dz-inspector h3 code{color:var(--mform-muted, #6b6580);font-weight:400}.dz-prop{display:flex;flex-direction:column;gap:4px;font-size:12px}.dz-prop span{color:var(--mform-muted, #6b6580)}.dz-prop input,.dz-prop select,.dz-prop textarea{font:inherit;font-size:13px;padding:7px 9px;border:1px solid var(--mform-border, #d9d5e6);border-radius:8px;background:#fff;width:100%;box-sizing:border-box}.dz-check{display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer}.dz-check input{accent-color:var(--mform-primary, #6d4bd0)}.dz-section{border-top:1px solid var(--mform-border, #d9d5e6);padding-top:8px}.dz-section summary{font-size:12px;font-weight:600;cursor:pointer;margin-bottom:8px}.dz-section .dz-prop{margin-bottom:8px}.dz-option-row{display:flex;gap:6px;margin-bottom:6px}.dz-option-row input{flex:1;min-width:0;font-size:12px;padding:6px 8px;border:1px solid var(--mform-border, #d9d5e6);border-radius:6px}.dz-option-row button{border:none;background:none;cursor:pointer;color:var(--mform-muted, #6b6580)}.dz-add{font:inherit;font-size:12px;border:1px dashed var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0);background:none;border-radius:8px;padding:6px 10px;cursor:pointer}.dz-preview{padding:20px;overflow-y:auto}.dz-preview-data{margin-top:16px;font-size:12px}.dz-preview-data pre{background:var(--mform-surface, #fff);border:1px solid var(--mform-border, #d9d5e6);border-radius:8px;padding:10px;overflow-x:auto}\n"], dependencies: [{ kind: "component", type: DesignerNodeComponent, selector: "mform-designer-node", inputs: ["node", "path", "engine", "selected"], outputs: ["selectNode"] }, { kind: "component", type: InspectorComponent, selector: "mform-inspector", inputs: ["node", "schema"], outputs: ["nodePatch"] }, { kind: "component", type: FormRendererComponent, selector: "mform-renderer", inputs: ["schema", "source", "initialData", "optionsProviders", "messages", "validators", "mode", "submitHandler", "labels"], outputs: ["submitted", "valueChanged"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "pipe", type: JsonPipe, name: "json" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1996
+ `, isInline: true, styles: [".dz-shell{display:flex;flex-direction:column;height:100%;min-height:480px;font-family:var(--mform-font, system-ui, \"Segoe UI\", sans-serif);color:var(--mform-text, #1f1b2e);background:var(--mform-bg, #f6f5fa);border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);overflow:hidden}.dz-toolbar{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:8px 12px;background:var(--mform-surface, #fff);border-bottom:1px solid var(--mform-border, #d9d5e6)}.dz-tools{display:flex;align-items:center;gap:6px}.dz-toolbar button{font:inherit;font-size:13px;padding:6px 10px;border:1px solid transparent;border-radius:8px;background:none;color:inherit;cursor:pointer}.dz-toolbar button:hover:not(:disabled){background:var(--mform-bg, #f6f5fa)}.dz-toolbar button:disabled{opacity:.35;cursor:default}.dz-toolbar .dz-primary{background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff)}.dz-sep{width:1px;height:18px;background:var(--mform-border, #d9d5e6)}.dz-diag-badge{color:#b45309;font-weight:600}.dz-diagnostics{max-height:140px;overflow-y:auto;padding:8px 14px;background:#fdf5ef;border-bottom:1px solid var(--mform-border, #d9d5e6);font-size:12px}.dz-diag{padding:2px 0;color:#9a3412}.dz-diag.is-warning{color:#92400e}.dz-diag code{font-weight:600;margin-right:6px}.dz-diag em{color:var(--mform-muted, #6b6580);margin-left:6px}.dz-dialog{padding:14px;background:var(--mform-surface, #fff);border-bottom:1px solid var(--mform-border, #d9d5e6)}.dz-dialog h4{margin:0 0 8px;font-size:13px}.dz-dialog textarea{width:100%;font-family:ui-monospace,Consolas,monospace;font-size:12px;border:1px solid var(--mform-border, #d9d5e6);border-radius:8px;padding:10px;box-sizing:border-box;resize:vertical}.dz-dialog-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}.dz-dialog-actions button{font:inherit;font-size:13px;padding:7px 14px;border:1px solid var(--mform-border, #d9d5e6);border-radius:8px;background:none;cursor:pointer}.dz-dialog-actions .dz-primary{background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff);border-color:transparent}.dz-import-error{color:var(--mform-danger, #c62842);font-size:12px}.dz-body{display:grid;flex:1;min-height:0}.dz-dock{display:flex;flex-direction:column;min-height:0;overflow-y:auto;background:var(--mform-surface, #fff)}.dz-dock-left{border-right:1px solid var(--mform-border, #d9d5e6);position:relative}.dz-dock-right{border-left:1px solid var(--mform-border, #d9d5e6);position:relative}.dz-splitter{position:absolute;top:0;bottom:0;width:6px;cursor:col-resize;touch-action:none;z-index:2}.dz-splitter-left{right:-3px}.dz-splitter-right{left:-3px}.dz-splitter:hover,.dz-splitter:active{background:var(--mform-primary, #6d4bd0);opacity:.35}.dz-panel{display:flex;flex-direction:column;border-bottom:1px solid var(--mform-border, #d9d5e6)}.dz-panel:last-child{border-bottom:none;flex:1}.dz-panel-head{display:flex;align-items:center;justify-content:space-between;padding:8px 10px;font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--mform-muted, #6b6580);background:var(--mform-bg, #f6f5fa);position:sticky;top:0;z-index:1}.dz-panel-actions{display:flex;gap:2px;opacity:0;transition:opacity .12s}.dz-panel:hover .dz-panel-actions,.dz-panel:focus-within .dz-panel-actions{opacity:1}.dz-panel-actions button{border:none;background:none;color:inherit;cursor:pointer;font-size:11px;padding:2px 4px;border-radius:4px}.dz-panel-actions button:hover{background:var(--mform-surface, #fff)}.dz-panel-body{padding:10px}.dz-menu{position:relative}.dz-menu-panel{position:absolute;top:calc(100% + 6px);right:0;z-index:20;min-width:260px;padding:8px;background:var(--mform-surface, #fff);border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);box-shadow:0 10px 30px #1f1b2e26}.dz-menu-title{display:block;font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--mform-muted, #6b6580);padding:6px 6px 4px}.dz-menu-row{display:flex;align-items:center;justify-content:space-between;padding:4px 6px;font-size:13px}.dz-seg{display:flex;gap:2px}.dz-seg button{border:1px solid var(--mform-border, #d9d5e6);background:none;color:var(--mform-muted, #6b6580);border-radius:6px;font-size:11px;padding:3px 8px;cursor:pointer}.dz-seg button.is-on{background:var(--mform-primary, #6d4bd0);border-color:transparent;color:var(--mform-primary-contrast, #fff)}.dz-menu-item{display:block;width:100%;text-align:left;font:inherit;font-size:13px;padding:6px;border:none;border-radius:6px;background:none;color:inherit;cursor:pointer}.dz-menu-item:hover{background:var(--mform-bg, #f6f5fa)}.dz-group{margin-bottom:4px}.dz-group-toggle{display:flex;align-items:center;gap:6px;width:100%;font:inherit;font-size:12px;font-weight:600;text-align:left;padding:6px;border:none;border-radius:6px;background:none;color:inherit;cursor:pointer}.dz-group-toggle:hover{background:var(--mform-bg, #f6f5fa)}.dz-caret{font-size:10px;color:var(--mform-muted, #6b6580);width:10px}.dz-group-count{margin-left:auto;font-size:10px;font-weight:400;color:var(--mform-muted, #6b6580);background:var(--mform-bg, #f6f5fa);border-radius:999px;padding:1px 7px}.dz-group-entries{display:flex;flex-direction:column;gap:2px;margin:2px 0 6px 10px}.dz-palette-item{display:flex;align-items:center;gap:8px;font:inherit;font-size:13px;text-align:left;padding:6px 8px;border:none;border-radius:8px;background:none;color:inherit;cursor:pointer}.dz-palette-item:hover{background:var(--mform-bg, #f6f5fa)}.dz-icon{width:22px;height:22px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--mform-border, #d9d5e6);border-radius:6px;font-size:11px}.dz-tree{display:flex;flex-direction:column;gap:1px}.dz-tree-item{font:inherit;font-size:12px;text-align:left;padding:4px 8px;border:none;border-radius:6px;background:none;color:inherit;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dz-tree-item:hover{background:var(--mform-bg, #f6f5fa)}.dz-tree-item.is-selected{background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff)}.dz-canvas{padding:20px;overflow-y:auto;display:flex;flex-direction:column;gap:10px}.dz-canvas-empty,.dz-empty{color:var(--mform-muted, #6b6580);font-size:13px;border:1px dashed var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);padding:18px;text-align:center}.dz-dropzone{height:4px;border-radius:999px;transition:height .1s,background .1s}.dz-canvas.is-dragging .dz-dropzone{height:14px;background:repeating-linear-gradient(90deg,var(--mform-border, #d9d5e6) 0 6px,transparent 6px 12px)}.dz-canvas.is-dragging .dz-dropzone.is-over{height:22px;background:var(--mform-primary, #6d4bd0)}.dz-palette-row{display:flex;align-items:center;gap:2px}.dz-palette-row .dz-palette-item{flex:1;cursor:grab}.dz-block-remove{border:none;background:none;color:var(--mform-muted, #6b6580);cursor:pointer;font-size:11px;padding:2px 4px;opacity:0}.dz-palette-row:hover .dz-block-remove{opacity:1}.dz-node{position:relative;border:1px solid transparent;border-radius:var(--mform-radius, 10px);padding:20px 10px 10px;cursor:pointer;background:var(--mform-surface, #fff)}.dz-node:hover{border-color:var(--mform-border, #d9d5e6)}.dz-node.is-selected{border-color:var(--mform-primary, #6d4bd0);box-shadow:0 0 0 1px var(--mform-primary, #6d4bd0)}.dz-chip{position:absolute;top:2px;left:10px;font-size:10px;color:var(--mform-muted, #6b6580);pointer-events:none}.dz-node.is-selected>.dz-chip{color:var(--mform-primary, #6d4bd0);font-weight:600}.dz-children{display:flex;flex-direction:column;gap:8px}.dz-columns{display:grid;gap:10px}.dz-column{display:flex;flex-direction:column;gap:8px;border:1px dashed var(--mform-border, #d9d5e6);border-radius:8px;padding:8px;min-height:40px}.dz-tabbar{display:flex;gap:4px;margin-bottom:8px;border-bottom:2px solid var(--mform-border, #d9d5e6)}.dz-tab{font:inherit;font-size:12px;padding:5px 12px;border:none;background:none;color:var(--mform-muted, #6b6580);cursor:pointer}.dz-tab.is-active{color:var(--mform-primary, #6d4bd0);font-weight:600;border-bottom:2px solid var(--mform-primary, #6d4bd0);margin-bottom:-2px}.dz-leaf{pointer-events:none}.dz-props-empty{margin:4px 0}.dz-inspector{display:flex;flex-direction:column;gap:10px}.dz-inspector h3{margin:0;font-size:13px}.dz-inspector h3 code{color:var(--mform-muted, #6b6580);font-weight:400}.dz-prop{display:flex;flex-direction:column;gap:4px;font-size:12px}.dz-prop span{color:var(--mform-muted, #6b6580)}.dz-prop input,.dz-prop select,.dz-prop textarea{font:inherit;font-size:13px;padding:7px 9px;border:1px solid var(--mform-border, #d9d5e6);border-radius:8px;background:#fff;width:100%;box-sizing:border-box}.dz-check{display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer}.dz-check input{accent-color:var(--mform-primary, #6d4bd0)}.dz-section{border-top:1px solid var(--mform-border, #d9d5e6);padding-top:8px}.dz-section summary{font-size:12px;font-weight:600;cursor:pointer;margin-bottom:8px}.dz-section .dz-prop{margin-bottom:8px}.dz-option-row{display:flex;gap:6px;margin-bottom:6px}.dz-option-row input{flex:1;min-width:0;font-size:12px;padding:6px 8px;border:1px solid var(--mform-border, #d9d5e6);border-radius:6px}.dz-option-row button{border:none;background:none;cursor:pointer;color:var(--mform-muted, #6b6580)}.dz-add{font:inherit;font-size:12px;border:1px dashed var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0);background:none;border-radius:8px;padding:6px 10px;cursor:pointer}.dz-preview{padding:20px;overflow-y:auto}.dz-preview-data{margin-top:16px;font-size:12px}.dz-preview-data pre{background:var(--mform-surface, #fff);border:1px solid var(--mform-border, #d9d5e6);border-radius:8px;padding:10px;overflow-x:auto}\n"], dependencies: [{ kind: "component", type: DesignerNodeComponent, selector: "mform-designer-node", inputs: ["node", "path", "engine", "selected", "dragging", "activeDrop"], outputs: ["selectNode", "nodeDragStart", "dragOverTarget", "dropOnTarget", "dragFinished"] }, { kind: "component", type: InspectorComponent, selector: "mform-inspector", inputs: ["node", "schema"], outputs: ["nodePatch"] }, { kind: "component", type: FormRendererComponent, selector: "mform-renderer", inputs: ["schema", "source", "initialData", "optionsProviders", "messages", "validators", "mode", "submitHandler", "labels"], outputs: ["submitted", "valueChanged"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "pipe", type: JsonPipe, name: "json" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1563
1997
  }
1564
1998
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FormDesignerComponent, decorators: [{
1565
1999
  type: Component,
@@ -1573,13 +2007,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1573
2007
  <div class="dz-shell" (keydown)="onKeydown($event)">
1574
2008
  <header class="dz-toolbar">
1575
2009
  <div class="dz-tools">
1576
- <button type="button" (click)="undo()" [disabled]="past().length === 0" title="Undo (Ctrl+Z)">↶</button>
1577
- <button type="button" (click)="redo()" [disabled]="future().length === 0" title="Redo (Ctrl+Y)">↷</button>
2010
+ <button type="button" (click)="undo()" [disabled]="past().length === 0" [title]="labels().undo">↶</button>
2011
+ <button type="button" (click)="redo()" [disabled]="future().length === 0" [title]="labels().redo">↷</button>
1578
2012
  <span class="dz-sep"></span>
1579
- <button type="button" (click)="moveSelected(-1)" [disabled]="!selected()" title="Move up">↑</button>
1580
- <button type="button" (click)="moveSelected(1)" [disabled]="!selected()" title="Move down">↓</button>
1581
- <button type="button" (click)="duplicateSelected()" [disabled]="!selected()" title="Duplicate (Ctrl+D)">⧉</button>
1582
- <button type="button" (click)="removeSelected()" [disabled]="!selected()" title="Delete (Del)">🗑</button>
2013
+ <button type="button" (click)="moveSelected(-1)" [disabled]="!selected()" [title]="labels().moveUp">↑</button>
2014
+ <button type="button" (click)="moveSelected(1)" [disabled]="!selected()" [title]="labels().moveDown">↓</button>
2015
+ <button type="button" (click)="duplicateSelected()" [disabled]="!selected()" [title]="labels().duplicate">⧉</button>
2016
+ <button type="button" (click)="removeSelected()" [disabled]="!selected()" [title]="labels().delete">🗑</button>
2017
+ <button type="button" (click)="saveAsBlock()" [disabled]="!selected()" [title]="labels().saveBlock">⊞</button>
1583
2018
  </div>
1584
2019
  <div class="dz-tools">
1585
2020
  @if (diagnostics().length > 0) {
@@ -1588,12 +2023,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1588
2023
  </button>
1589
2024
  }
1590
2025
  <div class="dz-menu">
1591
- <button type="button" (click)="workspaceMenu.set(!workspaceMenu())" title="Workspace layout">
1592
- Workspace
2026
+ <button type="button" (click)="workspaceMenu.set(!workspaceMenu())" [title]="labels().workspace">
2027
+ {{ labels().workspace }}
1593
2028
  </button>
1594
2029
  @if (workspaceMenu()) {
1595
2030
  <div class="dz-menu-panel" role="menu">
1596
- <span class="dz-menu-title">Panels</span>
2031
+ <span class="dz-menu-title">{{ labels().panels }}</span>
1597
2032
  @for (panel of panelIds; track panel) {
1598
2033
  <div class="dz-menu-row">
1599
2034
  <span>{{ panelLabel(panel) }}</span>
@@ -1608,20 +2043,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1608
2043
  </span>
1609
2044
  </div>
1610
2045
  }
1611
- <span class="dz-menu-title">Presets</span>
2046
+ <span class="dz-menu-title">{{ labels().presets }}</span>
1612
2047
  @for (preset of presets; track preset.id) {
1613
2048
  <button type="button" class="dz-menu-item" (click)="applyPreset(preset.id)">
1614
2049
  {{ preset.label }}
1615
2050
  </button>
1616
2051
  }
1617
- <button type="button" class="dz-menu-item" (click)="resetLayout()">Reset layout</button>
2052
+ <button type="button" class="dz-menu-item" (click)="resetLayout()">{{ labels().resetLayout }}</button>
1618
2053
  </div>
1619
2054
  }
1620
2055
  </div>
1621
- <button type="button" (click)="dialog.set(dialog() === 'import' ? null : 'import')">Import</button>
1622
- <button type="button" (click)="openExport()">Export</button>
2056
+ <button type="button" (click)="dialog.set(dialog() === 'import' ? null : 'import')">{{ labels().import }}</button>
2057
+ <button type="button" (click)="openExport()">{{ labels().export }}</button>
1623
2058
  <button type="button" class="dz-primary" (click)="preview.set(!preview())">
1624
- {{ preview() ? 'Design' : 'Preview' }}
2059
+ {{ preview() ? labels().design : labels().preview }}
1625
2060
  </button>
1626
2061
  </div>
1627
2062
  </header>
@@ -1638,23 +2073,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1638
2073
 
1639
2074
  @if (dialog() === 'export') {
1640
2075
  <div class="dz-dialog">
1641
- <h4>Schema JSON</h4>
2076
+ <h4>{{ labels().exportTitle }}</h4>
1642
2077
  <textarea readonly rows="12">{{ exportedJson() }}</textarea>
1643
2078
  <div class="dz-dialog-actions">
1644
- <button type="button" (click)="dialog.set(null)">Close</button>
2079
+ <button type="button" (click)="dialog.set(null)">{{ labels().close }}</button>
1645
2080
  </div>
1646
2081
  </div>
1647
2082
  }
1648
2083
  @if (dialog() === 'import') {
1649
2084
  <div class="dz-dialog">
1650
- <h4>Import definition (native schema or supported formats)</h4>
1651
- <textarea rows="12" #importArea placeholder='{"version":1,"fields":[…]} or a legacy definition'></textarea>
2085
+ <h4>{{ labels().importTitle }}</h4>
2086
+ <textarea rows="12" #importArea [placeholder]="labels().importPlaceholder"></textarea>
1652
2087
  @if (importError()) {
1653
2088
  <p class="dz-import-error" role="alert">{{ importError() }}</p>
1654
2089
  }
1655
2090
  <div class="dz-dialog-actions">
1656
- <button type="button" (click)="dialog.set(null)">Cancel</button>
1657
- <button type="button" class="dz-primary" (click)="doImport(importArea.value)">Import</button>
2091
+ <button type="button" (click)="dialog.set(null)">{{ labels().cancel }}</button>
2092
+ <button type="button" class="dz-primary" (click)="doImport(importArea.value)">{{ labels().import }}</button>
1658
2093
  </div>
1659
2094
  </div>
1660
2095
  }
@@ -1663,38 +2098,77 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1663
2098
  <div class="dz-preview">
1664
2099
  <mform-renderer [schema]="draft()" (valueChanged)="previewData.set($event)" />
1665
2100
  <details class="dz-preview-data">
1666
- <summary>Test data</summary>
2101
+ <summary>{{ labels().testData }}</summary>
1667
2102
  <pre>{{ previewData() | json }}</pre>
1668
2103
  </details>
1669
2104
  </div>
1670
2105
  } @else {
1671
- <div class="dz-body" [style.grid-template-columns]="bodyColumns()">
2106
+ <div
2107
+ class="dz-body"
2108
+ [style.grid-template-columns]="bodyColumns()"
2109
+ (pointermove)="onResizeMove($event)"
2110
+ (pointerup)="endResize()"
2111
+ (pointercancel)="endResize()"
2112
+ >
1672
2113
  @if (dockedPanels('left').length > 0) {
1673
2114
  <aside class="dz-dock dz-dock-left">
1674
2115
  @for (panel of dockedPanels('left'); track panel) {
1675
2116
  <ng-container *ngTemplateOutlet="panelTemplate; context: { $implicit: panel }" />
1676
2117
  }
2118
+ <div
2119
+ class="dz-splitter dz-splitter-left"
2120
+ role="separator"
2121
+ aria-orientation="vertical"
2122
+ (pointerdown)="startResize('left', $event)"
2123
+ ></div>
1677
2124
  </aside>
1678
2125
  }
1679
2126
 
1680
- <main class="dz-canvas" (click)="select(null)">
2127
+ <main class="dz-canvas" [class.is-dragging]="isDragging()" (click)="select(null)">
1681
2128
  @for (node of draft().fields; track node.key; let i = $index) {
2129
+ <div
2130
+ class="dz-dropzone"
2131
+ [class.is-over]="isDropTarget({ path: [i] })"
2132
+ (dragover)="allowDrop({ path: [i] }, $event)"
2133
+ (drop)="drop({ path: [i] }, $event)"
2134
+ ></div>
1682
2135
  <mform-designer-node
1683
2136
  [node]="node"
1684
2137
  [path]="[i]"
1685
2138
  [engine]="canvasEngine()"
1686
2139
  [selected]="selected()"
2140
+ [dragging]="isDragging()"
2141
+ [activeDrop]="dropTarget()"
2142
+ draggable="true"
2143
+ (dragstart)="startNodeDrag([i], $event)"
2144
+ (dragend)="endDrag()"
1687
2145
  (selectNode)="select($event)"
2146
+ (nodeDragStart)="startNodeDrag($event)"
2147
+ (dragOverTarget)="allowDrop($event)"
2148
+ (dropOnTarget)="drop($event)"
2149
+ (dragFinished)="endDrag()"
1688
2150
  />
1689
2151
  } @empty {
1690
2152
  <div class="dz-canvas-empty">
1691
- Click a field in the palette to start building.
2153
+ {{ labels().canvasEmpty }}
1692
2154
  </div>
1693
2155
  }
2156
+ <div
2157
+ class="dz-dropzone is-last"
2158
+ [class.is-over]="isDropTarget({ path: [draft().fields.length] })"
2159
+ (dragover)="allowDrop({ path: [draft().fields.length] }, $event)"
2160
+ (drop)="drop({ path: [draft().fields.length] }, $event)"
2161
+ ></div>
1694
2162
  </main>
1695
2163
 
1696
2164
  @if (dockedPanels('right').length > 0) {
1697
2165
  <aside class="dz-dock dz-dock-right">
2166
+ <div
2167
+ class="dz-splitter dz-splitter-right"
2168
+ role="separator"
2169
+ aria-orientation="vertical"
2170
+ (pointerdown)="startResize('right', $event)"
2171
+ ></div>
1698
2172
  @for (panel of dockedPanels('right'); track panel) {
1699
2173
  <ng-container *ngTemplateOutlet="panelTemplate; context: { $implicit: panel }" />
1700
2174
  }
@@ -1712,10 +2186,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1712
2186
  <span class="dz-panel-actions">
1713
2187
  <button
1714
2188
  type="button"
1715
- [title]="panelSide(panel) === 'left' ? 'Move right' : 'Move left'"
2189
+ [title]="panelSide(panel) === 'left' ? labels().moveRight : labels().moveLeft"
1716
2190
  (click)="dock(panel, panelSide(panel) === 'left' ? 'right' : 'left')"
1717
2191
  >{{ panelSide(panel) === 'left' ? '⇥' : '⇤' }}</button>
1718
- <button type="button" title="Hide panel" (click)="dock(panel, 'hidden')">✕</button>
2192
+ <button type="button" [title]="labels().hidePanel" (click)="dock(panel, 'hidden')">✕</button>
1719
2193
  </span>
1720
2194
  </header>
1721
2195
  <div class="dz-panel-body">
@@ -1737,10 +2211,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1737
2211
  @if (isGroupOpen(group.label)) {
1738
2212
  <div class="dz-group-entries">
1739
2213
  @for (entry of group.entries; track entry.type) {
1740
- <button type="button" class="dz-palette-item" (click)="add(entry)"
1741
- [title]="'Add ' + entry.label">
1742
- <span class="dz-icon">{{ entry.icon }}</span>{{ entry.label }}
1743
- </button>
2214
+ <div class="dz-palette-row">
2215
+ <button
2216
+ type="button"
2217
+ class="dz-palette-item"
2218
+ draggable="true"
2219
+ (dragstart)="startEntryDrag(entry, $event)"
2220
+ (dragend)="endDrag()"
2221
+ (click)="add(entry)"
2222
+ [title]="'Add ' + entry.label + ' (or drag onto the canvas)'"
2223
+ >
2224
+ <span class="dz-icon">{{ entry.icon }}</span>{{ entry.label }}
2225
+ </button>
2226
+ @if (isBlockEntry(entry)) {
2227
+ <button
2228
+ type="button"
2229
+ class="dz-block-remove"
2230
+ (click)="removeBlock(blockIdOf(entry))"
2231
+ [attr.aria-label]="'Remove block ' + entry.label"
2232
+ >✕</button>
2233
+ }
2234
+ </div>
1744
2235
  }
1745
2236
  </div>
1746
2237
  }
@@ -1761,7 +2252,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1761
2252
  {{ item.label }}
1762
2253
  </button>
1763
2254
  } @empty {
1764
- <span class="dz-empty">no fields yet</span>
2255
+ <span class="dz-empty">{{ labels().noFields }}</span>
1765
2256
  }
1766
2257
  </div>
1767
2258
  }
@@ -1773,15 +2264,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1773
2264
  (nodePatch)="applyPatch($event)"
1774
2265
  />
1775
2266
  } @else {
1776
- <div class="dz-empty dz-props-empty">Select a field to edit its properties.</div>
2267
+ <div class="dz-empty dz-props-empty">{{ labels().selectField }}</div>
1777
2268
  }
1778
2269
  }
1779
2270
  }
1780
2271
  </div>
1781
2272
  </section>
1782
2273
  </ng-template>
1783
- `, styles: [".dz-shell{display:flex;flex-direction:column;height:100%;min-height:480px;font-family:var(--mform-font, system-ui, \"Segoe UI\", sans-serif);color:var(--mform-text, #1f1b2e);background:var(--mform-bg, #f6f5fa);border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);overflow:hidden}.dz-toolbar{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:8px 12px;background:var(--mform-surface, #fff);border-bottom:1px solid var(--mform-border, #d9d5e6)}.dz-tools{display:flex;align-items:center;gap:6px}.dz-toolbar button{font:inherit;font-size:13px;padding:6px 10px;border:1px solid transparent;border-radius:8px;background:none;color:inherit;cursor:pointer}.dz-toolbar button:hover:not(:disabled){background:var(--mform-bg, #f6f5fa)}.dz-toolbar button:disabled{opacity:.35;cursor:default}.dz-toolbar .dz-primary{background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff)}.dz-sep{width:1px;height:18px;background:var(--mform-border, #d9d5e6)}.dz-diag-badge{color:#b45309;font-weight:600}.dz-diagnostics{max-height:140px;overflow-y:auto;padding:8px 14px;background:#fdf5ef;border-bottom:1px solid var(--mform-border, #d9d5e6);font-size:12px}.dz-diag{padding:2px 0;color:#9a3412}.dz-diag.is-warning{color:#92400e}.dz-diag code{font-weight:600;margin-right:6px}.dz-diag em{color:var(--mform-muted, #6b6580);margin-left:6px}.dz-dialog{padding:14px;background:var(--mform-surface, #fff);border-bottom:1px solid var(--mform-border, #d9d5e6)}.dz-dialog h4{margin:0 0 8px;font-size:13px}.dz-dialog textarea{width:100%;font-family:ui-monospace,Consolas,monospace;font-size:12px;border:1px solid var(--mform-border, #d9d5e6);border-radius:8px;padding:10px;box-sizing:border-box;resize:vertical}.dz-dialog-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}.dz-dialog-actions button{font:inherit;font-size:13px;padding:7px 14px;border:1px solid var(--mform-border, #d9d5e6);border-radius:8px;background:none;cursor:pointer}.dz-dialog-actions .dz-primary{background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff);border-color:transparent}.dz-import-error{color:var(--mform-danger, #c62842);font-size:12px}.dz-body{display:grid;flex:1;min-height:0}.dz-dock{display:flex;flex-direction:column;min-height:0;overflow-y:auto;background:var(--mform-surface, #fff)}.dz-dock-left{border-right:1px solid var(--mform-border, #d9d5e6)}.dz-dock-right{border-left:1px solid var(--mform-border, #d9d5e6)}.dz-panel{display:flex;flex-direction:column;border-bottom:1px solid var(--mform-border, #d9d5e6)}.dz-panel:last-child{border-bottom:none;flex:1}.dz-panel-head{display:flex;align-items:center;justify-content:space-between;padding:8px 10px;font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--mform-muted, #6b6580);background:var(--mform-bg, #f6f5fa);position:sticky;top:0;z-index:1}.dz-panel-actions{display:flex;gap:2px;opacity:0;transition:opacity .12s}.dz-panel:hover .dz-panel-actions,.dz-panel:focus-within .dz-panel-actions{opacity:1}.dz-panel-actions button{border:none;background:none;color:inherit;cursor:pointer;font-size:11px;padding:2px 4px;border-radius:4px}.dz-panel-actions button:hover{background:var(--mform-surface, #fff)}.dz-panel-body{padding:10px}.dz-menu{position:relative}.dz-menu-panel{position:absolute;top:calc(100% + 6px);right:0;z-index:20;min-width:260px;padding:8px;background:var(--mform-surface, #fff);border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);box-shadow:0 10px 30px #1f1b2e26}.dz-menu-title{display:block;font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--mform-muted, #6b6580);padding:6px 6px 4px}.dz-menu-row{display:flex;align-items:center;justify-content:space-between;padding:4px 6px;font-size:13px}.dz-seg{display:flex;gap:2px}.dz-seg button{border:1px solid var(--mform-border, #d9d5e6);background:none;color:var(--mform-muted, #6b6580);border-radius:6px;font-size:11px;padding:3px 8px;cursor:pointer}.dz-seg button.is-on{background:var(--mform-primary, #6d4bd0);border-color:transparent;color:var(--mform-primary-contrast, #fff)}.dz-menu-item{display:block;width:100%;text-align:left;font:inherit;font-size:13px;padding:6px;border:none;border-radius:6px;background:none;color:inherit;cursor:pointer}.dz-menu-item:hover{background:var(--mform-bg, #f6f5fa)}.dz-group{margin-bottom:4px}.dz-group-toggle{display:flex;align-items:center;gap:6px;width:100%;font:inherit;font-size:12px;font-weight:600;text-align:left;padding:6px;border:none;border-radius:6px;background:none;color:inherit;cursor:pointer}.dz-group-toggle:hover{background:var(--mform-bg, #f6f5fa)}.dz-caret{font-size:10px;color:var(--mform-muted, #6b6580);width:10px}.dz-group-count{margin-left:auto;font-size:10px;font-weight:400;color:var(--mform-muted, #6b6580);background:var(--mform-bg, #f6f5fa);border-radius:999px;padding:1px 7px}.dz-group-entries{display:flex;flex-direction:column;gap:2px;margin:2px 0 6px 10px}.dz-palette-item{display:flex;align-items:center;gap:8px;font:inherit;font-size:13px;text-align:left;padding:6px 8px;border:none;border-radius:8px;background:none;color:inherit;cursor:pointer}.dz-palette-item:hover{background:var(--mform-bg, #f6f5fa)}.dz-icon{width:22px;height:22px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--mform-border, #d9d5e6);border-radius:6px;font-size:11px}.dz-tree{display:flex;flex-direction:column;gap:1px}.dz-tree-item{font:inherit;font-size:12px;text-align:left;padding:4px 8px;border:none;border-radius:6px;background:none;color:inherit;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dz-tree-item:hover{background:var(--mform-bg, #f6f5fa)}.dz-tree-item.is-selected{background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff)}.dz-canvas{padding:20px;overflow-y:auto;display:flex;flex-direction:column;gap:10px}.dz-canvas-empty,.dz-empty{color:var(--mform-muted, #6b6580);font-size:13px;border:1px dashed var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);padding:18px;text-align:center}.dz-node{position:relative;border:1px solid transparent;border-radius:var(--mform-radius, 10px);padding:20px 10px 10px;cursor:pointer;background:var(--mform-surface, #fff)}.dz-node:hover{border-color:var(--mform-border, #d9d5e6)}.dz-node.is-selected{border-color:var(--mform-primary, #6d4bd0);box-shadow:0 0 0 1px var(--mform-primary, #6d4bd0)}.dz-chip{position:absolute;top:2px;left:10px;font-size:10px;color:var(--mform-muted, #6b6580);pointer-events:none}.dz-node.is-selected>.dz-chip{color:var(--mform-primary, #6d4bd0);font-weight:600}.dz-children{display:flex;flex-direction:column;gap:8px}.dz-columns{display:grid;gap:10px}.dz-column{display:flex;flex-direction:column;gap:8px;border:1px dashed var(--mform-border, #d9d5e6);border-radius:8px;padding:8px;min-height:40px}.dz-tabbar{display:flex;gap:4px;margin-bottom:8px;border-bottom:2px solid var(--mform-border, #d9d5e6)}.dz-tab{font:inherit;font-size:12px;padding:5px 12px;border:none;background:none;color:var(--mform-muted, #6b6580);cursor:pointer}.dz-tab.is-active{color:var(--mform-primary, #6d4bd0);font-weight:600;border-bottom:2px solid var(--mform-primary, #6d4bd0);margin-bottom:-2px}.dz-leaf{pointer-events:none}.dz-props-empty{margin:4px 0}.dz-inspector{display:flex;flex-direction:column;gap:10px}.dz-inspector h3{margin:0;font-size:13px}.dz-inspector h3 code{color:var(--mform-muted, #6b6580);font-weight:400}.dz-prop{display:flex;flex-direction:column;gap:4px;font-size:12px}.dz-prop span{color:var(--mform-muted, #6b6580)}.dz-prop input,.dz-prop select,.dz-prop textarea{font:inherit;font-size:13px;padding:7px 9px;border:1px solid var(--mform-border, #d9d5e6);border-radius:8px;background:#fff;width:100%;box-sizing:border-box}.dz-check{display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer}.dz-check input{accent-color:var(--mform-primary, #6d4bd0)}.dz-section{border-top:1px solid var(--mform-border, #d9d5e6);padding-top:8px}.dz-section summary{font-size:12px;font-weight:600;cursor:pointer;margin-bottom:8px}.dz-section .dz-prop{margin-bottom:8px}.dz-option-row{display:flex;gap:6px;margin-bottom:6px}.dz-option-row input{flex:1;min-width:0;font-size:12px;padding:6px 8px;border:1px solid var(--mform-border, #d9d5e6);border-radius:6px}.dz-option-row button{border:none;background:none;cursor:pointer;color:var(--mform-muted, #6b6580)}.dz-add{font:inherit;font-size:12px;border:1px dashed var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0);background:none;border-radius:8px;padding:6px 10px;cursor:pointer}.dz-preview{padding:20px;overflow-y:auto}.dz-preview-data{margin-top:16px;font-size:12px}.dz-preview-data pre{background:var(--mform-surface, #fff);border:1px solid var(--mform-border, #d9d5e6);border-radius:8px;padding:10px;overflow-x:auto}\n"] }]
1784
- }], ctorParameters: () => [], propDecorators: { schema: [{ type: i0.Input, args: [{ isSignal: true, alias: "schema", required: false }] }], source: [{ type: i0.Input, args: [{ isSignal: true, alias: "source", required: false }] }], extraCatalog: [{ type: i0.Input, args: [{ isSignal: true, alias: "extraCatalog", required: false }] }], schemaChanged: [{ type: i0.Output, args: ["schemaChanged"] }] } });
2274
+ `, styles: [".dz-shell{display:flex;flex-direction:column;height:100%;min-height:480px;font-family:var(--mform-font, system-ui, \"Segoe UI\", sans-serif);color:var(--mform-text, #1f1b2e);background:var(--mform-bg, #f6f5fa);border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);overflow:hidden}.dz-toolbar{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:8px 12px;background:var(--mform-surface, #fff);border-bottom:1px solid var(--mform-border, #d9d5e6)}.dz-tools{display:flex;align-items:center;gap:6px}.dz-toolbar button{font:inherit;font-size:13px;padding:6px 10px;border:1px solid transparent;border-radius:8px;background:none;color:inherit;cursor:pointer}.dz-toolbar button:hover:not(:disabled){background:var(--mform-bg, #f6f5fa)}.dz-toolbar button:disabled{opacity:.35;cursor:default}.dz-toolbar .dz-primary{background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff)}.dz-sep{width:1px;height:18px;background:var(--mform-border, #d9d5e6)}.dz-diag-badge{color:#b45309;font-weight:600}.dz-diagnostics{max-height:140px;overflow-y:auto;padding:8px 14px;background:#fdf5ef;border-bottom:1px solid var(--mform-border, #d9d5e6);font-size:12px}.dz-diag{padding:2px 0;color:#9a3412}.dz-diag.is-warning{color:#92400e}.dz-diag code{font-weight:600;margin-right:6px}.dz-diag em{color:var(--mform-muted, #6b6580);margin-left:6px}.dz-dialog{padding:14px;background:var(--mform-surface, #fff);border-bottom:1px solid var(--mform-border, #d9d5e6)}.dz-dialog h4{margin:0 0 8px;font-size:13px}.dz-dialog textarea{width:100%;font-family:ui-monospace,Consolas,monospace;font-size:12px;border:1px solid var(--mform-border, #d9d5e6);border-radius:8px;padding:10px;box-sizing:border-box;resize:vertical}.dz-dialog-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}.dz-dialog-actions button{font:inherit;font-size:13px;padding:7px 14px;border:1px solid var(--mform-border, #d9d5e6);border-radius:8px;background:none;cursor:pointer}.dz-dialog-actions .dz-primary{background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff);border-color:transparent}.dz-import-error{color:var(--mform-danger, #c62842);font-size:12px}.dz-body{display:grid;flex:1;min-height:0}.dz-dock{display:flex;flex-direction:column;min-height:0;overflow-y:auto;background:var(--mform-surface, #fff)}.dz-dock-left{border-right:1px solid var(--mform-border, #d9d5e6);position:relative}.dz-dock-right{border-left:1px solid var(--mform-border, #d9d5e6);position:relative}.dz-splitter{position:absolute;top:0;bottom:0;width:6px;cursor:col-resize;touch-action:none;z-index:2}.dz-splitter-left{right:-3px}.dz-splitter-right{left:-3px}.dz-splitter:hover,.dz-splitter:active{background:var(--mform-primary, #6d4bd0);opacity:.35}.dz-panel{display:flex;flex-direction:column;border-bottom:1px solid var(--mform-border, #d9d5e6)}.dz-panel:last-child{border-bottom:none;flex:1}.dz-panel-head{display:flex;align-items:center;justify-content:space-between;padding:8px 10px;font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--mform-muted, #6b6580);background:var(--mform-bg, #f6f5fa);position:sticky;top:0;z-index:1}.dz-panel-actions{display:flex;gap:2px;opacity:0;transition:opacity .12s}.dz-panel:hover .dz-panel-actions,.dz-panel:focus-within .dz-panel-actions{opacity:1}.dz-panel-actions button{border:none;background:none;color:inherit;cursor:pointer;font-size:11px;padding:2px 4px;border-radius:4px}.dz-panel-actions button:hover{background:var(--mform-surface, #fff)}.dz-panel-body{padding:10px}.dz-menu{position:relative}.dz-menu-panel{position:absolute;top:calc(100% + 6px);right:0;z-index:20;min-width:260px;padding:8px;background:var(--mform-surface, #fff);border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);box-shadow:0 10px 30px #1f1b2e26}.dz-menu-title{display:block;font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--mform-muted, #6b6580);padding:6px 6px 4px}.dz-menu-row{display:flex;align-items:center;justify-content:space-between;padding:4px 6px;font-size:13px}.dz-seg{display:flex;gap:2px}.dz-seg button{border:1px solid var(--mform-border, #d9d5e6);background:none;color:var(--mform-muted, #6b6580);border-radius:6px;font-size:11px;padding:3px 8px;cursor:pointer}.dz-seg button.is-on{background:var(--mform-primary, #6d4bd0);border-color:transparent;color:var(--mform-primary-contrast, #fff)}.dz-menu-item{display:block;width:100%;text-align:left;font:inherit;font-size:13px;padding:6px;border:none;border-radius:6px;background:none;color:inherit;cursor:pointer}.dz-menu-item:hover{background:var(--mform-bg, #f6f5fa)}.dz-group{margin-bottom:4px}.dz-group-toggle{display:flex;align-items:center;gap:6px;width:100%;font:inherit;font-size:12px;font-weight:600;text-align:left;padding:6px;border:none;border-radius:6px;background:none;color:inherit;cursor:pointer}.dz-group-toggle:hover{background:var(--mform-bg, #f6f5fa)}.dz-caret{font-size:10px;color:var(--mform-muted, #6b6580);width:10px}.dz-group-count{margin-left:auto;font-size:10px;font-weight:400;color:var(--mform-muted, #6b6580);background:var(--mform-bg, #f6f5fa);border-radius:999px;padding:1px 7px}.dz-group-entries{display:flex;flex-direction:column;gap:2px;margin:2px 0 6px 10px}.dz-palette-item{display:flex;align-items:center;gap:8px;font:inherit;font-size:13px;text-align:left;padding:6px 8px;border:none;border-radius:8px;background:none;color:inherit;cursor:pointer}.dz-palette-item:hover{background:var(--mform-bg, #f6f5fa)}.dz-icon{width:22px;height:22px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--mform-border, #d9d5e6);border-radius:6px;font-size:11px}.dz-tree{display:flex;flex-direction:column;gap:1px}.dz-tree-item{font:inherit;font-size:12px;text-align:left;padding:4px 8px;border:none;border-radius:6px;background:none;color:inherit;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dz-tree-item:hover{background:var(--mform-bg, #f6f5fa)}.dz-tree-item.is-selected{background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff)}.dz-canvas{padding:20px;overflow-y:auto;display:flex;flex-direction:column;gap:10px}.dz-canvas-empty,.dz-empty{color:var(--mform-muted, #6b6580);font-size:13px;border:1px dashed var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);padding:18px;text-align:center}.dz-dropzone{height:4px;border-radius:999px;transition:height .1s,background .1s}.dz-canvas.is-dragging .dz-dropzone{height:14px;background:repeating-linear-gradient(90deg,var(--mform-border, #d9d5e6) 0 6px,transparent 6px 12px)}.dz-canvas.is-dragging .dz-dropzone.is-over{height:22px;background:var(--mform-primary, #6d4bd0)}.dz-palette-row{display:flex;align-items:center;gap:2px}.dz-palette-row .dz-palette-item{flex:1;cursor:grab}.dz-block-remove{border:none;background:none;color:var(--mform-muted, #6b6580);cursor:pointer;font-size:11px;padding:2px 4px;opacity:0}.dz-palette-row:hover .dz-block-remove{opacity:1}.dz-node{position:relative;border:1px solid transparent;border-radius:var(--mform-radius, 10px);padding:20px 10px 10px;cursor:pointer;background:var(--mform-surface, #fff)}.dz-node:hover{border-color:var(--mform-border, #d9d5e6)}.dz-node.is-selected{border-color:var(--mform-primary, #6d4bd0);box-shadow:0 0 0 1px var(--mform-primary, #6d4bd0)}.dz-chip{position:absolute;top:2px;left:10px;font-size:10px;color:var(--mform-muted, #6b6580);pointer-events:none}.dz-node.is-selected>.dz-chip{color:var(--mform-primary, #6d4bd0);font-weight:600}.dz-children{display:flex;flex-direction:column;gap:8px}.dz-columns{display:grid;gap:10px}.dz-column{display:flex;flex-direction:column;gap:8px;border:1px dashed var(--mform-border, #d9d5e6);border-radius:8px;padding:8px;min-height:40px}.dz-tabbar{display:flex;gap:4px;margin-bottom:8px;border-bottom:2px solid var(--mform-border, #d9d5e6)}.dz-tab{font:inherit;font-size:12px;padding:5px 12px;border:none;background:none;color:var(--mform-muted, #6b6580);cursor:pointer}.dz-tab.is-active{color:var(--mform-primary, #6d4bd0);font-weight:600;border-bottom:2px solid var(--mform-primary, #6d4bd0);margin-bottom:-2px}.dz-leaf{pointer-events:none}.dz-props-empty{margin:4px 0}.dz-inspector{display:flex;flex-direction:column;gap:10px}.dz-inspector h3{margin:0;font-size:13px}.dz-inspector h3 code{color:var(--mform-muted, #6b6580);font-weight:400}.dz-prop{display:flex;flex-direction:column;gap:4px;font-size:12px}.dz-prop span{color:var(--mform-muted, #6b6580)}.dz-prop input,.dz-prop select,.dz-prop textarea{font:inherit;font-size:13px;padding:7px 9px;border:1px solid var(--mform-border, #d9d5e6);border-radius:8px;background:#fff;width:100%;box-sizing:border-box}.dz-check{display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer}.dz-check input{accent-color:var(--mform-primary, #6d4bd0)}.dz-section{border-top:1px solid var(--mform-border, #d9d5e6);padding-top:8px}.dz-section summary{font-size:12px;font-weight:600;cursor:pointer;margin-bottom:8px}.dz-section .dz-prop{margin-bottom:8px}.dz-option-row{display:flex;gap:6px;margin-bottom:6px}.dz-option-row input{flex:1;min-width:0;font-size:12px;padding:6px 8px;border:1px solid var(--mform-border, #d9d5e6);border-radius:6px}.dz-option-row button{border:none;background:none;cursor:pointer;color:var(--mform-muted, #6b6580)}.dz-add{font:inherit;font-size:12px;border:1px dashed var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0);background:none;border-radius:8px;padding:6px 10px;cursor:pointer}.dz-preview{padding:20px;overflow-y:auto}.dz-preview-data{margin-top:16px;font-size:12px}.dz-preview-data pre{background:var(--mform-surface, #fff);border:1px solid var(--mform-border, #d9d5e6);border-radius:8px;padding:10px;overflow-x:auto}\n"] }]
2275
+ }], ctorParameters: () => [], propDecorators: { schema: [{ type: i0.Input, args: [{ isSignal: true, alias: "schema", required: false }] }], source: [{ type: i0.Input, args: [{ isSignal: true, alias: "source", required: false }] }], extraCatalog: [{ type: i0.Input, args: [{ isSignal: true, alias: "extraCatalog", required: false }] }], catalogOverride: [{ type: i0.Input, args: [{ isSignal: true, alias: "catalog", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], schemaChanged: [{ type: i0.Output, args: ["schemaChanged"] }], blocks: [{ type: i0.Input, args: [{ isSignal: true, alias: "blocks", required: false }] }], blocksChanged: [{ type: i0.Output, args: ["blocksChanged"] }] } });
1785
2276
 
1786
2277
  /*
1787
2278
  * Designer entry point (`@mosaicoo/form-angular/designer`).
@@ -1795,5 +2286,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1795
2286
  * Generated bundle index. Do not edit.
1796
2287
  */
1797
2288
 
1798
- export { DEFAULT_CATALOG, DEFAULT_LAYOUT, DesignerNodeComponent, FormDesignerComponent, InspectorComponent, PANEL_LABELS, WORKSPACE_PRESETS };
2289
+ export { DEFAULT_CATALOG, DEFAULT_LAYOUT, DESIGNER_LABELS_EN, DesignerNodeComponent, FormDesignerComponent, InspectorComponent, PANEL_LABELS, WORKSPACE_PRESETS, dropId };
1799
2290
  //# sourceMappingURL=mosaicoo-form-angular-designer.mjs.map