@suntelecoms/ngx-dynamic-form 1.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,1100 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Injectable, Input, Component, EventEmitter, Output, inject, signal, computed, InjectionToken, makeEnvironmentProviders } from '@angular/core';
3
+ import { JsonPipe } from '@angular/common';
4
+ import * as i1 from '@angular/forms';
5
+ import { FormControl, FormGroup, Validators, ReactiveFormsModule, FormsModule } from '@angular/forms';
6
+ import { MatButton } from '@angular/material/button';
7
+ import { MatFormField, MatLabel, MatError, MatHint, MatPrefix, MatSuffix } from '@angular/material/form-field';
8
+ import { MatInput } from '@angular/material/input';
9
+ import { MatSelect } from '@angular/material/select';
10
+ import { MatOption } from '@angular/material/core';
11
+ import { MatRadioGroup, MatRadioButton } from '@angular/material/radio';
12
+ import { MatCheckbox } from '@angular/material/checkbox';
13
+ import { MatIcon } from '@angular/material/icon';
14
+ import { RouterLink, ActivatedRoute } from '@angular/router';
15
+ import { MatSnackBar } from '@angular/material/snack-bar';
16
+ import { MatTooltip } from '@angular/material/tooltip';
17
+ import { tap, of, map } from 'rxjs';
18
+ import { HttpClient } from '@angular/common/http';
19
+
20
+ class DynamicFormBuilderService {
21
+ /**
22
+ * Build a FormGroup from an array of DynamicFormField definitions.
23
+ */
24
+ buildFormGroup(fields) {
25
+ const group = {};
26
+ for (const field of fields) {
27
+ if (field.type === 'divider' || field.type === 'heading')
28
+ continue;
29
+ const validators = this.buildValidators(field.validation);
30
+ const control = new FormControl({
31
+ value: field.defaultValue ?? null,
32
+ disabled: field.disabled ?? false,
33
+ }, validators);
34
+ group[field.key] = control;
35
+ }
36
+ return new FormGroup(group);
37
+ }
38
+ /**
39
+ * Convert FieldValidation config into Angular ValidatorFn[].
40
+ */
41
+ buildValidators(validation) {
42
+ if (!validation)
43
+ return [];
44
+ const validators = [];
45
+ if (validation.required)
46
+ validators.push(Validators.required);
47
+ if (validation.email)
48
+ validators.push(Validators.email);
49
+ if (validation.minLength != null)
50
+ validators.push(Validators.minLength(validation.minLength));
51
+ if (validation.maxLength != null)
52
+ validators.push(Validators.maxLength(validation.maxLength));
53
+ if (validation.min != null)
54
+ validators.push(Validators.min(validation.min));
55
+ if (validation.max != null)
56
+ validators.push(Validators.max(validation.max));
57
+ if (validation.pattern)
58
+ validators.push(Validators.pattern(validation.pattern));
59
+ if (validation.custom) {
60
+ const customFn = validation.custom;
61
+ validators.push((control) => {
62
+ const msg = customFn(control.value);
63
+ return msg ? { custom: msg } : null;
64
+ });
65
+ }
66
+ return validators;
67
+ }
68
+ /**
69
+ * Get the human-readable error message for a form control.
70
+ */
71
+ getErrorMessage(control, validation) {
72
+ if (!control.errors || !control.touched)
73
+ return null;
74
+ const msgs = validation?.messages ?? {};
75
+ if (control.errors['required'])
76
+ return msgs['required'] ?? 'Ce champ est requis.';
77
+ if (control.errors['email'])
78
+ return msgs['email'] ?? 'Adresse e-mail invalide.';
79
+ if (control.errors['minlength'])
80
+ return (msgs['minLength'] ??
81
+ `Minimum ${control.errors['minlength'].requiredLength} caractères.`);
82
+ if (control.errors['maxlength'])
83
+ return (msgs['maxLength'] ??
84
+ `Maximum ${control.errors['maxlength'].requiredLength} caractères.`);
85
+ if (control.errors['min'])
86
+ return msgs['min'] ?? `Valeur minimum : ${control.errors['min'].min}.`;
87
+ if (control.errors['max'])
88
+ return msgs['max'] ?? `Valeur maximum : ${control.errors['max'].max}.`;
89
+ if (control.errors['pattern'])
90
+ return msgs['pattern'] ?? 'Format invalide.';
91
+ if (control.errors['custom'])
92
+ return control.errors['custom'];
93
+ return 'Valeur invalide.';
94
+ }
95
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: DynamicFormBuilderService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
96
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: DynamicFormBuilderService, providedIn: 'root' }); }
97
+ }
98
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: DynamicFormBuilderService, decorators: [{
99
+ type: Injectable,
100
+ args: [{ providedIn: 'root' }]
101
+ }] });
102
+
103
+ class DynamicFieldComponent {
104
+ constructor(builder) {
105
+ this.builder = builder;
106
+ this.resolvedOptions = [];
107
+ }
108
+ ngOnInit() {
109
+ if (this.hasOptions) {
110
+ this.resolveOptions();
111
+ }
112
+ }
113
+ resolveOptions() {
114
+ const opts = this.field.options;
115
+ if (!opts)
116
+ return;
117
+ if (Array.isArray(opts)) {
118
+ this.resolvedOptions = opts.map((o) => typeof o === 'string' ? { label: o, value: o } : o);
119
+ }
120
+ }
121
+ get formControl() {
122
+ return this.form.get(this.field.key);
123
+ }
124
+ get isInputField() {
125
+ return this.field.type !== 'divider' && this.field.type !== 'heading';
126
+ }
127
+ get hasOptions() {
128
+ return ['select', 'multiselect', 'radio'].includes(this.field.type);
129
+ }
130
+ get hasError() {
131
+ const ctrl = this.formControl;
132
+ return !!(ctrl && ctrl.invalid && ctrl.touched);
133
+ }
134
+ get errorMessage() {
135
+ const ctrl = this.formControl;
136
+ if (!ctrl)
137
+ return null;
138
+ return this.builder.getErrorMessage(ctrl, this.field.validation);
139
+ }
140
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: DynamicFieldComponent, deps: [{ token: DynamicFormBuilderService }], target: i0.ɵɵFactoryTarget.Component }); }
141
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: DynamicFieldComponent, isStandalone: true, selector: "ngx-dynamic-field", inputs: { field: "field", form: "form" }, ngImport: i0, template: `
142
+ @if (field.type === 'divider') {
143
+ <hr class="ngx-divider" />
144
+ }
145
+
146
+ @if (field.type === 'heading') {
147
+ @switch (field.level || 2) {
148
+ @case (1) { <h1 class="ngx-heading">{{ field.text || field.label }}</h1> }
149
+ @case (2) { <h2 class="ngx-heading">{{ field.text || field.label }}</h2> }
150
+ @case (3) { <h3 class="ngx-heading">{{ field.text || field.label }}</h3> }
151
+ @case (4) { <h4 class="ngx-heading">{{ field.text || field.label }}</h4> }
152
+ @case (5) { <h5 class="ngx-heading">{{ field.text || field.label }}</h5> }
153
+ @case (6) { <h6 class="ngx-heading">{{ field.text || field.label }}</h6> }
154
+ @default { <h2 class="ngx-heading">{{ field.text || field.label }}</h2> }
155
+ }
156
+ }
157
+
158
+ @if (isInputField) {
159
+ @if (field.type === 'checkbox') {
160
+ <div class="ngx-checkbox-wrapper">
161
+ <mat-checkbox color="primary" [formControl]="formControl">
162
+ {{ field.placeholder || field.label }}
163
+ </mat-checkbox>
164
+ @if (hasError) {
165
+ <small class="ngx-mat-standalone-error">{{ errorMessage }}</small>
166
+ }
167
+ </div>
168
+
169
+ } @else if (field.type === 'radio') {
170
+ <div class="ngx-radio-wrapper">
171
+ @if (field.label) {
172
+ <label class="ngx-radio-label">{{ field.label }}</label>
173
+ }
174
+ <mat-radio-group [formControl]="formControl" class="ngx-radio-group">
175
+ @for (opt of resolvedOptions; track opt.value) {
176
+ <mat-radio-button [value]="opt.value" color="primary">
177
+ {{ opt.label }}
178
+ </mat-radio-button>
179
+ }
180
+ </mat-radio-group>
181
+ @if (hasError) {
182
+ <small class="ngx-mat-standalone-error">{{ errorMessage }}</small>
183
+ }
184
+ </div>
185
+
186
+ } @else if (field.type === 'textarea') {
187
+ <mat-form-field appearance="outline" class="ngx-mat-field">
188
+ @if (field.label) { <mat-label>{{ field.label }}</mat-label> }
189
+ @if (field.icon) { <mat-icon matPrefix>{{ field.icon }}</mat-icon> }
190
+ <textarea
191
+ matInput
192
+ [id]="field.key"
193
+ [formControl]="formControl"
194
+ [placeholder]="field.placeholder || ''"
195
+ [rows]="field.rows || 3"
196
+ [readonly]="field.readonly || false"
197
+ ></textarea>
198
+ @if (field.iconSuffix) { <mat-icon matSuffix>{{ field.iconSuffix }}</mat-icon> }
199
+ @if (field.hint) { <mat-hint>{{ field.hint }}</mat-hint> }
200
+ <mat-error>{{ errorMessage }}</mat-error>
201
+ </mat-form-field>
202
+
203
+ } @else if (field.type === 'select' || field.type === 'multiselect') {
204
+ <mat-form-field appearance="outline" class="ngx-mat-field">
205
+ @if (field.label) { <mat-label>{{ field.label }}</mat-label> }
206
+ @if (field.icon) { <mat-icon matPrefix>{{ field.icon }}</mat-icon> }
207
+ <mat-select [formControl]="formControl" [multiple]="field.type === 'multiselect'">
208
+ @if (field.placeholder) {
209
+ <mat-option value="">{{ field.placeholder }}</mat-option>
210
+ }
211
+ @for (opt of resolvedOptions; track opt.value) {
212
+ <mat-option [value]="opt.value" [disabled]="opt.disabled || false">
213
+ {{ opt.label }}
214
+ </mat-option>
215
+ }
216
+ </mat-select>
217
+ @if (field.iconSuffix) { <mat-icon matSuffix>{{ field.iconSuffix }}</mat-icon> }
218
+ @if (field.hint) { <mat-hint>{{ field.hint }}</mat-hint> }
219
+ <mat-error>{{ errorMessage }}</mat-error>
220
+ </mat-form-field>
221
+
222
+ } @else if (field.type === 'file') {
223
+ <div class="ngx-file-wrapper">
224
+ @if (field.label) {
225
+ <label class="ngx-file-label">
226
+ {{ field.label }}
227
+ @if (field.validation?.required) {
228
+ <span class="ngx-required" aria-hidden="true"> *</span>
229
+ }
230
+ </label>
231
+ }
232
+ <input
233
+ type="file"
234
+ [id]="field.key"
235
+ [accept]="field.accept || '*'"
236
+ [multiple]="field.multiple || false"
237
+ class="ngx-file-input"
238
+ />
239
+ @if (field.hint) { <small class="ngx-hint">{{ field.hint }}</small> }
240
+ </div>
241
+
242
+ } @else if (field.type === 'hidden') {
243
+ <input type="hidden" [formControl]="formControl" />
244
+
245
+ } @else {
246
+ <mat-form-field appearance="outline" class="ngx-mat-field">
247
+ @if (field.label) { <mat-label>{{ field.label }}</mat-label> }
248
+ @if (field.icon) { <mat-icon matPrefix>{{ field.icon }}</mat-icon> }
249
+ <input
250
+ matInput
251
+ [id]="field.key"
252
+ [type]="field.type"
253
+ [formControl]="formControl"
254
+ [placeholder]="field.placeholder || ''"
255
+ [readonly]="field.readonly || false"
256
+ [attr.min]="field.validation?.min"
257
+ [attr.max]="field.validation?.max"
258
+ [attr.minlength]="field.validation?.minLength"
259
+ [attr.maxlength]="field.validation?.maxLength"
260
+ />
261
+ @if (field.iconSuffix) { <mat-icon matSuffix>{{ field.iconSuffix }}</mat-icon> }
262
+ @if (field.hint) { <mat-hint>{{ field.hint }}</mat-hint> }
263
+ <mat-error>{{ errorMessage }}</mat-error>
264
+ </mat-form-field>
265
+ }
266
+ }
267
+ `, isInline: true, styles: [":host{display:flex;flex-direction:column;width:100%}.ngx-mat-field{width:100%}.ngx-radio-wrapper{display:flex;flex-direction:column;gap:.5rem;padding:.25rem 0 .75rem}.ngx-radio-label{font-size:.875rem;font-weight:500;color:#0009}.ngx-radio-group{display:flex;flex-wrap:wrap;gap:.75rem}.ngx-checkbox-wrapper{display:flex;flex-direction:column;gap:.25rem;padding:.25rem 0}.ngx-file-wrapper{display:flex;flex-direction:column;gap:.375rem;padding-bottom:.75rem}.ngx-file-label{font-size:.875rem;font-weight:500;color:#000000de}.ngx-file-input{border:1px solid rgba(0,0,0,.23);border-radius:4px;padding:.625rem .75rem;font-size:.875rem;cursor:pointer;background:transparent;transition:border-color .15s}.ngx-file-input:hover{border-color:#000000de}.ngx-mat-standalone-error{font-size:.75rem;color:var(--mat-sys-error, #b00020)}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatLabel, selector: "mat-label" }, { kind: "directive", type: MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: MatPrefix, selector: "[matPrefix], [matIconPrefix], [matTextPrefix]", inputs: ["matTextPrefix"] }, { kind: "directive", type: MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: MatRadioGroup, selector: "mat-radio-group", inputs: ["color", "name", "labelPosition", "value", "selected", "disabled", "required", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioGroup"] }, { kind: "component", type: MatRadioButton, selector: "mat-radio-button", inputs: ["id", "name", "aria-label", "aria-labelledby", "aria-describedby", "disableRipple", "tabIndex", "checked", "value", "labelPosition", "disabled", "required", "color", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioButton"] }, { kind: "component", type: MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }] }); }
268
+ }
269
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: DynamicFieldComponent, decorators: [{
270
+ type: Component,
271
+ args: [{ selector: 'ngx-dynamic-field', standalone: true, imports: [
272
+ ReactiveFormsModule,
273
+ MatFormField, MatLabel, MatError, MatHint, MatPrefix, MatSuffix,
274
+ MatInput,
275
+ MatSelect, MatOption,
276
+ MatRadioGroup, MatRadioButton,
277
+ MatCheckbox,
278
+ MatIcon,
279
+ ], template: `
280
+ @if (field.type === 'divider') {
281
+ <hr class="ngx-divider" />
282
+ }
283
+
284
+ @if (field.type === 'heading') {
285
+ @switch (field.level || 2) {
286
+ @case (1) { <h1 class="ngx-heading">{{ field.text || field.label }}</h1> }
287
+ @case (2) { <h2 class="ngx-heading">{{ field.text || field.label }}</h2> }
288
+ @case (3) { <h3 class="ngx-heading">{{ field.text || field.label }}</h3> }
289
+ @case (4) { <h4 class="ngx-heading">{{ field.text || field.label }}</h4> }
290
+ @case (5) { <h5 class="ngx-heading">{{ field.text || field.label }}</h5> }
291
+ @case (6) { <h6 class="ngx-heading">{{ field.text || field.label }}</h6> }
292
+ @default { <h2 class="ngx-heading">{{ field.text || field.label }}</h2> }
293
+ }
294
+ }
295
+
296
+ @if (isInputField) {
297
+ @if (field.type === 'checkbox') {
298
+ <div class="ngx-checkbox-wrapper">
299
+ <mat-checkbox color="primary" [formControl]="formControl">
300
+ {{ field.placeholder || field.label }}
301
+ </mat-checkbox>
302
+ @if (hasError) {
303
+ <small class="ngx-mat-standalone-error">{{ errorMessage }}</small>
304
+ }
305
+ </div>
306
+
307
+ } @else if (field.type === 'radio') {
308
+ <div class="ngx-radio-wrapper">
309
+ @if (field.label) {
310
+ <label class="ngx-radio-label">{{ field.label }}</label>
311
+ }
312
+ <mat-radio-group [formControl]="formControl" class="ngx-radio-group">
313
+ @for (opt of resolvedOptions; track opt.value) {
314
+ <mat-radio-button [value]="opt.value" color="primary">
315
+ {{ opt.label }}
316
+ </mat-radio-button>
317
+ }
318
+ </mat-radio-group>
319
+ @if (hasError) {
320
+ <small class="ngx-mat-standalone-error">{{ errorMessage }}</small>
321
+ }
322
+ </div>
323
+
324
+ } @else if (field.type === 'textarea') {
325
+ <mat-form-field appearance="outline" class="ngx-mat-field">
326
+ @if (field.label) { <mat-label>{{ field.label }}</mat-label> }
327
+ @if (field.icon) { <mat-icon matPrefix>{{ field.icon }}</mat-icon> }
328
+ <textarea
329
+ matInput
330
+ [id]="field.key"
331
+ [formControl]="formControl"
332
+ [placeholder]="field.placeholder || ''"
333
+ [rows]="field.rows || 3"
334
+ [readonly]="field.readonly || false"
335
+ ></textarea>
336
+ @if (field.iconSuffix) { <mat-icon matSuffix>{{ field.iconSuffix }}</mat-icon> }
337
+ @if (field.hint) { <mat-hint>{{ field.hint }}</mat-hint> }
338
+ <mat-error>{{ errorMessage }}</mat-error>
339
+ </mat-form-field>
340
+
341
+ } @else if (field.type === 'select' || field.type === 'multiselect') {
342
+ <mat-form-field appearance="outline" class="ngx-mat-field">
343
+ @if (field.label) { <mat-label>{{ field.label }}</mat-label> }
344
+ @if (field.icon) { <mat-icon matPrefix>{{ field.icon }}</mat-icon> }
345
+ <mat-select [formControl]="formControl" [multiple]="field.type === 'multiselect'">
346
+ @if (field.placeholder) {
347
+ <mat-option value="">{{ field.placeholder }}</mat-option>
348
+ }
349
+ @for (opt of resolvedOptions; track opt.value) {
350
+ <mat-option [value]="opt.value" [disabled]="opt.disabled || false">
351
+ {{ opt.label }}
352
+ </mat-option>
353
+ }
354
+ </mat-select>
355
+ @if (field.iconSuffix) { <mat-icon matSuffix>{{ field.iconSuffix }}</mat-icon> }
356
+ @if (field.hint) { <mat-hint>{{ field.hint }}</mat-hint> }
357
+ <mat-error>{{ errorMessage }}</mat-error>
358
+ </mat-form-field>
359
+
360
+ } @else if (field.type === 'file') {
361
+ <div class="ngx-file-wrapper">
362
+ @if (field.label) {
363
+ <label class="ngx-file-label">
364
+ {{ field.label }}
365
+ @if (field.validation?.required) {
366
+ <span class="ngx-required" aria-hidden="true"> *</span>
367
+ }
368
+ </label>
369
+ }
370
+ <input
371
+ type="file"
372
+ [id]="field.key"
373
+ [accept]="field.accept || '*'"
374
+ [multiple]="field.multiple || false"
375
+ class="ngx-file-input"
376
+ />
377
+ @if (field.hint) { <small class="ngx-hint">{{ field.hint }}</small> }
378
+ </div>
379
+
380
+ } @else if (field.type === 'hidden') {
381
+ <input type="hidden" [formControl]="formControl" />
382
+
383
+ } @else {
384
+ <mat-form-field appearance="outline" class="ngx-mat-field">
385
+ @if (field.label) { <mat-label>{{ field.label }}</mat-label> }
386
+ @if (field.icon) { <mat-icon matPrefix>{{ field.icon }}</mat-icon> }
387
+ <input
388
+ matInput
389
+ [id]="field.key"
390
+ [type]="field.type"
391
+ [formControl]="formControl"
392
+ [placeholder]="field.placeholder || ''"
393
+ [readonly]="field.readonly || false"
394
+ [attr.min]="field.validation?.min"
395
+ [attr.max]="field.validation?.max"
396
+ [attr.minlength]="field.validation?.minLength"
397
+ [attr.maxlength]="field.validation?.maxLength"
398
+ />
399
+ @if (field.iconSuffix) { <mat-icon matSuffix>{{ field.iconSuffix }}</mat-icon> }
400
+ @if (field.hint) { <mat-hint>{{ field.hint }}</mat-hint> }
401
+ <mat-error>{{ errorMessage }}</mat-error>
402
+ </mat-form-field>
403
+ }
404
+ }
405
+ `, styles: [":host{display:flex;flex-direction:column;width:100%}.ngx-mat-field{width:100%}.ngx-radio-wrapper{display:flex;flex-direction:column;gap:.5rem;padding:.25rem 0 .75rem}.ngx-radio-label{font-size:.875rem;font-weight:500;color:#0009}.ngx-radio-group{display:flex;flex-wrap:wrap;gap:.75rem}.ngx-checkbox-wrapper{display:flex;flex-direction:column;gap:.25rem;padding:.25rem 0}.ngx-file-wrapper{display:flex;flex-direction:column;gap:.375rem;padding-bottom:.75rem}.ngx-file-label{font-size:.875rem;font-weight:500;color:#000000de}.ngx-file-input{border:1px solid rgba(0,0,0,.23);border-radius:4px;padding:.625rem .75rem;font-size:.875rem;cursor:pointer;background:transparent;transition:border-color .15s}.ngx-file-input:hover{border-color:#000000de}.ngx-mat-standalone-error{font-size:.75rem;color:var(--mat-sys-error, #b00020)}\n"] }]
406
+ }], ctorParameters: () => [{ type: DynamicFormBuilderService }], propDecorators: { field: [{
407
+ type: Input,
408
+ args: [{ required: true }]
409
+ }], form: [{
410
+ type: Input,
411
+ args: [{ required: true }]
412
+ }] } });
413
+
414
+ class ConditionEvaluatorService {
415
+ /**
416
+ * Evaluate whether a field should be visible based on its showWhen condition(s).
417
+ */
418
+ isVisible(field, form) {
419
+ if (field.hidden)
420
+ return false;
421
+ if (!field.showWhen)
422
+ return true;
423
+ const conditions = Array.isArray(field.showWhen)
424
+ ? field.showWhen
425
+ : [field.showWhen];
426
+ // ALL conditions must be true (AND logic)
427
+ return conditions.every((condition) => this.evaluate(condition, form));
428
+ }
429
+ evaluate(condition, form) {
430
+ const control = form.get(condition.field);
431
+ const value = control?.value;
432
+ switch (condition.operator) {
433
+ case 'equals':
434
+ return value === condition.value;
435
+ case 'notEquals':
436
+ return value !== condition.value;
437
+ case 'contains':
438
+ return Array.isArray(value)
439
+ ? value.includes(condition.value)
440
+ : String(value ?? '').includes(String(condition.value));
441
+ case 'greaterThan':
442
+ return Number(value) > Number(condition.value);
443
+ case 'lessThan':
444
+ return Number(value) < Number(condition.value);
445
+ case 'exists':
446
+ return value !== null && value !== undefined && value !== '';
447
+ case 'notExists':
448
+ return value === null || value === undefined || value === '';
449
+ default:
450
+ return true;
451
+ }
452
+ }
453
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: ConditionEvaluatorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
454
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: ConditionEvaluatorService, providedIn: 'root' }); }
455
+ }
456
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: ConditionEvaluatorService, decorators: [{
457
+ type: Injectable,
458
+ args: [{ providedIn: 'root' }]
459
+ }] });
460
+
461
+ class DynamicFormComponent {
462
+ constructor(builder, conditionEvaluator, cdr) {
463
+ this.builder = builder;
464
+ this.conditionEvaluator = conditionEvaluator;
465
+ this.cdr = cdr;
466
+ /** Emitted on valid form submit */
467
+ this.formSubmit = new EventEmitter();
468
+ /** Emitted on every value change */
469
+ this.formChange = new EventEmitter();
470
+ /** Emitted when form is reset */
471
+ this.formReset = new EventEmitter();
472
+ }
473
+ ngOnInit() {
474
+ this.buildForm();
475
+ }
476
+ ngOnChanges(changes) {
477
+ // Rebuild form if config changes (e.g. dynamic schema loading)
478
+ if (changes['config'] && !changes['config'].firstChange) {
479
+ this.buildForm();
480
+ }
481
+ // Patch values if initialValues is updated
482
+ if (changes['initialValues'] && this.form) {
483
+ this.form.patchValue(this.initialValues ?? {});
484
+ }
485
+ }
486
+ buildForm() {
487
+ this.form = this.builder.buildFormGroup(this.config.fields);
488
+ if (this.initialValues) {
489
+ this.form.patchValue(this.initialValues);
490
+ }
491
+ this.form.valueChanges.subscribe((val) => {
492
+ this.formChange.emit(val);
493
+ this.cdr.markForCheck();
494
+ });
495
+ }
496
+ isVisible(field) {
497
+ return this.conditionEvaluator.isVisible(field, this.form);
498
+ }
499
+ onSubmit() {
500
+ this.form.markAllAsTouched();
501
+ if (this.form.invalid) {
502
+ this.cdr.markForCheck();
503
+ return;
504
+ }
505
+ this.formSubmit.emit({
506
+ value: this.form.value,
507
+ rawValue: this.form.getRawValue(), // includes disabled fields
508
+ });
509
+ }
510
+ onReset() {
511
+ this.form.reset();
512
+ this.formReset.emit();
513
+ }
514
+ /** Public API: access the FormGroup from the parent */
515
+ getForm() {
516
+ return this.form;
517
+ }
518
+ /** Public API: patch values from outside */
519
+ patchValues(values) {
520
+ this.form.patchValue(values);
521
+ }
522
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: DynamicFormComponent, deps: [{ token: DynamicFormBuilderService }, { token: ConditionEvaluatorService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
523
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: DynamicFormComponent, isStandalone: true, selector: "ngx-dynamic-form", inputs: { config: "config", initialValues: "initialValues" }, outputs: { formSubmit: "formSubmit", formChange: "formChange", formReset: "formReset" }, usesOnChanges: true, ngImport: i0, template: `
524
+ <form
525
+ [formGroup]="form"
526
+ (ngSubmit)="onSubmit()"
527
+ [class]="'ngx-form ngx-form--' + (config.layout || 'vertical') + ' ' + (config.cssClass || '')"
528
+ novalidate
529
+ >
530
+ <div class="ngx-form-grid">
531
+ @for (field of config.fields; track field.key) {
532
+ @if (isVisible(field)) {
533
+ <div [style.grid-column]="'span ' + (field.col ?? 12)">
534
+ <ngx-dynamic-field [field]="field" [form]="form" />
535
+ </div>
536
+ }
537
+ }
538
+ </div>
539
+
540
+ @if (config.debug) {
541
+ <pre class="ngx-debug">{{ form.value | json }}</pre>
542
+ }
543
+
544
+ <div class="ngx-form-actions">
545
+ @if (config.showReset) {
546
+ <button type="button" mat-stroked-button (click)="onReset()">
547
+ {{ config.resetLabel || 'Réinitialiser' }}
548
+ </button>
549
+ }
550
+ <button
551
+ type="submit"
552
+ mat-flat-button
553
+ color="primary"
554
+ [disabled]="form.invalid && form.touched"
555
+ >
556
+ {{ config.submitLabel || 'Envoyer' }}
557
+ </button>
558
+ </div>
559
+ </form>
560
+ `, isInline: true, dependencies: [{ kind: "pipe", type: JsonPipe, name: "json" }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: DynamicFieldComponent, selector: "ngx-dynamic-field", inputs: ["field", "form"] }, { kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }] }); }
561
+ }
562
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: DynamicFormComponent, decorators: [{
563
+ type: Component,
564
+ args: [{ selector: 'ngx-dynamic-form', standalone: true, imports: [JsonPipe, ReactiveFormsModule, DynamicFieldComponent, MatButton], template: `
565
+ <form
566
+ [formGroup]="form"
567
+ (ngSubmit)="onSubmit()"
568
+ [class]="'ngx-form ngx-form--' + (config.layout || 'vertical') + ' ' + (config.cssClass || '')"
569
+ novalidate
570
+ >
571
+ <div class="ngx-form-grid">
572
+ @for (field of config.fields; track field.key) {
573
+ @if (isVisible(field)) {
574
+ <div [style.grid-column]="'span ' + (field.col ?? 12)">
575
+ <ngx-dynamic-field [field]="field" [form]="form" />
576
+ </div>
577
+ }
578
+ }
579
+ </div>
580
+
581
+ @if (config.debug) {
582
+ <pre class="ngx-debug">{{ form.value | json }}</pre>
583
+ }
584
+
585
+ <div class="ngx-form-actions">
586
+ @if (config.showReset) {
587
+ <button type="button" mat-stroked-button (click)="onReset()">
588
+ {{ config.resetLabel || 'Réinitialiser' }}
589
+ </button>
590
+ }
591
+ <button
592
+ type="submit"
593
+ mat-flat-button
594
+ color="primary"
595
+ [disabled]="form.invalid && form.touched"
596
+ >
597
+ {{ config.submitLabel || 'Envoyer' }}
598
+ </button>
599
+ </div>
600
+ </form>
601
+ ` }]
602
+ }], ctorParameters: () => [{ type: DynamicFormBuilderService }, { type: ConditionEvaluatorService }, { type: i0.ChangeDetectorRef }], propDecorators: { config: [{
603
+ type: Input,
604
+ args: [{ required: true }]
605
+ }], initialValues: [{
606
+ type: Input
607
+ }], formSubmit: [{
608
+ type: Output
609
+ }], formChange: [{
610
+ type: Output
611
+ }], formReset: [{
612
+ type: Output
613
+ }] } });
614
+
615
+ class FormStorageAdapter {
616
+ generateId() {
617
+ return Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
618
+ }
619
+ }
620
+
621
+ class FormConfigService {
622
+ constructor() {
623
+ this.adapter = inject(FormStorageAdapter);
624
+ this._schemas = signal([]);
625
+ this.schemas = this._schemas.asReadonly();
626
+ this.adapter.list().subscribe(list => this._schemas.set(list));
627
+ }
628
+ get(id) {
629
+ return this._schemas().find(s => s.id === id) ?? null;
630
+ }
631
+ save(id, title, config) {
632
+ const schema = {
633
+ id,
634
+ title: title.trim() || 'Sans titre',
635
+ config,
636
+ updatedAt: new Date().toISOString(),
637
+ };
638
+ return this.adapter.save(schema).pipe(tap(saved => this._schemas.update(list => [...list.filter(s => s.id !== saved.id), saved])));
639
+ }
640
+ delete(id) {
641
+ return this.adapter.delete(id).pipe(tap(() => this._schemas.update(list => list.filter(s => s.id !== id))));
642
+ }
643
+ generateId() {
644
+ return this.adapter.generateId();
645
+ }
646
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormConfigService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
647
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormConfigService }); }
648
+ }
649
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormConfigService, decorators: [{
650
+ type: Injectable
651
+ }], ctorParameters: () => [] });
652
+
653
+ const FIELD_TYPES = [
654
+ { type: 'text', label: 'Texte', icon: 'text_fields', group: 'Saisie' },
655
+ { type: 'email', label: 'E-mail', icon: 'email', group: 'Saisie' },
656
+ { type: 'password', label: 'Mot de passe', icon: 'lock', group: 'Saisie' },
657
+ { type: 'number', label: 'Nombre', icon: 'pin', group: 'Saisie' },
658
+ { type: 'tel', label: 'Téléphone', icon: 'phone', group: 'Saisie' },
659
+ { type: 'url', label: 'URL', icon: 'link', group: 'Saisie' },
660
+ { type: 'date', label: 'Date', icon: 'calendar_today', group: 'Saisie' },
661
+ { type: 'textarea', label: 'Zone de texte', icon: 'notes', group: 'Saisie' },
662
+ { type: 'file', label: 'Fichier', icon: 'attach_file', group: 'Saisie' },
663
+ { type: 'select', label: 'Liste déroulante', icon: 'arrow_drop_down_circle', group: 'Choix' },
664
+ { type: 'multiselect', label: 'Multi-sélection', icon: 'checklist', group: 'Choix' },
665
+ { type: 'radio', label: 'Boutons radio', icon: 'radio_button_checked', group: 'Choix' },
666
+ { type: 'checkbox', label: 'Case à cocher', icon: 'check_box', group: 'Choix' },
667
+ { type: 'heading', label: 'Titre de section', icon: 'title', group: 'Mise en page' },
668
+ { type: 'divider', label: 'Séparateur', icon: 'horizontal_rule', group: 'Mise en page' },
669
+ ];
670
+ const FIELD_ICONS = Object.fromEntries(FIELD_TYPES.map(f => [f.type, f.icon]));
671
+ const GROUPS = ['Saisie', 'Choix', 'Mise en page'];
672
+ const COL_OPTIONS = [1, 2, 3, 4, 6, 8, 9, 12];
673
+ class FormBuilderComponent {
674
+ constructor() {
675
+ /** Route du lien "Retour" dans la topbar */
676
+ this.backRoute = '/';
677
+ this.formService = inject(FormConfigService);
678
+ this.snack = inject(MatSnackBar);
679
+ this.fields = signal([]);
680
+ this.selectedIdx = signal(null);
681
+ this.activeTab = signal('schema');
682
+ this.formTitle = signal('Mon formulaire');
683
+ this.submitLabel = signal('Envoyer');
684
+ this.resetLabel = signal('Réinitialiser');
685
+ this.showReset = signal(false);
686
+ this.currentId = signal(null);
687
+ this.showExport = signal(false);
688
+ this.exportContent = signal('');
689
+ this.savedSchemas = this.formService.schemas;
690
+ this.groups = GROUPS;
691
+ this.fieldTypes = FIELD_TYPES;
692
+ this.colOptions = COL_OPTIONS;
693
+ this.selectedField = computed(() => {
694
+ const i = this.selectedIdx();
695
+ return i !== null ? this.fields()[i] : null;
696
+ });
697
+ this.previewConfig = computed(() => ({
698
+ submitLabel: this.submitLabel(),
699
+ resetLabel: this.resetLabel(),
700
+ showReset: this.showReset(),
701
+ fields: this.fields(),
702
+ }));
703
+ this.groupedTypes = computed(() => GROUPS.map(g => ({ label: g, types: FIELD_TYPES.filter(f => f.group === g) })));
704
+ }
705
+ getIcon(type) { return FIELD_ICONS[type] ?? 'input'; }
706
+ getFieldDisplay(field) {
707
+ return field.label || field.text || field.placeholder || field.key;
708
+ }
709
+ isInputField(type) { return type !== 'divider' && type !== 'heading'; }
710
+ hasOptions(type) { return ['select', 'multiselect', 'radio'].includes(type); }
711
+ needsRows(type) { return type === 'textarea'; }
712
+ isHeading(type) { return type === 'heading'; }
713
+ isDivider(type) { return type === 'divider'; }
714
+ addField(type) {
715
+ const existing = this.fields().filter(f => f.type === type).length;
716
+ const suffix = existing > 0 ? existing + 1 : '';
717
+ const key = `${type.replace('-', '_')}${suffix}`;
718
+ const base = { key, type, col: 12 };
719
+ if (this.isInputField(type))
720
+ base.label = this._defaultLabel(type);
721
+ if (this.hasOptions(type))
722
+ base.options = [{ label: 'Option 1', value: 'option1' }];
723
+ if (type === 'heading')
724
+ Object.assign(base, { text: 'Nouvelle section', level: 3 });
725
+ if (type === 'radio')
726
+ base.defaultValue = null;
727
+ this.fields.update(f => [...f, base]);
728
+ this.selectedIdx.set(this.fields().length - 1);
729
+ this.activeTab.set('schema');
730
+ }
731
+ removeField(i, e) {
732
+ e.stopPropagation();
733
+ this.fields.update(f => f.filter((_, idx) => idx !== i));
734
+ if (this.selectedIdx() === i)
735
+ this.selectedIdx.set(null);
736
+ else if ((this.selectedIdx() ?? 0) > i)
737
+ this.selectedIdx.update(v => (v ?? 1) - 1);
738
+ }
739
+ moveUp(i, e) {
740
+ e.stopPropagation();
741
+ if (i === 0)
742
+ return;
743
+ this.fields.update(f => { const a = [...f]; [a[i - 1], a[i]] = [a[i], a[i - 1]]; return a; });
744
+ if (this.selectedIdx() === i)
745
+ this.selectedIdx.set(i - 1);
746
+ }
747
+ moveDown(i, e) {
748
+ e.stopPropagation();
749
+ if (i >= this.fields().length - 1)
750
+ return;
751
+ this.fields.update(f => { const a = [...f]; [a[i], a[i + 1]] = [a[i + 1], a[i]]; return a; });
752
+ if (this.selectedIdx() === i)
753
+ this.selectedIdx.set(i + 1);
754
+ }
755
+ selectField(i) { this.selectedIdx.set(this.selectedIdx() === i ? null : i); }
756
+ duplicateField(i, e) {
757
+ e.stopPropagation();
758
+ const clone = {
759
+ ...this.fields()[i],
760
+ key: `${this.fields()[i].key}_copy${Date.now().toString(36).slice(-3)}`,
761
+ };
762
+ this.fields.update(f => [...f.slice(0, i + 1), clone, ...f.slice(i + 1)]);
763
+ this.selectedIdx.set(i + 1);
764
+ }
765
+ updateProp(prop, value) {
766
+ const i = this.selectedIdx();
767
+ if (i === null)
768
+ return;
769
+ this.fields.update(list => { const c = [...list]; c[i] = { ...c[i], [prop]: value }; return c; });
770
+ }
771
+ updateValidation(prop, value) {
772
+ const i = this.selectedIdx();
773
+ if (i === null)
774
+ return;
775
+ this.fields.update(list => {
776
+ const c = [...list];
777
+ const field = { ...c[i] };
778
+ const valid = { ...field.validation };
779
+ if (value === '' || value === null || value === undefined || value === false) {
780
+ delete valid[prop];
781
+ }
782
+ else {
783
+ valid[prop] = value;
784
+ }
785
+ field.validation = Object.keys(valid).length ? valid : undefined;
786
+ c[i] = field;
787
+ return c;
788
+ });
789
+ }
790
+ getOptions() { return this.selectedField()?.options ?? []; }
791
+ addOption() {
792
+ const n = this.getOptions().length + 1;
793
+ this.updateProp('options', [...this.getOptions(), { label: `Option ${n}`, value: `option${n}` }]);
794
+ }
795
+ updateOption(idx, prop, value) {
796
+ const opts = [...this.getOptions()];
797
+ opts[idx] = { ...opts[idx], [prop]: value };
798
+ this.updateProp('options', opts);
799
+ }
800
+ removeOption(idx) {
801
+ this.updateProp('options', this.getOptions().filter((_, i) => i !== idx));
802
+ }
803
+ save() {
804
+ const id = this.currentId() ?? this.formService.generateId();
805
+ const title = this.formTitle();
806
+ this.formService.save(id, title, this.previewConfig()).subscribe(() => {
807
+ this.currentId.set(id);
808
+ this.snack.open(`✓ "${title}" sauvegardé`, '', { duration: 2500 });
809
+ });
810
+ }
811
+ loadSchema(id) {
812
+ const schema = this.formService.get(id);
813
+ if (!schema)
814
+ return;
815
+ this.fields.set(schema.config.fields ?? []);
816
+ this.formTitle.set(schema.title);
817
+ this.submitLabel.set(schema.config.submitLabel ?? 'Envoyer');
818
+ this.resetLabel.set(schema.config.resetLabel ?? 'Réinitialiser');
819
+ this.showReset.set(schema.config.showReset ?? false);
820
+ this.currentId.set(id);
821
+ this.selectedIdx.set(null);
822
+ this.snack.open(`✓ "${schema.title}" chargé`, '', { duration: 2000 });
823
+ }
824
+ deleteSchema(id, e) {
825
+ e.stopPropagation();
826
+ this.formService.delete(id).subscribe(() => {
827
+ if (this.currentId() === id)
828
+ this.newForm();
829
+ });
830
+ }
831
+ newForm() {
832
+ this.fields.set([]);
833
+ this.formTitle.set('Mon formulaire');
834
+ this.submitLabel.set('Envoyer');
835
+ this.resetLabel.set('Réinitialiser');
836
+ this.showReset.set(false);
837
+ this.currentId.set(null);
838
+ this.selectedIdx.set(null);
839
+ }
840
+ exportJSON() {
841
+ this.exportContent.set(JSON.stringify(this.previewConfig(), null, 2));
842
+ this.showExport.set(true);
843
+ }
844
+ exportTypeScript() {
845
+ const ts = `const config: DynamicFormConfig = ${JSON.stringify(this.previewConfig(), null, 2)
846
+ .replace(/"([a-zA-Z_][a-zA-Z0-9_]*)"\s*:/g, '$1:')
847
+ .replace(/"/g, "'")};\n`;
848
+ this.exportContent.set(ts);
849
+ this.showExport.set(true);
850
+ }
851
+ async copyExport() {
852
+ await navigator.clipboard.writeText(this.exportContent());
853
+ this.snack.open('✓ Copié dans le presse-papiers', '', { duration: 2000 });
854
+ }
855
+ copyFormUrl() {
856
+ const id = this.currentId();
857
+ if (!id) {
858
+ this.snack.open('Sauvegardez d\'abord le formulaire', '', { duration: 2000 });
859
+ return;
860
+ }
861
+ navigator.clipboard.writeText(`${location.origin}/form/${id}`);
862
+ this.snack.open(`✓ Lien copié : /form/${id}`, '', { duration: 3000 });
863
+ }
864
+ validationOf(key) {
865
+ return this.selectedField()?.validation?.[key] ?? null;
866
+ }
867
+ labelOf(type) {
868
+ return FIELD_TYPES.find(f => f.type === type)?.label ?? type;
869
+ }
870
+ trackByKey(_i, field) { return field.key; }
871
+ trackByIdx(i) { return i; }
872
+ _defaultLabel(type) {
873
+ const map = {
874
+ text: 'Texte', email: 'E-mail', password: 'Mot de passe', number: 'Nombre',
875
+ tel: 'Téléphone', url: 'URL', date: 'Date', textarea: 'Message',
876
+ select: 'Sélection', multiselect: 'Sélection multiple', radio: 'Choix',
877
+ checkbox: 'Option', file: 'Fichier',
878
+ };
879
+ return map[type] ?? type;
880
+ }
881
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormBuilderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
882
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: FormBuilderComponent, isStandalone: true, selector: "ngx-form-builder", inputs: { backRoute: "backRoute" }, ngImport: i0, template: "<div class=\"builder-root\">\n\n <!-- \u2500\u2500 Top Bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <header class=\"topbar\">\n <a [routerLink]=\"backRoute\" class=\"topbar-back\">\n <mat-icon>arrow_back</mat-icon>\n <span>Retour</span>\n </a>\n\n <div class=\"topbar-center\">\n <mat-icon class=\"topbar-logo\">dashboard_customize</mat-icon>\n <input\n class=\"title-input\"\n [ngModel]=\"formTitle()\"\n (ngModelChange)=\"formTitle.set($event)\"\n placeholder=\"Titre du formulaire\"\n aria-label=\"Titre du formulaire\"\n />\n </div>\n\n <div class=\"topbar-actions\">\n <button class=\"btn-icon\" matTooltip=\"Nouveau formulaire\" (click)=\"newForm()\">\n <mat-icon>add_circle_outline</mat-icon>\n </button>\n <button class=\"btn-icon\" matTooltip=\"Copier le lien du formulaire\" (click)=\"copyFormUrl()\">\n <mat-icon>share</mat-icon>\n </button>\n <button class=\"btn-outline\" (click)=\"exportJSON()\">\n <mat-icon>data_object</mat-icon> JSON\n </button>\n <button class=\"btn-outline\" (click)=\"exportTypeScript()\">\n <mat-icon>code</mat-icon> TypeScript\n </button>\n <button class=\"btn-primary\" (click)=\"save()\">\n <mat-icon>save</mat-icon> Sauvegarder\n </button>\n </div>\n </header>\n\n <!-- \u2500\u2500 Main Layout \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"builder-body\">\n\n <!-- \u2500\u2500 Left: Palette \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <aside class=\"palette\">\n\n <div class=\"palette-section\">\n <p class=\"palette-section-title\">Types de champs</p>\n @for (group of groupedTypes(); track group.label) {\n <p class=\"palette-group-label\">{{ group.label }}</p>\n @for (ft of group.types; track ft.type) {\n <button class=\"palette-btn\" (click)=\"addField(ft.type)\" matTooltip=\"Ajouter {{ ft.label }}\">\n <mat-icon>{{ ft.icon }}</mat-icon>\n <span>{{ ft.label }}</span>\n </button>\n }\n }\n </div>\n\n <!-- Saved schemas -->\n <div class=\"palette-section\">\n <p class=\"palette-section-title\">\n <mat-icon style=\"font-size:1rem;width:1rem;height:1rem;vertical-align:middle\">folder_open</mat-icon>\n Formulaires sauvegard\u00E9s\n </p>\n @if (savedSchemas().length === 0) {\n <p class=\"palette-empty\">Aucun formulaire sauvegard\u00E9</p>\n }\n @for (schema of savedSchemas(); track schema.id) {\n <div class=\"saved-item\" [class.active]=\"currentId() === schema.id\">\n <button class=\"saved-item-name\" (click)=\"loadSchema(schema.id)\" matTooltip=\"Charger\">\n <mat-icon>description</mat-icon>\n <span>{{ schema.title }}</span>\n </button>\n <button class=\"saved-item-delete\" (click)=\"deleteSchema(schema.id, $event)\" matTooltip=\"Supprimer\">\n <mat-icon>delete_outline</mat-icon>\n </button>\n </div>\n }\n </div>\n\n </aside>\n\n <!-- \u2500\u2500 Center: Canvas \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <main class=\"canvas\">\n\n <!-- Tab bar -->\n <div class=\"canvas-tabs\">\n <button\n class=\"canvas-tab\"\n [class.active]=\"activeTab() === 'schema'\"\n (click)=\"activeTab.set('schema')\"\n >\n <mat-icon>list_alt</mat-icon> Sch\u00E9ma\n </button>\n <button\n class=\"canvas-tab\"\n [class.active]=\"activeTab() === 'preview'\"\n (click)=\"activeTab.set('preview')\"\n >\n <mat-icon>visibility</mat-icon> Aper\u00E7u\n </button>\n <span class=\"canvas-tabs-info\">\n {{ fields().length }} champ{{ fields().length !== 1 ? 's' : '' }}\n </span>\n </div>\n\n <!-- \u2500\u2500 Schema Tab \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (activeTab() === 'schema') {\n <div class=\"schema-view\">\n\n @if (fields().length === 0) {\n <div class=\"empty-canvas\">\n <mat-icon>add_box</mat-icon>\n <p>Cliquez sur un type de champ \u00E0 gauche pour commencer</p>\n </div>\n }\n\n <div class=\"field-list\">\n @for (field of fields(); track trackByKey($index, field); let i = $index) {\n <div\n class=\"field-card\"\n [class.selected]=\"selectedIdx() === i\"\n (click)=\"selectField(i)\"\n >\n <mat-icon class=\"field-card-icon\">{{ getIcon(field.type) }}</mat-icon>\n\n <div class=\"field-card-body\">\n <span class=\"field-card-label\">{{ getFieldDisplay(field) }}</span>\n <span class=\"field-card-meta\">{{ labelOf(field.type) }} \u00B7 col-{{ field.col ?? 12 }}</span>\n </div>\n\n @if (field.key) {\n <code class=\"field-card-key\">{{ field.key }}</code>\n }\n\n <div class=\"field-card-actions\">\n <button class=\"action-btn\" (click)=\"moveUp(i, $event)\" [disabled]=\"i === 0\" matTooltip=\"Monter\">\n <mat-icon>arrow_upward</mat-icon>\n </button>\n <button class=\"action-btn\" (click)=\"moveDown(i, $event)\" [disabled]=\"i === fields().length - 1\" matTooltip=\"Descendre\">\n <mat-icon>arrow_downward</mat-icon>\n </button>\n <button class=\"action-btn\" (click)=\"duplicateField(i, $event)\" matTooltip=\"Dupliquer\">\n <mat-icon>content_copy</mat-icon>\n </button>\n <button class=\"action-btn danger\" (click)=\"removeField(i, $event)\" matTooltip=\"Supprimer\">\n <mat-icon>delete_outline</mat-icon>\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Form settings -->\n <div class=\"form-settings\">\n <p class=\"settings-title\">\n <mat-icon>settings</mat-icon> Param\u00E8tres du formulaire\n </p>\n <div class=\"settings-row\">\n <label>Bouton Envoyer</label>\n <input [ngModel]=\"submitLabel()\" (ngModelChange)=\"submitLabel.set($event)\" placeholder=\"Envoyer\" />\n </div>\n <div class=\"settings-row\">\n <label>\n <input type=\"checkbox\" [ngModel]=\"showReset()\" (ngModelChange)=\"showReset.set($event)\" />\n Afficher R\u00E9initialiser\n </label>\n </div>\n @if (showReset()) {\n <div class=\"settings-row\">\n <label>Bouton Reset</label>\n <input [ngModel]=\"resetLabel()\" (ngModelChange)=\"resetLabel.set($event)\" placeholder=\"R\u00E9initialiser\" />\n </div>\n }\n </div>\n\n </div>\n }\n\n <!-- \u2500\u2500 Preview Tab \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (activeTab() === 'preview') {\n <div class=\"preview-view\">\n @if (fields().length === 0) {\n <div class=\"empty-canvas\">\n <mat-icon>preview</mat-icon>\n <p>Ajoutez des champs pour voir l'aper\u00E7u du formulaire</p>\n </div>\n } @else {\n <div class=\"preview-wrapper\">\n <p class=\"preview-hint\">\n <mat-icon>info_outline</mat-icon>\n Aper\u00E7u en temps r\u00E9el \u2014 les donn\u00E9es ne sont pas envoy\u00E9es\n </p>\n <ngx-dynamic-form [config]=\"previewConfig()\" />\n </div>\n }\n </div>\n }\n\n </main>\n\n <!-- \u2500\u2500 Right: Properties \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <aside class=\"properties\">\n\n @if (selectedField(); as field) {\n <div class=\"props-header\">\n <mat-icon>{{ getIcon(field.type) }}</mat-icon>\n <span>{{ labelOf(field.type) }}</span>\n <button class=\"props-close\" (click)=\"selectedIdx.set(null)\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n\n <div class=\"props-body\">\n\n <!-- \u2500\u2500 G\u00C9N\u00C9RAL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <section class=\"prop-section\">\n <p class=\"prop-section-title\">G\u00E9n\u00E9ral</p>\n\n <div class=\"prop-row\">\n <label>Cl\u00E9 (key)</label>\n <input [ngModel]=\"field.key\" (ngModelChange)=\"updateProp('key', $event)\" placeholder=\"mon_champ\" />\n </div>\n\n <div class=\"prop-row\">\n <label>Largeur (col)</label>\n <select [ngModel]=\"field.col ?? 12\" (ngModelChange)=\"updateProp('col', +$event)\">\n @for (c of colOptions; track c) {\n <option [value]=\"c\">{{ c }} / 12</option>\n }\n </select>\n </div>\n\n @if (!isDivider(field.type) && !isHeading(field.type)) {\n <div class=\"prop-row\">\n <label>Label</label>\n <input [ngModel]=\"field.label\" (ngModelChange)=\"updateProp('label', $event)\" placeholder=\"Libell\u00E9 du champ\" />\n </div>\n }\n\n @if (isHeading(field.type)) {\n <div class=\"prop-row\">\n <label>Texte</label>\n <input [ngModel]=\"field.text\" (ngModelChange)=\"updateProp('text', $event)\" placeholder=\"Titre de section\" />\n </div>\n <div class=\"prop-row\">\n <label>Niveau (h1\u2013h6)</label>\n <select [ngModel]=\"field.level ?? 3\" (ngModelChange)=\"updateProp('level', +$event)\">\n @for (n of [1,2,3,4,5,6]; track n) {\n <option [value]=\"n\">h{{ n }}</option>\n }\n </select>\n </div>\n }\n </section>\n\n <!-- \u2500\u2500 SAISIE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (isInputField(field.type)) {\n <section class=\"prop-section\">\n <p class=\"prop-section-title\">Saisie</p>\n\n <div class=\"prop-row\">\n <label>Placeholder</label>\n <input [ngModel]=\"field.placeholder\" (ngModelChange)=\"updateProp('placeholder', $event)\" placeholder=\"Texte indicatif\" />\n </div>\n\n <div class=\"prop-row\">\n <label>Texte d'aide (hint)</label>\n <input [ngModel]=\"field.hint\" (ngModelChange)=\"updateProp('hint', $event)\" placeholder=\"Aide visible sous le champ\" />\n </div>\n\n @if (needsRows(field.type)) {\n <div class=\"prop-row\">\n <label>Lignes (rows)</label>\n <input type=\"number\" min=\"2\" max=\"20\" [ngModel]=\"field.rows ?? 4\" (ngModelChange)=\"updateProp('rows', +$event)\" />\n </div>\n }\n\n <div class=\"prop-row\">\n <label>\n <input type=\"checkbox\" [ngModel]=\"field.readonly ?? false\" (ngModelChange)=\"updateProp('readonly', $event)\" />\n Lecture seule (readonly)\n </label>\n </div>\n </section>\n\n <!-- \u2500\u2500 IC\u00D4NES \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <section class=\"prop-section\">\n <p class=\"prop-section-title\">Ic\u00F4nes <span class=\"prop-hint\">Material Icons</span></p>\n <div class=\"prop-row\">\n <label>Pr\u00E9fixe (gauche)</label>\n <input [ngModel]=\"field.icon\" (ngModelChange)=\"updateProp('icon', $event || undefined)\" placeholder=\"person, email, lock\u2026\" />\n </div>\n <div class=\"prop-row\">\n <label>Suffixe (droite)</label>\n <input [ngModel]=\"field.iconSuffix\" (ngModelChange)=\"updateProp('iconSuffix', $event || undefined)\" placeholder=\"visibility, search\u2026\" />\n </div>\n </section>\n\n <!-- \u2500\u2500 VALIDATION \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <section class=\"prop-section\">\n <p class=\"prop-section-title\">Validation</p>\n\n <div class=\"prop-row\">\n <label>\n <input type=\"checkbox\" [ngModel]=\"!!field.validation?.required\" (ngModelChange)=\"updateValidation('required', $event)\" />\n Champ obligatoire (required)\n </label>\n </div>\n\n @if (['text','email','password','textarea','url'].includes(field.type)) {\n <div class=\"prop-row\">\n <label>Longueur min</label>\n <input type=\"number\" min=\"0\"\n [ngModel]=\"field.validation?.minLength ?? ''\"\n (ngModelChange)=\"updateValidation('minLength', $event ? +$event : null)\"\n placeholder=\"\u2013\" />\n </div>\n <div class=\"prop-row\">\n <label>Longueur max</label>\n <input type=\"number\" min=\"0\"\n [ngModel]=\"field.validation?.maxLength ?? ''\"\n (ngModelChange)=\"updateValidation('maxLength', $event ? +$event : null)\"\n placeholder=\"\u2013\" />\n </div>\n }\n\n @if (field.type === 'number') {\n <div class=\"prop-row\">\n <label>Valeur min</label>\n <input type=\"number\"\n [ngModel]=\"field.validation?.min ?? ''\"\n (ngModelChange)=\"updateValidation('min', $event !== '' ? +$event : null)\"\n placeholder=\"\u2013\" />\n </div>\n <div class=\"prop-row\">\n <label>Valeur max</label>\n <input type=\"number\"\n [ngModel]=\"field.validation?.max ?? ''\"\n (ngModelChange)=\"updateValidation('max', $event !== '' ? +$event : null)\"\n placeholder=\"\u2013\" />\n </div>\n }\n\n @if (field.type === 'email') {\n <div class=\"prop-row\">\n <label>\n <input type=\"checkbox\" [ngModel]=\"!!field.validation?.email\" (ngModelChange)=\"updateValidation('email', $event)\" />\n Valider le format e-mail\n </label>\n </div>\n }\n\n @if (field.type === 'file') {\n <div class=\"prop-row\">\n <label>Types accept\u00E9s</label>\n <input [ngModel]=\"field.accept\" (ngModelChange)=\"updateProp('accept', $event)\" placeholder=\".pdf,.docx,image/*\" />\n </div>\n <div class=\"prop-row\">\n <label>\n <input type=\"checkbox\" [ngModel]=\"field.multiple ?? false\" (ngModelChange)=\"updateProp('multiple', $event)\" />\n Fichiers multiples\n </label>\n </div>\n }\n </section>\n }\n\n <!-- \u2500\u2500 OPTIONS (select / radio) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (hasOptions(field.type)) {\n <section class=\"prop-section\">\n <p class=\"prop-section-title\">Options</p>\n <div class=\"options-list\">\n @for (opt of getOptions(); track trackByIdx($index); let oi = $index) {\n <div class=\"option-row\">\n <input placeholder=\"Label\" [ngModel]=\"opt.label\" (ngModelChange)=\"updateOption(oi, 'label', $event)\" />\n <input placeholder=\"Valeur\" [ngModel]=\"opt.value\" (ngModelChange)=\"updateOption(oi, 'value', $event)\" />\n <button class=\"action-btn danger\" (click)=\"removeOption(oi)\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n }\n </div>\n <button class=\"btn-add-option\" (click)=\"addOption()\">\n <mat-icon>add</mat-icon> Ajouter une option\n </button>\n </section>\n }\n\n </div>\n\n } @else {\n <div class=\"props-empty\">\n <mat-icon>tune</mat-icon>\n <p>S\u00E9lectionnez un champ pour modifier ses propri\u00E9t\u00E9s</p>\n </div>\n }\n\n </aside>\n\n </div><!-- /.builder-body -->\n\n</div><!-- /.builder-root -->\n\n<!-- \u2500\u2500 Export Modal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n@if (showExport()) {\n <div class=\"modal-overlay\" (click)=\"showExport.set(false)\">\n <div class=\"modal\" (click)=\"$event.stopPropagation()\">\n <div class=\"modal-header\">\n <span>Code export\u00E9</span>\n <button (click)=\"showExport.set(false)\"><mat-icon>close</mat-icon></button>\n </div>\n <textarea class=\"modal-code\" readonly [value]=\"exportContent()\"></textarea>\n <div class=\"modal-footer\">\n <button class=\"btn-primary\" (click)=\"copyExport()\">\n <mat-icon>content_copy</mat-icon> Copier\n </button>\n <button class=\"btn-outline\" (click)=\"showExport.set(false)\">Fermer</button>\n </div>\n </div>\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;height:100vh;overflow:hidden;font-family:Roboto,sans-serif;--palette-w: 210px;--props-w: 280px;--topbar-h: 52px;--border: #e2e8f0;--bg: #f8fafc;--surface: #ffffff;--primary: #2563eb;--primary-light: #eff6ff;--danger: #dc2626;--text: #1e293b;--text-muted: #64748b;--radius: 8px}.topbar{display:flex;align-items:center;gap:1rem;height:var(--topbar-h);padding:0 1rem;background:var(--surface);border-bottom:1px solid var(--border);box-shadow:0 1px 4px #0000000f;z-index:10;flex-shrink:0}.topbar-back{display:inline-flex;align-items:center;gap:.25rem;color:var(--text-muted);text-decoration:none;font-size:.85rem;white-space:nowrap}.topbar-back:hover{color:var(--primary)}.topbar-back mat-icon{font-size:1rem;width:1rem;height:1rem}.topbar-center{display:flex;align-items:center;gap:.5rem;flex:1;min-width:0}.topbar-logo{color:var(--primary);flex-shrink:0}.title-input{flex:1;min-width:0;border:none;border-bottom:2px solid transparent;background:transparent;font-size:1rem;font-weight:600;color:var(--text);padding:.25rem .5rem;border-radius:4px;outline:none;transition:border-color .15s}.title-input:focus{border-bottom-color:var(--primary)}.title-input::placeholder{color:var(--text-muted);font-weight:400}.topbar-actions{display:flex;align-items:center;gap:.5rem;flex-shrink:0}.btn-icon{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;border:none;background:transparent;border-radius:var(--radius);color:var(--text-muted);cursor:pointer;transition:background .15s,color .15s}.btn-icon:hover{background:var(--bg);color:var(--text)}.btn-icon mat-icon{font-size:1.1rem;width:1.1rem;height:1.1rem}.btn-outline{display:inline-flex;align-items:center;gap:.3rem;padding:.3rem .75rem;border:1px solid var(--border);background:var(--surface);border-radius:var(--radius);font-size:.82rem;color:var(--text);cursor:pointer;transition:background .15s,border-color .15s}.btn-outline:hover{background:var(--bg);border-color:#cbd5e1}.btn-outline mat-icon{font-size:.95rem;width:.95rem;height:.95rem}.btn-primary{display:inline-flex;align-items:center;gap:.3rem;padding:.35rem .85rem;border:none;background:var(--primary);color:#fff;border-radius:var(--radius);font-size:.85rem;font-weight:500;cursor:pointer;transition:background .15s}.btn-primary:hover{background:#1d4ed8}.btn-primary mat-icon{font-size:.95rem;width:.95rem;height:.95rem}.builder-body{display:grid;grid-template-columns:var(--palette-w) 1fr var(--props-w);height:calc(100vh - var(--topbar-h));overflow:hidden}.palette{background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow-y:auto;padding:.75rem .5rem;gap:1rem}.palette-section{display:flex;flex-direction:column;gap:.2rem}.palette-section-title{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);padding:.25rem .5rem;margin:.4rem 0 .2rem}.palette-group-label{font-size:.7rem;color:var(--text-muted);padding:.1rem .5rem;margin-top:.4rem;margin-bottom:.05rem}.palette-btn{display:flex;align-items:center;gap:.5rem;padding:.45rem .65rem;border:none;background:transparent;border-radius:6px;color:var(--text);font-size:.82rem;cursor:pointer;text-align:left;transition:background .12s}.palette-btn:hover{background:var(--primary-light);color:var(--primary)}.palette-btn mat-icon{font-size:1rem;width:1rem;height:1rem;color:var(--text-muted)}.palette-btn:hover mat-icon{color:var(--primary)}.palette-empty{font-size:.78rem;color:var(--text-muted);padding:.25rem .5rem;font-style:italic}.saved-item{display:flex;align-items:center;border-radius:6px;overflow:hidden}.saved-item.active{background:var(--primary-light)}.saved-item .saved-item-name{flex:1;display:flex;align-items:center;gap:.35rem;padding:.4rem .5rem;border:none;background:transparent;font-size:.8rem;color:var(--text);cursor:pointer;text-align:left;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;min-width:0;transition:color .12s}.saved-item .saved-item-name:hover{color:var(--primary)}.saved-item .saved-item-name mat-icon{font-size:.9rem;width:.9rem;height:.9rem;flex-shrink:0}.saved-item .saved-item-name span{overflow:hidden;text-overflow:ellipsis}.saved-item .saved-item-delete{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;background:transparent;color:var(--text-muted);cursor:pointer;border-radius:4px;flex-shrink:0;transition:color .12s,background .12s}.saved-item .saved-item-delete:hover{color:var(--danger);background:#fee2e2}.saved-item .saved-item-delete mat-icon{font-size:.9rem;width:.9rem;height:.9rem}.canvas{display:flex;flex-direction:column;background:var(--bg);overflow:hidden}.canvas-tabs{display:flex;align-items:center;gap:.25rem;padding:.5rem .75rem;background:var(--surface);border-bottom:1px solid var(--border);flex-shrink:0}.canvas-tab{display:inline-flex;align-items:center;gap:.3rem;padding:.3rem .75rem;border:1px solid transparent;background:transparent;border-radius:var(--radius);font-size:.82rem;color:var(--text-muted);cursor:pointer;transition:background .12s,color .12s,border-color .12s}.canvas-tab mat-icon{font-size:.95rem;width:.95rem;height:.95rem}.canvas-tab.active{background:var(--primary-light);color:var(--primary);border-color:#bfdbfe;font-weight:500}.canvas-tab:not(.active):hover{background:var(--bg);color:var(--text)}.canvas-tabs-info{margin-left:auto;font-size:.78rem;color:var(--text-muted)}.schema-view{flex:1;overflow-y:auto;padding:1rem;display:flex;flex-direction:column;gap:.75rem}.empty-canvas{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;padding:3rem 1rem;color:var(--text-muted);text-align:center;border:2px dashed var(--border);border-radius:var(--radius)}.empty-canvas mat-icon{font-size:2.5rem;width:2.5rem;height:2.5rem;opacity:.5}.empty-canvas p{font-size:.9rem;margin:0}.field-list{display:flex;flex-direction:column;gap:.5rem}.field-card{display:flex;align-items:center;gap:.75rem;background:var(--surface);border:1.5px solid var(--border);border-radius:var(--radius);padding:.6rem .75rem;cursor:pointer;transition:border-color .15s,box-shadow .15s}.field-card:hover{border-color:#93c5fd;box-shadow:0 1px 6px #2563eb1a}.field-card.selected{border-color:var(--primary);box-shadow:0 0 0 2px #bfdbfe}.field-card-icon{font-size:1.2rem;width:1.2rem;height:1.2rem;color:var(--primary);flex-shrink:0}.field-card-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:.1rem}.field-card-label{font-size:.85rem;font-weight:500;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.field-card-meta{font-size:.72rem;color:var(--text-muted)}.field-card-key{font-size:.72rem;background:#f1f5f9;color:#0f172a;padding:.1rem .35rem;border-radius:4px;font-family:Fira Code,Courier New,monospace;white-space:nowrap;flex-shrink:0}.field-card-actions{display:flex;align-items:center;gap:.15rem;flex-shrink:0}.action-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;background:transparent;border-radius:5px;color:var(--text-muted);cursor:pointer;transition:background .12s,color .12s}.action-btn:hover{background:var(--bg);color:var(--text)}.action-btn.danger:hover{background:#fee2e2;color:var(--danger)}.action-btn:disabled{opacity:.35;pointer-events:none}.action-btn mat-icon{font-size:.95rem;width:.95rem;height:.95rem}.form-settings{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:.75rem 1rem;display:flex;flex-direction:column;gap:.5rem;margin-top:.25rem}.settings-title{display:flex;align-items:center;gap:.35rem;font-size:.78rem;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.04em;margin:0 0 .25rem}.settings-title mat-icon{font-size:.9rem;width:.9rem;height:.9rem}.settings-row{display:flex;align-items:center;gap:.75rem;font-size:.83rem}.settings-row label{display:flex;align-items:center;gap:.4rem;color:var(--text);white-space:nowrap;cursor:pointer;min-width:130px}.settings-row input[type=text],.settings-row input:not([type=checkbox]){flex:1;padding:.3rem .5rem;border:1px solid var(--border);border-radius:5px;font-size:.83rem;outline:none}.settings-row input[type=text]:focus,.settings-row input:not([type=checkbox]):focus{border-color:var(--primary)}.preview-view{flex:1;overflow-y:auto;padding:1.5rem;display:flex;flex-direction:column;align-items:center}.preview-wrapper{width:100%;max-width:720px;display:flex;flex-direction:column;gap:.75rem}.preview-hint{display:flex;align-items:center;gap:.4rem;font-size:.78rem;color:var(--text-muted);background:#fffbeb;border:1px solid #fde68a;border-radius:6px;padding:.4rem .75rem;margin:0}.preview-hint mat-icon{font-size:.9rem;width:.9rem;height:.9rem;color:#b45309}.properties{background:var(--surface);border-left:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden}.props-header{display:flex;align-items:center;gap:.5rem;padding:.65rem .85rem;background:var(--primary-light);border-bottom:1px solid #bfdbfe;flex-shrink:0}.props-header mat-icon{color:var(--primary);font-size:1rem;width:1rem;height:1rem}.props-header span{flex:1;font-size:.85rem;font-weight:600;color:var(--primary)}.props-close{display:flex;align-items:center;justify-content:center;width:26px;height:26px;border:none;background:transparent;color:var(--text-muted);cursor:pointer;border-radius:4px;transition:background .12s}.props-close:hover{background:#dbeafe}.props-close mat-icon{font-size:.9rem;width:.9rem;height:.9rem}.props-body{flex:1;overflow-y:auto;padding:.5rem;display:flex;flex-direction:column;gap:.25rem}.props-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.75rem;color:var(--text-muted);padding:2rem 1rem;text-align:center}.props-empty mat-icon{font-size:2rem;width:2rem;height:2rem;opacity:.4}.props-empty p{font-size:.85rem;margin:0;max-width:180px}.prop-section{background:var(--bg);border:1px solid var(--border);border-radius:7px;padding:.6rem .75rem;display:flex;flex-direction:column;gap:.4rem}.prop-section-title{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);margin:0 0 .3rem;display:flex;align-items:center;gap:.4rem}.prop-hint{font-size:.68rem;font-weight:400;text-transform:none;letter-spacing:0;color:#94a3b8;margin-left:.25rem}.prop-row{display:flex;flex-direction:column;gap:.2rem;font-size:.82rem}.prop-row>label{display:flex;align-items:center;gap:.4rem;color:var(--text);font-size:.8rem;cursor:pointer}.prop-row>input[type=text],.prop-row>input[type=number],.prop-row>input:not([type=checkbox]),.prop-row>select{width:100%;padding:.3rem .5rem;border:1px solid var(--border);border-radius:5px;font-size:.82rem;color:var(--text);background:var(--surface);outline:none;box-sizing:border-box;transition:border-color .12s}.prop-row>input[type=text]:focus,.prop-row>input[type=number]:focus,.prop-row>input:not([type=checkbox]):focus,.prop-row>select:focus{border-color:var(--primary)}.options-list{display:flex;flex-direction:column;gap:.35rem}.option-row{display:flex;align-items:center;gap:.3rem}.option-row input{flex:1;padding:.28rem .45rem;border:1px solid var(--border);border-radius:5px;font-size:.78rem;outline:none}.option-row input:focus{border-color:var(--primary)}.option-row .action-btn{width:24px;height:24px;flex-shrink:0}.option-row .action-btn mat-icon{font-size:.85rem;width:.85rem;height:.85rem}.btn-add-option{display:inline-flex;align-items:center;gap:.25rem;padding:.28rem .6rem;border:1px dashed var(--border);background:transparent;border-radius:5px;font-size:.78rem;color:var(--text-muted);cursor:pointer;transition:border-color .12s,color .12s;margin-top:.25rem}.btn-add-option:hover{border-color:var(--primary);color:var(--primary)}.btn-add-option mat-icon{font-size:.9rem;width:.9rem;height:.9rem}.modal-overlay{position:fixed;inset:0;background:#00000073;z-index:200;display:flex;align-items:center;justify-content:center;padding:1.5rem}.modal{background:var(--surface);border-radius:10px;box-shadow:0 20px 60px #0003;display:flex;flex-direction:column;width:100%;max-width:700px;max-height:80vh;overflow:hidden}.modal-header{display:flex;align-items:center;justify-content:space-between;padding:.75rem 1rem;border-bottom:1px solid var(--border);font-weight:600;font-size:.9rem}.modal-header button{display:flex;align-items:center;border:none;background:transparent;cursor:pointer;color:var(--text-muted);border-radius:4px;padding:.2rem;transition:background .12s}.modal-header button:hover{background:var(--bg)}.modal-header button mat-icon{font-size:1rem;width:1rem;height:1rem}.modal-code{flex:1;padding:1rem;font-family:Fira Code,Courier New,monospace;font-size:.8rem;color:var(--text);border:none;resize:none;outline:none;background:#1e293b;color:#e2e8f0;overflow-y:auto;min-height:320px}.modal-footer{display:flex;gap:.5rem;padding:.75rem 1rem;border-top:1px solid var(--border);justify-content:flex-end}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: DynamicFormComponent, selector: "ngx-dynamic-form", inputs: ["config", "initialValues"], outputs: ["formSubmit", "formChange", "formReset"] }] }); }
883
+ }
884
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormBuilderComponent, decorators: [{
885
+ type: Component,
886
+ args: [{ selector: 'ngx-form-builder', standalone: true, imports: [FormsModule, MatIcon, MatTooltip, RouterLink, DynamicFormComponent], template: "<div class=\"builder-root\">\n\n <!-- \u2500\u2500 Top Bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <header class=\"topbar\">\n <a [routerLink]=\"backRoute\" class=\"topbar-back\">\n <mat-icon>arrow_back</mat-icon>\n <span>Retour</span>\n </a>\n\n <div class=\"topbar-center\">\n <mat-icon class=\"topbar-logo\">dashboard_customize</mat-icon>\n <input\n class=\"title-input\"\n [ngModel]=\"formTitle()\"\n (ngModelChange)=\"formTitle.set($event)\"\n placeholder=\"Titre du formulaire\"\n aria-label=\"Titre du formulaire\"\n />\n </div>\n\n <div class=\"topbar-actions\">\n <button class=\"btn-icon\" matTooltip=\"Nouveau formulaire\" (click)=\"newForm()\">\n <mat-icon>add_circle_outline</mat-icon>\n </button>\n <button class=\"btn-icon\" matTooltip=\"Copier le lien du formulaire\" (click)=\"copyFormUrl()\">\n <mat-icon>share</mat-icon>\n </button>\n <button class=\"btn-outline\" (click)=\"exportJSON()\">\n <mat-icon>data_object</mat-icon> JSON\n </button>\n <button class=\"btn-outline\" (click)=\"exportTypeScript()\">\n <mat-icon>code</mat-icon> TypeScript\n </button>\n <button class=\"btn-primary\" (click)=\"save()\">\n <mat-icon>save</mat-icon> Sauvegarder\n </button>\n </div>\n </header>\n\n <!-- \u2500\u2500 Main Layout \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"builder-body\">\n\n <!-- \u2500\u2500 Left: Palette \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <aside class=\"palette\">\n\n <div class=\"palette-section\">\n <p class=\"palette-section-title\">Types de champs</p>\n @for (group of groupedTypes(); track group.label) {\n <p class=\"palette-group-label\">{{ group.label }}</p>\n @for (ft of group.types; track ft.type) {\n <button class=\"palette-btn\" (click)=\"addField(ft.type)\" matTooltip=\"Ajouter {{ ft.label }}\">\n <mat-icon>{{ ft.icon }}</mat-icon>\n <span>{{ ft.label }}</span>\n </button>\n }\n }\n </div>\n\n <!-- Saved schemas -->\n <div class=\"palette-section\">\n <p class=\"palette-section-title\">\n <mat-icon style=\"font-size:1rem;width:1rem;height:1rem;vertical-align:middle\">folder_open</mat-icon>\n Formulaires sauvegard\u00E9s\n </p>\n @if (savedSchemas().length === 0) {\n <p class=\"palette-empty\">Aucun formulaire sauvegard\u00E9</p>\n }\n @for (schema of savedSchemas(); track schema.id) {\n <div class=\"saved-item\" [class.active]=\"currentId() === schema.id\">\n <button class=\"saved-item-name\" (click)=\"loadSchema(schema.id)\" matTooltip=\"Charger\">\n <mat-icon>description</mat-icon>\n <span>{{ schema.title }}</span>\n </button>\n <button class=\"saved-item-delete\" (click)=\"deleteSchema(schema.id, $event)\" matTooltip=\"Supprimer\">\n <mat-icon>delete_outline</mat-icon>\n </button>\n </div>\n }\n </div>\n\n </aside>\n\n <!-- \u2500\u2500 Center: Canvas \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <main class=\"canvas\">\n\n <!-- Tab bar -->\n <div class=\"canvas-tabs\">\n <button\n class=\"canvas-tab\"\n [class.active]=\"activeTab() === 'schema'\"\n (click)=\"activeTab.set('schema')\"\n >\n <mat-icon>list_alt</mat-icon> Sch\u00E9ma\n </button>\n <button\n class=\"canvas-tab\"\n [class.active]=\"activeTab() === 'preview'\"\n (click)=\"activeTab.set('preview')\"\n >\n <mat-icon>visibility</mat-icon> Aper\u00E7u\n </button>\n <span class=\"canvas-tabs-info\">\n {{ fields().length }} champ{{ fields().length !== 1 ? 's' : '' }}\n </span>\n </div>\n\n <!-- \u2500\u2500 Schema Tab \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (activeTab() === 'schema') {\n <div class=\"schema-view\">\n\n @if (fields().length === 0) {\n <div class=\"empty-canvas\">\n <mat-icon>add_box</mat-icon>\n <p>Cliquez sur un type de champ \u00E0 gauche pour commencer</p>\n </div>\n }\n\n <div class=\"field-list\">\n @for (field of fields(); track trackByKey($index, field); let i = $index) {\n <div\n class=\"field-card\"\n [class.selected]=\"selectedIdx() === i\"\n (click)=\"selectField(i)\"\n >\n <mat-icon class=\"field-card-icon\">{{ getIcon(field.type) }}</mat-icon>\n\n <div class=\"field-card-body\">\n <span class=\"field-card-label\">{{ getFieldDisplay(field) }}</span>\n <span class=\"field-card-meta\">{{ labelOf(field.type) }} \u00B7 col-{{ field.col ?? 12 }}</span>\n </div>\n\n @if (field.key) {\n <code class=\"field-card-key\">{{ field.key }}</code>\n }\n\n <div class=\"field-card-actions\">\n <button class=\"action-btn\" (click)=\"moveUp(i, $event)\" [disabled]=\"i === 0\" matTooltip=\"Monter\">\n <mat-icon>arrow_upward</mat-icon>\n </button>\n <button class=\"action-btn\" (click)=\"moveDown(i, $event)\" [disabled]=\"i === fields().length - 1\" matTooltip=\"Descendre\">\n <mat-icon>arrow_downward</mat-icon>\n </button>\n <button class=\"action-btn\" (click)=\"duplicateField(i, $event)\" matTooltip=\"Dupliquer\">\n <mat-icon>content_copy</mat-icon>\n </button>\n <button class=\"action-btn danger\" (click)=\"removeField(i, $event)\" matTooltip=\"Supprimer\">\n <mat-icon>delete_outline</mat-icon>\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Form settings -->\n <div class=\"form-settings\">\n <p class=\"settings-title\">\n <mat-icon>settings</mat-icon> Param\u00E8tres du formulaire\n </p>\n <div class=\"settings-row\">\n <label>Bouton Envoyer</label>\n <input [ngModel]=\"submitLabel()\" (ngModelChange)=\"submitLabel.set($event)\" placeholder=\"Envoyer\" />\n </div>\n <div class=\"settings-row\">\n <label>\n <input type=\"checkbox\" [ngModel]=\"showReset()\" (ngModelChange)=\"showReset.set($event)\" />\n Afficher R\u00E9initialiser\n </label>\n </div>\n @if (showReset()) {\n <div class=\"settings-row\">\n <label>Bouton Reset</label>\n <input [ngModel]=\"resetLabel()\" (ngModelChange)=\"resetLabel.set($event)\" placeholder=\"R\u00E9initialiser\" />\n </div>\n }\n </div>\n\n </div>\n }\n\n <!-- \u2500\u2500 Preview Tab \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (activeTab() === 'preview') {\n <div class=\"preview-view\">\n @if (fields().length === 0) {\n <div class=\"empty-canvas\">\n <mat-icon>preview</mat-icon>\n <p>Ajoutez des champs pour voir l'aper\u00E7u du formulaire</p>\n </div>\n } @else {\n <div class=\"preview-wrapper\">\n <p class=\"preview-hint\">\n <mat-icon>info_outline</mat-icon>\n Aper\u00E7u en temps r\u00E9el \u2014 les donn\u00E9es ne sont pas envoy\u00E9es\n </p>\n <ngx-dynamic-form [config]=\"previewConfig()\" />\n </div>\n }\n </div>\n }\n\n </main>\n\n <!-- \u2500\u2500 Right: Properties \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <aside class=\"properties\">\n\n @if (selectedField(); as field) {\n <div class=\"props-header\">\n <mat-icon>{{ getIcon(field.type) }}</mat-icon>\n <span>{{ labelOf(field.type) }}</span>\n <button class=\"props-close\" (click)=\"selectedIdx.set(null)\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n\n <div class=\"props-body\">\n\n <!-- \u2500\u2500 G\u00C9N\u00C9RAL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <section class=\"prop-section\">\n <p class=\"prop-section-title\">G\u00E9n\u00E9ral</p>\n\n <div class=\"prop-row\">\n <label>Cl\u00E9 (key)</label>\n <input [ngModel]=\"field.key\" (ngModelChange)=\"updateProp('key', $event)\" placeholder=\"mon_champ\" />\n </div>\n\n <div class=\"prop-row\">\n <label>Largeur (col)</label>\n <select [ngModel]=\"field.col ?? 12\" (ngModelChange)=\"updateProp('col', +$event)\">\n @for (c of colOptions; track c) {\n <option [value]=\"c\">{{ c }} / 12</option>\n }\n </select>\n </div>\n\n @if (!isDivider(field.type) && !isHeading(field.type)) {\n <div class=\"prop-row\">\n <label>Label</label>\n <input [ngModel]=\"field.label\" (ngModelChange)=\"updateProp('label', $event)\" placeholder=\"Libell\u00E9 du champ\" />\n </div>\n }\n\n @if (isHeading(field.type)) {\n <div class=\"prop-row\">\n <label>Texte</label>\n <input [ngModel]=\"field.text\" (ngModelChange)=\"updateProp('text', $event)\" placeholder=\"Titre de section\" />\n </div>\n <div class=\"prop-row\">\n <label>Niveau (h1\u2013h6)</label>\n <select [ngModel]=\"field.level ?? 3\" (ngModelChange)=\"updateProp('level', +$event)\">\n @for (n of [1,2,3,4,5,6]; track n) {\n <option [value]=\"n\">h{{ n }}</option>\n }\n </select>\n </div>\n }\n </section>\n\n <!-- \u2500\u2500 SAISIE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (isInputField(field.type)) {\n <section class=\"prop-section\">\n <p class=\"prop-section-title\">Saisie</p>\n\n <div class=\"prop-row\">\n <label>Placeholder</label>\n <input [ngModel]=\"field.placeholder\" (ngModelChange)=\"updateProp('placeholder', $event)\" placeholder=\"Texte indicatif\" />\n </div>\n\n <div class=\"prop-row\">\n <label>Texte d'aide (hint)</label>\n <input [ngModel]=\"field.hint\" (ngModelChange)=\"updateProp('hint', $event)\" placeholder=\"Aide visible sous le champ\" />\n </div>\n\n @if (needsRows(field.type)) {\n <div class=\"prop-row\">\n <label>Lignes (rows)</label>\n <input type=\"number\" min=\"2\" max=\"20\" [ngModel]=\"field.rows ?? 4\" (ngModelChange)=\"updateProp('rows', +$event)\" />\n </div>\n }\n\n <div class=\"prop-row\">\n <label>\n <input type=\"checkbox\" [ngModel]=\"field.readonly ?? false\" (ngModelChange)=\"updateProp('readonly', $event)\" />\n Lecture seule (readonly)\n </label>\n </div>\n </section>\n\n <!-- \u2500\u2500 IC\u00D4NES \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <section class=\"prop-section\">\n <p class=\"prop-section-title\">Ic\u00F4nes <span class=\"prop-hint\">Material Icons</span></p>\n <div class=\"prop-row\">\n <label>Pr\u00E9fixe (gauche)</label>\n <input [ngModel]=\"field.icon\" (ngModelChange)=\"updateProp('icon', $event || undefined)\" placeholder=\"person, email, lock\u2026\" />\n </div>\n <div class=\"prop-row\">\n <label>Suffixe (droite)</label>\n <input [ngModel]=\"field.iconSuffix\" (ngModelChange)=\"updateProp('iconSuffix', $event || undefined)\" placeholder=\"visibility, search\u2026\" />\n </div>\n </section>\n\n <!-- \u2500\u2500 VALIDATION \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <section class=\"prop-section\">\n <p class=\"prop-section-title\">Validation</p>\n\n <div class=\"prop-row\">\n <label>\n <input type=\"checkbox\" [ngModel]=\"!!field.validation?.required\" (ngModelChange)=\"updateValidation('required', $event)\" />\n Champ obligatoire (required)\n </label>\n </div>\n\n @if (['text','email','password','textarea','url'].includes(field.type)) {\n <div class=\"prop-row\">\n <label>Longueur min</label>\n <input type=\"number\" min=\"0\"\n [ngModel]=\"field.validation?.minLength ?? ''\"\n (ngModelChange)=\"updateValidation('minLength', $event ? +$event : null)\"\n placeholder=\"\u2013\" />\n </div>\n <div class=\"prop-row\">\n <label>Longueur max</label>\n <input type=\"number\" min=\"0\"\n [ngModel]=\"field.validation?.maxLength ?? ''\"\n (ngModelChange)=\"updateValidation('maxLength', $event ? +$event : null)\"\n placeholder=\"\u2013\" />\n </div>\n }\n\n @if (field.type === 'number') {\n <div class=\"prop-row\">\n <label>Valeur min</label>\n <input type=\"number\"\n [ngModel]=\"field.validation?.min ?? ''\"\n (ngModelChange)=\"updateValidation('min', $event !== '' ? +$event : null)\"\n placeholder=\"\u2013\" />\n </div>\n <div class=\"prop-row\">\n <label>Valeur max</label>\n <input type=\"number\"\n [ngModel]=\"field.validation?.max ?? ''\"\n (ngModelChange)=\"updateValidation('max', $event !== '' ? +$event : null)\"\n placeholder=\"\u2013\" />\n </div>\n }\n\n @if (field.type === 'email') {\n <div class=\"prop-row\">\n <label>\n <input type=\"checkbox\" [ngModel]=\"!!field.validation?.email\" (ngModelChange)=\"updateValidation('email', $event)\" />\n Valider le format e-mail\n </label>\n </div>\n }\n\n @if (field.type === 'file') {\n <div class=\"prop-row\">\n <label>Types accept\u00E9s</label>\n <input [ngModel]=\"field.accept\" (ngModelChange)=\"updateProp('accept', $event)\" placeholder=\".pdf,.docx,image/*\" />\n </div>\n <div class=\"prop-row\">\n <label>\n <input type=\"checkbox\" [ngModel]=\"field.multiple ?? false\" (ngModelChange)=\"updateProp('multiple', $event)\" />\n Fichiers multiples\n </label>\n </div>\n }\n </section>\n }\n\n <!-- \u2500\u2500 OPTIONS (select / radio) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (hasOptions(field.type)) {\n <section class=\"prop-section\">\n <p class=\"prop-section-title\">Options</p>\n <div class=\"options-list\">\n @for (opt of getOptions(); track trackByIdx($index); let oi = $index) {\n <div class=\"option-row\">\n <input placeholder=\"Label\" [ngModel]=\"opt.label\" (ngModelChange)=\"updateOption(oi, 'label', $event)\" />\n <input placeholder=\"Valeur\" [ngModel]=\"opt.value\" (ngModelChange)=\"updateOption(oi, 'value', $event)\" />\n <button class=\"action-btn danger\" (click)=\"removeOption(oi)\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n }\n </div>\n <button class=\"btn-add-option\" (click)=\"addOption()\">\n <mat-icon>add</mat-icon> Ajouter une option\n </button>\n </section>\n }\n\n </div>\n\n } @else {\n <div class=\"props-empty\">\n <mat-icon>tune</mat-icon>\n <p>S\u00E9lectionnez un champ pour modifier ses propri\u00E9t\u00E9s</p>\n </div>\n }\n\n </aside>\n\n </div><!-- /.builder-body -->\n\n</div><!-- /.builder-root -->\n\n<!-- \u2500\u2500 Export Modal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n@if (showExport()) {\n <div class=\"modal-overlay\" (click)=\"showExport.set(false)\">\n <div class=\"modal\" (click)=\"$event.stopPropagation()\">\n <div class=\"modal-header\">\n <span>Code export\u00E9</span>\n <button (click)=\"showExport.set(false)\"><mat-icon>close</mat-icon></button>\n </div>\n <textarea class=\"modal-code\" readonly [value]=\"exportContent()\"></textarea>\n <div class=\"modal-footer\">\n <button class=\"btn-primary\" (click)=\"copyExport()\">\n <mat-icon>content_copy</mat-icon> Copier\n </button>\n <button class=\"btn-outline\" (click)=\"showExport.set(false)\">Fermer</button>\n </div>\n </div>\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;height:100vh;overflow:hidden;font-family:Roboto,sans-serif;--palette-w: 210px;--props-w: 280px;--topbar-h: 52px;--border: #e2e8f0;--bg: #f8fafc;--surface: #ffffff;--primary: #2563eb;--primary-light: #eff6ff;--danger: #dc2626;--text: #1e293b;--text-muted: #64748b;--radius: 8px}.topbar{display:flex;align-items:center;gap:1rem;height:var(--topbar-h);padding:0 1rem;background:var(--surface);border-bottom:1px solid var(--border);box-shadow:0 1px 4px #0000000f;z-index:10;flex-shrink:0}.topbar-back{display:inline-flex;align-items:center;gap:.25rem;color:var(--text-muted);text-decoration:none;font-size:.85rem;white-space:nowrap}.topbar-back:hover{color:var(--primary)}.topbar-back mat-icon{font-size:1rem;width:1rem;height:1rem}.topbar-center{display:flex;align-items:center;gap:.5rem;flex:1;min-width:0}.topbar-logo{color:var(--primary);flex-shrink:0}.title-input{flex:1;min-width:0;border:none;border-bottom:2px solid transparent;background:transparent;font-size:1rem;font-weight:600;color:var(--text);padding:.25rem .5rem;border-radius:4px;outline:none;transition:border-color .15s}.title-input:focus{border-bottom-color:var(--primary)}.title-input::placeholder{color:var(--text-muted);font-weight:400}.topbar-actions{display:flex;align-items:center;gap:.5rem;flex-shrink:0}.btn-icon{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;border:none;background:transparent;border-radius:var(--radius);color:var(--text-muted);cursor:pointer;transition:background .15s,color .15s}.btn-icon:hover{background:var(--bg);color:var(--text)}.btn-icon mat-icon{font-size:1.1rem;width:1.1rem;height:1.1rem}.btn-outline{display:inline-flex;align-items:center;gap:.3rem;padding:.3rem .75rem;border:1px solid var(--border);background:var(--surface);border-radius:var(--radius);font-size:.82rem;color:var(--text);cursor:pointer;transition:background .15s,border-color .15s}.btn-outline:hover{background:var(--bg);border-color:#cbd5e1}.btn-outline mat-icon{font-size:.95rem;width:.95rem;height:.95rem}.btn-primary{display:inline-flex;align-items:center;gap:.3rem;padding:.35rem .85rem;border:none;background:var(--primary);color:#fff;border-radius:var(--radius);font-size:.85rem;font-weight:500;cursor:pointer;transition:background .15s}.btn-primary:hover{background:#1d4ed8}.btn-primary mat-icon{font-size:.95rem;width:.95rem;height:.95rem}.builder-body{display:grid;grid-template-columns:var(--palette-w) 1fr var(--props-w);height:calc(100vh - var(--topbar-h));overflow:hidden}.palette{background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow-y:auto;padding:.75rem .5rem;gap:1rem}.palette-section{display:flex;flex-direction:column;gap:.2rem}.palette-section-title{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);padding:.25rem .5rem;margin:.4rem 0 .2rem}.palette-group-label{font-size:.7rem;color:var(--text-muted);padding:.1rem .5rem;margin-top:.4rem;margin-bottom:.05rem}.palette-btn{display:flex;align-items:center;gap:.5rem;padding:.45rem .65rem;border:none;background:transparent;border-radius:6px;color:var(--text);font-size:.82rem;cursor:pointer;text-align:left;transition:background .12s}.palette-btn:hover{background:var(--primary-light);color:var(--primary)}.palette-btn mat-icon{font-size:1rem;width:1rem;height:1rem;color:var(--text-muted)}.palette-btn:hover mat-icon{color:var(--primary)}.palette-empty{font-size:.78rem;color:var(--text-muted);padding:.25rem .5rem;font-style:italic}.saved-item{display:flex;align-items:center;border-radius:6px;overflow:hidden}.saved-item.active{background:var(--primary-light)}.saved-item .saved-item-name{flex:1;display:flex;align-items:center;gap:.35rem;padding:.4rem .5rem;border:none;background:transparent;font-size:.8rem;color:var(--text);cursor:pointer;text-align:left;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;min-width:0;transition:color .12s}.saved-item .saved-item-name:hover{color:var(--primary)}.saved-item .saved-item-name mat-icon{font-size:.9rem;width:.9rem;height:.9rem;flex-shrink:0}.saved-item .saved-item-name span{overflow:hidden;text-overflow:ellipsis}.saved-item .saved-item-delete{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;background:transparent;color:var(--text-muted);cursor:pointer;border-radius:4px;flex-shrink:0;transition:color .12s,background .12s}.saved-item .saved-item-delete:hover{color:var(--danger);background:#fee2e2}.saved-item .saved-item-delete mat-icon{font-size:.9rem;width:.9rem;height:.9rem}.canvas{display:flex;flex-direction:column;background:var(--bg);overflow:hidden}.canvas-tabs{display:flex;align-items:center;gap:.25rem;padding:.5rem .75rem;background:var(--surface);border-bottom:1px solid var(--border);flex-shrink:0}.canvas-tab{display:inline-flex;align-items:center;gap:.3rem;padding:.3rem .75rem;border:1px solid transparent;background:transparent;border-radius:var(--radius);font-size:.82rem;color:var(--text-muted);cursor:pointer;transition:background .12s,color .12s,border-color .12s}.canvas-tab mat-icon{font-size:.95rem;width:.95rem;height:.95rem}.canvas-tab.active{background:var(--primary-light);color:var(--primary);border-color:#bfdbfe;font-weight:500}.canvas-tab:not(.active):hover{background:var(--bg);color:var(--text)}.canvas-tabs-info{margin-left:auto;font-size:.78rem;color:var(--text-muted)}.schema-view{flex:1;overflow-y:auto;padding:1rem;display:flex;flex-direction:column;gap:.75rem}.empty-canvas{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;padding:3rem 1rem;color:var(--text-muted);text-align:center;border:2px dashed var(--border);border-radius:var(--radius)}.empty-canvas mat-icon{font-size:2.5rem;width:2.5rem;height:2.5rem;opacity:.5}.empty-canvas p{font-size:.9rem;margin:0}.field-list{display:flex;flex-direction:column;gap:.5rem}.field-card{display:flex;align-items:center;gap:.75rem;background:var(--surface);border:1.5px solid var(--border);border-radius:var(--radius);padding:.6rem .75rem;cursor:pointer;transition:border-color .15s,box-shadow .15s}.field-card:hover{border-color:#93c5fd;box-shadow:0 1px 6px #2563eb1a}.field-card.selected{border-color:var(--primary);box-shadow:0 0 0 2px #bfdbfe}.field-card-icon{font-size:1.2rem;width:1.2rem;height:1.2rem;color:var(--primary);flex-shrink:0}.field-card-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:.1rem}.field-card-label{font-size:.85rem;font-weight:500;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.field-card-meta{font-size:.72rem;color:var(--text-muted)}.field-card-key{font-size:.72rem;background:#f1f5f9;color:#0f172a;padding:.1rem .35rem;border-radius:4px;font-family:Fira Code,Courier New,monospace;white-space:nowrap;flex-shrink:0}.field-card-actions{display:flex;align-items:center;gap:.15rem;flex-shrink:0}.action-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;background:transparent;border-radius:5px;color:var(--text-muted);cursor:pointer;transition:background .12s,color .12s}.action-btn:hover{background:var(--bg);color:var(--text)}.action-btn.danger:hover{background:#fee2e2;color:var(--danger)}.action-btn:disabled{opacity:.35;pointer-events:none}.action-btn mat-icon{font-size:.95rem;width:.95rem;height:.95rem}.form-settings{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:.75rem 1rem;display:flex;flex-direction:column;gap:.5rem;margin-top:.25rem}.settings-title{display:flex;align-items:center;gap:.35rem;font-size:.78rem;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.04em;margin:0 0 .25rem}.settings-title mat-icon{font-size:.9rem;width:.9rem;height:.9rem}.settings-row{display:flex;align-items:center;gap:.75rem;font-size:.83rem}.settings-row label{display:flex;align-items:center;gap:.4rem;color:var(--text);white-space:nowrap;cursor:pointer;min-width:130px}.settings-row input[type=text],.settings-row input:not([type=checkbox]){flex:1;padding:.3rem .5rem;border:1px solid var(--border);border-radius:5px;font-size:.83rem;outline:none}.settings-row input[type=text]:focus,.settings-row input:not([type=checkbox]):focus{border-color:var(--primary)}.preview-view{flex:1;overflow-y:auto;padding:1.5rem;display:flex;flex-direction:column;align-items:center}.preview-wrapper{width:100%;max-width:720px;display:flex;flex-direction:column;gap:.75rem}.preview-hint{display:flex;align-items:center;gap:.4rem;font-size:.78rem;color:var(--text-muted);background:#fffbeb;border:1px solid #fde68a;border-radius:6px;padding:.4rem .75rem;margin:0}.preview-hint mat-icon{font-size:.9rem;width:.9rem;height:.9rem;color:#b45309}.properties{background:var(--surface);border-left:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden}.props-header{display:flex;align-items:center;gap:.5rem;padding:.65rem .85rem;background:var(--primary-light);border-bottom:1px solid #bfdbfe;flex-shrink:0}.props-header mat-icon{color:var(--primary);font-size:1rem;width:1rem;height:1rem}.props-header span{flex:1;font-size:.85rem;font-weight:600;color:var(--primary)}.props-close{display:flex;align-items:center;justify-content:center;width:26px;height:26px;border:none;background:transparent;color:var(--text-muted);cursor:pointer;border-radius:4px;transition:background .12s}.props-close:hover{background:#dbeafe}.props-close mat-icon{font-size:.9rem;width:.9rem;height:.9rem}.props-body{flex:1;overflow-y:auto;padding:.5rem;display:flex;flex-direction:column;gap:.25rem}.props-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.75rem;color:var(--text-muted);padding:2rem 1rem;text-align:center}.props-empty mat-icon{font-size:2rem;width:2rem;height:2rem;opacity:.4}.props-empty p{font-size:.85rem;margin:0;max-width:180px}.prop-section{background:var(--bg);border:1px solid var(--border);border-radius:7px;padding:.6rem .75rem;display:flex;flex-direction:column;gap:.4rem}.prop-section-title{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);margin:0 0 .3rem;display:flex;align-items:center;gap:.4rem}.prop-hint{font-size:.68rem;font-weight:400;text-transform:none;letter-spacing:0;color:#94a3b8;margin-left:.25rem}.prop-row{display:flex;flex-direction:column;gap:.2rem;font-size:.82rem}.prop-row>label{display:flex;align-items:center;gap:.4rem;color:var(--text);font-size:.8rem;cursor:pointer}.prop-row>input[type=text],.prop-row>input[type=number],.prop-row>input:not([type=checkbox]),.prop-row>select{width:100%;padding:.3rem .5rem;border:1px solid var(--border);border-radius:5px;font-size:.82rem;color:var(--text);background:var(--surface);outline:none;box-sizing:border-box;transition:border-color .12s}.prop-row>input[type=text]:focus,.prop-row>input[type=number]:focus,.prop-row>input:not([type=checkbox]):focus,.prop-row>select:focus{border-color:var(--primary)}.options-list{display:flex;flex-direction:column;gap:.35rem}.option-row{display:flex;align-items:center;gap:.3rem}.option-row input{flex:1;padding:.28rem .45rem;border:1px solid var(--border);border-radius:5px;font-size:.78rem;outline:none}.option-row input:focus{border-color:var(--primary)}.option-row .action-btn{width:24px;height:24px;flex-shrink:0}.option-row .action-btn mat-icon{font-size:.85rem;width:.85rem;height:.85rem}.btn-add-option{display:inline-flex;align-items:center;gap:.25rem;padding:.28rem .6rem;border:1px dashed var(--border);background:transparent;border-radius:5px;font-size:.78rem;color:var(--text-muted);cursor:pointer;transition:border-color .12s,color .12s;margin-top:.25rem}.btn-add-option:hover{border-color:var(--primary);color:var(--primary)}.btn-add-option mat-icon{font-size:.9rem;width:.9rem;height:.9rem}.modal-overlay{position:fixed;inset:0;background:#00000073;z-index:200;display:flex;align-items:center;justify-content:center;padding:1.5rem}.modal{background:var(--surface);border-radius:10px;box-shadow:0 20px 60px #0003;display:flex;flex-direction:column;width:100%;max-width:700px;max-height:80vh;overflow:hidden}.modal-header{display:flex;align-items:center;justify-content:space-between;padding:.75rem 1rem;border-bottom:1px solid var(--border);font-weight:600;font-size:.9rem}.modal-header button{display:flex;align-items:center;border:none;background:transparent;cursor:pointer;color:var(--text-muted);border-radius:4px;padding:.2rem;transition:background .12s}.modal-header button:hover{background:var(--bg)}.modal-header button mat-icon{font-size:1rem;width:1rem;height:1rem}.modal-code{flex:1;padding:1rem;font-family:Fira Code,Courier New,monospace;font-size:.8rem;color:var(--text);border:none;resize:none;outline:none;background:#1e293b;color:#e2e8f0;overflow-y:auto;min-height:320px}.modal-footer{display:flex;gap:.5rem;padding:.75rem 1rem;border-top:1px solid var(--border);justify-content:flex-end}\n"] }]
887
+ }], propDecorators: { backRoute: [{
888
+ type: Input
889
+ }] } });
890
+
891
+ class MesFormulairesComponent {
892
+ constructor() {
893
+ /** Route vers le builder (lien "Nouveau formulaire" et actions d'édition) */
894
+ this.builderRoute = '/builder';
895
+ /** Préfixe de route pour l'ouverture d'un formulaire par ID */
896
+ this.formRoute = '/form';
897
+ /** Route du lien "Retour" */
898
+ this.backRoute = '/';
899
+ this.formService = inject(FormConfigService);
900
+ this.savedForms = this.formService.schemas;
901
+ this.selectedId = signal(null);
902
+ this.submitted = signal(false);
903
+ this.submitData = signal({});
904
+ this.activeConfig = computed(() => {
905
+ const id = this.selectedId();
906
+ return id ? (this.formService.get(id)?.config ?? null) : null;
907
+ });
908
+ this.activeSchema = computed(() => {
909
+ const id = this.selectedId();
910
+ return id ? (this.formService.get(id) ?? null) : null;
911
+ });
912
+ }
913
+ selectForm(id) {
914
+ if (this.selectedId() === id) {
915
+ this.selectedId.set(null);
916
+ }
917
+ else {
918
+ this.selectedId.set(id);
919
+ this.submitted.set(false);
920
+ this.submitData.set({});
921
+ }
922
+ }
923
+ onSubmit(event) {
924
+ this.submitData.set(event.value);
925
+ this.submitted.set(true);
926
+ }
927
+ resetForm() {
928
+ this.submitted.set(false);
929
+ this.submitData.set({});
930
+ }
931
+ deleteForm(id, e) {
932
+ e.stopPropagation();
933
+ this.formService.delete(id).subscribe(() => {
934
+ if (this.selectedId() === id)
935
+ this.selectedId.set(null);
936
+ });
937
+ }
938
+ formatDate(iso) {
939
+ return new Date(iso).toLocaleDateString('fr-FR', {
940
+ day: '2-digit', month: 'short', year: 'numeric',
941
+ hour: '2-digit', minute: '2-digit',
942
+ });
943
+ }
944
+ fieldCount(config) {
945
+ return config.fields?.filter(f => f.type !== 'divider' && f.type !== 'heading').length ?? 0;
946
+ }
947
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: MesFormulairesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
948
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: MesFormulairesComponent, isStandalone: true, selector: "ngx-mes-formulaires", inputs: { builderRoute: "builderRoute", formRoute: "formRoute", backRoute: "backRoute" }, ngImport: i0, template: "<div class=\"page\">\n\n <header class=\"page-header\">\n <a [routerLink]=\"backRoute\" class=\"back-link\">\n <mat-icon>arrow_back</mat-icon>\n </a>\n <div class=\"header-text\">\n <h1>Mes Formulaires</h1>\n <p>S\u00E9lectionnez un formulaire pour le charger via <code>FormConfigService</code></p>\n </div>\n <a [routerLink]=\"builderRoute\" class=\"btn-primary\">\n <mat-icon>add</mat-icon> Nouveau formulaire\n </a>\n </header>\n\n <div class=\"page-body\">\n\n <!-- \u2550\u2550\u2550\u2550 COLONNE GAUCHE : liste des formulaires \u2550\u2550\u2550\u2550 -->\n <aside class=\"sidebar\">\n\n <p class=\"sidebar-title\">\n <mat-icon>folder_open</mat-icon>\n {{ savedForms().length }} formulaire{{ savedForms().length !== 1 ? 's' : '' }} sauvegard\u00E9{{ savedForms().length !== 1 ? 's' : '' }}\n </p>\n\n @if (savedForms().length === 0) {\n <div class=\"empty-state\">\n <mat-icon>inbox</mat-icon>\n <p>Aucun formulaire sauvegard\u00E9</p>\n <a [routerLink]=\"builderRoute\" class=\"btn-outline-sm\">\n <mat-icon>add</mat-icon> Cr\u00E9er un formulaire\n </a>\n </div>\n }\n\n <div class=\"form-list\">\n @for (schema of savedForms(); track schema.id) {\n <div\n class=\"form-item\"\n [class.active]=\"selectedId() === schema.id\"\n (click)=\"selectForm(schema.id)\"\n role=\"button\"\n tabindex=\"0\"\n >\n <div class=\"form-item-icon\">\n <mat-icon>description</mat-icon>\n </div>\n\n <div class=\"form-item-body\">\n <span class=\"form-item-title\">{{ schema.title }}</span>\n <span class=\"form-item-meta\">\n {{ fieldCount(schema.config) }} champ{{ fieldCount(schema.config) !== 1 ? 's' : '' }}\n &nbsp;\u00B7&nbsp; {{ formatDate(schema.updatedAt) }}\n </span>\n <code class=\"form-item-id\">ID : {{ schema.id }}</code>\n </div>\n\n <div class=\"form-item-actions\">\n <a\n [routerLink]=\"[formRoute, schema.id]\"\n class=\"action-icon\"\n title=\"Ouvrir dans la page d\u00E9di\u00E9e\"\n (click)=\"$event.stopPropagation()\"\n >\n <mat-icon>open_in_new</mat-icon>\n </a>\n <a\n [routerLink]=\"builderRoute\"\n class=\"action-icon\"\n title=\"\u00C9diter dans le Builder\"\n (click)=\"$event.stopPropagation()\"\n >\n <mat-icon>edit</mat-icon>\n </a>\n <button\n class=\"action-icon danger\"\n title=\"Supprimer\"\n (click)=\"deleteForm(schema.id, $event)\"\n >\n <mat-icon>delete_outline</mat-icon>\n </button>\n </div>\n </div>\n }\n </div>\n\n <div class=\"code-hint\">\n <p class=\"code-hint-title\">\n <mat-icon>integration_instructions</mat-icon> Chargement par ID\n </p>\n <pre class=\"code-block\">private formService = inject(FormConfigService);\n\nconst schema = this.formService.get('{{ selectedId() ?? \"votre-id\" }}');\nthis.config.set(schema?.config ?? null);</pre>\n </div>\n\n </aside>\n\n <!-- \u2550\u2550\u2550\u2550 COLONNE DROITE : rendu du formulaire \u2550\u2550\u2550\u2550 -->\n <main class=\"content\">\n\n @if (!selectedId()) {\n <div class=\"select-hint\">\n <mat-icon>arrow_back</mat-icon>\n <p>S\u00E9lectionnez un formulaire \u00E0 gauche pour le charger</p>\n </div>\n }\n\n @if (selectedId() && activeConfig(); as config) {\n\n @if (!submitted()) {\n <div class=\"form-card\">\n\n <div class=\"form-card-header\">\n <mat-icon>dynamic_form</mat-icon>\n <div>\n <h2>{{ activeSchema()?.title }}</h2>\n <span class=\"loaded-badge\">\n <mat-icon>check_circle</mat-icon>\n Charg\u00E9 via <code>FormConfigService.get('{{ selectedId() }}')</code>\n </span>\n </div>\n </div>\n\n <ngx-dynamic-form\n [config]=\"config\"\n (formSubmit)=\"onSubmit($event)\"\n />\n\n </div>\n }\n\n @if (submitted()) {\n <div class=\"result-card\">\n <mat-icon class=\"result-icon\">check_circle</mat-icon>\n <h2>Donn\u00E9es soumises avec succ\u00E8s</h2>\n <p class=\"result-sub\">\n Les donn\u00E9es ci-dessous ont \u00E9t\u00E9 re\u00E7ues par <code>onSubmit(event)</code>\n </p>\n\n <div class=\"result-data\">\n <p class=\"result-data-title\">event.value :</p>\n <pre>{{ submitData() | json }}</pre>\n </div>\n\n <div class=\"result-actions\">\n <button class=\"btn-outline\" (click)=\"resetForm()\">\n <mat-icon>refresh</mat-icon> Recommencer\n </button>\n <a class=\"btn-primary\" [routerLink]=\"builderRoute\">\n <mat-icon>dashboard_customize</mat-icon> Builder\n </a>\n </div>\n </div>\n }\n\n }\n\n @if (selectedId() && !activeConfig()) {\n <div class=\"select-hint error\">\n <mat-icon>error_outline</mat-icon>\n <p>Formulaire introuvable pour l'ID <code>{{ selectedId() }}</code></p>\n </div>\n }\n\n </main>\n\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block;min-height:100vh;background:#f8fafc;font-family:Roboto,sans-serif}.page{display:flex;flex-direction:column;height:100vh;overflow:hidden}.page-header{display:flex;align-items:center;gap:1rem;padding:.85rem 1.5rem;background:#fff;border-bottom:1px solid #e2e8f0;box-shadow:0 1px 4px #0000000f;flex-shrink:0}.back-link{display:flex;align-items:center;color:#64748b;text-decoration:none;padding:.2rem;border-radius:6px;transition:color .15s,background .15s}.back-link:hover{color:#2563eb;background:#eff6ff}.back-link mat-icon{font-size:1.1rem;width:1.1rem;height:1.1rem}.header-text{flex:1}.header-text h1{font-size:1.1rem;font-weight:700;color:#1e293b;margin:0}.header-text p{font-size:.78rem;color:#64748b;margin:.15rem 0 0}.header-text p code{background:#f1f5f9;padding:.1rem .3rem;border-radius:3px;font-size:.75rem;color:#0f172a}.page-body{display:grid;grid-template-columns:340px 1fr;height:calc(100vh - 64px);overflow:hidden}.sidebar{background:#fff;border-right:1px solid #e2e8f0;display:flex;flex-direction:column;overflow-y:auto;padding:1rem .75rem;gap:.75rem}.sidebar-title{display:flex;align-items:center;gap:.4rem;font-size:.78rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:#64748b;margin:0 0 .25rem .1rem}.sidebar-title mat-icon{font-size:.9rem;width:.9rem;height:.9rem}.empty-state{display:flex;flex-direction:column;align-items:center;gap:.6rem;padding:2rem 1rem;text-align:center;color:#94a3b8;border:2px dashed #e2e8f0;border-radius:8px}.empty-state mat-icon{font-size:2rem;width:2rem;height:2rem;opacity:.5}.empty-state p{font-size:.85rem;margin:0}.form-list{display:flex;flex-direction:column;gap:.4rem}.form-item{display:flex;align-items:flex-start;gap:.65rem;padding:.75rem;border:1.5px solid #e2e8f0;border-radius:8px;cursor:pointer;transition:border-color .15s,background .15s,box-shadow .15s;position:relative}.form-item:hover{border-color:#93c5fd;background:#f8faff}.form-item.active{border-color:#2563eb;background:#eff6ff;box-shadow:0 0 0 2px #bfdbfe}.form-item-icon{display:flex;align-items:center;justify-content:center;width:36px;height:36px;background:#f1f5f9;border-radius:7px;flex-shrink:0;margin-top:.1rem}.form-item.active .form-item-icon{background:#dbeafe}.form-item-icon mat-icon{font-size:1.1rem;width:1.1rem;height:1.1rem;color:#64748b}.form-item.active .form-item-icon mat-icon{color:#2563eb}.form-item-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:.2rem}.form-item-title{font-size:.88rem;font-weight:600;color:#1e293b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.form-item-meta{font-size:.72rem;color:#94a3b8}.form-item-id{font-size:.68rem;color:#94a3b8;font-family:Courier New,monospace;background:#f1f5f9;padding:.1rem .3rem;border-radius:3px;align-self:flex-start}.form-item-actions{display:flex;flex-direction:column;gap:.15rem;flex-shrink:0}.action-icon{display:flex;align-items:center;justify-content:center;width:26px;height:26px;border:none;background:transparent;border-radius:5px;color:#94a3b8;cursor:pointer;text-decoration:none;transition:background .12s,color .12s}.action-icon:hover{background:#f1f5f9;color:#1e293b}.action-icon.danger:hover{background:#fee2e2;color:#dc2626}.action-icon mat-icon{font-size:.85rem;width:.85rem;height:.85rem}.code-hint{background:#1e293b;border-radius:8px;padding:.85rem;margin-top:auto}.code-hint-title{display:flex;align-items:center;gap:.35rem;font-size:.72rem;color:#94a3b8;margin:0 0 .6rem}.code-hint-title mat-icon{font-size:.85rem;width:.85rem;height:.85rem;color:#60a5fa}.code-block{margin:0;font-size:.72rem;font-family:Fira Code,Courier New,monospace;color:#7dd3fc;white-space:pre-wrap;line-height:1.65}.content{overflow-y:auto;padding:1.5rem;display:flex;flex-direction:column;align-items:center}.select-hint{display:flex;flex-direction:column;align-items:center;gap:.75rem;padding:4rem 1rem;color:#94a3b8;text-align:center}.select-hint mat-icon{font-size:2.5rem;width:2.5rem;height:2.5rem;opacity:.5}.select-hint p{font-size:.9rem;margin:0;max-width:260px}.select-hint.error{color:#dc2626}.select-hint.error mat-icon{opacity:1}.select-hint.error code{background:#fee2e2;padding:.1rem .35rem;border-radius:4px;font-family:monospace;font-size:.85rem}.form-card{width:100%;max-width:700px;background:#fff;border:1.5px solid #e2e8f0;border-radius:12px;overflow:hidden;box-shadow:0 2px 12px #0000000f}.form-card-header{display:flex;align-items:flex-start;gap:.85rem;padding:1.1rem 1.25rem;background:#eff6ff;border-bottom:1px solid #bfdbfe}.form-card-header>mat-icon{font-size:1.5rem;width:1.5rem;height:1.5rem;color:#2563eb;margin-top:.15rem;flex-shrink:0}.form-card-header h2{font-size:1rem;font-weight:700;color:#1e293b;margin:0 0 .35rem}.loaded-badge{display:inline-flex;align-items:center;gap:.3rem;font-size:.72rem;color:#059669}.loaded-badge mat-icon{font-size:.8rem;width:.8rem;height:.8rem}.loaded-badge code{background:#d1fae5;padding:.1rem .3rem;border-radius:3px;font-size:.7rem;color:#065f46;font-family:Courier New,monospace}.form-card ngx-dynamic-form{display:block;padding:1.25rem}.result-card{display:flex;flex-direction:column;align-items:center;gap:1rem;width:100%;max-width:620px;background:#fff;border:1.5px solid #bbf7d0;border-radius:12px;padding:2.5rem 2rem;text-align:center;box-shadow:0 4px 20px #16a34a14}.result-icon{font-size:3rem;width:3rem;height:3rem;color:#16a34a}.result-card h2{font-size:1.2rem;color:#1e293b;margin:0}.result-sub{font-size:.85rem;color:#64748b;margin:0}.result-sub code{background:#f1f5f9;padding:.1rem .3rem;border-radius:3px;font-family:monospace;font-size:.8rem}.result-data{width:100%;background:#1e293b;border-radius:8px;padding:1rem;text-align:left;overflow:auto}.result-data-title{font-size:.72rem;color:#94a3b8;text-transform:uppercase;letter-spacing:.04em;margin:0 0 .5rem}.result-data pre{margin:0;font-size:.78rem;font-family:Fira Code,monospace;color:#7dd3fc;white-space:pre-wrap;word-break:break-all}.result-actions{display:flex;gap:.75rem;flex-wrap:wrap;justify-content:center}.btn-primary{display:inline-flex;align-items:center;gap:.35rem;padding:.45rem 1rem;border:none;background:#2563eb;color:#fff;border-radius:7px;font-size:.85rem;font-weight:500;text-decoration:none;cursor:pointer;transition:background .15s}.btn-primary:hover{background:#1d4ed8}.btn-primary mat-icon{font-size:1rem;width:1rem;height:1rem}.btn-outline{display:inline-flex;align-items:center;gap:.35rem;padding:.45rem 1rem;border:1px solid #e2e8f0;background:#fff;color:#1e293b;border-radius:7px;font-size:.85rem;cursor:pointer;text-decoration:none;transition:border-color .15s}.btn-outline:hover{border-color:#94a3b8}.btn-outline mat-icon{font-size:1rem;width:1rem;height:1rem}.btn-outline-sm{display:inline-flex;align-items:center;gap:.25rem;padding:.3rem .7rem;border:1px solid #cbd5e1;background:#fff;color:#64748b;border-radius:6px;font-size:.78rem;text-decoration:none;transition:border-color .15s,color .15s}.btn-outline-sm:hover{border-color:#2563eb;color:#2563eb}.btn-outline-sm mat-icon{font-size:.85rem;width:.85rem;height:.85rem}\n"], dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: DynamicFormComponent, selector: "ngx-dynamic-form", inputs: ["config", "initialValues"], outputs: ["formSubmit", "formChange", "formReset"] }, { kind: "pipe", type: JsonPipe, name: "json" }] }); }
949
+ }
950
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: MesFormulairesComponent, decorators: [{
951
+ type: Component,
952
+ args: [{ selector: 'ngx-mes-formulaires', standalone: true, imports: [RouterLink, MatIcon, DynamicFormComponent, JsonPipe], template: "<div class=\"page\">\n\n <header class=\"page-header\">\n <a [routerLink]=\"backRoute\" class=\"back-link\">\n <mat-icon>arrow_back</mat-icon>\n </a>\n <div class=\"header-text\">\n <h1>Mes Formulaires</h1>\n <p>S\u00E9lectionnez un formulaire pour le charger via <code>FormConfigService</code></p>\n </div>\n <a [routerLink]=\"builderRoute\" class=\"btn-primary\">\n <mat-icon>add</mat-icon> Nouveau formulaire\n </a>\n </header>\n\n <div class=\"page-body\">\n\n <!-- \u2550\u2550\u2550\u2550 COLONNE GAUCHE : liste des formulaires \u2550\u2550\u2550\u2550 -->\n <aside class=\"sidebar\">\n\n <p class=\"sidebar-title\">\n <mat-icon>folder_open</mat-icon>\n {{ savedForms().length }} formulaire{{ savedForms().length !== 1 ? 's' : '' }} sauvegard\u00E9{{ savedForms().length !== 1 ? 's' : '' }}\n </p>\n\n @if (savedForms().length === 0) {\n <div class=\"empty-state\">\n <mat-icon>inbox</mat-icon>\n <p>Aucun formulaire sauvegard\u00E9</p>\n <a [routerLink]=\"builderRoute\" class=\"btn-outline-sm\">\n <mat-icon>add</mat-icon> Cr\u00E9er un formulaire\n </a>\n </div>\n }\n\n <div class=\"form-list\">\n @for (schema of savedForms(); track schema.id) {\n <div\n class=\"form-item\"\n [class.active]=\"selectedId() === schema.id\"\n (click)=\"selectForm(schema.id)\"\n role=\"button\"\n tabindex=\"0\"\n >\n <div class=\"form-item-icon\">\n <mat-icon>description</mat-icon>\n </div>\n\n <div class=\"form-item-body\">\n <span class=\"form-item-title\">{{ schema.title }}</span>\n <span class=\"form-item-meta\">\n {{ fieldCount(schema.config) }} champ{{ fieldCount(schema.config) !== 1 ? 's' : '' }}\n &nbsp;\u00B7&nbsp; {{ formatDate(schema.updatedAt) }}\n </span>\n <code class=\"form-item-id\">ID : {{ schema.id }}</code>\n </div>\n\n <div class=\"form-item-actions\">\n <a\n [routerLink]=\"[formRoute, schema.id]\"\n class=\"action-icon\"\n title=\"Ouvrir dans la page d\u00E9di\u00E9e\"\n (click)=\"$event.stopPropagation()\"\n >\n <mat-icon>open_in_new</mat-icon>\n </a>\n <a\n [routerLink]=\"builderRoute\"\n class=\"action-icon\"\n title=\"\u00C9diter dans le Builder\"\n (click)=\"$event.stopPropagation()\"\n >\n <mat-icon>edit</mat-icon>\n </a>\n <button\n class=\"action-icon danger\"\n title=\"Supprimer\"\n (click)=\"deleteForm(schema.id, $event)\"\n >\n <mat-icon>delete_outline</mat-icon>\n </button>\n </div>\n </div>\n }\n </div>\n\n <div class=\"code-hint\">\n <p class=\"code-hint-title\">\n <mat-icon>integration_instructions</mat-icon> Chargement par ID\n </p>\n <pre class=\"code-block\">private formService = inject(FormConfigService);\n\nconst schema = this.formService.get('{{ selectedId() ?? \"votre-id\" }}');\nthis.config.set(schema?.config ?? null);</pre>\n </div>\n\n </aside>\n\n <!-- \u2550\u2550\u2550\u2550 COLONNE DROITE : rendu du formulaire \u2550\u2550\u2550\u2550 -->\n <main class=\"content\">\n\n @if (!selectedId()) {\n <div class=\"select-hint\">\n <mat-icon>arrow_back</mat-icon>\n <p>S\u00E9lectionnez un formulaire \u00E0 gauche pour le charger</p>\n </div>\n }\n\n @if (selectedId() && activeConfig(); as config) {\n\n @if (!submitted()) {\n <div class=\"form-card\">\n\n <div class=\"form-card-header\">\n <mat-icon>dynamic_form</mat-icon>\n <div>\n <h2>{{ activeSchema()?.title }}</h2>\n <span class=\"loaded-badge\">\n <mat-icon>check_circle</mat-icon>\n Charg\u00E9 via <code>FormConfigService.get('{{ selectedId() }}')</code>\n </span>\n </div>\n </div>\n\n <ngx-dynamic-form\n [config]=\"config\"\n (formSubmit)=\"onSubmit($event)\"\n />\n\n </div>\n }\n\n @if (submitted()) {\n <div class=\"result-card\">\n <mat-icon class=\"result-icon\">check_circle</mat-icon>\n <h2>Donn\u00E9es soumises avec succ\u00E8s</h2>\n <p class=\"result-sub\">\n Les donn\u00E9es ci-dessous ont \u00E9t\u00E9 re\u00E7ues par <code>onSubmit(event)</code>\n </p>\n\n <div class=\"result-data\">\n <p class=\"result-data-title\">event.value :</p>\n <pre>{{ submitData() | json }}</pre>\n </div>\n\n <div class=\"result-actions\">\n <button class=\"btn-outline\" (click)=\"resetForm()\">\n <mat-icon>refresh</mat-icon> Recommencer\n </button>\n <a class=\"btn-primary\" [routerLink]=\"builderRoute\">\n <mat-icon>dashboard_customize</mat-icon> Builder\n </a>\n </div>\n </div>\n }\n\n }\n\n @if (selectedId() && !activeConfig()) {\n <div class=\"select-hint error\">\n <mat-icon>error_outline</mat-icon>\n <p>Formulaire introuvable pour l'ID <code>{{ selectedId() }}</code></p>\n </div>\n }\n\n </main>\n\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block;min-height:100vh;background:#f8fafc;font-family:Roboto,sans-serif}.page{display:flex;flex-direction:column;height:100vh;overflow:hidden}.page-header{display:flex;align-items:center;gap:1rem;padding:.85rem 1.5rem;background:#fff;border-bottom:1px solid #e2e8f0;box-shadow:0 1px 4px #0000000f;flex-shrink:0}.back-link{display:flex;align-items:center;color:#64748b;text-decoration:none;padding:.2rem;border-radius:6px;transition:color .15s,background .15s}.back-link:hover{color:#2563eb;background:#eff6ff}.back-link mat-icon{font-size:1.1rem;width:1.1rem;height:1.1rem}.header-text{flex:1}.header-text h1{font-size:1.1rem;font-weight:700;color:#1e293b;margin:0}.header-text p{font-size:.78rem;color:#64748b;margin:.15rem 0 0}.header-text p code{background:#f1f5f9;padding:.1rem .3rem;border-radius:3px;font-size:.75rem;color:#0f172a}.page-body{display:grid;grid-template-columns:340px 1fr;height:calc(100vh - 64px);overflow:hidden}.sidebar{background:#fff;border-right:1px solid #e2e8f0;display:flex;flex-direction:column;overflow-y:auto;padding:1rem .75rem;gap:.75rem}.sidebar-title{display:flex;align-items:center;gap:.4rem;font-size:.78rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:#64748b;margin:0 0 .25rem .1rem}.sidebar-title mat-icon{font-size:.9rem;width:.9rem;height:.9rem}.empty-state{display:flex;flex-direction:column;align-items:center;gap:.6rem;padding:2rem 1rem;text-align:center;color:#94a3b8;border:2px dashed #e2e8f0;border-radius:8px}.empty-state mat-icon{font-size:2rem;width:2rem;height:2rem;opacity:.5}.empty-state p{font-size:.85rem;margin:0}.form-list{display:flex;flex-direction:column;gap:.4rem}.form-item{display:flex;align-items:flex-start;gap:.65rem;padding:.75rem;border:1.5px solid #e2e8f0;border-radius:8px;cursor:pointer;transition:border-color .15s,background .15s,box-shadow .15s;position:relative}.form-item:hover{border-color:#93c5fd;background:#f8faff}.form-item.active{border-color:#2563eb;background:#eff6ff;box-shadow:0 0 0 2px #bfdbfe}.form-item-icon{display:flex;align-items:center;justify-content:center;width:36px;height:36px;background:#f1f5f9;border-radius:7px;flex-shrink:0;margin-top:.1rem}.form-item.active .form-item-icon{background:#dbeafe}.form-item-icon mat-icon{font-size:1.1rem;width:1.1rem;height:1.1rem;color:#64748b}.form-item.active .form-item-icon mat-icon{color:#2563eb}.form-item-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:.2rem}.form-item-title{font-size:.88rem;font-weight:600;color:#1e293b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.form-item-meta{font-size:.72rem;color:#94a3b8}.form-item-id{font-size:.68rem;color:#94a3b8;font-family:Courier New,monospace;background:#f1f5f9;padding:.1rem .3rem;border-radius:3px;align-self:flex-start}.form-item-actions{display:flex;flex-direction:column;gap:.15rem;flex-shrink:0}.action-icon{display:flex;align-items:center;justify-content:center;width:26px;height:26px;border:none;background:transparent;border-radius:5px;color:#94a3b8;cursor:pointer;text-decoration:none;transition:background .12s,color .12s}.action-icon:hover{background:#f1f5f9;color:#1e293b}.action-icon.danger:hover{background:#fee2e2;color:#dc2626}.action-icon mat-icon{font-size:.85rem;width:.85rem;height:.85rem}.code-hint{background:#1e293b;border-radius:8px;padding:.85rem;margin-top:auto}.code-hint-title{display:flex;align-items:center;gap:.35rem;font-size:.72rem;color:#94a3b8;margin:0 0 .6rem}.code-hint-title mat-icon{font-size:.85rem;width:.85rem;height:.85rem;color:#60a5fa}.code-block{margin:0;font-size:.72rem;font-family:Fira Code,Courier New,monospace;color:#7dd3fc;white-space:pre-wrap;line-height:1.65}.content{overflow-y:auto;padding:1.5rem;display:flex;flex-direction:column;align-items:center}.select-hint{display:flex;flex-direction:column;align-items:center;gap:.75rem;padding:4rem 1rem;color:#94a3b8;text-align:center}.select-hint mat-icon{font-size:2.5rem;width:2.5rem;height:2.5rem;opacity:.5}.select-hint p{font-size:.9rem;margin:0;max-width:260px}.select-hint.error{color:#dc2626}.select-hint.error mat-icon{opacity:1}.select-hint.error code{background:#fee2e2;padding:.1rem .35rem;border-radius:4px;font-family:monospace;font-size:.85rem}.form-card{width:100%;max-width:700px;background:#fff;border:1.5px solid #e2e8f0;border-radius:12px;overflow:hidden;box-shadow:0 2px 12px #0000000f}.form-card-header{display:flex;align-items:flex-start;gap:.85rem;padding:1.1rem 1.25rem;background:#eff6ff;border-bottom:1px solid #bfdbfe}.form-card-header>mat-icon{font-size:1.5rem;width:1.5rem;height:1.5rem;color:#2563eb;margin-top:.15rem;flex-shrink:0}.form-card-header h2{font-size:1rem;font-weight:700;color:#1e293b;margin:0 0 .35rem}.loaded-badge{display:inline-flex;align-items:center;gap:.3rem;font-size:.72rem;color:#059669}.loaded-badge mat-icon{font-size:.8rem;width:.8rem;height:.8rem}.loaded-badge code{background:#d1fae5;padding:.1rem .3rem;border-radius:3px;font-size:.7rem;color:#065f46;font-family:Courier New,monospace}.form-card ngx-dynamic-form{display:block;padding:1.25rem}.result-card{display:flex;flex-direction:column;align-items:center;gap:1rem;width:100%;max-width:620px;background:#fff;border:1.5px solid #bbf7d0;border-radius:12px;padding:2.5rem 2rem;text-align:center;box-shadow:0 4px 20px #16a34a14}.result-icon{font-size:3rem;width:3rem;height:3rem;color:#16a34a}.result-card h2{font-size:1.2rem;color:#1e293b;margin:0}.result-sub{font-size:.85rem;color:#64748b;margin:0}.result-sub code{background:#f1f5f9;padding:.1rem .3rem;border-radius:3px;font-family:monospace;font-size:.8rem}.result-data{width:100%;background:#1e293b;border-radius:8px;padding:1rem;text-align:left;overflow:auto}.result-data-title{font-size:.72rem;color:#94a3b8;text-transform:uppercase;letter-spacing:.04em;margin:0 0 .5rem}.result-data pre{margin:0;font-size:.78rem;font-family:Fira Code,monospace;color:#7dd3fc;white-space:pre-wrap;word-break:break-all}.result-actions{display:flex;gap:.75rem;flex-wrap:wrap;justify-content:center}.btn-primary{display:inline-flex;align-items:center;gap:.35rem;padding:.45rem 1rem;border:none;background:#2563eb;color:#fff;border-radius:7px;font-size:.85rem;font-weight:500;text-decoration:none;cursor:pointer;transition:background .15s}.btn-primary:hover{background:#1d4ed8}.btn-primary mat-icon{font-size:1rem;width:1rem;height:1rem}.btn-outline{display:inline-flex;align-items:center;gap:.35rem;padding:.45rem 1rem;border:1px solid #e2e8f0;background:#fff;color:#1e293b;border-radius:7px;font-size:.85rem;cursor:pointer;text-decoration:none;transition:border-color .15s}.btn-outline:hover{border-color:#94a3b8}.btn-outline mat-icon{font-size:1rem;width:1rem;height:1rem}.btn-outline-sm{display:inline-flex;align-items:center;gap:.25rem;padding:.3rem .7rem;border:1px solid #cbd5e1;background:#fff;color:#64748b;border-radius:6px;font-size:.78rem;text-decoration:none;transition:border-color .15s,color .15s}.btn-outline-sm:hover{border-color:#2563eb;color:#2563eb}.btn-outline-sm mat-icon{font-size:.85rem;width:.85rem;height:.85rem}\n"] }]
953
+ }], propDecorators: { builderRoute: [{
954
+ type: Input
955
+ }], formRoute: [{
956
+ type: Input
957
+ }], backRoute: [{
958
+ type: Input
959
+ }] } });
960
+
961
+ class FormLoaderComponent {
962
+ constructor() {
963
+ /** Route du lien "Retour" */
964
+ this.backRoute = '/';
965
+ /** Route vers le builder */
966
+ this.builderRoute = '/builder';
967
+ this.route = inject(ActivatedRoute, { optional: true });
968
+ this.formService = inject(FormConfigService);
969
+ this.config = signal(null);
970
+ this.formTitle = signal('');
971
+ this.schemaId = signal('');
972
+ this.submitted = signal(false);
973
+ this.submitData = signal({});
974
+ this.notFound = signal(false);
975
+ }
976
+ ngOnInit() {
977
+ const id = this.formId
978
+ ?? this.route?.snapshot?.paramMap?.get('id')
979
+ ?? '';
980
+ this.schemaId.set(id);
981
+ const schema = this.formService.get(id);
982
+ if (schema) {
983
+ this.config.set(schema.config);
984
+ this.formTitle.set(schema.title);
985
+ }
986
+ else {
987
+ this.notFound.set(true);
988
+ }
989
+ }
990
+ onSubmit(event) {
991
+ this.submitData.set(event.value);
992
+ this.submitted.set(true);
993
+ }
994
+ reset() {
995
+ this.submitted.set(false);
996
+ this.submitData.set({});
997
+ }
998
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormLoaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
999
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: FormLoaderComponent, isStandalone: true, selector: "ngx-form-loader", inputs: { formId: "formId", backRoute: "backRoute", builderRoute: "builderRoute" }, ngImport: i0, template: "<div class=\"loader-page\">\n\n <header class=\"loader-header\">\n <a [routerLink]=\"backRoute\" class=\"back-link\">\n <mat-icon>arrow_back</mat-icon>\n </a>\n @if (formTitle()) {\n <h1 class=\"loader-title\">{{ formTitle() }}</h1>\n }\n <a [routerLink]=\"builderRoute\" class=\"edit-link\" title=\"\u00C9diter dans le Builder\">\n <mat-icon>edit</mat-icon>\n <span>\u00C9diter</span>\n </a>\n </header>\n\n <div class=\"loader-body\">\n\n @if (config(); as cfg) {\n\n @if (!submitted()) {\n <div class=\"form-wrapper\">\n <p class=\"api-badge\">\n <mat-icon>api</mat-icon>\n Charg\u00E9 via FormConfigService \u00B7 ID :\n <code>{{ schemaId() }}</code>\n </p>\n <ngx-dynamic-form [config]=\"cfg\" (formSubmit)=\"onSubmit($event)\" />\n </div>\n }\n\n @if (submitted()) {\n <div class=\"success-card\">\n <mat-icon class=\"success-icon\">check_circle</mat-icon>\n <h2>Formulaire soumis avec succ\u00E8s</h2>\n <p>Les donn\u00E9es ont \u00E9t\u00E9 re\u00E7ues (simulation \u2014 aucun envoi r\u00E9seau).</p>\n\n <div class=\"data-preview\">\n <p class=\"data-title\">Donn\u00E9es re\u00E7ues :</p>\n <pre>{{ submitData() | json }}</pre>\n </div>\n\n <div class=\"success-actions\">\n <button class=\"btn-outline\" (click)=\"reset()\">\n <mat-icon>refresh</mat-icon> Recommencer\n </button>\n <a class=\"btn-primary\" [routerLink]=\"builderRoute\">\n <mat-icon>dashboard_customize</mat-icon> Retour au builder\n </a>\n </div>\n </div>\n }\n\n }\n\n @if (notFound()) {\n <div class=\"not-found\">\n <mat-icon>search_off</mat-icon>\n <h2>Formulaire introuvable</h2>\n <p>\n Aucun formulaire avec l'identifiant <code>{{ schemaId() }}</code> n'a \u00E9t\u00E9 trouv\u00E9\n dans le stockage.\n </p>\n <p class=\"not-found-hint\">\n Cr\u00E9ez un formulaire dans le <a [routerLink]=\"builderRoute\">Form Builder</a>,\n sauvegardez-le, puis copiez son lien.\n </p>\n </div>\n }\n\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block;min-height:100vh;background:#f8fafc;font-family:Roboto,sans-serif}.loader-page{display:flex;flex-direction:column;min-height:100vh}.loader-header{display:flex;align-items:center;gap:1rem;padding:.85rem 1.5rem;background:#fff;border-bottom:1px solid #e2e8f0;box-shadow:0 1px 4px #0000000f}.back-link{display:flex;align-items:center;color:#64748b;text-decoration:none;border-radius:6px;padding:.2rem;transition:color .15s,background .15s}.back-link:hover{color:#2563eb;background:#eff6ff}.back-link mat-icon{font-size:1.1rem;width:1.1rem;height:1.1rem}.loader-title{flex:1;font-size:1.05rem;font-weight:600;color:#1e293b;margin:0}.edit-link{display:inline-flex;align-items:center;gap:.3rem;padding:.3rem .7rem;border:1px solid #e2e8f0;border-radius:6px;font-size:.82rem;color:#64748b;text-decoration:none;transition:border-color .15s,color .15s}.edit-link:hover{border-color:#2563eb;color:#2563eb}.edit-link mat-icon{font-size:.95rem;width:.95rem;height:.95rem}.loader-body{flex:1;display:flex;flex-direction:column;align-items:center;padding:2rem 1rem}.form-wrapper{width:100%;max-width:700px;display:flex;flex-direction:column;gap:1rem}.api-badge{display:inline-flex;align-items:center;gap:.4rem;font-size:.78rem;color:#64748b;background:#fff;border:1px solid #e2e8f0;border-radius:20px;padding:.3rem .8rem;margin:0}.api-badge mat-icon{font-size:.9rem;width:.9rem;height:.9rem;color:#2563eb}.api-badge code{background:#f1f5f9;padding:.1rem .35rem;border-radius:4px;font-family:monospace;font-size:.8rem;color:#0f172a}.success-card{display:flex;flex-direction:column;align-items:center;gap:1rem;background:#fff;border:1px solid #bbf7d0;border-radius:12px;padding:2.5rem 2rem;max-width:560px;width:100%;text-align:center;box-shadow:0 4px 20px #16a34a14}.success-icon{font-size:3rem;width:3rem;height:3rem;color:#16a34a}.success-card h2{font-size:1.25rem;color:#1e293b;margin:0}.success-card>p{font-size:.88rem;color:#64748b;margin:0}.data-preview{width:100%;text-align:left;background:#1e293b;border-radius:8px;padding:1rem;overflow:auto}.data-title{font-size:.75rem;color:#94a3b8;margin:0 0 .5rem;text-transform:uppercase;letter-spacing:.05em}.data-preview pre{margin:0;font-size:.78rem;font-family:Fira Code,monospace;color:#7dd3fc;white-space:pre-wrap;word-break:break-all}.success-actions{display:flex;gap:.75rem;flex-wrap:wrap;justify-content:center}.btn-outline{display:inline-flex;align-items:center;gap:.35rem;padding:.45rem 1rem;border:1px solid #e2e8f0;background:#fff;border-radius:7px;font-size:.85rem;color:#1e293b;cursor:pointer;text-decoration:none;transition:border-color .15s}.btn-outline:hover{border-color:#94a3b8}.btn-outline mat-icon{font-size:1rem;width:1rem;height:1rem}.btn-primary{display:inline-flex;align-items:center;gap:.35rem;padding:.45rem 1rem;border:none;background:#2563eb;color:#fff;border-radius:7px;font-size:.85rem;font-weight:500;text-decoration:none;transition:background .15s}.btn-primary:hover{background:#1d4ed8}.btn-primary mat-icon{font-size:1rem;width:1rem;height:1rem}.not-found{display:flex;flex-direction:column;align-items:center;gap:.75rem;text-align:center;padding:3rem 1rem;color:#64748b}.not-found mat-icon{font-size:3rem;width:3rem;height:3rem;color:#cbd5e1}.not-found h2{font-size:1.2rem;color:#1e293b;margin:0}.not-found p{margin:0;font-size:.88rem}.not-found code{background:#f1f5f9;padding:.1rem .35rem;border-radius:4px;font-family:monospace}.not-found-hint a{color:#2563eb}\n"], dependencies: [{ kind: "component", type: DynamicFormComponent, selector: "ngx-dynamic-form", inputs: ["config", "initialValues"], outputs: ["formSubmit", "formChange", "formReset"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "pipe", type: JsonPipe, name: "json" }] }); }
1000
+ }
1001
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormLoaderComponent, decorators: [{
1002
+ type: Component,
1003
+ args: [{ selector: 'ngx-form-loader', standalone: true, imports: [DynamicFormComponent, MatIcon, RouterLink, JsonPipe], template: "<div class=\"loader-page\">\n\n <header class=\"loader-header\">\n <a [routerLink]=\"backRoute\" class=\"back-link\">\n <mat-icon>arrow_back</mat-icon>\n </a>\n @if (formTitle()) {\n <h1 class=\"loader-title\">{{ formTitle() }}</h1>\n }\n <a [routerLink]=\"builderRoute\" class=\"edit-link\" title=\"\u00C9diter dans le Builder\">\n <mat-icon>edit</mat-icon>\n <span>\u00C9diter</span>\n </a>\n </header>\n\n <div class=\"loader-body\">\n\n @if (config(); as cfg) {\n\n @if (!submitted()) {\n <div class=\"form-wrapper\">\n <p class=\"api-badge\">\n <mat-icon>api</mat-icon>\n Charg\u00E9 via FormConfigService \u00B7 ID :\n <code>{{ schemaId() }}</code>\n </p>\n <ngx-dynamic-form [config]=\"cfg\" (formSubmit)=\"onSubmit($event)\" />\n </div>\n }\n\n @if (submitted()) {\n <div class=\"success-card\">\n <mat-icon class=\"success-icon\">check_circle</mat-icon>\n <h2>Formulaire soumis avec succ\u00E8s</h2>\n <p>Les donn\u00E9es ont \u00E9t\u00E9 re\u00E7ues (simulation \u2014 aucun envoi r\u00E9seau).</p>\n\n <div class=\"data-preview\">\n <p class=\"data-title\">Donn\u00E9es re\u00E7ues :</p>\n <pre>{{ submitData() | json }}</pre>\n </div>\n\n <div class=\"success-actions\">\n <button class=\"btn-outline\" (click)=\"reset()\">\n <mat-icon>refresh</mat-icon> Recommencer\n </button>\n <a class=\"btn-primary\" [routerLink]=\"builderRoute\">\n <mat-icon>dashboard_customize</mat-icon> Retour au builder\n </a>\n </div>\n </div>\n }\n\n }\n\n @if (notFound()) {\n <div class=\"not-found\">\n <mat-icon>search_off</mat-icon>\n <h2>Formulaire introuvable</h2>\n <p>\n Aucun formulaire avec l'identifiant <code>{{ schemaId() }}</code> n'a \u00E9t\u00E9 trouv\u00E9\n dans le stockage.\n </p>\n <p class=\"not-found-hint\">\n Cr\u00E9ez un formulaire dans le <a [routerLink]=\"builderRoute\">Form Builder</a>,\n sauvegardez-le, puis copiez son lien.\n </p>\n </div>\n }\n\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block;min-height:100vh;background:#f8fafc;font-family:Roboto,sans-serif}.loader-page{display:flex;flex-direction:column;min-height:100vh}.loader-header{display:flex;align-items:center;gap:1rem;padding:.85rem 1.5rem;background:#fff;border-bottom:1px solid #e2e8f0;box-shadow:0 1px 4px #0000000f}.back-link{display:flex;align-items:center;color:#64748b;text-decoration:none;border-radius:6px;padding:.2rem;transition:color .15s,background .15s}.back-link:hover{color:#2563eb;background:#eff6ff}.back-link mat-icon{font-size:1.1rem;width:1.1rem;height:1.1rem}.loader-title{flex:1;font-size:1.05rem;font-weight:600;color:#1e293b;margin:0}.edit-link{display:inline-flex;align-items:center;gap:.3rem;padding:.3rem .7rem;border:1px solid #e2e8f0;border-radius:6px;font-size:.82rem;color:#64748b;text-decoration:none;transition:border-color .15s,color .15s}.edit-link:hover{border-color:#2563eb;color:#2563eb}.edit-link mat-icon{font-size:.95rem;width:.95rem;height:.95rem}.loader-body{flex:1;display:flex;flex-direction:column;align-items:center;padding:2rem 1rem}.form-wrapper{width:100%;max-width:700px;display:flex;flex-direction:column;gap:1rem}.api-badge{display:inline-flex;align-items:center;gap:.4rem;font-size:.78rem;color:#64748b;background:#fff;border:1px solid #e2e8f0;border-radius:20px;padding:.3rem .8rem;margin:0}.api-badge mat-icon{font-size:.9rem;width:.9rem;height:.9rem;color:#2563eb}.api-badge code{background:#f1f5f9;padding:.1rem .35rem;border-radius:4px;font-family:monospace;font-size:.8rem;color:#0f172a}.success-card{display:flex;flex-direction:column;align-items:center;gap:1rem;background:#fff;border:1px solid #bbf7d0;border-radius:12px;padding:2.5rem 2rem;max-width:560px;width:100%;text-align:center;box-shadow:0 4px 20px #16a34a14}.success-icon{font-size:3rem;width:3rem;height:3rem;color:#16a34a}.success-card h2{font-size:1.25rem;color:#1e293b;margin:0}.success-card>p{font-size:.88rem;color:#64748b;margin:0}.data-preview{width:100%;text-align:left;background:#1e293b;border-radius:8px;padding:1rem;overflow:auto}.data-title{font-size:.75rem;color:#94a3b8;margin:0 0 .5rem;text-transform:uppercase;letter-spacing:.05em}.data-preview pre{margin:0;font-size:.78rem;font-family:Fira Code,monospace;color:#7dd3fc;white-space:pre-wrap;word-break:break-all}.success-actions{display:flex;gap:.75rem;flex-wrap:wrap;justify-content:center}.btn-outline{display:inline-flex;align-items:center;gap:.35rem;padding:.45rem 1rem;border:1px solid #e2e8f0;background:#fff;border-radius:7px;font-size:.85rem;color:#1e293b;cursor:pointer;text-decoration:none;transition:border-color .15s}.btn-outline:hover{border-color:#94a3b8}.btn-outline mat-icon{font-size:1rem;width:1rem;height:1rem}.btn-primary{display:inline-flex;align-items:center;gap:.35rem;padding:.45rem 1rem;border:none;background:#2563eb;color:#fff;border-radius:7px;font-size:.85rem;font-weight:500;text-decoration:none;transition:background .15s}.btn-primary:hover{background:#1d4ed8}.btn-primary mat-icon{font-size:1rem;width:1rem;height:1rem}.not-found{display:flex;flex-direction:column;align-items:center;gap:.75rem;text-align:center;padding:3rem 1rem;color:#64748b}.not-found mat-icon{font-size:3rem;width:3rem;height:3rem;color:#cbd5e1}.not-found h2{font-size:1.2rem;color:#1e293b;margin:0}.not-found p{margin:0;font-size:.88rem}.not-found code{background:#f1f5f9;padding:.1rem .35rem;border-radius:4px;font-family:monospace}.not-found-hint a{color:#2563eb}\n"] }]
1004
+ }], propDecorators: { formId: [{
1005
+ type: Input
1006
+ }], backRoute: [{
1007
+ type: Input
1008
+ }], builderRoute: [{
1009
+ type: Input
1010
+ }] } });
1011
+
1012
+ const KEY = 'ngx-dynamic-form__schemas';
1013
+ class LocalStorageAdapter extends FormStorageAdapter {
1014
+ list() {
1015
+ return of(this._read());
1016
+ }
1017
+ get(id) {
1018
+ return of(this._read().find(s => s.id === id) ?? null);
1019
+ }
1020
+ save(schema) {
1021
+ const list = this._read().filter(s => s.id !== schema.id);
1022
+ this._write([...list, schema]);
1023
+ return of(schema);
1024
+ }
1025
+ delete(id) {
1026
+ this._write(this._read().filter(s => s.id !== id));
1027
+ return of(undefined);
1028
+ }
1029
+ _read() {
1030
+ try {
1031
+ const raw = localStorage.getItem(KEY);
1032
+ return raw ? JSON.parse(raw) : [];
1033
+ }
1034
+ catch {
1035
+ return [];
1036
+ }
1037
+ }
1038
+ _write(schemas) {
1039
+ try {
1040
+ localStorage.setItem(KEY, JSON.stringify(schemas));
1041
+ }
1042
+ catch {
1043
+ console.warn('[LocalStorageAdapter] Cannot write to localStorage');
1044
+ }
1045
+ }
1046
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: LocalStorageAdapter, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
1047
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: LocalStorageAdapter }); }
1048
+ }
1049
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: LocalStorageAdapter, decorators: [{
1050
+ type: Injectable
1051
+ }] });
1052
+
1053
+ const FORM_API_URL = new InjectionToken('FORM_API_URL');
1054
+ class HttpStorageAdapter extends FormStorageAdapter {
1055
+ constructor() {
1056
+ super(...arguments);
1057
+ this.http = inject(HttpClient);
1058
+ this.apiUrl = inject(FORM_API_URL);
1059
+ }
1060
+ list() {
1061
+ return this.http.get(this.apiUrl);
1062
+ }
1063
+ get(id) {
1064
+ return this.http.get(`${this.apiUrl}/${id}`).pipe(map(s => s ?? null));
1065
+ }
1066
+ save(schema) {
1067
+ return this.http.put(`${this.apiUrl}/${schema.id}`, schema);
1068
+ }
1069
+ delete(id) {
1070
+ return this.http.delete(`${this.apiUrl}/${id}`);
1071
+ }
1072
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HttpStorageAdapter, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
1073
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HttpStorageAdapter }); }
1074
+ }
1075
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HttpStorageAdapter, decorators: [{
1076
+ type: Injectable
1077
+ }] });
1078
+
1079
+ function provideNgxForms(options) {
1080
+ if (options.storage === 'http') {
1081
+ return makeEnvironmentProviders([
1082
+ { provide: FORM_API_URL, useValue: options.apiUrl },
1083
+ { provide: FormStorageAdapter, useClass: HttpStorageAdapter },
1084
+ FormConfigService,
1085
+ ]);
1086
+ }
1087
+ return makeEnvironmentProviders([
1088
+ { provide: FormStorageAdapter, useClass: LocalStorageAdapter },
1089
+ FormConfigService,
1090
+ ]);
1091
+ }
1092
+
1093
+ // Components
1094
+
1095
+ /**
1096
+ * Generated bundle index. Do not edit.
1097
+ */
1098
+
1099
+ export { ConditionEvaluatorService, DynamicFieldComponent, DynamicFormBuilderService, DynamicFormComponent, FORM_API_URL, FormBuilderComponent, FormConfigService, FormLoaderComponent, FormStorageAdapter, HttpStorageAdapter, LocalStorageAdapter, MesFormulairesComponent, provideNgxForms };
1100
+ //# sourceMappingURL=suntelecoms-ngx-dynamic-form.mjs.map