@mosaicoo/form-angular 0.1.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.
@@ -0,0 +1,1047 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, signal, effect, untracked, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, output } from '@angular/core';
3
+ import { importForm, createLegacyImporter, createFormEngine, collectInputs } from '@mosaicoo/form-core';
4
+
5
+ /**
6
+ * Renders one schema node (input, container or static) and recurses into
7
+ * children. State lives in the headless engine — this component only projects
8
+ * it and forwards user interaction. `tick` changes whenever the engine emits,
9
+ * invalidating the OnPush view.
10
+ */
11
+ class FormNodeComponent {
12
+ node = input.required(...(ngDevMode ? [{ debugName: "node" }] : /* istanbul ignore next */ []));
13
+ engine = input.required(...(ngDevMode ? [{ debugName: "engine" }] : /* istanbul ignore next */ []));
14
+ /** Monotonic counter bumped on every engine event — invalidates the view. */
15
+ tick = input.required(...(ngDevMode ? [{ debugName: "tick" }] : /* istanbul ignore next */ []));
16
+ /** Data-path prefix (set by array rows and nested groups above this node). */
17
+ scope = input('', ...(ngDevMode ? [{ debugName: "scope" }] : /* istanbul ignore next */ []));
18
+ /** Suppresses the panel chrome (used when a tab already framed the child). */
19
+ bare = input(false, ...(ngDevMode ? [{ debugName: "bare" }] : /* istanbul ignore next */ []));
20
+ /** Host-registered providers for `optionsSource: provider` fields. */
21
+ providers = input({}, ...(ngDevMode ? [{ debugName: "providers" }] : /* istanbul ignore next */ []));
22
+ activeTab = signal(0, ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
23
+ /** Options resolved from a dynamic source (provider or remote URL). */
24
+ resolvedOptions = signal(null, ...(ngDevMode ? [{ debugName: "resolvedOptions" }] : /* istanbul ignore next */ []));
25
+ /** Snapshot of the observed refresh source at the last resolution. */
26
+ lastRefreshKey = null;
27
+ constructor() {
28
+ effect(() => {
29
+ const node = this.node();
30
+ const registry = this.providers();
31
+ if (node.kind !== 'input' || !node.optionsSource) {
32
+ this.resolvedOptions.set(null);
33
+ return;
34
+ }
35
+ const source = node.optionsSource;
36
+ // With refreshOn, engine events re-run this effect; the refresh key
37
+ // then decides whether the observed data actually changed.
38
+ if (source.refreshOn)
39
+ this.tick();
40
+ const engine = untracked(() => this.engine());
41
+ const path = untracked(() => this.path());
42
+ const refreshKey = !source.refreshOn
43
+ ? 'static'
44
+ : source.refreshOn === 'data'
45
+ ? JSON.stringify(engine.getData())
46
+ : JSON.stringify(engine.getValue(source.refreshOn) ?? null);
47
+ if (refreshKey === this.lastRefreshKey)
48
+ return;
49
+ this.lastRefreshKey = refreshKey;
50
+ if (source.type === 'provider') {
51
+ const provider = registry[source.name];
52
+ if (!provider)
53
+ return; // no provider: static options/fallback stand
54
+ Promise.resolve(provider({ field: node, path, data: engine.getData(), args: source.args }))
55
+ .then((options) => this.resolvedOptions.set(options))
56
+ .catch(() => { }); // provider failure keeps the static fallback
57
+ }
58
+ else {
59
+ fetch(source.url)
60
+ .then((response) => (response.ok ? response.json() : []))
61
+ .then((list) => this.resolvedOptions.set((Array.isArray(list) ? list : []).map((item) => ({
62
+ label: String(item[source.labelKey ?? 'label'] ?? item[source.valueKey ?? 'value'] ?? ''),
63
+ value: (item[source.valueKey ?? 'value'] ?? ''),
64
+ }))))
65
+ .catch(() => { });
66
+ }
67
+ });
68
+ }
69
+ /** Effective options: dynamically resolved first, static as fallback. */
70
+ options() {
71
+ return this.resolvedOptions() ?? this.field().options ?? [];
72
+ }
73
+ // -- typed views of the node ---------------------------------------------
74
+ field() {
75
+ return this.node();
76
+ }
77
+ container() {
78
+ return this.node();
79
+ }
80
+ content() {
81
+ return this.node().content ?? '';
82
+ }
83
+ buttonAction() {
84
+ return String(this.node().props?.['action'] ?? 'submit');
85
+ }
86
+ sourceType() {
87
+ return String(this.node().props?.['sourceType'] ?? '');
88
+ }
89
+ rowsProp() {
90
+ const rows = this.node().props?.['rows'];
91
+ return typeof rows === 'number' ? rows : null;
92
+ }
93
+ // -- paths and visibility -------------------------------------------------
94
+ /** Data path of this node (inputs) or of its scope (containers). */
95
+ path = computed(() => {
96
+ const scope = this.scope();
97
+ const node = this.node();
98
+ if (node.kind === 'input')
99
+ return scope ? `${scope}.${node.key}` : node.key;
100
+ if (node.kind === 'container' && node.dataScope !== 'inherit') {
101
+ return scope ? `${scope}.${node.key}` : node.key;
102
+ }
103
+ return scope;
104
+ }, ...(ngDevMode ? [{ debugName: "path" }] : /* istanbul ignore next */ []));
105
+ /** Scope handed to children (nested/array containers extend it). */
106
+ childScope = computed(() => {
107
+ const node = this.node();
108
+ if (node.kind === 'container' && node.dataScope === 'nested')
109
+ return this.path();
110
+ return this.scope();
111
+ }, ...(ngDevMode ? [{ debugName: "childScope" }] : /* istanbul ignore next */ []));
112
+ rowScope(index) {
113
+ return `${this.path()}.${index}`;
114
+ }
115
+ visible() {
116
+ this.tick();
117
+ const node = this.node();
118
+ const key = node.kind === 'input' ? this.path() : node.key;
119
+ return this.engine().isVisible(key);
120
+ }
121
+ containerLayout() {
122
+ const node = this.container();
123
+ if (node.dataScope === 'array')
124
+ return 'array';
125
+ if (node.type === 'tabs')
126
+ return 'tabs';
127
+ if (node.type === 'columns' && node.columns?.length)
128
+ return 'columns';
129
+ return 'block';
130
+ }
131
+ columns() {
132
+ const node = this.container();
133
+ return (node.columns ?? []).map((column) => column.children
134
+ .map((index) => node.children[index])
135
+ .filter((child) => child !== undefined));
136
+ }
137
+ columnsTemplate() {
138
+ const cols = this.container().columns ?? [];
139
+ const total = cols.reduce((sum, c) => sum + c.width, 0) || 12;
140
+ return cols.map((c) => `${((c.width / total) * 100).toFixed(3)}fr`).join(' ');
141
+ }
142
+ // -- values ---------------------------------------------------------------
143
+ value() {
144
+ this.tick();
145
+ return this.engine().getValue(this.path());
146
+ }
147
+ stringValue() {
148
+ const value = this.value();
149
+ return value === undefined || value === null ? '' : String(value);
150
+ }
151
+ mapValue() {
152
+ const value = this.value();
153
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
154
+ ? value
155
+ : {};
156
+ }
157
+ rows() {
158
+ const value = this.value();
159
+ return Array.isArray(value) ? value : [];
160
+ }
161
+ required() {
162
+ return this.field().validators?.some((v) => v.type === 'required') ?? false;
163
+ }
164
+ errors() {
165
+ this.tick();
166
+ return this.engine().getFieldErrors(this.path());
167
+ }
168
+ controlId() {
169
+ return `mform-${this.path().replace(/\./g, '-')}`;
170
+ }
171
+ describedBy() {
172
+ const parts = [];
173
+ if (this.field().description)
174
+ parts.push(`${this.controlId()}-desc`);
175
+ if (this.errors().length > 0)
176
+ parts.push(`${this.controlId()}-err`);
177
+ return parts.length ? parts.join(' ') : null;
178
+ }
179
+ inputType() {
180
+ switch (this.field().type) {
181
+ case 'number':
182
+ case 'currency':
183
+ return 'number';
184
+ case 'email':
185
+ return 'email';
186
+ case 'password':
187
+ return 'password';
188
+ case 'phone':
189
+ return 'tel';
190
+ case 'url':
191
+ return 'url';
192
+ case 'time':
193
+ return 'time';
194
+ case 'datetime':
195
+ return 'datetime-local';
196
+ case 'day':
197
+ return 'date';
198
+ default:
199
+ return 'text';
200
+ }
201
+ }
202
+ // -- interaction ----------------------------------------------------------
203
+ setValue(value) {
204
+ this.engine().setValue(this.path(), value);
205
+ }
206
+ setString(event) {
207
+ const raw = event.target.value;
208
+ if (this.field().dataType === 'number') {
209
+ const parsed = raw === '' ? undefined : Number(raw);
210
+ this.setValue(parsed !== undefined && Number.isFinite(parsed) ? parsed : undefined);
211
+ }
212
+ else {
213
+ this.setValue(raw);
214
+ }
215
+ }
216
+ setSelect(event) {
217
+ this.setValue(event.target.value);
218
+ }
219
+ setChecked(event) {
220
+ this.setValue(event.target.checked);
221
+ }
222
+ toggleMap(option, event) {
223
+ const checked = event.target.checked;
224
+ this.setValue({ ...this.mapValue(), [option]: checked });
225
+ }
226
+ setFiles(event) {
227
+ const files = event.target.files;
228
+ this.setValue(files ? Array.from(files).map((f) => f.name) : []);
229
+ }
230
+ touch() {
231
+ this.engine().markTouched(this.path());
232
+ this.engine().validateField(this.path());
233
+ }
234
+ addRow() {
235
+ this.engine().addRow(this.path());
236
+ }
237
+ removeRow(index) {
238
+ this.engine().removeRow(this.path(), index);
239
+ }
240
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FormNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
241
+ 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: `
242
+ @let n = node();
243
+ @if (visible()) {
244
+ @switch (n.kind) {
245
+ @case ('static') {
246
+ @if (n.type === 'button') {
247
+ <button
248
+ class="mform-btn"
249
+ [type]="buttonAction() === 'submit' ? 'submit' : 'button'"
250
+ >
251
+ {{ n.label || 'Submit' }}
252
+ </button>
253
+ } @else if (n.type === 'content') {
254
+ <div class="mform-content" [innerHTML]="content()"></div>
255
+ } @else {
256
+ <div class="mform-unsupported" role="note">
257
+ {{ n.label || n.key }} — unsupported content
258
+ </div>
259
+ }
260
+ }
261
+ @case ('container') {
262
+ @switch (containerLayout()) {
263
+ @case ('columns') {
264
+ <div class="mform-columns" [style.grid-template-columns]="columnsTemplate()">
265
+ @for (column of columns(); track $index) {
266
+ <div class="mform-column">
267
+ @for (child of column; track child.key) {
268
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" />
269
+ }
270
+ </div>
271
+ }
272
+ </div>
273
+ }
274
+ @case ('tabs') {
275
+ <div class="mform-tabs">
276
+ <div class="mform-tabbar" role="tablist">
277
+ @for (tab of container().children; track tab.key; let i = $index) {
278
+ <button
279
+ type="button"
280
+ class="mform-tab"
281
+ role="tab"
282
+ [class.is-active]="activeTab() === i"
283
+ [attr.aria-selected]="activeTab() === i"
284
+ (click)="activeTab.set(i)"
285
+ >
286
+ {{ tab.label || 'Tab ' + (i + 1) }}
287
+ </button>
288
+ }
289
+ </div>
290
+ @for (tab of container().children; track tab.key; let i = $index) {
291
+ @if (activeTab() === i) {
292
+ <div class="mform-tabpanel" role="tabpanel">
293
+ <mform-node [node]="tab" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" [bare]="true" />
294
+ </div>
295
+ }
296
+ }
297
+ </div>
298
+ }
299
+ @case ('array') {
300
+ <section class="mform-panel mform-grid">
301
+ @if (container().label) {
302
+ <header class="mform-panel-head">{{ container().label }}</header>
303
+ }
304
+ <div class="mform-panel-body">
305
+ @for (row of rows(); track $index; let i = $index) {
306
+ <div class="mform-grid-row" role="group" [attr.aria-label]="(container().label || container().key) + ' ' + (i + 1)">
307
+ <div class="mform-grid-fields">
308
+ @for (child of container().children; track child.key) {
309
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="rowScope(i)" />
310
+ }
311
+ </div>
312
+ <button
313
+ type="button"
314
+ class="mform-row-remove"
315
+ (click)="removeRow(i)"
316
+ [attr.aria-label]="'Remove row ' + (i + 1)"
317
+ >
318
+
319
+ </button>
320
+ </div>
321
+ }
322
+ <button type="button" class="mform-row-add" (click)="addRow()">+ Add</button>
323
+ </div>
324
+ </section>
325
+ }
326
+ @default {
327
+ @if (container().label && !bare()) {
328
+ <section class="mform-panel">
329
+ <header class="mform-panel-head">{{ container().label }}</header>
330
+ <div class="mform-panel-body">
331
+ @for (child of container().children; track child.key) {
332
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" />
333
+ }
334
+ </div>
335
+ </section>
336
+ } @else {
337
+ <div class="mform-group">
338
+ @for (child of container().children; track child.key) {
339
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" />
340
+ }
341
+ </div>
342
+ }
343
+ }
344
+ }
345
+ }
346
+ @case ('input') {
347
+ @if (field().type !== 'hidden') {
348
+ <div class="mform-field" [class.has-error]="errors().length > 0">
349
+ @if (field().type !== 'checkbox') {
350
+ <label class="mform-label" [attr.for]="controlId()">
351
+ {{ field().label || field().key }}
352
+ @if (required()) {
353
+ <span class="mform-req" aria-hidden="true">*</span>
354
+ }
355
+ </label>
356
+ }
357
+ @switch (field().type) {
358
+ @case ('textarea') {
359
+ <textarea
360
+ class="mform-control"
361
+ [id]="controlId()"
362
+ [value]="stringValue()"
363
+ [placeholder]="field().placeholder || ''"
364
+ [disabled]="field().disabled === true"
365
+ [attr.rows]="rowsProp()"
366
+ [attr.aria-required]="required() || null"
367
+ [attr.aria-invalid]="errors().length > 0 || null"
368
+ [attr.aria-describedby]="describedBy()"
369
+ (input)="setString($event)"
370
+ (blur)="touch()"
371
+ ></textarea>
372
+ }
373
+ @case ('select') {
374
+ <select
375
+ class="mform-control"
376
+ [id]="controlId()"
377
+ [disabled]="field().disabled === true"
378
+ [attr.aria-required]="required() || null"
379
+ [attr.aria-invalid]="errors().length > 0 || null"
380
+ [attr.aria-describedby]="describedBy()"
381
+ (change)="setSelect($event)"
382
+ (blur)="touch()"
383
+ >
384
+ <option value="" [selected]="stringValue() === ''"></option>
385
+ @for (option of options(); track option.value) {
386
+ <option
387
+ [value]="option.value"
388
+ [selected]="stringValue() === '' + option.value"
389
+ >
390
+ {{ option.label }}
391
+ </option>
392
+ }
393
+ </select>
394
+ }
395
+ @case ('radio') {
396
+ <div class="mform-choices" role="radiogroup" [attr.aria-label]="field().label || field().key">
397
+ @for (option of options(); track option.value) {
398
+ <label class="mform-check">
399
+ <input
400
+ type="radio"
401
+ [name]="controlId()"
402
+ [checked]="stringValue() === '' + option.value"
403
+ [disabled]="field().disabled === true"
404
+ (change)="setValue(option.value)"
405
+ (blur)="touch()"
406
+ />
407
+ <span>{{ option.label }}</span>
408
+ </label>
409
+ }
410
+ </div>
411
+ }
412
+ @case ('checkbox') {
413
+ <label class="mform-check">
414
+ <input
415
+ type="checkbox"
416
+ [id]="controlId()"
417
+ [checked]="value() === true"
418
+ [disabled]="field().disabled === true"
419
+ [attr.aria-invalid]="errors().length > 0 || null"
420
+ [attr.aria-describedby]="describedBy()"
421
+ (change)="setChecked($event)"
422
+ (blur)="touch()"
423
+ />
424
+ <span>
425
+ {{ field().label || field().key }}
426
+ @if (required()) {
427
+ <span class="mform-req" aria-hidden="true">*</span>
428
+ }
429
+ </span>
430
+ </label>
431
+ }
432
+ @case ('checkbox-group') {
433
+ <div class="mform-choices" role="group" [attr.aria-label]="field().label || field().key">
434
+ @for (option of options(); track option.value) {
435
+ <label class="mform-check">
436
+ <input
437
+ type="checkbox"
438
+ [checked]="mapValue()['' + option.value] === true"
439
+ [disabled]="field().disabled === true"
440
+ (change)="toggleMap('' + option.value, $event)"
441
+ (blur)="touch()"
442
+ />
443
+ <span>{{ option.label }}</span>
444
+ </label>
445
+ }
446
+ </div>
447
+ }
448
+ @case ('file') {
449
+ <input
450
+ class="mform-control"
451
+ type="file"
452
+ multiple
453
+ [id]="controlId()"
454
+ [disabled]="field().disabled === true"
455
+ [attr.aria-describedby]="describedBy()"
456
+ (change)="setFiles($event)"
457
+ (blur)="touch()"
458
+ />
459
+ }
460
+ @case ('unsupported') {
461
+ <input
462
+ class="mform-control"
463
+ type="text"
464
+ disabled
465
+ [id]="controlId()"
466
+ [value]="'Unsupported field (' + (sourceType() || 'unknown') + ')'"
467
+ />
468
+ }
469
+ @default {
470
+ <input
471
+ class="mform-control"
472
+ [type]="inputType()"
473
+ [id]="controlId()"
474
+ [value]="stringValue()"
475
+ [placeholder]="field().placeholder || ''"
476
+ [disabled]="field().disabled === true"
477
+ [attr.aria-required]="required() || null"
478
+ [attr.aria-invalid]="errors().length > 0 || null"
479
+ [attr.aria-describedby]="describedBy()"
480
+ (input)="setString($event)"
481
+ (blur)="touch()"
482
+ />
483
+ }
484
+ }
485
+ @if (field().description) {
486
+ <div class="mform-desc" [id]="controlId() + '-desc'">{{ field().description }}</div>
487
+ }
488
+ @for (error of errors(); track error.rule) {
489
+ <div class="mform-error" [id]="controlId() + '-err'" role="alert">{{ error.message }}</div>
490
+ }
491
+ </div>
492
+ }
493
+ }
494
+ }
495
+ }
496
+ `, isInline: true, dependencies: [{ kind: "component", type: FormNodeComponent, selector: "mform-node", inputs: ["node", "engine", "tick", "scope", "bare", "providers"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
497
+ }
498
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FormNodeComponent, decorators: [{
499
+ type: Component,
500
+ args: [{
501
+ selector: 'mform-node',
502
+ changeDetection: ChangeDetectionStrategy.OnPush,
503
+ encapsulation: ViewEncapsulation.None,
504
+ host: { class: 'mform-node' },
505
+ template: `
506
+ @let n = node();
507
+ @if (visible()) {
508
+ @switch (n.kind) {
509
+ @case ('static') {
510
+ @if (n.type === 'button') {
511
+ <button
512
+ class="mform-btn"
513
+ [type]="buttonAction() === 'submit' ? 'submit' : 'button'"
514
+ >
515
+ {{ n.label || 'Submit' }}
516
+ </button>
517
+ } @else if (n.type === 'content') {
518
+ <div class="mform-content" [innerHTML]="content()"></div>
519
+ } @else {
520
+ <div class="mform-unsupported" role="note">
521
+ {{ n.label || n.key }} — unsupported content
522
+ </div>
523
+ }
524
+ }
525
+ @case ('container') {
526
+ @switch (containerLayout()) {
527
+ @case ('columns') {
528
+ <div class="mform-columns" [style.grid-template-columns]="columnsTemplate()">
529
+ @for (column of columns(); track $index) {
530
+ <div class="mform-column">
531
+ @for (child of column; track child.key) {
532
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" />
533
+ }
534
+ </div>
535
+ }
536
+ </div>
537
+ }
538
+ @case ('tabs') {
539
+ <div class="mform-tabs">
540
+ <div class="mform-tabbar" role="tablist">
541
+ @for (tab of container().children; track tab.key; let i = $index) {
542
+ <button
543
+ type="button"
544
+ class="mform-tab"
545
+ role="tab"
546
+ [class.is-active]="activeTab() === i"
547
+ [attr.aria-selected]="activeTab() === i"
548
+ (click)="activeTab.set(i)"
549
+ >
550
+ {{ tab.label || 'Tab ' + (i + 1) }}
551
+ </button>
552
+ }
553
+ </div>
554
+ @for (tab of container().children; track tab.key; let i = $index) {
555
+ @if (activeTab() === i) {
556
+ <div class="mform-tabpanel" role="tabpanel">
557
+ <mform-node [node]="tab" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" [bare]="true" />
558
+ </div>
559
+ }
560
+ }
561
+ </div>
562
+ }
563
+ @case ('array') {
564
+ <section class="mform-panel mform-grid">
565
+ @if (container().label) {
566
+ <header class="mform-panel-head">{{ container().label }}</header>
567
+ }
568
+ <div class="mform-panel-body">
569
+ @for (row of rows(); track $index; let i = $index) {
570
+ <div class="mform-grid-row" role="group" [attr.aria-label]="(container().label || container().key) + ' ' + (i + 1)">
571
+ <div class="mform-grid-fields">
572
+ @for (child of container().children; track child.key) {
573
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="rowScope(i)" />
574
+ }
575
+ </div>
576
+ <button
577
+ type="button"
578
+ class="mform-row-remove"
579
+ (click)="removeRow(i)"
580
+ [attr.aria-label]="'Remove row ' + (i + 1)"
581
+ >
582
+
583
+ </button>
584
+ </div>
585
+ }
586
+ <button type="button" class="mform-row-add" (click)="addRow()">+ Add</button>
587
+ </div>
588
+ </section>
589
+ }
590
+ @default {
591
+ @if (container().label && !bare()) {
592
+ <section class="mform-panel">
593
+ <header class="mform-panel-head">{{ container().label }}</header>
594
+ <div class="mform-panel-body">
595
+ @for (child of container().children; track child.key) {
596
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" />
597
+ }
598
+ </div>
599
+ </section>
600
+ } @else {
601
+ <div class="mform-group">
602
+ @for (child of container().children; track child.key) {
603
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" />
604
+ }
605
+ </div>
606
+ }
607
+ }
608
+ }
609
+ }
610
+ @case ('input') {
611
+ @if (field().type !== 'hidden') {
612
+ <div class="mform-field" [class.has-error]="errors().length > 0">
613
+ @if (field().type !== 'checkbox') {
614
+ <label class="mform-label" [attr.for]="controlId()">
615
+ {{ field().label || field().key }}
616
+ @if (required()) {
617
+ <span class="mform-req" aria-hidden="true">*</span>
618
+ }
619
+ </label>
620
+ }
621
+ @switch (field().type) {
622
+ @case ('textarea') {
623
+ <textarea
624
+ class="mform-control"
625
+ [id]="controlId()"
626
+ [value]="stringValue()"
627
+ [placeholder]="field().placeholder || ''"
628
+ [disabled]="field().disabled === true"
629
+ [attr.rows]="rowsProp()"
630
+ [attr.aria-required]="required() || null"
631
+ [attr.aria-invalid]="errors().length > 0 || null"
632
+ [attr.aria-describedby]="describedBy()"
633
+ (input)="setString($event)"
634
+ (blur)="touch()"
635
+ ></textarea>
636
+ }
637
+ @case ('select') {
638
+ <select
639
+ class="mform-control"
640
+ [id]="controlId()"
641
+ [disabled]="field().disabled === true"
642
+ [attr.aria-required]="required() || null"
643
+ [attr.aria-invalid]="errors().length > 0 || null"
644
+ [attr.aria-describedby]="describedBy()"
645
+ (change)="setSelect($event)"
646
+ (blur)="touch()"
647
+ >
648
+ <option value="" [selected]="stringValue() === ''"></option>
649
+ @for (option of options(); track option.value) {
650
+ <option
651
+ [value]="option.value"
652
+ [selected]="stringValue() === '' + option.value"
653
+ >
654
+ {{ option.label }}
655
+ </option>
656
+ }
657
+ </select>
658
+ }
659
+ @case ('radio') {
660
+ <div class="mform-choices" role="radiogroup" [attr.aria-label]="field().label || field().key">
661
+ @for (option of options(); track option.value) {
662
+ <label class="mform-check">
663
+ <input
664
+ type="radio"
665
+ [name]="controlId()"
666
+ [checked]="stringValue() === '' + option.value"
667
+ [disabled]="field().disabled === true"
668
+ (change)="setValue(option.value)"
669
+ (blur)="touch()"
670
+ />
671
+ <span>{{ option.label }}</span>
672
+ </label>
673
+ }
674
+ </div>
675
+ }
676
+ @case ('checkbox') {
677
+ <label class="mform-check">
678
+ <input
679
+ type="checkbox"
680
+ [id]="controlId()"
681
+ [checked]="value() === true"
682
+ [disabled]="field().disabled === true"
683
+ [attr.aria-invalid]="errors().length > 0 || null"
684
+ [attr.aria-describedby]="describedBy()"
685
+ (change)="setChecked($event)"
686
+ (blur)="touch()"
687
+ />
688
+ <span>
689
+ {{ field().label || field().key }}
690
+ @if (required()) {
691
+ <span class="mform-req" aria-hidden="true">*</span>
692
+ }
693
+ </span>
694
+ </label>
695
+ }
696
+ @case ('checkbox-group') {
697
+ <div class="mform-choices" role="group" [attr.aria-label]="field().label || field().key">
698
+ @for (option of options(); track option.value) {
699
+ <label class="mform-check">
700
+ <input
701
+ type="checkbox"
702
+ [checked]="mapValue()['' + option.value] === true"
703
+ [disabled]="field().disabled === true"
704
+ (change)="toggleMap('' + option.value, $event)"
705
+ (blur)="touch()"
706
+ />
707
+ <span>{{ option.label }}</span>
708
+ </label>
709
+ }
710
+ </div>
711
+ }
712
+ @case ('file') {
713
+ <input
714
+ class="mform-control"
715
+ type="file"
716
+ multiple
717
+ [id]="controlId()"
718
+ [disabled]="field().disabled === true"
719
+ [attr.aria-describedby]="describedBy()"
720
+ (change)="setFiles($event)"
721
+ (blur)="touch()"
722
+ />
723
+ }
724
+ @case ('unsupported') {
725
+ <input
726
+ class="mform-control"
727
+ type="text"
728
+ disabled
729
+ [id]="controlId()"
730
+ [value]="'Unsupported field (' + (sourceType() || 'unknown') + ')'"
731
+ />
732
+ }
733
+ @default {
734
+ <input
735
+ class="mform-control"
736
+ [type]="inputType()"
737
+ [id]="controlId()"
738
+ [value]="stringValue()"
739
+ [placeholder]="field().placeholder || ''"
740
+ [disabled]="field().disabled === true"
741
+ [attr.aria-required]="required() || null"
742
+ [attr.aria-invalid]="errors().length > 0 || null"
743
+ [attr.aria-describedby]="describedBy()"
744
+ (input)="setString($event)"
745
+ (blur)="touch()"
746
+ />
747
+ }
748
+ }
749
+ @if (field().description) {
750
+ <div class="mform-desc" [id]="controlId() + '-desc'">{{ field().description }}</div>
751
+ }
752
+ @for (error of errors(); track error.rule) {
753
+ <div class="mform-error" [id]="controlId() + '-err'" role="alert">{{ error.message }}</div>
754
+ }
755
+ </div>
756
+ }
757
+ }
758
+ }
759
+ }
760
+ `,
761
+ }]
762
+ }], 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 }] }] } });
763
+
764
+ /**
765
+ * `<mform-renderer>` — renders a form at runtime from a `FormSchema` (or any
766
+ * definition a registered importer understands) and manages its lifecycle:
767
+ * values, validation, conditional visibility and submission. Wizard schemas
768
+ * (`settings.display: 'wizard'`) render as steps with per-step validation.
769
+ *
770
+ * Theming: every visual primitive reads a `--mform-*` CSS custom property
771
+ * with a self-contained fallback, so the component looks finished on its own
772
+ * and adopts the host design system when the host overrides the tokens.
773
+ */
774
+ class FormRendererComponent {
775
+ /** Native schema input. Takes precedence over `source`. */
776
+ schema = input(null, ...(ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []));
777
+ /** Foreign definition (object or JSON string) resolved via importers. */
778
+ source = input(null, ...(ngDevMode ? [{ debugName: "source" }] : /* istanbul ignore next */ []));
779
+ /** Initial data merged over schema defaults. */
780
+ initialData = input(null, ...(ngDevMode ? [{ debugName: "initialData" }] : /* istanbul ignore next */ []));
781
+ /** Named providers resolving `optionsSource: provider` fields. */
782
+ optionsProviders = input({}, ...(ngDevMode ? [{ debugName: "optionsProviders" }] : /* istanbul ignore next */ []));
783
+ /** Host-supplied message resolver (localization of validation messages). */
784
+ messages = input(null, ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
785
+ /** Fixed renderer texts (override to localize). `{{count}}` in `summary`. */
786
+ labels = input({
787
+ previous: 'Previous',
788
+ next: 'Next',
789
+ submit: 'Submit',
790
+ summary: '{{count}} field(s) need attention:',
791
+ }, { ...(ngDevMode ? { debugName: "labels" } : /* istanbul ignore next */ {}), transform: (value) => ({
792
+ previous: 'Previous',
793
+ next: 'Next',
794
+ submit: 'Submit',
795
+ summary: '{{count}} field(s) need attention:',
796
+ ...value,
797
+ }) });
798
+ summaryTitle() {
799
+ return this.labels().summary.replace(/\{\{\s*count\s*\}\}/g, String(this.errorSummary().length));
800
+ }
801
+ /** Fired on submit; `ok` is false when validation failed. */
802
+ submitted = output();
803
+ /** Fired on every value change with the full data snapshot. */
804
+ valueChanged = output();
805
+ /** Bumped on every engine event; nodes read it to refresh. */
806
+ tick = signal(0, ...(ngDevMode ? [{ debugName: "tick" }] : /* istanbul ignore next */ []));
807
+ /** Active wizard step index. */
808
+ currentStep = signal(0, ...(ngDevMode ? [{ debugName: "currentStep" }] : /* istanbul ignore next */ []));
809
+ resolved = computed(() => {
810
+ const native = this.schema();
811
+ if (native)
812
+ return { schema: native, warnings: [] };
813
+ const source = this.source();
814
+ if (source === null || source === undefined)
815
+ return { schema: null, warnings: [] };
816
+ try {
817
+ const result = importForm(source, [createLegacyImporter()]);
818
+ return { schema: result.schema, warnings: result.warnings };
819
+ }
820
+ catch {
821
+ return {
822
+ schema: null,
823
+ warnings: [
824
+ { code: 'unreadable-source', message: 'The form definition could not be read.' },
825
+ ],
826
+ };
827
+ }
828
+ }, ...(ngDevMode ? [{ debugName: "resolved" }] : /* istanbul ignore next */ []));
829
+ /** Import degradations for the current `source`, if any. */
830
+ importWarnings = computed(() => this.resolved().warnings, ...(ngDevMode ? [{ debugName: "importWarnings" }] : /* istanbul ignore next */ []));
831
+ engine = computed(() => {
832
+ const schema = this.resolved().schema;
833
+ return schema
834
+ ? createFormEngine(schema, {
835
+ initialData: this.initialData() ?? undefined,
836
+ messages: this.messages() ?? undefined,
837
+ })
838
+ : null;
839
+ }, ...(ngDevMode ? [{ debugName: "engine" }] : /* istanbul ignore next */ []));
840
+ isWizard = computed(() => this.resolved().schema?.settings?.display === 'wizard' && this.steps().length > 0, ...(ngDevMode ? [{ debugName: "isWizard" }] : /* istanbul ignore next */ []));
841
+ /** Wizard pages: the top-level containers of the schema. */
842
+ steps = computed(() => (this.resolved().schema?.fields ?? []).filter((node) => node.kind === 'container'), ...(ngDevMode ? [{ debugName: "steps" }] : /* istanbul ignore next */ []));
843
+ errorSummary = signal([], ...(ngDevMode ? [{ debugName: "errorSummary" }] : /* istanbul ignore next */ []));
844
+ constructor() {
845
+ effect((onCleanup) => {
846
+ const engine = this.engine();
847
+ this.errorSummary.set([]);
848
+ this.tick.set(0);
849
+ this.currentStep.set(0);
850
+ if (!engine)
851
+ return;
852
+ const unsubscribe = engine.subscribe((event) => {
853
+ this.tick.update((v) => v + 1);
854
+ if (event.type === 'value-change') {
855
+ this.valueChanged.emit(engine.getData());
856
+ }
857
+ if (event.type === 'validation') {
858
+ // Keep the banner in sync as the user fixes fields after a submit.
859
+ if (this.errorSummary().length > 0) {
860
+ this.errorSummary.set(event.errors);
861
+ }
862
+ }
863
+ });
864
+ onCleanup(unsubscribe);
865
+ });
866
+ }
867
+ // -- wizard navigation ----------------------------------------------------
868
+ previousStep() {
869
+ this.errorSummary.set([]);
870
+ this.currentStep.update((step) => Math.max(0, step - 1));
871
+ }
872
+ nextStep() {
873
+ const engine = this.engine();
874
+ const step = this.steps()[this.currentStep()];
875
+ if (!engine || !step)
876
+ return;
877
+ const errors = engine.validateContainer(step.key);
878
+ if (errors.length > 0) {
879
+ this.errorSummary.set(errors);
880
+ this.focusFirstError(errors);
881
+ return;
882
+ }
883
+ this.errorSummary.set([]);
884
+ this.currentStep.update((index) => Math.min(this.steps().length - 1, index + 1));
885
+ }
886
+ // -- submission -----------------------------------------------------------
887
+ onSubmit(event) {
888
+ event.preventDefault();
889
+ const engine = this.engine();
890
+ if (!engine)
891
+ return;
892
+ const result = engine.submit();
893
+ this.errorSummary.set(result.errors);
894
+ if (!result.ok) {
895
+ if (this.isWizard()) {
896
+ const stepIndex = this.stepIndexForPath(result.errors[0]?.path ?? '');
897
+ if (stepIndex >= 0)
898
+ this.currentStep.set(stepIndex);
899
+ }
900
+ this.focusFirstError(result.errors);
901
+ }
902
+ this.submitted.emit(result);
903
+ }
904
+ /** Finds the wizard step that owns a (possibly row-indexed) data path. */
905
+ stepIndexForPath(path) {
906
+ const pattern = path
907
+ .split('.')
908
+ .map((part) => (/^\d+$/.test(part) ? '*' : part))
909
+ .join('.');
910
+ return this.steps().findIndex((step) => collectInputs({ version: 1, fields: step.children }).some((entry) => entry.path === pattern));
911
+ }
912
+ focusFirstError(errors) {
913
+ const first = errors[0];
914
+ if (!first)
915
+ return;
916
+ const id = `mform-${first.path.replace(/\./g, '-')}`;
917
+ // Focus after the view updated with the error states.
918
+ queueMicrotask(() => document.getElementById(id)?.focus());
919
+ }
920
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FormRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
921
+ 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: `
922
+ @if (engine(); as engine) {
923
+ <form class="mform-root" novalidate (submit)="onSubmit($event)">
924
+ @if (errorSummary().length > 0) {
925
+ <div class="mform-summary" role="alert" tabindex="-1">
926
+ <strong>{{ summaryTitle() }}</strong>
927
+ <ul>
928
+ @for (error of errorSummary(); track error.path + error.rule) {
929
+ <li>{{ error.message }}</li>
930
+ }
931
+ </ul>
932
+ </div>
933
+ }
934
+ @if (isWizard()) {
935
+ <ol class="mform-steps">
936
+ @for (step of steps(); track step.key; let i = $index) {
937
+ <li
938
+ class="mform-step"
939
+ [class.is-active]="currentStep() === i"
940
+ [class.is-done]="i < currentStep()"
941
+ [attr.aria-current]="currentStep() === i ? 'step' : null"
942
+ >
943
+ <span class="mform-step-index">{{ i + 1 }}</span>
944
+ <span class="mform-step-label">{{ step.label || 'Step ' + (i + 1) }}</span>
945
+ </li>
946
+ }
947
+ </ol>
948
+ @if (steps()[currentStep()]; as step) {
949
+ <mform-node
950
+ [node]="step"
951
+ [engine]="engine"
952
+ [tick]="tick()"
953
+ [providers]="optionsProviders()"
954
+ [bare]="true"
955
+ />
956
+ }
957
+ <div class="mform-wizard-nav">
958
+ @if (currentStep() > 0) {
959
+ <button type="button" class="mform-btn mform-btn-secondary" (click)="previousStep()">
960
+ {{ labels().previous }}
961
+ </button>
962
+ }
963
+ @if (currentStep() < steps().length - 1) {
964
+ <button type="button" class="mform-btn" (click)="nextStep()">
965
+ {{ labels().next }}
966
+ </button>
967
+ } @else {
968
+ <button type="submit" class="mform-btn">{{ labels().submit }}</button>
969
+ }
970
+ </div>
971
+ } @else {
972
+ @for (node of engine.schema.fields; track node.key) {
973
+ <mform-node [node]="node" [engine]="engine" [tick]="tick()" [providers]="optionsProviders()" />
974
+ }
975
+ }
976
+ </form>
977
+ }
978
+ `, 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)}.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 });
979
+ }
980
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FormRendererComponent, decorators: [{
981
+ type: Component,
982
+ args: [{ selector: 'mform-renderer', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [FormNodeComponent], template: `
983
+ @if (engine(); as engine) {
984
+ <form class="mform-root" novalidate (submit)="onSubmit($event)">
985
+ @if (errorSummary().length > 0) {
986
+ <div class="mform-summary" role="alert" tabindex="-1">
987
+ <strong>{{ summaryTitle() }}</strong>
988
+ <ul>
989
+ @for (error of errorSummary(); track error.path + error.rule) {
990
+ <li>{{ error.message }}</li>
991
+ }
992
+ </ul>
993
+ </div>
994
+ }
995
+ @if (isWizard()) {
996
+ <ol class="mform-steps">
997
+ @for (step of steps(); track step.key; let i = $index) {
998
+ <li
999
+ class="mform-step"
1000
+ [class.is-active]="currentStep() === i"
1001
+ [class.is-done]="i < currentStep()"
1002
+ [attr.aria-current]="currentStep() === i ? 'step' : null"
1003
+ >
1004
+ <span class="mform-step-index">{{ i + 1 }}</span>
1005
+ <span class="mform-step-label">{{ step.label || 'Step ' + (i + 1) }}</span>
1006
+ </li>
1007
+ }
1008
+ </ol>
1009
+ @if (steps()[currentStep()]; as step) {
1010
+ <mform-node
1011
+ [node]="step"
1012
+ [engine]="engine"
1013
+ [tick]="tick()"
1014
+ [providers]="optionsProviders()"
1015
+ [bare]="true"
1016
+ />
1017
+ }
1018
+ <div class="mform-wizard-nav">
1019
+ @if (currentStep() > 0) {
1020
+ <button type="button" class="mform-btn mform-btn-secondary" (click)="previousStep()">
1021
+ {{ labels().previous }}
1022
+ </button>
1023
+ }
1024
+ @if (currentStep() < steps().length - 1) {
1025
+ <button type="button" class="mform-btn" (click)="nextStep()">
1026
+ {{ labels().next }}
1027
+ </button>
1028
+ } @else {
1029
+ <button type="submit" class="mform-btn">{{ labels().submit }}</button>
1030
+ }
1031
+ </div>
1032
+ } @else {
1033
+ @for (node of engine.schema.fields; track node.key) {
1034
+ <mform-node [node]="node" [engine]="engine" [tick]="tick()" [providers]="optionsProviders()" />
1035
+ }
1036
+ }
1037
+ </form>
1038
+ }
1039
+ `, 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)}.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"] }]
1040
+ }], 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"] }] } });
1041
+
1042
+ /**
1043
+ * Generated bundle index. Do not edit.
1044
+ */
1045
+
1046
+ export { FormNodeComponent, FormRendererComponent };
1047
+ //# sourceMappingURL=mosaicoo-form-angular.mjs.map