@mosaicoo/form-angular 0.3.0 → 0.5.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.
@@ -1,7 +1,7 @@
1
1
  import { NgComponentOutlet, DOCUMENT } from '@angular/common';
2
2
  import * as i0 from '@angular/core';
3
- import { InjectionToken, makeEnvironmentProviders, input, signal, inject, effect, untracked, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, output } from '@angular/core';
4
- import { importForm, createLegacyImporter, createFormEngine, collectInputs } from '@mosaicoo/form-core';
3
+ import { InjectionToken, makeEnvironmentProviders, input, signal, inject, ChangeDetectorRef, reflectComponentType, effect, untracked, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, output } from '@angular/core';
4
+ import { maskValue, importForm, createLegacyImporter, createFormEngine, collectInputs } from '@mosaicoo/form-core';
5
5
 
6
6
  /** Multi-provider token: maps merge, later providers win on conflicts. */
7
7
  const MFORM_FIELD_COMPONENTS = new InjectionToken('MFORM_FIELD_COMPONENTS');
@@ -11,6 +11,7 @@ const MFORM_FIELD_COMPONENTS = new InjectionToken('MFORM_FIELD_COMPONENTS');
11
11
  * base URLs, caching). Without it, a plain `fetch` is used.
12
12
  */
13
13
  const MFORM_REMOTE_FETCHER = new InjectionToken('MFORM_REMOTE_FETCHER');
14
+ const MFORM_UPLOAD = new InjectionToken('MFORM_UPLOAD');
14
15
  /**
15
16
  * Registers renderer extensions at any injector level:
16
17
  *
@@ -33,6 +34,9 @@ function provideMosaicooForm(config) {
33
34
  if (config.remoteFetcher) {
34
35
  providers.push({ provide: MFORM_REMOTE_FETCHER, useValue: config.remoteFetcher });
35
36
  }
37
+ if (config.uploadFiles) {
38
+ providers.push({ provide: MFORM_UPLOAD, useValue: config.uploadFiles });
39
+ }
36
40
  return makeEnvironmentProviders(providers);
37
41
  }
38
42
 
@@ -53,11 +57,36 @@ class FormNodeComponent {
53
57
  bare = input(false, ...(ngDevMode ? [{ debugName: "bare" }] : /* istanbul ignore next */ []));
54
58
  /** Host-registered providers for `optionsSource: provider` fields. */
55
59
  providers = input({}, ...(ngDevMode ? [{ debugName: "providers" }] : /* istanbul ignore next */ []));
60
+ /** Rendering mode inherited from the renderer. */
61
+ mode = input('edit', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
62
+ /** Renderer-supplied refresh hook for async completions (zoneless-safe). */
63
+ notify = input(() => { }, ...(ngDevMode ? [{ debugName: "notify" }] : /* istanbul ignore next */ []));
56
64
  activeTab = signal(0, ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
57
65
  /** DI-registered custom field components (maps merge; later wins). */
58
66
  componentMaps = inject(MFORM_FIELD_COMPONENTS, { optional: true });
59
67
  /** Host transport for remote options; plain fetch when absent. */
60
68
  remoteFetcher = inject(MFORM_REMOTE_FETCHER, { optional: true });
69
+ /** Host-owned upload for `file` fields. */
70
+ uploader = inject(MFORM_UPLOAD, { optional: true });
71
+ /**
72
+ * Async resolutions (debounce timers, provider promises) finish outside
73
+ * any template event — explicitly schedule the refresh for zoneless apps.
74
+ */
75
+ changeDetector = inject(ChangeDetectorRef);
76
+ uploading = signal(false, ...(ngDevMode ? [{ debugName: "uploading" }] : /* istanbul ignore next */ []));
77
+ /** Effective disabled state: mode, schema flag or calculated field. */
78
+ controlDisabled() {
79
+ return (this.mode() !== 'edit' ||
80
+ this.field().disabled === true ||
81
+ this.field().calculate !== undefined);
82
+ }
83
+ interactive() {
84
+ return this.mode() === 'edit';
85
+ }
86
+ pending() {
87
+ this.tick();
88
+ return this.engine().isPending(this.path());
89
+ }
61
90
  /** Custom component registered for this field type, if any. */
62
91
  customComponent() {
63
92
  const node = this.node();
@@ -70,14 +99,22 @@ class FormNodeComponent {
70
99
  }
71
100
  return null;
72
101
  }
73
- /** Inputs handed to a custom component (MformFieldComponent contract). */
102
+ /**
103
+ * Inputs handed to a custom component (MformFieldComponent contract).
104
+ * Only inputs the component actually declares are passed, so optional
105
+ * contract members (`mode`) never break third-party components.
106
+ */
74
107
  customInputs() {
75
- return {
108
+ const component = this.customComponent();
109
+ const declared = new Set(component ? (reflectComponentType(component)?.inputs ?? []).map((i) => i.propName) : []);
110
+ const all = {
76
111
  field: this.field(),
77
112
  engine: this.engine(),
78
113
  path: this.path(),
79
114
  tick: this.tick(),
115
+ mode: this.mode(),
80
116
  };
117
+ return Object.fromEntries(Object.entries(all).filter(([key]) => declared.has(key)));
81
118
  }
82
119
  /** Options resolved from a dynamic source (provider or remote URL). */
83
120
  resolvedOptions = signal(null, ...(ngDevMode ? [{ debugName: "resolvedOptions" }] : /* istanbul ignore next */ []));
@@ -119,7 +156,11 @@ class FormNodeComponent {
119
156
  if (!provider)
120
157
  return; // no provider: static options/fallback stand
121
158
  Promise.resolve(provider({ field: node, path, data: engine.getData(), args: source.args }))
122
- .then(apply)
159
+ .then((options) => {
160
+ apply(options);
161
+ this.changeDetector.markForCheck();
162
+ this.notify()();
163
+ })
123
164
  .catch(() => { }); // provider failure keeps the static fallback
124
165
  }
125
166
  else {
@@ -133,6 +174,7 @@ class FormNodeComponent {
133
174
  label: String(item[source.labelKey ?? 'label'] ?? item[source.valueKey ?? 'value'] ?? ''),
134
175
  value: (item[source.valueKey ?? 'value'] ?? ''),
135
176
  }))))
177
+ .then(() => this.changeDetector.markForCheck())
136
178
  .catch(() => { });
137
179
  }
138
180
  });
@@ -217,7 +259,12 @@ class FormNodeComponent {
217
259
  }
218
260
  stringValue() {
219
261
  const value = this.value();
220
- return value === undefined || value === null ? '' : String(value);
262
+ if (value === undefined || value === null)
263
+ return '';
264
+ const mask = this.field().format?.mask;
265
+ if (mask && typeof value === 'string')
266
+ return maskValue(value, mask).masked;
267
+ return String(value);
221
268
  }
222
269
  mapValue() {
223
270
  const value = this.value();
@@ -275,7 +322,15 @@ class FormNodeComponent {
275
322
  this.engine().setValue(this.path(), value);
276
323
  }
277
324
  setString(event) {
278
- const raw = event.target.value;
325
+ const target = event.target;
326
+ const raw = target.value;
327
+ const mask = this.field().format?.mask;
328
+ if (mask) {
329
+ const { masked, raw: stripped } = maskValue(raw, mask);
330
+ target.value = masked; // the control always shows the masked shape
331
+ this.setValue(this.field().format?.keepMask ? masked : stripped);
332
+ return;
333
+ }
279
334
  if (this.field().dataType === 'number') {
280
335
  const parsed = raw === '' ? undefined : Number(raw);
281
336
  this.setValue(parsed !== undefined && Number.isFinite(parsed) ? parsed : undefined);
@@ -296,7 +351,32 @@ class FormNodeComponent {
296
351
  }
297
352
  setFiles(event) {
298
353
  const files = event.target.files;
299
- this.setValue(files ? Array.from(files).map((f) => f.name) : []);
354
+ const list = files ? Array.from(files) : [];
355
+ if (!this.uploader || list.length === 0) {
356
+ // no host uploader: only file names are stored
357
+ this.setValue(list.map((f) => f.name));
358
+ return;
359
+ }
360
+ const field = this.field();
361
+ const path = this.path();
362
+ this.uploading.set(true);
363
+ Promise.all(list.map((file) => this.uploader(file, { field, path })))
364
+ .then((refs) => this.setValue(refs))
365
+ .catch(() => { }) // host upload failure keeps the previous value
366
+ .finally(() => {
367
+ this.uploading.set(false);
368
+ this.changeDetector.markForCheck();
369
+ this.notify()();
370
+ });
371
+ }
372
+ /** Names shown under a `file` control (refs or plain names). */
373
+ fileNames() {
374
+ const value = this.value();
375
+ if (!Array.isArray(value))
376
+ return [];
377
+ return value.map((item) => typeof item === 'object' && item !== null
378
+ ? String(item.name ?? '')
379
+ : String(item));
300
380
  }
301
381
  prefix() {
302
382
  const value = this.node().props?.['prefix'];
@@ -326,7 +406,69 @@ class FormNodeComponent {
326
406
  }
327
407
  touch() {
328
408
  this.engine().markTouched(this.path());
329
- this.engine().validateField(this.path());
409
+ // Async-aware: resolves immediately for sync-only fields; pending events
410
+ // drive the busy indicator otherwise.
411
+ void this.engine().validateFieldAsync(this.path());
412
+ }
413
+ // -- reference (autocomplete) ---------------------------------------------
414
+ refOptions = signal([], ...(ngDevMode ? [{ debugName: "refOptions" }] : /* istanbul ignore next */ []));
415
+ refOpen = signal(false, ...(ngDevMode ? [{ debugName: "refOpen" }] : /* istanbul ignore next */ []));
416
+ refTimer = null;
417
+ /** Label of the chosen option (falls back to the raw value). */
418
+ refLabel = signal(null, ...(ngDevMode ? [{ debugName: "refLabel" }] : /* istanbul ignore next */ []));
419
+ refDisplay() {
420
+ return this.refLabel() ?? this.stringValue();
421
+ }
422
+ refSearch(event) {
423
+ const query = event.target.value;
424
+ this.refLabel.set(null);
425
+ if (this.refTimer)
426
+ clearTimeout(this.refTimer);
427
+ this.refTimer = setTimeout(() => this.refResolve(query), 250);
428
+ }
429
+ refResolve(query) {
430
+ const source = this.field().optionsSource;
431
+ if (!source || source.type !== 'provider') {
432
+ // static options filter locally
433
+ const filter = query.toLowerCase();
434
+ this.refOptions.set((this.field().options ?? []).filter((o) => o.label.toLowerCase().includes(filter)));
435
+ this.refOpen.set(true);
436
+ this.changeDetector.markForCheck();
437
+ this.notify()();
438
+ return;
439
+ }
440
+ const provider = this.providers()[source.name];
441
+ if (!provider)
442
+ return;
443
+ Promise.resolve(provider({
444
+ field: this.field(),
445
+ path: this.path(),
446
+ data: this.engine().getData(),
447
+ args: source.args,
448
+ query,
449
+ }))
450
+ .then((options) => {
451
+ this.refOptions.set(options);
452
+ this.refOpen.set(true);
453
+ this.changeDetector.markForCheck();
454
+ this.notify()();
455
+ })
456
+ .catch(() => { });
457
+ }
458
+ refPick(option) {
459
+ this.setValue(option.value);
460
+ this.refLabel.set(option.label);
461
+ this.refOpen.set(false);
462
+ this.touch();
463
+ }
464
+ refBlur() {
465
+ // let a click on an option land before closing
466
+ setTimeout(() => {
467
+ this.refOpen.set(false);
468
+ this.changeDetector.markForCheck();
469
+ this.notify()();
470
+ }, 150);
471
+ this.touch();
330
472
  }
331
473
  addRow() {
332
474
  this.engine().addRow(this.path());
@@ -335,18 +477,20 @@ class FormNodeComponent {
335
477
  this.engine().removeRow(this.path(), index);
336
478
  }
337
479
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FormNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
338
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FormNodeComponent, isStandalone: true, selector: "mform-node", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null }, engine: { classPropertyName: "engine", publicName: "engine", isSignal: true, isRequired: true, transformFunction: null }, tick: { classPropertyName: "tick", publicName: "tick", isSignal: true, isRequired: true, transformFunction: null }, scope: { classPropertyName: "scope", publicName: "scope", isSignal: true, isRequired: false, transformFunction: null }, bare: { classPropertyName: "bare", publicName: "bare", isSignal: true, isRequired: false, transformFunction: null }, providers: { classPropertyName: "providers", publicName: "providers", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "mform-node" }, ngImport: i0, template: `
480
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FormNodeComponent, isStandalone: true, selector: "mform-node", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null }, engine: { classPropertyName: "engine", publicName: "engine", isSignal: true, isRequired: true, transformFunction: null }, tick: { classPropertyName: "tick", publicName: "tick", isSignal: true, isRequired: true, transformFunction: null }, scope: { classPropertyName: "scope", publicName: "scope", isSignal: true, isRequired: false, transformFunction: null }, bare: { classPropertyName: "bare", publicName: "bare", isSignal: true, isRequired: false, transformFunction: null }, providers: { classPropertyName: "providers", publicName: "providers", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, notify: { classPropertyName: "notify", publicName: "notify", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "mform-node" }, ngImport: i0, template: `
339
481
  @let n = node();
340
482
  @if (visible()) {
341
483
  @switch (n.kind) {
342
484
  @case ('static') {
343
485
  @if (n.type === 'button') {
344
- <button
345
- class="mform-btn"
346
- [type]="buttonAction() === 'submit' ? 'submit' : 'button'"
347
- >
348
- {{ n.label || 'Submit' }}
349
- </button>
486
+ @if (interactive()) {
487
+ <button
488
+ class="mform-btn"
489
+ [type]="buttonAction() === 'submit' ? 'submit' : 'button'"
490
+ >
491
+ {{ n.label || 'Submit' }}
492
+ </button>
493
+ }
350
494
  } @else if (n.type === 'content') {
351
495
  <div class="mform-content" [innerHTML]="content()"></div>
352
496
  } @else {
@@ -362,7 +506,7 @@ class FormNodeComponent {
362
506
  @for (column of columns(); track $index) {
363
507
  <div class="mform-column">
364
508
  @for (child of column; track child.key) {
365
- <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" />
509
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="childScope()" />
366
510
  }
367
511
  </div>
368
512
  }
@@ -387,7 +531,7 @@ class FormNodeComponent {
387
531
  @for (tab of container().children; track tab.key; let i = $index) {
388
532
  @if (activeTab() === i) {
389
533
  <div class="mform-tabpanel" role="tabpanel">
390
- <mform-node [node]="tab" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" [bare]="true" />
534
+ <mform-node [node]="tab" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="childScope()" [bare]="true" />
391
535
  </div>
392
536
  }
393
537
  }
@@ -403,20 +547,24 @@ class FormNodeComponent {
403
547
  <div class="mform-grid-row" role="group" [attr.aria-label]="(container().label || container().key) + ' ' + (i + 1)">
404
548
  <div class="mform-grid-fields">
405
549
  @for (child of container().children; track child.key) {
406
- <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="rowScope(i)" />
550
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="rowScope(i)" />
407
551
  }
408
552
  </div>
409
- <button
410
- type="button"
411
- class="mform-row-remove"
412
- (click)="removeRow(i)"
413
- [attr.aria-label]="'Remove row ' + (i + 1)"
414
- >
415
-
416
- </button>
553
+ @if (interactive()) {
554
+ <button
555
+ type="button"
556
+ class="mform-row-remove"
557
+ (click)="removeRow(i)"
558
+ [attr.aria-label]="'Remove row ' + (i + 1)"
559
+ >
560
+
561
+ </button>
562
+ }
417
563
  </div>
418
564
  }
419
- <button type="button" class="mform-row-add" (click)="addRow()">+ Add</button>
565
+ @if (interactive()) {
566
+ <button type="button" class="mform-row-add" (click)="addRow()">+ Add</button>
567
+ }
420
568
  </div>
421
569
  </section>
422
570
  }
@@ -426,14 +574,14 @@ class FormNodeComponent {
426
574
  <header class="mform-panel-head">{{ container().label }}</header>
427
575
  <div class="mform-panel-body">
428
576
  @for (child of container().children; track child.key) {
429
- <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" />
577
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="childScope()" />
430
578
  }
431
579
  </div>
432
580
  </section>
433
581
  } @else {
434
582
  <div class="mform-group">
435
583
  @for (child of container().children; track child.key) {
436
- <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" />
584
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="childScope()" />
437
585
  }
438
586
  </div>
439
587
  }
@@ -447,7 +595,7 @@ class FormNodeComponent {
447
595
  [ngComponentOutletInputs]="customInputs()"
448
596
  />
449
597
  } @else if (field().type !== 'hidden') {
450
- <div class="mform-field" [class.has-error]="errors().length > 0">
598
+ <div class="mform-field" [class.has-error]="errors().length > 0" [class.is-pending]="pending()">
451
599
  @if (field().type !== 'checkbox') {
452
600
  <label class="mform-label" [attr.for]="controlId()">
453
601
  {{ field().label || field().key }}
@@ -463,7 +611,7 @@ class FormNodeComponent {
463
611
  [id]="controlId()"
464
612
  [value]="stringValue()"
465
613
  [placeholder]="field().placeholder || ''"
466
- [disabled]="field().disabled === true"
614
+ [disabled]="controlDisabled()"
467
615
  [attr.rows]="rowsProp()"
468
616
  [attr.aria-required]="required() || null"
469
617
  [attr.aria-invalid]="errors().length > 0 || null"
@@ -476,7 +624,7 @@ class FormNodeComponent {
476
624
  <select
477
625
  class="mform-control"
478
626
  [id]="controlId()"
479
- [disabled]="field().disabled === true"
627
+ [disabled]="controlDisabled()"
480
628
  [attr.aria-required]="required() || null"
481
629
  [attr.aria-invalid]="errors().length > 0 || null"
482
630
  [attr.aria-describedby]="describedBy()"
@@ -502,7 +650,7 @@ class FormNodeComponent {
502
650
  type="radio"
503
651
  [name]="controlId()"
504
652
  [checked]="stringValue() === '' + option.value"
505
- [disabled]="field().disabled === true"
653
+ [disabled]="controlDisabled()"
506
654
  (change)="setValue(option.value)"
507
655
  (blur)="touch()"
508
656
  />
@@ -517,7 +665,7 @@ class FormNodeComponent {
517
665
  type="checkbox"
518
666
  [id]="controlId()"
519
667
  [checked]="value() === true"
520
- [disabled]="field().disabled === true"
668
+ [disabled]="controlDisabled()"
521
669
  [attr.aria-invalid]="errors().length > 0 || null"
522
670
  [attr.aria-describedby]="describedBy()"
523
671
  (change)="setChecked($event)"
@@ -538,7 +686,7 @@ class FormNodeComponent {
538
686
  <input
539
687
  type="checkbox"
540
688
  [checked]="mapValue()['' + option.value] === true"
541
- [disabled]="field().disabled === true"
689
+ [disabled]="controlDisabled()"
542
690
  (change)="toggleMap('' + option.value, $event)"
543
691
  (blur)="touch()"
544
692
  />
@@ -564,7 +712,7 @@ class FormNodeComponent {
564
712
  type="text"
565
713
  [id]="controlId()"
566
714
  [placeholder]="field().placeholder || ''"
567
- [disabled]="field().disabled === true"
715
+ [disabled]="controlDisabled()"
568
716
  [attr.aria-describedby]="describedBy()"
569
717
  (keydown.enter)="addTag($event)"
570
718
  (blur)="touch()"
@@ -577,11 +725,45 @@ class FormNodeComponent {
577
725
  type="file"
578
726
  multiple
579
727
  [id]="controlId()"
580
- [disabled]="field().disabled === true"
728
+ [disabled]="controlDisabled() || uploading()"
729
+ [attr.aria-busy]="uploading() || null"
581
730
  [attr.aria-describedby]="describedBy()"
582
731
  (change)="setFiles($event)"
583
732
  (blur)="touch()"
584
733
  />
734
+ @if (uploading()) {
735
+ <div class="mform-desc" role="status">Uploading…</div>
736
+ }
737
+ @for (name of fileNames(); track $index) {
738
+ <div class="mform-file-name">📎 {{ name }}</div>
739
+ }
740
+ }
741
+ @case ('reference') {
742
+ <div class="mform-reference">
743
+ <input
744
+ class="mform-control"
745
+ type="text"
746
+ role="combobox"
747
+ [id]="controlId()"
748
+ [value]="refDisplay()"
749
+ [placeholder]="field().placeholder || ''"
750
+ [disabled]="controlDisabled()"
751
+ [attr.aria-expanded]="refOpen()"
752
+ [attr.aria-invalid]="errors().length > 0 || null"
753
+ [attr.aria-describedby]="describedBy()"
754
+ (input)="refSearch($event)"
755
+ (blur)="refBlur()"
756
+ />
757
+ @if (refOpen() && refOptions().length > 0) {
758
+ <ul class="mform-ref-list" role="listbox">
759
+ @for (option of refOptions(); track option.value) {
760
+ <li role="option">
761
+ <button type="button" (click)="refPick(option)">{{ option.label }}</button>
762
+ </li>
763
+ }
764
+ </ul>
765
+ }
766
+ </div>
585
767
  }
586
768
  @case ('unsupported') {
587
769
  <input
@@ -603,7 +785,7 @@ class FormNodeComponent {
603
785
  [id]="controlId()"
604
786
  [value]="stringValue()"
605
787
  [placeholder]="field().placeholder || ''"
606
- [disabled]="field().disabled === true"
788
+ [disabled]="controlDisabled()"
607
789
  [attr.step]="field().type === 'currency' ? '0.01' : null"
608
790
  [attr.inputmode]="field().type === 'currency' ? 'decimal' : null"
609
791
  [attr.aria-required]="required() || null"
@@ -629,7 +811,7 @@ class FormNodeComponent {
629
811
  }
630
812
  }
631
813
  }
632
- `, isInline: true, dependencies: [{ kind: "component", type: FormNodeComponent, selector: "mform-node", inputs: ["node", "engine", "tick", "scope", "bare", "providers"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
814
+ `, isInline: true, dependencies: [{ kind: "component", type: FormNodeComponent, selector: "mform-node", inputs: ["node", "engine", "tick", "scope", "bare", "providers", "mode", "notify"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
633
815
  }
634
816
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FormNodeComponent, decorators: [{
635
817
  type: Component,
@@ -645,12 +827,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
645
827
  @switch (n.kind) {
646
828
  @case ('static') {
647
829
  @if (n.type === 'button') {
648
- <button
649
- class="mform-btn"
650
- [type]="buttonAction() === 'submit' ? 'submit' : 'button'"
651
- >
652
- {{ n.label || 'Submit' }}
653
- </button>
830
+ @if (interactive()) {
831
+ <button
832
+ class="mform-btn"
833
+ [type]="buttonAction() === 'submit' ? 'submit' : 'button'"
834
+ >
835
+ {{ n.label || 'Submit' }}
836
+ </button>
837
+ }
654
838
  } @else if (n.type === 'content') {
655
839
  <div class="mform-content" [innerHTML]="content()"></div>
656
840
  } @else {
@@ -666,7 +850,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
666
850
  @for (column of columns(); track $index) {
667
851
  <div class="mform-column">
668
852
  @for (child of column; track child.key) {
669
- <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" />
853
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="childScope()" />
670
854
  }
671
855
  </div>
672
856
  }
@@ -691,7 +875,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
691
875
  @for (tab of container().children; track tab.key; let i = $index) {
692
876
  @if (activeTab() === i) {
693
877
  <div class="mform-tabpanel" role="tabpanel">
694
- <mform-node [node]="tab" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" [bare]="true" />
878
+ <mform-node [node]="tab" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="childScope()" [bare]="true" />
695
879
  </div>
696
880
  }
697
881
  }
@@ -707,20 +891,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
707
891
  <div class="mform-grid-row" role="group" [attr.aria-label]="(container().label || container().key) + ' ' + (i + 1)">
708
892
  <div class="mform-grid-fields">
709
893
  @for (child of container().children; track child.key) {
710
- <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="rowScope(i)" />
894
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="rowScope(i)" />
711
895
  }
712
896
  </div>
713
- <button
714
- type="button"
715
- class="mform-row-remove"
716
- (click)="removeRow(i)"
717
- [attr.aria-label]="'Remove row ' + (i + 1)"
718
- >
719
-
720
- </button>
897
+ @if (interactive()) {
898
+ <button
899
+ type="button"
900
+ class="mform-row-remove"
901
+ (click)="removeRow(i)"
902
+ [attr.aria-label]="'Remove row ' + (i + 1)"
903
+ >
904
+
905
+ </button>
906
+ }
721
907
  </div>
722
908
  }
723
- <button type="button" class="mform-row-add" (click)="addRow()">+ Add</button>
909
+ @if (interactive()) {
910
+ <button type="button" class="mform-row-add" (click)="addRow()">+ Add</button>
911
+ }
724
912
  </div>
725
913
  </section>
726
914
  }
@@ -730,14 +918,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
730
918
  <header class="mform-panel-head">{{ container().label }}</header>
731
919
  <div class="mform-panel-body">
732
920
  @for (child of container().children; track child.key) {
733
- <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" />
921
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="childScope()" />
734
922
  }
735
923
  </div>
736
924
  </section>
737
925
  } @else {
738
926
  <div class="mform-group">
739
927
  @for (child of container().children; track child.key) {
740
- <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" />
928
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="childScope()" />
741
929
  }
742
930
  </div>
743
931
  }
@@ -751,7 +939,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
751
939
  [ngComponentOutletInputs]="customInputs()"
752
940
  />
753
941
  } @else if (field().type !== 'hidden') {
754
- <div class="mform-field" [class.has-error]="errors().length > 0">
942
+ <div class="mform-field" [class.has-error]="errors().length > 0" [class.is-pending]="pending()">
755
943
  @if (field().type !== 'checkbox') {
756
944
  <label class="mform-label" [attr.for]="controlId()">
757
945
  {{ field().label || field().key }}
@@ -767,7 +955,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
767
955
  [id]="controlId()"
768
956
  [value]="stringValue()"
769
957
  [placeholder]="field().placeholder || ''"
770
- [disabled]="field().disabled === true"
958
+ [disabled]="controlDisabled()"
771
959
  [attr.rows]="rowsProp()"
772
960
  [attr.aria-required]="required() || null"
773
961
  [attr.aria-invalid]="errors().length > 0 || null"
@@ -780,7 +968,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
780
968
  <select
781
969
  class="mform-control"
782
970
  [id]="controlId()"
783
- [disabled]="field().disabled === true"
971
+ [disabled]="controlDisabled()"
784
972
  [attr.aria-required]="required() || null"
785
973
  [attr.aria-invalid]="errors().length > 0 || null"
786
974
  [attr.aria-describedby]="describedBy()"
@@ -806,7 +994,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
806
994
  type="radio"
807
995
  [name]="controlId()"
808
996
  [checked]="stringValue() === '' + option.value"
809
- [disabled]="field().disabled === true"
997
+ [disabled]="controlDisabled()"
810
998
  (change)="setValue(option.value)"
811
999
  (blur)="touch()"
812
1000
  />
@@ -821,7 +1009,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
821
1009
  type="checkbox"
822
1010
  [id]="controlId()"
823
1011
  [checked]="value() === true"
824
- [disabled]="field().disabled === true"
1012
+ [disabled]="controlDisabled()"
825
1013
  [attr.aria-invalid]="errors().length > 0 || null"
826
1014
  [attr.aria-describedby]="describedBy()"
827
1015
  (change)="setChecked($event)"
@@ -842,7 +1030,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
842
1030
  <input
843
1031
  type="checkbox"
844
1032
  [checked]="mapValue()['' + option.value] === true"
845
- [disabled]="field().disabled === true"
1033
+ [disabled]="controlDisabled()"
846
1034
  (change)="toggleMap('' + option.value, $event)"
847
1035
  (blur)="touch()"
848
1036
  />
@@ -868,7 +1056,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
868
1056
  type="text"
869
1057
  [id]="controlId()"
870
1058
  [placeholder]="field().placeholder || ''"
871
- [disabled]="field().disabled === true"
1059
+ [disabled]="controlDisabled()"
872
1060
  [attr.aria-describedby]="describedBy()"
873
1061
  (keydown.enter)="addTag($event)"
874
1062
  (blur)="touch()"
@@ -881,11 +1069,45 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
881
1069
  type="file"
882
1070
  multiple
883
1071
  [id]="controlId()"
884
- [disabled]="field().disabled === true"
1072
+ [disabled]="controlDisabled() || uploading()"
1073
+ [attr.aria-busy]="uploading() || null"
885
1074
  [attr.aria-describedby]="describedBy()"
886
1075
  (change)="setFiles($event)"
887
1076
  (blur)="touch()"
888
1077
  />
1078
+ @if (uploading()) {
1079
+ <div class="mform-desc" role="status">Uploading…</div>
1080
+ }
1081
+ @for (name of fileNames(); track $index) {
1082
+ <div class="mform-file-name">📎 {{ name }}</div>
1083
+ }
1084
+ }
1085
+ @case ('reference') {
1086
+ <div class="mform-reference">
1087
+ <input
1088
+ class="mform-control"
1089
+ type="text"
1090
+ role="combobox"
1091
+ [id]="controlId()"
1092
+ [value]="refDisplay()"
1093
+ [placeholder]="field().placeholder || ''"
1094
+ [disabled]="controlDisabled()"
1095
+ [attr.aria-expanded]="refOpen()"
1096
+ [attr.aria-invalid]="errors().length > 0 || null"
1097
+ [attr.aria-describedby]="describedBy()"
1098
+ (input)="refSearch($event)"
1099
+ (blur)="refBlur()"
1100
+ />
1101
+ @if (refOpen() && refOptions().length > 0) {
1102
+ <ul class="mform-ref-list" role="listbox">
1103
+ @for (option of refOptions(); track option.value) {
1104
+ <li role="option">
1105
+ <button type="button" (click)="refPick(option)">{{ option.label }}</button>
1106
+ </li>
1107
+ }
1108
+ </ul>
1109
+ }
1110
+ </div>
889
1111
  }
890
1112
  @case ('unsupported') {
891
1113
  <input
@@ -907,7 +1129,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
907
1129
  [id]="controlId()"
908
1130
  [value]="stringValue()"
909
1131
  [placeholder]="field().placeholder || ''"
910
- [disabled]="field().disabled === true"
1132
+ [disabled]="controlDisabled()"
911
1133
  [attr.step]="field().type === 'currency' ? '0.01' : null"
912
1134
  [attr.inputmode]="field().type === 'currency' ? 'decimal' : null"
913
1135
  [attr.aria-required]="required() || null"
@@ -935,7 +1157,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
935
1157
  }
936
1158
  `,
937
1159
  }]
938
- }], ctorParameters: () => [], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }], engine: [{ type: i0.Input, args: [{ isSignal: true, alias: "engine", required: true }] }], tick: [{ type: i0.Input, args: [{ isSignal: true, alias: "tick", required: true }] }], scope: [{ type: i0.Input, args: [{ isSignal: true, alias: "scope", required: false }] }], bare: [{ type: i0.Input, args: [{ isSignal: true, alias: "bare", required: false }] }], providers: [{ type: i0.Input, args: [{ isSignal: true, alias: "providers", required: false }] }] } });
1160
+ }], ctorParameters: () => [], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }], engine: [{ type: i0.Input, args: [{ isSignal: true, alias: "engine", required: true }] }], tick: [{ type: i0.Input, args: [{ isSignal: true, alias: "tick", required: true }] }], scope: [{ type: i0.Input, args: [{ isSignal: true, alias: "scope", required: false }] }], bare: [{ type: i0.Input, args: [{ isSignal: true, alias: "bare", required: false }] }], providers: [{ type: i0.Input, args: [{ isSignal: true, alias: "providers", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], notify: [{ type: i0.Input, args: [{ isSignal: true, alias: "notify", required: false }] }] } });
939
1161
 
940
1162
  /**
941
1163
  * `<mform-renderer>` — renders a form at runtime from a `FormSchema` (or any
@@ -958,6 +1180,20 @@ class FormRendererComponent {
958
1180
  optionsProviders = input({}, ...(ngDevMode ? [{ debugName: "optionsProviders" }] : /* istanbul ignore next */ []));
959
1181
  /** Host-supplied message resolver (localization of validation messages). */
960
1182
  messages = input(null, ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
1183
+ /** Named custom validators (sync or async — Promises get pending states). */
1184
+ validators = input(null, ...(ngDevMode ? [{ debugName: "validators" }] : /* istanbul ignore next */ []));
1185
+ /** `edit` (default), `readonly` (no actions) or `disabled`. */
1186
+ mode = input('edit', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
1187
+ /**
1188
+ * Optional host submission: called after a VALID submit with the result;
1189
+ * rejections surface as the submission error state. Persistence transport
1190
+ * stays entirely on the host side.
1191
+ */
1192
+ submitHandler = input(null, ...(ngDevMode ? [{ debugName: "submitHandler" }] : /* istanbul ignore next */ []));
1193
+ /** True while async validation or the submit handler is running. */
1194
+ submitting = signal(false, ...(ngDevMode ? [{ debugName: "submitting" }] : /* istanbul ignore next */ []));
1195
+ /** Message when the host submission rejected. */
1196
+ submitError = signal(null, ...(ngDevMode ? [{ debugName: "submitError" }] : /* istanbul ignore next */ []));
961
1197
  /** Fixed renderer texts (override to localize). `{{count}}` in `summary`. */
962
1198
  labels = input({
963
1199
  previous: 'Previous',
@@ -980,6 +1216,8 @@ class FormRendererComponent {
980
1216
  valueChanged = output();
981
1217
  /** Bumped on every engine event; nodes read it to refresh. */
982
1218
  tick = signal(0, ...(ngDevMode ? [{ debugName: "tick" }] : /* istanbul ignore next */ []));
1219
+ /** Handed to nodes so async completions re-render (zoneless-safe). */
1220
+ bumpTick = () => this.tick.update((v) => v + 1);
983
1221
  /** Active wizard step index. */
984
1222
  currentStep = signal(0, ...(ngDevMode ? [{ debugName: "currentStep" }] : /* istanbul ignore next */ []));
985
1223
  resolved = computed(() => {
@@ -1010,6 +1248,7 @@ class FormRendererComponent {
1010
1248
  ? createFormEngine(schema, {
1011
1249
  initialData: this.initialData() ?? undefined,
1012
1250
  messages: this.messages() ?? undefined,
1251
+ validators: this.validators() ?? undefined,
1013
1252
  })
1014
1253
  : null;
1015
1254
  }, ...(ngDevMode ? [{ debugName: "engine" }] : /* istanbul ignore next */ []));
@@ -1060,22 +1299,37 @@ class FormRendererComponent {
1060
1299
  this.currentStep.update((index) => Math.min(this.steps().length - 1, index + 1));
1061
1300
  }
1062
1301
  // -- submission -----------------------------------------------------------
1063
- onSubmit(event) {
1302
+ async onSubmit(event) {
1064
1303
  event.preventDefault();
1065
1304
  const engine = this.engine();
1066
- if (!engine)
1305
+ if (!engine || this.submitting() || this.mode() !== 'edit')
1067
1306
  return;
1068
- const result = engine.submit();
1069
- this.errorSummary.set(result.errors);
1070
- if (!result.ok) {
1071
- if (this.isWizard()) {
1072
- const stepIndex = this.stepIndexForPath(result.errors[0]?.path ?? '');
1073
- if (stepIndex >= 0)
1074
- this.currentStep.set(stepIndex);
1307
+ this.submitting.set(true);
1308
+ this.submitError.set(null);
1309
+ try {
1310
+ const result = await engine.submitAsync();
1311
+ this.errorSummary.set(result.errors);
1312
+ if (!result.ok) {
1313
+ if (this.isWizard()) {
1314
+ const stepIndex = this.stepIndexForPath(result.errors[0]?.path ?? '');
1315
+ if (stepIndex >= 0)
1316
+ this.currentStep.set(stepIndex);
1317
+ }
1318
+ this.focusFirstError(result.errors);
1075
1319
  }
1076
- this.focusFirstError(result.errors);
1320
+ else if (this.submitHandler()) {
1321
+ try {
1322
+ await this.submitHandler()(result);
1323
+ }
1324
+ catch (error) {
1325
+ this.submitError.set(String(error instanceof Error ? error.message : error));
1326
+ }
1327
+ }
1328
+ this.submitted.emit(result);
1329
+ }
1330
+ finally {
1331
+ this.submitting.set(false);
1077
1332
  }
1078
- this.submitted.emit(result);
1079
1333
  }
1080
1334
  /** Finds the wizard step that owns a (possibly row-indexed) data path. */
1081
1335
  stepIndexForPath(path) {
@@ -1095,7 +1349,7 @@ class FormRendererComponent {
1095
1349
  queueMicrotask(() => this.documentRef.getElementById(id)?.focus());
1096
1350
  }
1097
1351
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FormRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1098
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FormRendererComponent, isStandalone: true, selector: "mform-renderer", inputs: { schema: { classPropertyName: "schema", publicName: "schema", isSignal: true, isRequired: false, transformFunction: null }, source: { classPropertyName: "source", publicName: "source", isSignal: true, isRequired: false, transformFunction: null }, initialData: { classPropertyName: "initialData", publicName: "initialData", isSignal: true, isRequired: false, transformFunction: null }, optionsProviders: { classPropertyName: "optionsProviders", publicName: "optionsProviders", isSignal: true, isRequired: false, transformFunction: null }, messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { submitted: "submitted", valueChanged: "valueChanged" }, ngImport: i0, template: `
1352
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FormRendererComponent, isStandalone: true, selector: "mform-renderer", inputs: { schema: { classPropertyName: "schema", publicName: "schema", isSignal: true, isRequired: false, transformFunction: null }, source: { classPropertyName: "source", publicName: "source", isSignal: true, isRequired: false, transformFunction: null }, initialData: { classPropertyName: "initialData", publicName: "initialData", isSignal: true, isRequired: false, transformFunction: null }, optionsProviders: { classPropertyName: "optionsProviders", publicName: "optionsProviders", isSignal: true, isRequired: false, transformFunction: null }, messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null }, validators: { classPropertyName: "validators", publicName: "validators", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, submitHandler: { classPropertyName: "submitHandler", publicName: "submitHandler", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { submitted: "submitted", valueChanged: "valueChanged" }, ngImport: i0, template: `
1099
1353
  @if (engine(); as engine) {
1100
1354
  <form class="mform-root" novalidate (submit)="onSubmit($event)">
1101
1355
  @if (errorSummary().length > 0) {
@@ -1128,6 +1382,7 @@ class FormRendererComponent {
1128
1382
  [engine]="engine"
1129
1383
  [tick]="tick()"
1130
1384
  [providers]="optionsProviders()"
1385
+ [mode]="mode()" [notify]="bumpTick"
1131
1386
  [bare]="true"
1132
1387
  />
1133
1388
  }
@@ -1141,18 +1396,23 @@ class FormRendererComponent {
1141
1396
  <button type="button" class="mform-btn" (click)="nextStep()">
1142
1397
  {{ labels().next }}
1143
1398
  </button>
1144
- } @else {
1145
- <button type="submit" class="mform-btn">{{ labels().submit }}</button>
1399
+ } @else if (mode() === 'edit') {
1400
+ <button type="submit" class="mform-btn" [disabled]="submitting()">
1401
+ {{ submitting() ? '…' : labels().submit }}
1402
+ </button>
1146
1403
  }
1147
1404
  </div>
1148
1405
  } @else {
1149
1406
  @for (node of engine.schema.fields; track node.key) {
1150
- <mform-node [node]="node" [engine]="engine" [tick]="tick()" [providers]="optionsProviders()" />
1407
+ <mform-node [node]="node" [engine]="engine" [tick]="tick()" [providers]="optionsProviders()" [mode]="mode()" [notify]="bumpTick" />
1151
1408
  }
1152
1409
  }
1410
+ @if (submitError(); as error) {
1411
+ <div class="mform-summary" role="alert">{{ error }}</div>
1412
+ }
1153
1413
  </form>
1154
1414
  }
1155
- `, isInline: true, styles: [".mform-root{display:flex;flex-direction:column;gap:var(--mform-gap, 16px);font-family:var(--mform-font, system-ui, \"Segoe UI\", sans-serif);color:var(--mform-text, #1f1b2e)}.mform-node{display:contents}.mform-field{display:flex;flex-direction:column;gap:6px}.mform-label{font-size:var(--mform-label-size, 13px);font-weight:var(--mform-label-weight, 600)}.mform-req{color:var(--mform-danger, #c62842);margin-left:2px}.mform-control{font:inherit;width:100%;padding:var(--mform-control-padding, 9px 12px);border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-control-radius, 8px);background:var(--mform-control-bg, #fff);color:inherit;box-sizing:border-box}textarea.mform-control{min-height:72px;resize:vertical}.mform-control:focus-visible{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:1px;border-color:var(--mform-focus, #8e6ff0)}.mform-field.has-error .mform-control{border-color:var(--mform-danger, #c62842)}.mform-desc{font-size:12px;color:var(--mform-muted, #6b6580)}.mform-error{font-size:12px;color:var(--mform-danger, #c62842)}.mform-choices{display:flex;flex-direction:column;gap:6px}.mform-check{display:flex;align-items:center;gap:8px;font-size:14px;cursor:pointer}.mform-check input{width:16px;height:16px;accent-color:var(--mform-primary, #6d4bd0)}.mform-panel{border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);overflow:hidden}.mform-panel-head{background:var(--mform-panel-head-bg, linear-gradient(90deg, #efeaff, #f8f6ff));color:var(--mform-panel-head-text, inherit);padding:10px 16px;font-weight:600;font-size:14px;border-bottom:1px solid var(--mform-border, #d9d5e6)}.mform-panel-body,.mform-group,.mform-column,.mform-tabpanel{display:flex;flex-direction:column;gap:var(--mform-gap, 16px)}.mform-panel-body{padding:var(--mform-panel-padding, 16px)}.mform-columns{display:grid;gap:var(--mform-gap, 16px)}@media(max-width:640px){.mform-columns{grid-template-columns:1fr!important}}.mform-affix{display:flex;align-items:center}.mform-affix.has-affix{border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-control-radius, 8px);background:var(--mform-control-bg, #fff)}.mform-affix.has-affix .mform-control{border:none;background:none;flex:1}.mform-affix.has-affix:focus-within{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:1px}.mform-prefix,.mform-suffix{padding:0 10px;color:var(--mform-muted, #6b6580);font-size:13px;white-space:nowrap}.mform-tags{display:flex;flex-wrap:wrap;gap:6px;align-items:center}.mform-tag{background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff);border-radius:999px;padding:3px 10px;font-size:12px;display:inline-flex;align-items:center;gap:6px}.mform-tag button{border:none;background:none;color:inherit;cursor:pointer;font-size:12px;padding:0;line-height:1}.mform-tag-input{flex:1;min-width:140px;width:auto}.mform-tabbar{display:flex;gap:4px;border-bottom:2px solid var(--mform-border, #d9d5e6);margin-bottom:14px}.mform-tab{padding:8px 18px;font:inherit;font-size:14px;border:none;background:none;color:var(--mform-muted, #6b6580);cursor:pointer}.mform-tab.is-active{color:var(--mform-primary, #6d4bd0);font-weight:600;border-bottom:2px solid var(--mform-primary, #6d4bd0);margin-bottom:-2px}.mform-grid-row{display:flex;align-items:flex-start;gap:8px;padding:10px 0;border-bottom:1px dashed var(--mform-border, #d9d5e6)}.mform-grid-fields{flex:1;display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:var(--mform-gap, 16px)}.mform-row-remove{border:none;background:none;color:var(--mform-muted, #6b6580);font:inherit;cursor:pointer;padding:6px 8px}.mform-row-remove:hover{color:var(--mform-danger, #c62842)}.mform-row-add{align-self:flex-start;background:none;border:1px dashed var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0);padding:7px 14px;border-radius:var(--mform-control-radius, 8px);font:inherit;font-size:13px;cursor:pointer}.mform-btn{align-self:flex-start;background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff);border:none;border-radius:var(--mform-control-radius, 8px);padding:11px 26px;font:inherit;font-weight:600;cursor:pointer}.mform-btn:hover{filter:brightness(1.08)}.mform-btn:focus-visible{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:2px}.mform-content{font-size:14px}.mform-unsupported{font-size:13px;color:var(--mform-muted, #6b6580);border:1px dashed var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);padding:10px 14px}.mform-steps{display:flex;gap:18px;list-style:none;margin:0 0 4px;padding:0 0 12px;border-bottom:1px solid var(--mform-border, #d9d5e6);flex-wrap:wrap}.mform-step{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--mform-muted, #6b6580)}.mform-step-index{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;border:1.5px solid var(--mform-border, #d9d5e6);font-size:11px;font-weight:600}.mform-step.is-active{color:var(--mform-primary, #6d4bd0);font-weight:600}.mform-step.is-active .mform-step-index{border-color:var(--mform-primary, #6d4bd0);background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff)}.mform-step.is-done .mform-step-index{border-color:var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0)}.mform-wizard-nav{display:flex;gap:10px;padding-top:4px}.mform-btn-secondary{background:none;border:1px solid var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0)}.mform-summary{border:1px solid var(--mform-danger, #c62842);background:var(--mform-danger-bg, #fdf0f2);color:var(--mform-danger, #c62842);border-radius:var(--mform-radius, 10px);padding:12px 16px;font-size:13px}.mform-summary ul{margin:6px 0 0;padding-left:18px}\n"], dependencies: [{ kind: "component", type: FormNodeComponent, selector: "mform-node", inputs: ["node", "engine", "tick", "scope", "bare", "providers"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1415
+ `, isInline: true, styles: [".mform-root{display:flex;flex-direction:column;gap:var(--mform-gap, 16px);font-family:var(--mform-font, system-ui, \"Segoe UI\", sans-serif);color:var(--mform-text, #1f1b2e)}.mform-node{display:contents}.mform-field{display:flex;flex-direction:column;gap:6px}.mform-label{font-size:var(--mform-label-size, 13px);font-weight:var(--mform-label-weight, 600)}.mform-req{color:var(--mform-danger, #c62842);margin-left:2px}.mform-control{font:inherit;width:100%;padding:var(--mform-control-padding, 9px 12px);border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-control-radius, 8px);background:var(--mform-control-bg, #fff);color:inherit;box-sizing:border-box}textarea.mform-control{min-height:72px;resize:vertical}.mform-control:focus-visible{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:1px;border-color:var(--mform-focus, #8e6ff0)}.mform-field.has-error .mform-control{border-color:var(--mform-danger, #c62842)}.mform-desc{font-size:12px;color:var(--mform-muted, #6b6580)}.mform-error{font-size:12px;color:var(--mform-danger, #c62842)}.mform-choices{display:flex;flex-direction:column;gap:6px}.mform-check{display:flex;align-items:center;gap:8px;font-size:14px;cursor:pointer}.mform-check input{width:16px;height:16px;accent-color:var(--mform-primary, #6d4bd0)}.mform-panel{border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);overflow:hidden}.mform-panel-head{background:var(--mform-panel-head-bg, linear-gradient(90deg, #efeaff, #f8f6ff));color:var(--mform-panel-head-text, inherit);padding:10px 16px;font-weight:600;font-size:14px;border-bottom:1px solid var(--mform-border, #d9d5e6)}.mform-panel-body,.mform-group,.mform-column,.mform-tabpanel{display:flex;flex-direction:column;gap:var(--mform-gap, 16px)}.mform-panel-body{padding:var(--mform-panel-padding, 16px)}.mform-columns{display:grid;gap:var(--mform-gap, 16px)}@media(max-width:640px){.mform-columns{grid-template-columns:1fr!important}}.mform-affix{display:flex;align-items:center}.mform-affix.has-affix{border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-control-radius, 8px);background:var(--mform-control-bg, #fff)}.mform-affix.has-affix .mform-control{border:none;background:none;flex:1}.mform-affix.has-affix:focus-within{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:1px}.mform-prefix,.mform-suffix{padding:0 10px;color:var(--mform-muted, #6b6580);font-size:13px;white-space:nowrap}.mform-tags{display:flex;flex-wrap:wrap;gap:6px;align-items:center}.mform-tag{background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff);border-radius:999px;padding:3px 10px;font-size:12px;display:inline-flex;align-items:center;gap:6px}.mform-tag button{border:none;background:none;color:inherit;cursor:pointer;font-size:12px;padding:0;line-height:1}.mform-tag-input{flex:1;min-width:140px;width:auto}.mform-field.is-pending .mform-control{background-image:linear-gradient(90deg,transparent 0%,rgb(109 75 208 / 12%) 50%,transparent 100%);background-size:200% 100%;animation:mform-pending 1.2s linear infinite}@keyframes mform-pending{0%{background-position:200% 0}to{background-position:-200% 0}}.mform-file-name{font-size:12px;color:var(--mform-muted, #6b6580);padding:2px 0}.mform-reference{position:relative}.mform-ref-list{position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:10;margin:0;padding:4px;list-style:none;background:var(--mform-control-bg, #fff);border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-control-radius, 8px);box-shadow:0 8px 24px #1f1b2e1f;max-height:220px;overflow-y:auto}.mform-ref-list button{display:block;width:100%;text-align:left;font:inherit;font-size:13px;padding:7px 10px;border:none;border-radius:6px;background:none;color:inherit;cursor:pointer}.mform-ref-list button:hover{background:var(--mform-bg, #f6f5fa)}.mform-tabbar{display:flex;gap:4px;border-bottom:2px solid var(--mform-border, #d9d5e6);margin-bottom:14px}.mform-tab{padding:8px 18px;font:inherit;font-size:14px;border:none;background:none;color:var(--mform-muted, #6b6580);cursor:pointer}.mform-tab.is-active{color:var(--mform-primary, #6d4bd0);font-weight:600;border-bottom:2px solid var(--mform-primary, #6d4bd0);margin-bottom:-2px}.mform-grid-row{display:flex;align-items:flex-start;gap:8px;padding:10px 0;border-bottom:1px dashed var(--mform-border, #d9d5e6)}.mform-grid-fields{flex:1;display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:var(--mform-gap, 16px)}.mform-row-remove{border:none;background:none;color:var(--mform-muted, #6b6580);font:inherit;cursor:pointer;padding:6px 8px}.mform-row-remove:hover{color:var(--mform-danger, #c62842)}.mform-row-add{align-self:flex-start;background:none;border:1px dashed var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0);padding:7px 14px;border-radius:var(--mform-control-radius, 8px);font:inherit;font-size:13px;cursor:pointer}.mform-btn{align-self:flex-start;background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff);border:none;border-radius:var(--mform-control-radius, 8px);padding:11px 26px;font:inherit;font-weight:600;cursor:pointer}.mform-btn:hover{filter:brightness(1.08)}.mform-btn:focus-visible{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:2px}.mform-content{font-size:14px}.mform-unsupported{font-size:13px;color:var(--mform-muted, #6b6580);border:1px dashed var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);padding:10px 14px}.mform-steps{display:flex;gap:18px;list-style:none;margin:0 0 4px;padding:0 0 12px;border-bottom:1px solid var(--mform-border, #d9d5e6);flex-wrap:wrap}.mform-step{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--mform-muted, #6b6580)}.mform-step-index{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;border:1.5px solid var(--mform-border, #d9d5e6);font-size:11px;font-weight:600}.mform-step.is-active{color:var(--mform-primary, #6d4bd0);font-weight:600}.mform-step.is-active .mform-step-index{border-color:var(--mform-primary, #6d4bd0);background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff)}.mform-step.is-done .mform-step-index{border-color:var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0)}.mform-wizard-nav{display:flex;gap:10px;padding-top:4px}.mform-btn-secondary{background:none;border:1px solid var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0)}.mform-summary{border:1px solid var(--mform-danger, #c62842);background:var(--mform-danger-bg, #fdf0f2);color:var(--mform-danger, #c62842);border-radius:var(--mform-radius, 10px);padding:12px 16px;font-size:13px}.mform-summary ul{margin:6px 0 0;padding-left:18px}\n"], dependencies: [{ kind: "component", type: FormNodeComponent, selector: "mform-node", inputs: ["node", "engine", "tick", "scope", "bare", "providers", "mode", "notify"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1156
1416
  }
1157
1417
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FormRendererComponent, decorators: [{
1158
1418
  type: Component,
@@ -1189,6 +1449,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1189
1449
  [engine]="engine"
1190
1450
  [tick]="tick()"
1191
1451
  [providers]="optionsProviders()"
1452
+ [mode]="mode()" [notify]="bumpTick"
1192
1453
  [bare]="true"
1193
1454
  />
1194
1455
  }
@@ -1202,19 +1463,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1202
1463
  <button type="button" class="mform-btn" (click)="nextStep()">
1203
1464
  {{ labels().next }}
1204
1465
  </button>
1205
- } @else {
1206
- <button type="submit" class="mform-btn">{{ labels().submit }}</button>
1466
+ } @else if (mode() === 'edit') {
1467
+ <button type="submit" class="mform-btn" [disabled]="submitting()">
1468
+ {{ submitting() ? '…' : labels().submit }}
1469
+ </button>
1207
1470
  }
1208
1471
  </div>
1209
1472
  } @else {
1210
1473
  @for (node of engine.schema.fields; track node.key) {
1211
- <mform-node [node]="node" [engine]="engine" [tick]="tick()" [providers]="optionsProviders()" />
1474
+ <mform-node [node]="node" [engine]="engine" [tick]="tick()" [providers]="optionsProviders()" [mode]="mode()" [notify]="bumpTick" />
1212
1475
  }
1213
1476
  }
1477
+ @if (submitError(); as error) {
1478
+ <div class="mform-summary" role="alert">{{ error }}</div>
1479
+ }
1214
1480
  </form>
1215
1481
  }
1216
- `, styles: [".mform-root{display:flex;flex-direction:column;gap:var(--mform-gap, 16px);font-family:var(--mform-font, system-ui, \"Segoe UI\", sans-serif);color:var(--mform-text, #1f1b2e)}.mform-node{display:contents}.mform-field{display:flex;flex-direction:column;gap:6px}.mform-label{font-size:var(--mform-label-size, 13px);font-weight:var(--mform-label-weight, 600)}.mform-req{color:var(--mform-danger, #c62842);margin-left:2px}.mform-control{font:inherit;width:100%;padding:var(--mform-control-padding, 9px 12px);border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-control-radius, 8px);background:var(--mform-control-bg, #fff);color:inherit;box-sizing:border-box}textarea.mform-control{min-height:72px;resize:vertical}.mform-control:focus-visible{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:1px;border-color:var(--mform-focus, #8e6ff0)}.mform-field.has-error .mform-control{border-color:var(--mform-danger, #c62842)}.mform-desc{font-size:12px;color:var(--mform-muted, #6b6580)}.mform-error{font-size:12px;color:var(--mform-danger, #c62842)}.mform-choices{display:flex;flex-direction:column;gap:6px}.mform-check{display:flex;align-items:center;gap:8px;font-size:14px;cursor:pointer}.mform-check input{width:16px;height:16px;accent-color:var(--mform-primary, #6d4bd0)}.mform-panel{border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);overflow:hidden}.mform-panel-head{background:var(--mform-panel-head-bg, linear-gradient(90deg, #efeaff, #f8f6ff));color:var(--mform-panel-head-text, inherit);padding:10px 16px;font-weight:600;font-size:14px;border-bottom:1px solid var(--mform-border, #d9d5e6)}.mform-panel-body,.mform-group,.mform-column,.mform-tabpanel{display:flex;flex-direction:column;gap:var(--mform-gap, 16px)}.mform-panel-body{padding:var(--mform-panel-padding, 16px)}.mform-columns{display:grid;gap:var(--mform-gap, 16px)}@media(max-width:640px){.mform-columns{grid-template-columns:1fr!important}}.mform-affix{display:flex;align-items:center}.mform-affix.has-affix{border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-control-radius, 8px);background:var(--mform-control-bg, #fff)}.mform-affix.has-affix .mform-control{border:none;background:none;flex:1}.mform-affix.has-affix:focus-within{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:1px}.mform-prefix,.mform-suffix{padding:0 10px;color:var(--mform-muted, #6b6580);font-size:13px;white-space:nowrap}.mform-tags{display:flex;flex-wrap:wrap;gap:6px;align-items:center}.mform-tag{background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff);border-radius:999px;padding:3px 10px;font-size:12px;display:inline-flex;align-items:center;gap:6px}.mform-tag button{border:none;background:none;color:inherit;cursor:pointer;font-size:12px;padding:0;line-height:1}.mform-tag-input{flex:1;min-width:140px;width:auto}.mform-tabbar{display:flex;gap:4px;border-bottom:2px solid var(--mform-border, #d9d5e6);margin-bottom:14px}.mform-tab{padding:8px 18px;font:inherit;font-size:14px;border:none;background:none;color:var(--mform-muted, #6b6580);cursor:pointer}.mform-tab.is-active{color:var(--mform-primary, #6d4bd0);font-weight:600;border-bottom:2px solid var(--mform-primary, #6d4bd0);margin-bottom:-2px}.mform-grid-row{display:flex;align-items:flex-start;gap:8px;padding:10px 0;border-bottom:1px dashed var(--mform-border, #d9d5e6)}.mform-grid-fields{flex:1;display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:var(--mform-gap, 16px)}.mform-row-remove{border:none;background:none;color:var(--mform-muted, #6b6580);font:inherit;cursor:pointer;padding:6px 8px}.mform-row-remove:hover{color:var(--mform-danger, #c62842)}.mform-row-add{align-self:flex-start;background:none;border:1px dashed var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0);padding:7px 14px;border-radius:var(--mform-control-radius, 8px);font:inherit;font-size:13px;cursor:pointer}.mform-btn{align-self:flex-start;background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff);border:none;border-radius:var(--mform-control-radius, 8px);padding:11px 26px;font:inherit;font-weight:600;cursor:pointer}.mform-btn:hover{filter:brightness(1.08)}.mform-btn:focus-visible{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:2px}.mform-content{font-size:14px}.mform-unsupported{font-size:13px;color:var(--mform-muted, #6b6580);border:1px dashed var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);padding:10px 14px}.mform-steps{display:flex;gap:18px;list-style:none;margin:0 0 4px;padding:0 0 12px;border-bottom:1px solid var(--mform-border, #d9d5e6);flex-wrap:wrap}.mform-step{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--mform-muted, #6b6580)}.mform-step-index{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;border:1.5px solid var(--mform-border, #d9d5e6);font-size:11px;font-weight:600}.mform-step.is-active{color:var(--mform-primary, #6d4bd0);font-weight:600}.mform-step.is-active .mform-step-index{border-color:var(--mform-primary, #6d4bd0);background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff)}.mform-step.is-done .mform-step-index{border-color:var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0)}.mform-wizard-nav{display:flex;gap:10px;padding-top:4px}.mform-btn-secondary{background:none;border:1px solid var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0)}.mform-summary{border:1px solid var(--mform-danger, #c62842);background:var(--mform-danger-bg, #fdf0f2);color:var(--mform-danger, #c62842);border-radius:var(--mform-radius, 10px);padding:12px 16px;font-size:13px}.mform-summary ul{margin:6px 0 0;padding-left:18px}\n"] }]
1217
- }], ctorParameters: () => [], propDecorators: { schema: [{ type: i0.Input, args: [{ isSignal: true, alias: "schema", required: false }] }], source: [{ type: i0.Input, args: [{ isSignal: true, alias: "source", required: false }] }], initialData: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialData", required: false }] }], optionsProviders: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionsProviders", required: false }] }], messages: [{ type: i0.Input, args: [{ isSignal: true, alias: "messages", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], submitted: [{ type: i0.Output, args: ["submitted"] }], valueChanged: [{ type: i0.Output, args: ["valueChanged"] }] } });
1482
+ `, styles: [".mform-root{display:flex;flex-direction:column;gap:var(--mform-gap, 16px);font-family:var(--mform-font, system-ui, \"Segoe UI\", sans-serif);color:var(--mform-text, #1f1b2e)}.mform-node{display:contents}.mform-field{display:flex;flex-direction:column;gap:6px}.mform-label{font-size:var(--mform-label-size, 13px);font-weight:var(--mform-label-weight, 600)}.mform-req{color:var(--mform-danger, #c62842);margin-left:2px}.mform-control{font:inherit;width:100%;padding:var(--mform-control-padding, 9px 12px);border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-control-radius, 8px);background:var(--mform-control-bg, #fff);color:inherit;box-sizing:border-box}textarea.mform-control{min-height:72px;resize:vertical}.mform-control:focus-visible{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:1px;border-color:var(--mform-focus, #8e6ff0)}.mform-field.has-error .mform-control{border-color:var(--mform-danger, #c62842)}.mform-desc{font-size:12px;color:var(--mform-muted, #6b6580)}.mform-error{font-size:12px;color:var(--mform-danger, #c62842)}.mform-choices{display:flex;flex-direction:column;gap:6px}.mform-check{display:flex;align-items:center;gap:8px;font-size:14px;cursor:pointer}.mform-check input{width:16px;height:16px;accent-color:var(--mform-primary, #6d4bd0)}.mform-panel{border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);overflow:hidden}.mform-panel-head{background:var(--mform-panel-head-bg, linear-gradient(90deg, #efeaff, #f8f6ff));color:var(--mform-panel-head-text, inherit);padding:10px 16px;font-weight:600;font-size:14px;border-bottom:1px solid var(--mform-border, #d9d5e6)}.mform-panel-body,.mform-group,.mform-column,.mform-tabpanel{display:flex;flex-direction:column;gap:var(--mform-gap, 16px)}.mform-panel-body{padding:var(--mform-panel-padding, 16px)}.mform-columns{display:grid;gap:var(--mform-gap, 16px)}@media(max-width:640px){.mform-columns{grid-template-columns:1fr!important}}.mform-affix{display:flex;align-items:center}.mform-affix.has-affix{border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-control-radius, 8px);background:var(--mform-control-bg, #fff)}.mform-affix.has-affix .mform-control{border:none;background:none;flex:1}.mform-affix.has-affix:focus-within{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:1px}.mform-prefix,.mform-suffix{padding:0 10px;color:var(--mform-muted, #6b6580);font-size:13px;white-space:nowrap}.mform-tags{display:flex;flex-wrap:wrap;gap:6px;align-items:center}.mform-tag{background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff);border-radius:999px;padding:3px 10px;font-size:12px;display:inline-flex;align-items:center;gap:6px}.mform-tag button{border:none;background:none;color:inherit;cursor:pointer;font-size:12px;padding:0;line-height:1}.mform-tag-input{flex:1;min-width:140px;width:auto}.mform-field.is-pending .mform-control{background-image:linear-gradient(90deg,transparent 0%,rgb(109 75 208 / 12%) 50%,transparent 100%);background-size:200% 100%;animation:mform-pending 1.2s linear infinite}@keyframes mform-pending{0%{background-position:200% 0}to{background-position:-200% 0}}.mform-file-name{font-size:12px;color:var(--mform-muted, #6b6580);padding:2px 0}.mform-reference{position:relative}.mform-ref-list{position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:10;margin:0;padding:4px;list-style:none;background:var(--mform-control-bg, #fff);border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-control-radius, 8px);box-shadow:0 8px 24px #1f1b2e1f;max-height:220px;overflow-y:auto}.mform-ref-list button{display:block;width:100%;text-align:left;font:inherit;font-size:13px;padding:7px 10px;border:none;border-radius:6px;background:none;color:inherit;cursor:pointer}.mform-ref-list button:hover{background:var(--mform-bg, #f6f5fa)}.mform-tabbar{display:flex;gap:4px;border-bottom:2px solid var(--mform-border, #d9d5e6);margin-bottom:14px}.mform-tab{padding:8px 18px;font:inherit;font-size:14px;border:none;background:none;color:var(--mform-muted, #6b6580);cursor:pointer}.mform-tab.is-active{color:var(--mform-primary, #6d4bd0);font-weight:600;border-bottom:2px solid var(--mform-primary, #6d4bd0);margin-bottom:-2px}.mform-grid-row{display:flex;align-items:flex-start;gap:8px;padding:10px 0;border-bottom:1px dashed var(--mform-border, #d9d5e6)}.mform-grid-fields{flex:1;display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:var(--mform-gap, 16px)}.mform-row-remove{border:none;background:none;color:var(--mform-muted, #6b6580);font:inherit;cursor:pointer;padding:6px 8px}.mform-row-remove:hover{color:var(--mform-danger, #c62842)}.mform-row-add{align-self:flex-start;background:none;border:1px dashed var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0);padding:7px 14px;border-radius:var(--mform-control-radius, 8px);font:inherit;font-size:13px;cursor:pointer}.mform-btn{align-self:flex-start;background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff);border:none;border-radius:var(--mform-control-radius, 8px);padding:11px 26px;font:inherit;font-weight:600;cursor:pointer}.mform-btn:hover{filter:brightness(1.08)}.mform-btn:focus-visible{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:2px}.mform-content{font-size:14px}.mform-unsupported{font-size:13px;color:var(--mform-muted, #6b6580);border:1px dashed var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);padding:10px 14px}.mform-steps{display:flex;gap:18px;list-style:none;margin:0 0 4px;padding:0 0 12px;border-bottom:1px solid var(--mform-border, #d9d5e6);flex-wrap:wrap}.mform-step{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--mform-muted, #6b6580)}.mform-step-index{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;border:1.5px solid var(--mform-border, #d9d5e6);font-size:11px;font-weight:600}.mform-step.is-active{color:var(--mform-primary, #6d4bd0);font-weight:600}.mform-step.is-active .mform-step-index{border-color:var(--mform-primary, #6d4bd0);background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff)}.mform-step.is-done .mform-step-index{border-color:var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0)}.mform-wizard-nav{display:flex;gap:10px;padding-top:4px}.mform-btn-secondary{background:none;border:1px solid var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0)}.mform-summary{border:1px solid var(--mform-danger, #c62842);background:var(--mform-danger-bg, #fdf0f2);color:var(--mform-danger, #c62842);border-radius:var(--mform-radius, 10px);padding:12px 16px;font-size:13px}.mform-summary ul{margin:6px 0 0;padding-left:18px}\n"] }]
1483
+ }], ctorParameters: () => [], propDecorators: { schema: [{ type: i0.Input, args: [{ isSignal: true, alias: "schema", required: false }] }], source: [{ type: i0.Input, args: [{ isSignal: true, alias: "source", required: false }] }], initialData: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialData", required: false }] }], optionsProviders: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionsProviders", required: false }] }], messages: [{ type: i0.Input, args: [{ isSignal: true, alias: "messages", required: false }] }], validators: [{ type: i0.Input, args: [{ isSignal: true, alias: "validators", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], submitHandler: [{ type: i0.Input, args: [{ isSignal: true, alias: "submitHandler", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], submitted: [{ type: i0.Output, args: ["submitted"] }], valueChanged: [{ type: i0.Output, args: ["valueChanged"] }] } });
1218
1484
 
1219
1485
  /*
1220
1486
  * Runtime entry point of @mosaicoo/form-angular.
@@ -1229,5 +1495,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1229
1495
  * Generated bundle index. Do not edit.
1230
1496
  */
1231
1497
 
1232
- export { FormNodeComponent, FormRendererComponent, MFORM_FIELD_COMPONENTS, MFORM_REMOTE_FETCHER, provideMosaicooForm };
1498
+ export { FormNodeComponent, FormRendererComponent, MFORM_FIELD_COMPONENTS, MFORM_REMOTE_FETCHER, MFORM_UPLOAD, provideMosaicooForm };
1233
1499
  //# sourceMappingURL=mosaicoo-form-angular.mjs.map