@mosaicoo/form-angular 0.3.0 → 0.4.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, 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();
@@ -119,7 +148,11 @@ class FormNodeComponent {
119
148
  if (!provider)
120
149
  return; // no provider: static options/fallback stand
121
150
  Promise.resolve(provider({ field: node, path, data: engine.getData(), args: source.args }))
122
- .then(apply)
151
+ .then((options) => {
152
+ apply(options);
153
+ this.changeDetector.markForCheck();
154
+ this.notify()();
155
+ })
123
156
  .catch(() => { }); // provider failure keeps the static fallback
124
157
  }
125
158
  else {
@@ -133,6 +166,7 @@ class FormNodeComponent {
133
166
  label: String(item[source.labelKey ?? 'label'] ?? item[source.valueKey ?? 'value'] ?? ''),
134
167
  value: (item[source.valueKey ?? 'value'] ?? ''),
135
168
  }))))
169
+ .then(() => this.changeDetector.markForCheck())
136
170
  .catch(() => { });
137
171
  }
138
172
  });
@@ -217,7 +251,12 @@ class FormNodeComponent {
217
251
  }
218
252
  stringValue() {
219
253
  const value = this.value();
220
- return value === undefined || value === null ? '' : String(value);
254
+ if (value === undefined || value === null)
255
+ return '';
256
+ const mask = this.field().format?.mask;
257
+ if (mask && typeof value === 'string')
258
+ return maskValue(value, mask).masked;
259
+ return String(value);
221
260
  }
222
261
  mapValue() {
223
262
  const value = this.value();
@@ -275,7 +314,15 @@ class FormNodeComponent {
275
314
  this.engine().setValue(this.path(), value);
276
315
  }
277
316
  setString(event) {
278
- const raw = event.target.value;
317
+ const target = event.target;
318
+ const raw = target.value;
319
+ const mask = this.field().format?.mask;
320
+ if (mask) {
321
+ const { masked, raw: stripped } = maskValue(raw, mask);
322
+ target.value = masked; // the control always shows the masked shape
323
+ this.setValue(this.field().format?.keepMask ? masked : stripped);
324
+ return;
325
+ }
279
326
  if (this.field().dataType === 'number') {
280
327
  const parsed = raw === '' ? undefined : Number(raw);
281
328
  this.setValue(parsed !== undefined && Number.isFinite(parsed) ? parsed : undefined);
@@ -296,7 +343,32 @@ class FormNodeComponent {
296
343
  }
297
344
  setFiles(event) {
298
345
  const files = event.target.files;
299
- this.setValue(files ? Array.from(files).map((f) => f.name) : []);
346
+ const list = files ? Array.from(files) : [];
347
+ if (!this.uploader || list.length === 0) {
348
+ // no host uploader: only file names are stored
349
+ this.setValue(list.map((f) => f.name));
350
+ return;
351
+ }
352
+ const field = this.field();
353
+ const path = this.path();
354
+ this.uploading.set(true);
355
+ Promise.all(list.map((file) => this.uploader(file, { field, path })))
356
+ .then((refs) => this.setValue(refs))
357
+ .catch(() => { }) // host upload failure keeps the previous value
358
+ .finally(() => {
359
+ this.uploading.set(false);
360
+ this.changeDetector.markForCheck();
361
+ this.notify()();
362
+ });
363
+ }
364
+ /** Names shown under a `file` control (refs or plain names). */
365
+ fileNames() {
366
+ const value = this.value();
367
+ if (!Array.isArray(value))
368
+ return [];
369
+ return value.map((item) => typeof item === 'object' && item !== null
370
+ ? String(item.name ?? '')
371
+ : String(item));
300
372
  }
301
373
  prefix() {
302
374
  const value = this.node().props?.['prefix'];
@@ -326,7 +398,69 @@ class FormNodeComponent {
326
398
  }
327
399
  touch() {
328
400
  this.engine().markTouched(this.path());
329
- this.engine().validateField(this.path());
401
+ // Async-aware: resolves immediately for sync-only fields; pending events
402
+ // drive the busy indicator otherwise.
403
+ void this.engine().validateFieldAsync(this.path());
404
+ }
405
+ // -- reference (autocomplete) ---------------------------------------------
406
+ refOptions = signal([], ...(ngDevMode ? [{ debugName: "refOptions" }] : /* istanbul ignore next */ []));
407
+ refOpen = signal(false, ...(ngDevMode ? [{ debugName: "refOpen" }] : /* istanbul ignore next */ []));
408
+ refTimer = null;
409
+ /** Label of the chosen option (falls back to the raw value). */
410
+ refLabel = signal(null, ...(ngDevMode ? [{ debugName: "refLabel" }] : /* istanbul ignore next */ []));
411
+ refDisplay() {
412
+ return this.refLabel() ?? this.stringValue();
413
+ }
414
+ refSearch(event) {
415
+ const query = event.target.value;
416
+ this.refLabel.set(null);
417
+ if (this.refTimer)
418
+ clearTimeout(this.refTimer);
419
+ this.refTimer = setTimeout(() => this.refResolve(query), 250);
420
+ }
421
+ refResolve(query) {
422
+ const source = this.field().optionsSource;
423
+ if (!source || source.type !== 'provider') {
424
+ // static options filter locally
425
+ const filter = query.toLowerCase();
426
+ this.refOptions.set((this.field().options ?? []).filter((o) => o.label.toLowerCase().includes(filter)));
427
+ this.refOpen.set(true);
428
+ this.changeDetector.markForCheck();
429
+ this.notify()();
430
+ return;
431
+ }
432
+ const provider = this.providers()[source.name];
433
+ if (!provider)
434
+ return;
435
+ Promise.resolve(provider({
436
+ field: this.field(),
437
+ path: this.path(),
438
+ data: this.engine().getData(),
439
+ args: source.args,
440
+ query,
441
+ }))
442
+ .then((options) => {
443
+ this.refOptions.set(options);
444
+ this.refOpen.set(true);
445
+ this.changeDetector.markForCheck();
446
+ this.notify()();
447
+ })
448
+ .catch(() => { });
449
+ }
450
+ refPick(option) {
451
+ this.setValue(option.value);
452
+ this.refLabel.set(option.label);
453
+ this.refOpen.set(false);
454
+ this.touch();
455
+ }
456
+ refBlur() {
457
+ // let a click on an option land before closing
458
+ setTimeout(() => {
459
+ this.refOpen.set(false);
460
+ this.changeDetector.markForCheck();
461
+ this.notify()();
462
+ }, 150);
463
+ this.touch();
330
464
  }
331
465
  addRow() {
332
466
  this.engine().addRow(this.path());
@@ -335,18 +469,20 @@ class FormNodeComponent {
335
469
  this.engine().removeRow(this.path(), index);
336
470
  }
337
471
  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: `
472
+ 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
473
  @let n = node();
340
474
  @if (visible()) {
341
475
  @switch (n.kind) {
342
476
  @case ('static') {
343
477
  @if (n.type === 'button') {
344
- <button
345
- class="mform-btn"
346
- [type]="buttonAction() === 'submit' ? 'submit' : 'button'"
347
- >
348
- {{ n.label || 'Submit' }}
349
- </button>
478
+ @if (interactive()) {
479
+ <button
480
+ class="mform-btn"
481
+ [type]="buttonAction() === 'submit' ? 'submit' : 'button'"
482
+ >
483
+ {{ n.label || 'Submit' }}
484
+ </button>
485
+ }
350
486
  } @else if (n.type === 'content') {
351
487
  <div class="mform-content" [innerHTML]="content()"></div>
352
488
  } @else {
@@ -362,7 +498,7 @@ class FormNodeComponent {
362
498
  @for (column of columns(); track $index) {
363
499
  <div class="mform-column">
364
500
  @for (child of column; track child.key) {
365
- <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" />
501
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="childScope()" />
366
502
  }
367
503
  </div>
368
504
  }
@@ -387,7 +523,7 @@ class FormNodeComponent {
387
523
  @for (tab of container().children; track tab.key; let i = $index) {
388
524
  @if (activeTab() === i) {
389
525
  <div class="mform-tabpanel" role="tabpanel">
390
- <mform-node [node]="tab" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" [bare]="true" />
526
+ <mform-node [node]="tab" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="childScope()" [bare]="true" />
391
527
  </div>
392
528
  }
393
529
  }
@@ -403,20 +539,24 @@ class FormNodeComponent {
403
539
  <div class="mform-grid-row" role="group" [attr.aria-label]="(container().label || container().key) + ' ' + (i + 1)">
404
540
  <div class="mform-grid-fields">
405
541
  @for (child of container().children; track child.key) {
406
- <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="rowScope(i)" />
542
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="rowScope(i)" />
407
543
  }
408
544
  </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>
545
+ @if (interactive()) {
546
+ <button
547
+ type="button"
548
+ class="mform-row-remove"
549
+ (click)="removeRow(i)"
550
+ [attr.aria-label]="'Remove row ' + (i + 1)"
551
+ >
552
+
553
+ </button>
554
+ }
417
555
  </div>
418
556
  }
419
- <button type="button" class="mform-row-add" (click)="addRow()">+ Add</button>
557
+ @if (interactive()) {
558
+ <button type="button" class="mform-row-add" (click)="addRow()">+ Add</button>
559
+ }
420
560
  </div>
421
561
  </section>
422
562
  }
@@ -426,14 +566,14 @@ class FormNodeComponent {
426
566
  <header class="mform-panel-head">{{ container().label }}</header>
427
567
  <div class="mform-panel-body">
428
568
  @for (child of container().children; track child.key) {
429
- <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" />
569
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="childScope()" />
430
570
  }
431
571
  </div>
432
572
  </section>
433
573
  } @else {
434
574
  <div class="mform-group">
435
575
  @for (child of container().children; track child.key) {
436
- <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" />
576
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="childScope()" />
437
577
  }
438
578
  </div>
439
579
  }
@@ -447,7 +587,7 @@ class FormNodeComponent {
447
587
  [ngComponentOutletInputs]="customInputs()"
448
588
  />
449
589
  } @else if (field().type !== 'hidden') {
450
- <div class="mform-field" [class.has-error]="errors().length > 0">
590
+ <div class="mform-field" [class.has-error]="errors().length > 0" [class.is-pending]="pending()">
451
591
  @if (field().type !== 'checkbox') {
452
592
  <label class="mform-label" [attr.for]="controlId()">
453
593
  {{ field().label || field().key }}
@@ -463,7 +603,7 @@ class FormNodeComponent {
463
603
  [id]="controlId()"
464
604
  [value]="stringValue()"
465
605
  [placeholder]="field().placeholder || ''"
466
- [disabled]="field().disabled === true"
606
+ [disabled]="controlDisabled()"
467
607
  [attr.rows]="rowsProp()"
468
608
  [attr.aria-required]="required() || null"
469
609
  [attr.aria-invalid]="errors().length > 0 || null"
@@ -476,7 +616,7 @@ class FormNodeComponent {
476
616
  <select
477
617
  class="mform-control"
478
618
  [id]="controlId()"
479
- [disabled]="field().disabled === true"
619
+ [disabled]="controlDisabled()"
480
620
  [attr.aria-required]="required() || null"
481
621
  [attr.aria-invalid]="errors().length > 0 || null"
482
622
  [attr.aria-describedby]="describedBy()"
@@ -502,7 +642,7 @@ class FormNodeComponent {
502
642
  type="radio"
503
643
  [name]="controlId()"
504
644
  [checked]="stringValue() === '' + option.value"
505
- [disabled]="field().disabled === true"
645
+ [disabled]="controlDisabled()"
506
646
  (change)="setValue(option.value)"
507
647
  (blur)="touch()"
508
648
  />
@@ -517,7 +657,7 @@ class FormNodeComponent {
517
657
  type="checkbox"
518
658
  [id]="controlId()"
519
659
  [checked]="value() === true"
520
- [disabled]="field().disabled === true"
660
+ [disabled]="controlDisabled()"
521
661
  [attr.aria-invalid]="errors().length > 0 || null"
522
662
  [attr.aria-describedby]="describedBy()"
523
663
  (change)="setChecked($event)"
@@ -538,7 +678,7 @@ class FormNodeComponent {
538
678
  <input
539
679
  type="checkbox"
540
680
  [checked]="mapValue()['' + option.value] === true"
541
- [disabled]="field().disabled === true"
681
+ [disabled]="controlDisabled()"
542
682
  (change)="toggleMap('' + option.value, $event)"
543
683
  (blur)="touch()"
544
684
  />
@@ -564,7 +704,7 @@ class FormNodeComponent {
564
704
  type="text"
565
705
  [id]="controlId()"
566
706
  [placeholder]="field().placeholder || ''"
567
- [disabled]="field().disabled === true"
707
+ [disabled]="controlDisabled()"
568
708
  [attr.aria-describedby]="describedBy()"
569
709
  (keydown.enter)="addTag($event)"
570
710
  (blur)="touch()"
@@ -577,11 +717,45 @@ class FormNodeComponent {
577
717
  type="file"
578
718
  multiple
579
719
  [id]="controlId()"
580
- [disabled]="field().disabled === true"
720
+ [disabled]="controlDisabled() || uploading()"
721
+ [attr.aria-busy]="uploading() || null"
581
722
  [attr.aria-describedby]="describedBy()"
582
723
  (change)="setFiles($event)"
583
724
  (blur)="touch()"
584
725
  />
726
+ @if (uploading()) {
727
+ <div class="mform-desc" role="status">Uploading…</div>
728
+ }
729
+ @for (name of fileNames(); track $index) {
730
+ <div class="mform-file-name">📎 {{ name }}</div>
731
+ }
732
+ }
733
+ @case ('reference') {
734
+ <div class="mform-reference">
735
+ <input
736
+ class="mform-control"
737
+ type="text"
738
+ role="combobox"
739
+ [id]="controlId()"
740
+ [value]="refDisplay()"
741
+ [placeholder]="field().placeholder || ''"
742
+ [disabled]="controlDisabled()"
743
+ [attr.aria-expanded]="refOpen()"
744
+ [attr.aria-invalid]="errors().length > 0 || null"
745
+ [attr.aria-describedby]="describedBy()"
746
+ (input)="refSearch($event)"
747
+ (blur)="refBlur()"
748
+ />
749
+ @if (refOpen() && refOptions().length > 0) {
750
+ <ul class="mform-ref-list" role="listbox">
751
+ @for (option of refOptions(); track option.value) {
752
+ <li role="option">
753
+ <button type="button" (click)="refPick(option)">{{ option.label }}</button>
754
+ </li>
755
+ }
756
+ </ul>
757
+ }
758
+ </div>
585
759
  }
586
760
  @case ('unsupported') {
587
761
  <input
@@ -603,7 +777,7 @@ class FormNodeComponent {
603
777
  [id]="controlId()"
604
778
  [value]="stringValue()"
605
779
  [placeholder]="field().placeholder || ''"
606
- [disabled]="field().disabled === true"
780
+ [disabled]="controlDisabled()"
607
781
  [attr.step]="field().type === 'currency' ? '0.01' : null"
608
782
  [attr.inputmode]="field().type === 'currency' ? 'decimal' : null"
609
783
  [attr.aria-required]="required() || null"
@@ -629,7 +803,7 @@ class FormNodeComponent {
629
803
  }
630
804
  }
631
805
  }
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 });
806
+ `, 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
807
  }
634
808
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FormNodeComponent, decorators: [{
635
809
  type: Component,
@@ -645,12 +819,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
645
819
  @switch (n.kind) {
646
820
  @case ('static') {
647
821
  @if (n.type === 'button') {
648
- <button
649
- class="mform-btn"
650
- [type]="buttonAction() === 'submit' ? 'submit' : 'button'"
651
- >
652
- {{ n.label || 'Submit' }}
653
- </button>
822
+ @if (interactive()) {
823
+ <button
824
+ class="mform-btn"
825
+ [type]="buttonAction() === 'submit' ? 'submit' : 'button'"
826
+ >
827
+ {{ n.label || 'Submit' }}
828
+ </button>
829
+ }
654
830
  } @else if (n.type === 'content') {
655
831
  <div class="mform-content" [innerHTML]="content()"></div>
656
832
  } @else {
@@ -666,7 +842,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
666
842
  @for (column of columns(); track $index) {
667
843
  <div class="mform-column">
668
844
  @for (child of column; track child.key) {
669
- <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" />
845
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="childScope()" />
670
846
  }
671
847
  </div>
672
848
  }
@@ -691,7 +867,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
691
867
  @for (tab of container().children; track tab.key; let i = $index) {
692
868
  @if (activeTab() === i) {
693
869
  <div class="mform-tabpanel" role="tabpanel">
694
- <mform-node [node]="tab" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" [bare]="true" />
870
+ <mform-node [node]="tab" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="childScope()" [bare]="true" />
695
871
  </div>
696
872
  }
697
873
  }
@@ -707,20 +883,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
707
883
  <div class="mform-grid-row" role="group" [attr.aria-label]="(container().label || container().key) + ' ' + (i + 1)">
708
884
  <div class="mform-grid-fields">
709
885
  @for (child of container().children; track child.key) {
710
- <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="rowScope(i)" />
886
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="rowScope(i)" />
711
887
  }
712
888
  </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>
889
+ @if (interactive()) {
890
+ <button
891
+ type="button"
892
+ class="mform-row-remove"
893
+ (click)="removeRow(i)"
894
+ [attr.aria-label]="'Remove row ' + (i + 1)"
895
+ >
896
+
897
+ </button>
898
+ }
721
899
  </div>
722
900
  }
723
- <button type="button" class="mform-row-add" (click)="addRow()">+ Add</button>
901
+ @if (interactive()) {
902
+ <button type="button" class="mform-row-add" (click)="addRow()">+ Add</button>
903
+ }
724
904
  </div>
725
905
  </section>
726
906
  }
@@ -730,14 +910,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
730
910
  <header class="mform-panel-head">{{ container().label }}</header>
731
911
  <div class="mform-panel-body">
732
912
  @for (child of container().children; track child.key) {
733
- <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" />
913
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="childScope()" />
734
914
  }
735
915
  </div>
736
916
  </section>
737
917
  } @else {
738
918
  <div class="mform-group">
739
919
  @for (child of container().children; track child.key) {
740
- <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" />
920
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="childScope()" />
741
921
  }
742
922
  </div>
743
923
  }
@@ -751,7 +931,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
751
931
  [ngComponentOutletInputs]="customInputs()"
752
932
  />
753
933
  } @else if (field().type !== 'hidden') {
754
- <div class="mform-field" [class.has-error]="errors().length > 0">
934
+ <div class="mform-field" [class.has-error]="errors().length > 0" [class.is-pending]="pending()">
755
935
  @if (field().type !== 'checkbox') {
756
936
  <label class="mform-label" [attr.for]="controlId()">
757
937
  {{ field().label || field().key }}
@@ -767,7 +947,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
767
947
  [id]="controlId()"
768
948
  [value]="stringValue()"
769
949
  [placeholder]="field().placeholder || ''"
770
- [disabled]="field().disabled === true"
950
+ [disabled]="controlDisabled()"
771
951
  [attr.rows]="rowsProp()"
772
952
  [attr.aria-required]="required() || null"
773
953
  [attr.aria-invalid]="errors().length > 0 || null"
@@ -780,7 +960,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
780
960
  <select
781
961
  class="mform-control"
782
962
  [id]="controlId()"
783
- [disabled]="field().disabled === true"
963
+ [disabled]="controlDisabled()"
784
964
  [attr.aria-required]="required() || null"
785
965
  [attr.aria-invalid]="errors().length > 0 || null"
786
966
  [attr.aria-describedby]="describedBy()"
@@ -806,7 +986,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
806
986
  type="radio"
807
987
  [name]="controlId()"
808
988
  [checked]="stringValue() === '' + option.value"
809
- [disabled]="field().disabled === true"
989
+ [disabled]="controlDisabled()"
810
990
  (change)="setValue(option.value)"
811
991
  (blur)="touch()"
812
992
  />
@@ -821,7 +1001,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
821
1001
  type="checkbox"
822
1002
  [id]="controlId()"
823
1003
  [checked]="value() === true"
824
- [disabled]="field().disabled === true"
1004
+ [disabled]="controlDisabled()"
825
1005
  [attr.aria-invalid]="errors().length > 0 || null"
826
1006
  [attr.aria-describedby]="describedBy()"
827
1007
  (change)="setChecked($event)"
@@ -842,7 +1022,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
842
1022
  <input
843
1023
  type="checkbox"
844
1024
  [checked]="mapValue()['' + option.value] === true"
845
- [disabled]="field().disabled === true"
1025
+ [disabled]="controlDisabled()"
846
1026
  (change)="toggleMap('' + option.value, $event)"
847
1027
  (blur)="touch()"
848
1028
  />
@@ -868,7 +1048,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
868
1048
  type="text"
869
1049
  [id]="controlId()"
870
1050
  [placeholder]="field().placeholder || ''"
871
- [disabled]="field().disabled === true"
1051
+ [disabled]="controlDisabled()"
872
1052
  [attr.aria-describedby]="describedBy()"
873
1053
  (keydown.enter)="addTag($event)"
874
1054
  (blur)="touch()"
@@ -881,11 +1061,45 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
881
1061
  type="file"
882
1062
  multiple
883
1063
  [id]="controlId()"
884
- [disabled]="field().disabled === true"
1064
+ [disabled]="controlDisabled() || uploading()"
1065
+ [attr.aria-busy]="uploading() || null"
885
1066
  [attr.aria-describedby]="describedBy()"
886
1067
  (change)="setFiles($event)"
887
1068
  (blur)="touch()"
888
1069
  />
1070
+ @if (uploading()) {
1071
+ <div class="mform-desc" role="status">Uploading…</div>
1072
+ }
1073
+ @for (name of fileNames(); track $index) {
1074
+ <div class="mform-file-name">📎 {{ name }}</div>
1075
+ }
1076
+ }
1077
+ @case ('reference') {
1078
+ <div class="mform-reference">
1079
+ <input
1080
+ class="mform-control"
1081
+ type="text"
1082
+ role="combobox"
1083
+ [id]="controlId()"
1084
+ [value]="refDisplay()"
1085
+ [placeholder]="field().placeholder || ''"
1086
+ [disabled]="controlDisabled()"
1087
+ [attr.aria-expanded]="refOpen()"
1088
+ [attr.aria-invalid]="errors().length > 0 || null"
1089
+ [attr.aria-describedby]="describedBy()"
1090
+ (input)="refSearch($event)"
1091
+ (blur)="refBlur()"
1092
+ />
1093
+ @if (refOpen() && refOptions().length > 0) {
1094
+ <ul class="mform-ref-list" role="listbox">
1095
+ @for (option of refOptions(); track option.value) {
1096
+ <li role="option">
1097
+ <button type="button" (click)="refPick(option)">{{ option.label }}</button>
1098
+ </li>
1099
+ }
1100
+ </ul>
1101
+ }
1102
+ </div>
889
1103
  }
890
1104
  @case ('unsupported') {
891
1105
  <input
@@ -907,7 +1121,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
907
1121
  [id]="controlId()"
908
1122
  [value]="stringValue()"
909
1123
  [placeholder]="field().placeholder || ''"
910
- [disabled]="field().disabled === true"
1124
+ [disabled]="controlDisabled()"
911
1125
  [attr.step]="field().type === 'currency' ? '0.01' : null"
912
1126
  [attr.inputmode]="field().type === 'currency' ? 'decimal' : null"
913
1127
  [attr.aria-required]="required() || null"
@@ -935,7 +1149,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
935
1149
  }
936
1150
  `,
937
1151
  }]
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 }] }] } });
1152
+ }], 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
1153
 
940
1154
  /**
941
1155
  * `<mform-renderer>` — renders a form at runtime from a `FormSchema` (or any
@@ -958,6 +1172,20 @@ class FormRendererComponent {
958
1172
  optionsProviders = input({}, ...(ngDevMode ? [{ debugName: "optionsProviders" }] : /* istanbul ignore next */ []));
959
1173
  /** Host-supplied message resolver (localization of validation messages). */
960
1174
  messages = input(null, ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
1175
+ /** Named custom validators (sync or async — Promises get pending states). */
1176
+ validators = input(null, ...(ngDevMode ? [{ debugName: "validators" }] : /* istanbul ignore next */ []));
1177
+ /** `edit` (default), `readonly` (no actions) or `disabled`. */
1178
+ mode = input('edit', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
1179
+ /**
1180
+ * Optional host submission: called after a VALID submit with the result;
1181
+ * rejections surface as the submission error state. Persistence transport
1182
+ * stays entirely on the host side.
1183
+ */
1184
+ submitHandler = input(null, ...(ngDevMode ? [{ debugName: "submitHandler" }] : /* istanbul ignore next */ []));
1185
+ /** True while async validation or the submit handler is running. */
1186
+ submitting = signal(false, ...(ngDevMode ? [{ debugName: "submitting" }] : /* istanbul ignore next */ []));
1187
+ /** Message when the host submission rejected. */
1188
+ submitError = signal(null, ...(ngDevMode ? [{ debugName: "submitError" }] : /* istanbul ignore next */ []));
961
1189
  /** Fixed renderer texts (override to localize). `{{count}}` in `summary`. */
962
1190
  labels = input({
963
1191
  previous: 'Previous',
@@ -980,6 +1208,8 @@ class FormRendererComponent {
980
1208
  valueChanged = output();
981
1209
  /** Bumped on every engine event; nodes read it to refresh. */
982
1210
  tick = signal(0, ...(ngDevMode ? [{ debugName: "tick" }] : /* istanbul ignore next */ []));
1211
+ /** Handed to nodes so async completions re-render (zoneless-safe). */
1212
+ bumpTick = () => this.tick.update((v) => v + 1);
983
1213
  /** Active wizard step index. */
984
1214
  currentStep = signal(0, ...(ngDevMode ? [{ debugName: "currentStep" }] : /* istanbul ignore next */ []));
985
1215
  resolved = computed(() => {
@@ -1010,6 +1240,7 @@ class FormRendererComponent {
1010
1240
  ? createFormEngine(schema, {
1011
1241
  initialData: this.initialData() ?? undefined,
1012
1242
  messages: this.messages() ?? undefined,
1243
+ validators: this.validators() ?? undefined,
1013
1244
  })
1014
1245
  : null;
1015
1246
  }, ...(ngDevMode ? [{ debugName: "engine" }] : /* istanbul ignore next */ []));
@@ -1060,22 +1291,37 @@ class FormRendererComponent {
1060
1291
  this.currentStep.update((index) => Math.min(this.steps().length - 1, index + 1));
1061
1292
  }
1062
1293
  // -- submission -----------------------------------------------------------
1063
- onSubmit(event) {
1294
+ async onSubmit(event) {
1064
1295
  event.preventDefault();
1065
1296
  const engine = this.engine();
1066
- if (!engine)
1297
+ if (!engine || this.submitting() || this.mode() !== 'edit')
1067
1298
  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);
1299
+ this.submitting.set(true);
1300
+ this.submitError.set(null);
1301
+ try {
1302
+ const result = await engine.submitAsync();
1303
+ this.errorSummary.set(result.errors);
1304
+ if (!result.ok) {
1305
+ if (this.isWizard()) {
1306
+ const stepIndex = this.stepIndexForPath(result.errors[0]?.path ?? '');
1307
+ if (stepIndex >= 0)
1308
+ this.currentStep.set(stepIndex);
1309
+ }
1310
+ this.focusFirstError(result.errors);
1075
1311
  }
1076
- this.focusFirstError(result.errors);
1312
+ else if (this.submitHandler()) {
1313
+ try {
1314
+ await this.submitHandler()(result);
1315
+ }
1316
+ catch (error) {
1317
+ this.submitError.set(String(error instanceof Error ? error.message : error));
1318
+ }
1319
+ }
1320
+ this.submitted.emit(result);
1321
+ }
1322
+ finally {
1323
+ this.submitting.set(false);
1077
1324
  }
1078
- this.submitted.emit(result);
1079
1325
  }
1080
1326
  /** Finds the wizard step that owns a (possibly row-indexed) data path. */
1081
1327
  stepIndexForPath(path) {
@@ -1095,7 +1341,7 @@ class FormRendererComponent {
1095
1341
  queueMicrotask(() => this.documentRef.getElementById(id)?.focus());
1096
1342
  }
1097
1343
  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: `
1344
+ 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
1345
  @if (engine(); as engine) {
1100
1346
  <form class="mform-root" novalidate (submit)="onSubmit($event)">
1101
1347
  @if (errorSummary().length > 0) {
@@ -1128,6 +1374,7 @@ class FormRendererComponent {
1128
1374
  [engine]="engine"
1129
1375
  [tick]="tick()"
1130
1376
  [providers]="optionsProviders()"
1377
+ [mode]="mode()" [notify]="bumpTick"
1131
1378
  [bare]="true"
1132
1379
  />
1133
1380
  }
@@ -1141,18 +1388,23 @@ class FormRendererComponent {
1141
1388
  <button type="button" class="mform-btn" (click)="nextStep()">
1142
1389
  {{ labels().next }}
1143
1390
  </button>
1144
- } @else {
1145
- <button type="submit" class="mform-btn">{{ labels().submit }}</button>
1391
+ } @else if (mode() === 'edit') {
1392
+ <button type="submit" class="mform-btn" [disabled]="submitting()">
1393
+ {{ submitting() ? '…' : labels().submit }}
1394
+ </button>
1146
1395
  }
1147
1396
  </div>
1148
1397
  } @else {
1149
1398
  @for (node of engine.schema.fields; track node.key) {
1150
- <mform-node [node]="node" [engine]="engine" [tick]="tick()" [providers]="optionsProviders()" />
1399
+ <mform-node [node]="node" [engine]="engine" [tick]="tick()" [providers]="optionsProviders()" [mode]="mode()" [notify]="bumpTick" />
1151
1400
  }
1152
1401
  }
1402
+ @if (submitError(); as error) {
1403
+ <div class="mform-summary" role="alert">{{ error }}</div>
1404
+ }
1153
1405
  </form>
1154
1406
  }
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 });
1407
+ `, 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
1408
  }
1157
1409
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FormRendererComponent, decorators: [{
1158
1410
  type: Component,
@@ -1189,6 +1441,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1189
1441
  [engine]="engine"
1190
1442
  [tick]="tick()"
1191
1443
  [providers]="optionsProviders()"
1444
+ [mode]="mode()" [notify]="bumpTick"
1192
1445
  [bare]="true"
1193
1446
  />
1194
1447
  }
@@ -1202,19 +1455,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1202
1455
  <button type="button" class="mform-btn" (click)="nextStep()">
1203
1456
  {{ labels().next }}
1204
1457
  </button>
1205
- } @else {
1206
- <button type="submit" class="mform-btn">{{ labels().submit }}</button>
1458
+ } @else if (mode() === 'edit') {
1459
+ <button type="submit" class="mform-btn" [disabled]="submitting()">
1460
+ {{ submitting() ? '…' : labels().submit }}
1461
+ </button>
1207
1462
  }
1208
1463
  </div>
1209
1464
  } @else {
1210
1465
  @for (node of engine.schema.fields; track node.key) {
1211
- <mform-node [node]="node" [engine]="engine" [tick]="tick()" [providers]="optionsProviders()" />
1466
+ <mform-node [node]="node" [engine]="engine" [tick]="tick()" [providers]="optionsProviders()" [mode]="mode()" [notify]="bumpTick" />
1212
1467
  }
1213
1468
  }
1469
+ @if (submitError(); as error) {
1470
+ <div class="mform-summary" role="alert">{{ error }}</div>
1471
+ }
1214
1472
  </form>
1215
1473
  }
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"] }] } });
1474
+ `, 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"] }]
1475
+ }], 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
1476
 
1219
1477
  /*
1220
1478
  * Runtime entry point of @mosaicoo/form-angular.
@@ -1229,5 +1487,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1229
1487
  * Generated bundle index. Do not edit.
1230
1488
  */
1231
1489
 
1232
- export { FormNodeComponent, FormRendererComponent, MFORM_FIELD_COMPONENTS, MFORM_REMOTE_FETCHER, provideMosaicooForm };
1490
+ export { FormNodeComponent, FormRendererComponent, MFORM_FIELD_COMPONENTS, MFORM_REMOTE_FETCHER, MFORM_UPLOAD, provideMosaicooForm };
1233
1491
  //# sourceMappingURL=mosaicoo-form-angular.mjs.map