@praxisui/stepper 1.0.0-beta.17

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,2046 @@
1
+ import * as i1$1 from '@angular/common';
2
+ import { CommonModule } from '@angular/common';
3
+ import * as i0 from '@angular/core';
4
+ import { Inject, Component, inject, signal, EventEmitter, computed, TemplateRef, Output, Input, ContentChild, ChangeDetectionStrategy, ENVIRONMENT_INITIALIZER } from '@angular/core';
5
+ import * as i2$1 from '@angular/material/stepper';
6
+ import { MatStepperModule } from '@angular/material/stepper';
7
+ import * as i6 from '@angular/material/button';
8
+ import { MatButtonModule } from '@angular/material/button';
9
+ import * as i5$1 from '@angular/material/icon';
10
+ import { MatIconModule } from '@angular/material/icon';
11
+ import { IconPickerService, PraxisIconDirective, DynamicWidgetLoaderDirective, EmptyStateCardComponent, ComponentMetadataRegistry } from '@praxisui/core';
12
+ import * as i2 from '@angular/forms';
13
+ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
14
+ import { PraxisDynamicFormConfigEditor, PraxisDynamicForm } from '@praxisui/dynamic-form';
15
+ import { SettingsPanelService, SETTINGS_PANEL_DATA } from '@praxisui/settings-panel';
16
+ import * as i3 from '@angular/material/form-field';
17
+ import { MatFormFieldModule } from '@angular/material/form-field';
18
+ import * as i4 from '@angular/material/input';
19
+ import { MatInputModule } from '@angular/material/input';
20
+ import * as i7 from '@angular/material/slide-toggle';
21
+ import { MatSlideToggleModule } from '@angular/material/slide-toggle';
22
+ import * as i8 from '@angular/cdk/drag-drop';
23
+ import { moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop';
24
+ import * as i9 from '@angular/material/tooltip';
25
+ import { MatTooltipModule } from '@angular/material/tooltip';
26
+ import { MatChipsModule } from '@angular/material/chips';
27
+ import * as i10 from '@angular/material/card';
28
+ import { MatCardModule } from '@angular/material/card';
29
+ import * as i11 from '@angular/material/button-toggle';
30
+ import { MatButtonToggleModule } from '@angular/material/button-toggle';
31
+ import * as i1 from '@angular/material/dialog';
32
+ import { MAT_DIALOG_DATA, MatDialogModule, MatDialog } from '@angular/material/dialog';
33
+ import * as i12 from '@angular/material/tabs';
34
+ import { MatTabsModule } from '@angular/material/tabs';
35
+ import { BehaviorSubject } from 'rxjs';
36
+ import { PraxisListConfigEditor } from '@praxisui/list';
37
+ import { PraxisFilesUploadConfigEditor } from '@praxisui/files-upload';
38
+ import { ComponentPaletteDialogComponent } from '@praxisui/page-builder';
39
+ import * as i5 from '@angular/material/checkbox';
40
+ import { MatCheckboxModule } from '@angular/material/checkbox';
41
+
42
+ class SelectQuickConfigDialogComponent {
43
+ ref;
44
+ model = {
45
+ label: '',
46
+ name: '',
47
+ resourcePath: '',
48
+ optionLabelKey: 'label',
49
+ optionValueKey: 'id',
50
+ multiple: false,
51
+ required: false,
52
+ };
53
+ constructor(ref, data) {
54
+ this.ref = ref;
55
+ if (data?.from)
56
+ this.model = { ...this.model, ...data.from };
57
+ }
58
+ cancel() { this.ref.close(); }
59
+ confirm() { this.ref.close(this.model); }
60
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: SelectQuickConfigDialogComponent, deps: [{ token: i1.MatDialogRef }, { token: MAT_DIALOG_DATA }], target: i0.ɵɵFactoryTarget.Component });
61
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.4", type: SelectQuickConfigDialogComponent, isStandalone: true, selector: "praxis-select-quick-config-dialog", ngImport: i0, template: `
62
+ <h2 mat-dialog-title>Configurar Select (busca)</h2>
63
+ <div mat-dialog-content class="g gap-12">
64
+ <mat-form-field appearance="outline">
65
+ <mat-label>Rótulo</mat-label>
66
+ <input matInput [(ngModel)]="model.label" placeholder="Ex.: Cliente" />
67
+ </mat-form-field>
68
+ <mat-form-field appearance="outline">
69
+ <mat-label>Nome</mat-label>
70
+ <input matInput [(ngModel)]="model.name" placeholder="Ex.: customerId" />
71
+ </mat-form-field>
72
+ <mat-form-field appearance="outline" class="w-full">
73
+ <mat-label>resourcePath</mat-label>
74
+ <input matInput [(ngModel)]="model.resourcePath" placeholder="Ex.: customers" />
75
+ </mat-form-field>
76
+ <div class="g g-auto-220 gap-12">
77
+ <mat-form-field appearance="outline">
78
+ <mat-label>optionLabelKey</mat-label>
79
+ <input matInput [(ngModel)]="model.optionLabelKey" placeholder="Ex.: name" />
80
+ </mat-form-field>
81
+ <mat-form-field appearance="outline">
82
+ <mat-label>optionValueKey</mat-label>
83
+ <input matInput [(ngModel)]="model.optionValueKey" placeholder="Ex.: id" />
84
+ </mat-form-field>
85
+ </div>
86
+ <div class="row gap-16 ai-center">
87
+ <mat-checkbox [(ngModel)]="model.multiple">Múltipla seleção</mat-checkbox>
88
+ <mat-checkbox [(ngModel)]="model.required">Obrigatório</mat-checkbox>
89
+ </div>
90
+ </div>
91
+ <div mat-dialog-actions align="end">
92
+ <button mat-button (click)="cancel()">Cancelar</button>
93
+ <button mat-flat-button color="primary" (click)="confirm()">Adicionar</button>
94
+ </div>
95
+ `, isInline: true, styles: [".g{display:grid}.gap-12{gap:12px}.w-full{width:100%}.row{display:flex}.ai-center{align-items:center}.gap-16{gap:16px}.g-auto-220{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.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: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatDialogModule }, { kind: "directive", type: i1.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i1.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i1.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i4.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: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i5.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: "ngmodule", type: MatButtonModule }, { kind: "component", type: i6.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }] });
96
+ }
97
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: SelectQuickConfigDialogComponent, decorators: [{
98
+ type: Component,
99
+ args: [{ selector: 'praxis-select-quick-config-dialog', standalone: true, imports: [CommonModule, FormsModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatCheckboxModule, MatButtonModule], template: `
100
+ <h2 mat-dialog-title>Configurar Select (busca)</h2>
101
+ <div mat-dialog-content class="g gap-12">
102
+ <mat-form-field appearance="outline">
103
+ <mat-label>Rótulo</mat-label>
104
+ <input matInput [(ngModel)]="model.label" placeholder="Ex.: Cliente" />
105
+ </mat-form-field>
106
+ <mat-form-field appearance="outline">
107
+ <mat-label>Nome</mat-label>
108
+ <input matInput [(ngModel)]="model.name" placeholder="Ex.: customerId" />
109
+ </mat-form-field>
110
+ <mat-form-field appearance="outline" class="w-full">
111
+ <mat-label>resourcePath</mat-label>
112
+ <input matInput [(ngModel)]="model.resourcePath" placeholder="Ex.: customers" />
113
+ </mat-form-field>
114
+ <div class="g g-auto-220 gap-12">
115
+ <mat-form-field appearance="outline">
116
+ <mat-label>optionLabelKey</mat-label>
117
+ <input matInput [(ngModel)]="model.optionLabelKey" placeholder="Ex.: name" />
118
+ </mat-form-field>
119
+ <mat-form-field appearance="outline">
120
+ <mat-label>optionValueKey</mat-label>
121
+ <input matInput [(ngModel)]="model.optionValueKey" placeholder="Ex.: id" />
122
+ </mat-form-field>
123
+ </div>
124
+ <div class="row gap-16 ai-center">
125
+ <mat-checkbox [(ngModel)]="model.multiple">Múltipla seleção</mat-checkbox>
126
+ <mat-checkbox [(ngModel)]="model.required">Obrigatório</mat-checkbox>
127
+ </div>
128
+ </div>
129
+ <div mat-dialog-actions align="end">
130
+ <button mat-button (click)="cancel()">Cancelar</button>
131
+ <button mat-flat-button color="primary" (click)="confirm()">Adicionar</button>
132
+ </div>
133
+ `, styles: [".g{display:grid}.gap-12{gap:12px}.w-full{width:100%}.row{display:flex}.ai-center{align-items:center}.gap-16{gap:16px}.g-auto-220{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}\n"] }]
134
+ }], ctorParameters: () => [{ type: i1.MatDialogRef }, { type: undefined, decorators: [{
135
+ type: Inject,
136
+ args: [MAT_DIALOG_DATA]
137
+ }] }] });
138
+
139
+ class PraxisStepperConfigEditor {
140
+ config = { steps: [], orientation: 'horizontal', headerPosition: 'top', linear: false };
141
+ activeIndex = 0;
142
+ // SettingsValueProvider observables
143
+ isDirty$ = new BehaviorSubject(false);
144
+ isValid$ = new BehaviorSubject(true);
145
+ isBusy$ = new BehaviorSubject(false);
146
+ settings = inject(SettingsPanelService);
147
+ dialog = inject(MatDialog);
148
+ iconPicker = inject(IconPickerService);
149
+ constructor(data) {
150
+ let seed = data?.config ?? data;
151
+ // Normalize when seed is a JSON string
152
+ if (typeof seed === 'string') {
153
+ try {
154
+ seed = JSON.parse(seed);
155
+ }
156
+ catch {
157
+ seed = undefined;
158
+ }
159
+ }
160
+ if (seed && typeof seed === 'object') {
161
+ // shallow clone to detach references
162
+ this.config = JSON.parse(JSON.stringify(seed));
163
+ if (!Array.isArray(this.config.steps)) {
164
+ this.config.steps = [];
165
+ }
166
+ }
167
+ else {
168
+ this.config.steps = [{ id: 'step1', label: 'Novo passo' }];
169
+ this.isDirty$.next(true);
170
+ }
171
+ }
172
+ markDirty() { this.isDirty$.next(true); this.validate(); }
173
+ validate() {
174
+ const valid = Array.isArray(this.config.steps) && this.config.steps.every(s => !!s.label);
175
+ this.isValid$.next(valid);
176
+ }
177
+ // ------- Appearance (theming tokens) -------
178
+ get navigationCfg() {
179
+ const nav = (this.config.navigation ??= {});
180
+ return nav;
181
+ }
182
+ get appearance() {
183
+ const ap = (this.config.appearance ??= {});
184
+ (ap.tokens ??= {});
185
+ return ap;
186
+ }
187
+ stepperTokenKeys = [
188
+ 'container-color',
189
+ 'line-color',
190
+ 'header-label-text-color',
191
+ 'header-optional-label-text-color',
192
+ 'header-icon-foreground-color',
193
+ 'header-icon-background-color',
194
+ 'header-done-state-icon-background-color',
195
+ 'header-done-state-icon-foreground-color',
196
+ 'header-edit-state-icon-background-color',
197
+ 'header-edit-state-icon-foreground-color',
198
+ 'header-error-state-icon-background-color',
199
+ 'header-error-state-icon-foreground-color',
200
+ 'header-error-state-label-text-color',
201
+ 'header-focus-state-layer-color',
202
+ 'header-hover-state-layer-color',
203
+ 'header-label-text-font',
204
+ 'header-label-text-size',
205
+ 'header-label-text-weight',
206
+ 'header-selected-state-label-text-size',
207
+ 'header-selected-state-label-text-weight',
208
+ 'container-text-font',
209
+ 'header-height',
210
+ 'header-focus-state-layer-shape',
211
+ 'header-hover-state-layer-shape',
212
+ 'header-selected-state-icon-background-color',
213
+ 'header-selected-state-icon-foreground-color',
214
+ 'header-selected-state-label-text-color',
215
+ ];
216
+ onAppearanceChange() { this.markDirty(); }
217
+ onTokenChange(key, value) {
218
+ const tokens = (this.appearance.tokens = this.appearance.tokens || {});
219
+ if (!value) {
220
+ delete tokens[key];
221
+ }
222
+ else {
223
+ tokens[key] = value;
224
+ }
225
+ this.onAppearanceChange();
226
+ }
227
+ clearTokens() {
228
+ if (!this.config.appearance)
229
+ return;
230
+ this.config.appearance.tokens = {};
231
+ this.onAppearanceChange();
232
+ }
233
+ applyPreset(id) {
234
+ const presets = {
235
+ primary: {
236
+ 'container-color': 'var(--md-sys-color-surface-container)',
237
+ 'header-label-text-color': 'var(--md-sys-color-primary)',
238
+ 'header-selected-state-icon-background-color': 'var(--md-sys-color-primary)',
239
+ 'header-selected-state-icon-foreground-color': '#fff',
240
+ 'line-color': 'color-mix(in oklab, var(--md-sys-color-primary) 40%, transparent)',
241
+ },
242
+ neutral: {
243
+ 'container-color': 'var(--md-sys-color-surface)',
244
+ 'header-label-text-color': 'var(--md-sys-color-on-surface)',
245
+ 'line-color': 'var(--md-sys-color-outline-variant)',
246
+ },
247
+ 'high-contrast': {
248
+ 'container-color': '#000',
249
+ 'header-label-text-color': '#fff',
250
+ 'header-selected-state-icon-background-color': 'var(--md-sys-color-on-primary, #ffffff)',
251
+ 'header-selected-state-icon-foreground-color': '#000',
252
+ 'line-color': '#fff',
253
+ },
254
+ };
255
+ const preset = presets[id] || {};
256
+ const tokens = (this.appearance.tokens = this.appearance.tokens || {});
257
+ for (const [k, v] of Object.entries(preset)) {
258
+ tokens[k] = v;
259
+ }
260
+ this.onAppearanceChange();
261
+ }
262
+ scssSnippet() {
263
+ const tokens = this.config.appearance?.tokens || {};
264
+ const entries = Object.entries(tokens).filter(([, v]) => !!v);
265
+ const selector = this.config.appearance?.themeClass ? `.${this.config.appearance.themeClass}` : ':root';
266
+ const map = entries.map(([k, v]) => ` ${k}: ${v},`).join('\n');
267
+ return `${selector} {\n @include mat.stepper-overrides((\n${map}\n ));\n}`;
268
+ }
269
+ // ===== Quick editors for common tokens =====
270
+ get headerHeightPx() {
271
+ const raw = this.appearance.tokens?.['header-height'];
272
+ if (!raw)
273
+ return undefined;
274
+ const m = /([0-9]+)\s*px/.exec(raw);
275
+ return m ? Number(m[1]) : undefined;
276
+ }
277
+ setHeaderHeightPx(v) {
278
+ const n = Number(v);
279
+ if (!n) {
280
+ delete this.appearance.tokens['header-height'];
281
+ }
282
+ else {
283
+ this.appearance.tokens['header-height'] = `${n}px`;
284
+ }
285
+ this.onAppearanceChange();
286
+ }
287
+ get headerLabelTextSizePx() {
288
+ const raw = this.appearance.tokens?.['header-label-text-size'];
289
+ if (!raw)
290
+ return undefined;
291
+ const m = /([0-9]+)\s*px/.exec(raw);
292
+ return m ? Number(m[1]) : undefined;
293
+ }
294
+ setHeaderLabelTextSizePx(v) {
295
+ const n = Number(v);
296
+ if (!n) {
297
+ delete this.appearance.tokens['header-label-text-size'];
298
+ }
299
+ else {
300
+ this.appearance.tokens['header-label-text-size'] = `${n}px`;
301
+ }
302
+ this.onAppearanceChange();
303
+ }
304
+ get headerLabelTextWeight() {
305
+ return this.appearance.tokens['header-label-text-weight'];
306
+ }
307
+ setHeaderLabelTextWeight(v) {
308
+ if (!v) {
309
+ delete this.appearance.tokens['header-label-text-weight'];
310
+ }
311
+ else {
312
+ this.appearance.tokens['header-label-text-weight'] = v;
313
+ }
314
+ this.onAppearanceChange();
315
+ }
316
+ // ===== Icons (using IconPickerService) =====
317
+ get icons() {
318
+ const ap = this.appearance;
319
+ ap.icons ||= {};
320
+ return ap.icons;
321
+ }
322
+ async pickNavIcon(field) {
323
+ const picked = await this.iconPicker.openDialog({});
324
+ if (picked) {
325
+ (this.config.navigation ||= {})[field] = picked;
326
+ this.onAppearanceChange();
327
+ }
328
+ }
329
+ async pickIcon(kind) {
330
+ const picked = await this.iconPicker.openDialog({});
331
+ if (picked) {
332
+ this.icons[kind] = picked;
333
+ this.onAppearanceChange();
334
+ }
335
+ }
336
+ // ===== Icon set suggestions =====
337
+ symbolsLikely(name) {
338
+ if (!name)
339
+ return false;
340
+ // Heurística simples: counter_1..10 e replace_image são típicos do Material Symbols.
341
+ if (/^counter_\d+$/.test(name))
342
+ return true;
343
+ if (name === 'replace_image')
344
+ return true;
345
+ return false;
346
+ }
347
+ get symbolsLikelySelected() {
348
+ const names = [this.icons.number, this.icons.done, this.icons.edit, this.icons.error].filter(Boolean);
349
+ return names.filter((n) => this.symbolsLikely(n));
350
+ }
351
+ setIconsSetToSymbols() {
352
+ this.appearance.iconsSet = 'material-symbols-outlined';
353
+ this.onAppearanceChange();
354
+ }
355
+ drop(ev) {
356
+ moveItemInArray(this.config.steps, ev.previousIndex, ev.currentIndex);
357
+ this.markDirty();
358
+ }
359
+ setActive(i) { this.activeIndex = i; }
360
+ onActiveIndexChange() { this.activeIndex = Math.max(0, Math.min(this.activeIndex, (this.config.steps.length || 1) - 1)); }
361
+ addStep() {
362
+ const n = this.config.steps.length + 1;
363
+ this.config.steps.push({ id: `step${n}`, label: `Passo ${n}` });
364
+ this.markDirty();
365
+ }
366
+ removeStep(idx) {
367
+ this.config.steps.splice(idx, 1);
368
+ this.markDirty();
369
+ }
370
+ // ------- Active step helpers -------
371
+ get activeStep() { return this.config.steps?.[this.activeIndex] || null; }
372
+ ensureWidgets() { const s = this.activeStep; if (!s.widgets)
373
+ s.widgets = []; return s.widgets; }
374
+ displayWidgetName(w) {
375
+ switch (w.id) {
376
+ case 'praxis-list': return 'Lista (seleção)';
377
+ case 'pdx-material-searchable-select': return 'Select (busca)';
378
+ case 'praxis-files-upload': return 'Upload de arquivos';
379
+ case 'praxis-dynamic-form': return 'Formulário';
380
+ default: return w.inputs?.title || w.inputs?.label || 'Widget';
381
+ }
382
+ }
383
+ dropWidget(ev) {
384
+ const arr = this.ensureWidgets();
385
+ moveItemInArray(arr, ev.previousIndex, ev.currentIndex);
386
+ this.markDirty();
387
+ }
388
+ removeWidget(idx) {
389
+ const arr = this.ensureWidgets();
390
+ arr.splice(idx, 1);
391
+ this.markDirty();
392
+ }
393
+ // ------- Main form -------
394
+ addMainForm() {
395
+ const step = this.activeStep;
396
+ if (!step)
397
+ return;
398
+ const ref = this.settings.open({ id: `stepper:form:${step.id || this.activeIndex}`, title: 'Configurar Formulário da Etapa', content: { component: PraxisDynamicFormConfigEditor, inputs: { formConfig: step.form?.config || { sections: [] }, formId: step.form?.formId || step.id, mode: step.form?.mode || 'create' } } });
399
+ const apply = (val) => {
400
+ const v = val || {};
401
+ step.form = {
402
+ config: v.formConfig || step.form?.config || { sections: [] },
403
+ formId: v?.formId || step.form?.formId || step.id,
404
+ mode: v?.inputsPatch?.mode || step.form?.mode || 'create',
405
+ };
406
+ this.markDirty();
407
+ };
408
+ ref.applied$.subscribe(apply);
409
+ ref.saved$.subscribe(apply);
410
+ }
411
+ editMainForm() { this.addMainForm(); }
412
+ removeMainForm() { const s = this.activeStep; if (!s)
413
+ return; delete s.form; this.markDirty(); }
414
+ // ------- Curated widgets -------
415
+ addListSelection() {
416
+ const step = this.activeStep;
417
+ if (!step)
418
+ return;
419
+ const current = { ...(this.ensureWidgets().find(w => w.id === 'praxis-list')?.inputs?.config || {}) };
420
+ // Ensure minimal selection setup
421
+ current.selection = current.selection || { mode: 'single', return: 'value' };
422
+ const ref = this.settings.open({ id: `stepper:list:${step.id || this.activeIndex}`, title: 'Configurar Lista (seleção)', content: { component: PraxisListConfigEditor, inputs: { config: current, listId: step.id || 'list' } } });
423
+ const apply = (val) => {
424
+ const cfg = (val && (val.config || val)) || current;
425
+ const wd = { id: 'praxis-list', inputs: { config: cfg } };
426
+ const arr = this.ensureWidgets();
427
+ arr.push(wd);
428
+ this.markDirty();
429
+ };
430
+ ref.applied$.subscribe(apply);
431
+ ref.saved$.subscribe(apply);
432
+ }
433
+ addSearchableSelect() {
434
+ const step = this.activeStep;
435
+ if (!step)
436
+ return;
437
+ const dlg = this.dialog.open(SelectQuickConfigDialogComponent, { width: '520px', data: {} });
438
+ dlg.afterClosed().subscribe((res) => {
439
+ if (!res)
440
+ return;
441
+ const meta = {
442
+ name: res.name || 'selection',
443
+ label: res.label || 'Seleção',
444
+ required: !!res.required,
445
+ searchable: true,
446
+ multiple: !!res.multiple,
447
+ resourcePath: res.resourcePath || undefined,
448
+ optionLabelKey: res.optionLabelKey || 'label',
449
+ optionValueKey: res.optionValueKey || 'id',
450
+ loadOn: 'open',
451
+ };
452
+ const wd = { id: 'pdx-material-searchable-select', inputs: { metadata: meta } };
453
+ this.ensureWidgets().push(wd);
454
+ this.markDirty();
455
+ });
456
+ }
457
+ addFilesUpload() {
458
+ const step = this.activeStep;
459
+ if (!step)
460
+ return;
461
+ const ref = this.settings.open({ id: `stepper:upload:${step.id || this.activeIndex}`, title: 'Configurar Upload de Arquivos', content: { component: PraxisFilesUploadConfigEditor, inputs: {} } });
462
+ const apply = (cfg) => {
463
+ const wd = { id: 'praxis-files-upload', inputs: { config: cfg, componentId: `${step.id || 'step'}-upload-${Date.now()}` } };
464
+ this.ensureWidgets().push(wd);
465
+ this.markDirty();
466
+ };
467
+ ref.applied$.subscribe(apply);
468
+ ref.saved$.subscribe(apply);
469
+ }
470
+ openMoreComponents() {
471
+ const dlg = this.dialog.open(ComponentPaletteDialogComponent, { width: '560px', data: { title: 'Adicionar componente à etapa', allowedWidgetIds: ['praxis-dynamic-form', 'praxis-list', 'pdx-material-searchable-select', 'praxis-files-upload'] } });
472
+ dlg.afterClosed().subscribe((id) => {
473
+ if (!id)
474
+ return;
475
+ switch (id) {
476
+ case 'praxis-dynamic-form':
477
+ this.addMainForm();
478
+ break;
479
+ case 'praxis-list':
480
+ this.addListSelection();
481
+ break;
482
+ case 'pdx-material-searchable-select':
483
+ this.addSearchableSelect();
484
+ break;
485
+ case 'praxis-files-upload':
486
+ this.addFilesUpload();
487
+ break;
488
+ default:
489
+ // Fallback: insert minimal widget with no inputs
490
+ this.ensureWidgets().push({ id });
491
+ this.markDirty();
492
+ }
493
+ });
494
+ }
495
+ // Quick CTAs
496
+ addTreeList() {
497
+ const step = this.activeStep;
498
+ if (!step)
499
+ return;
500
+ const cfg = { mode: 'tree', selection: { mode: 'single' } };
501
+ const wd = { id: 'praxis-list', inputs: { config: cfg } };
502
+ this.ensureWidgets().push(wd);
503
+ this.markDirty();
504
+ }
505
+ addTransferListQuick() {
506
+ const step = this.activeStep;
507
+ if (!step)
508
+ return;
509
+ // Seed a simple form with a transfer list field
510
+ const formConfig = {
511
+ sections: [
512
+ { rows: [[{ name: 'items', label: 'Itens', controlType: 'TRANSFER_LIST', transferOptions: { source: [], target: [] } }]] }
513
+ ]
514
+ };
515
+ const ref = this.settings.open({ id: `stepper:transfer:${step.id || this.activeIndex}`, title: 'Configurar Transferência de Itens', content: { component: PraxisDynamicFormConfigEditor, inputs: { formConfig, formId: step.id || 'transfer' } } });
516
+ const apply = (val) => {
517
+ const v = val || {};
518
+ step.form = { config: v.formConfig || formConfig, formId: v?.formId || step.id, mode: v?.inputsPatch?.mode || 'create' };
519
+ this.markDirty();
520
+ };
521
+ ref.applied$.subscribe(apply);
522
+ ref.saved$.subscribe(apply);
523
+ }
524
+ editWidget(w, index) {
525
+ switch (w.id) {
526
+ case 'praxis-dynamic-form':
527
+ // treat as embedded form
528
+ this.addMainForm();
529
+ break;
530
+ case 'praxis-list': {
531
+ const cfg = w.inputs?.config || {};
532
+ const step = this.activeStep;
533
+ if (!step)
534
+ return;
535
+ const ref = this.settings.open({ id: `stepper:list:${step.id || this.activeIndex}`, title: 'Configurar Lista (seleção)', content: { component: PraxisListConfigEditor, inputs: { config: cfg, listId: step.id || 'list' } } });
536
+ const apply = (val) => {
537
+ const next = (val && (val.config || val)) || cfg;
538
+ w.inputs = { ...(w.inputs || {}), config: next };
539
+ this.markDirty();
540
+ };
541
+ ref.applied$.subscribe(apply);
542
+ ref.saved$.subscribe(apply);
543
+ break;
544
+ }
545
+ case 'pdx-material-searchable-select': {
546
+ const meta = w.inputs?.metadata || {};
547
+ const dlg = this.dialog.open(SelectQuickConfigDialogComponent, { width: '520px', data: { from: meta } });
548
+ dlg.afterClosed().subscribe((res) => {
549
+ if (!res)
550
+ return;
551
+ const next = { ...meta, ...res, searchable: true };
552
+ w.inputs = { ...(w.inputs || {}), metadata: next };
553
+ this.markDirty();
554
+ });
555
+ break;
556
+ }
557
+ case 'praxis-files-upload': {
558
+ const cfg = w.inputs?.config || {};
559
+ const ref = this.settings.open({ id: `stepper:upload:edit`, title: 'Configurar Upload de Arquivos', content: { component: PraxisFilesUploadConfigEditor, inputs: { ...cfg } } });
560
+ const apply = (val) => { w.inputs = { ...(w.inputs || {}), config: val }; this.markDirty(); };
561
+ ref.applied$.subscribe(apply);
562
+ ref.saved$.subscribe(apply);
563
+ break;
564
+ }
565
+ default:
566
+ // No editor available
567
+ break;
568
+ }
569
+ }
570
+ // SettingsValueProvider
571
+ getSettingsValue() { return this.config; }
572
+ onSave() { return this.getSettingsValue(); }
573
+ reset() {
574
+ this.isBusy$.next(true);
575
+ try {
576
+ this.config = { steps: [], orientation: 'horizontal', headerPosition: 'top', linear: false };
577
+ this.isDirty$.next(true);
578
+ this.isValid$.next(true);
579
+ }
580
+ finally {
581
+ this.isBusy$.next(false);
582
+ }
583
+ }
584
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PraxisStepperConfigEditor, deps: [{ token: SETTINGS_PANEL_DATA }], target: i0.ɵɵFactoryTarget.Component });
585
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.4", type: PraxisStepperConfigEditor, isStandalone: true, selector: "praxis-stepper-config-editor", ngImport: i0, template: `
586
+ <div class="pdx-editor">
587
+ <mat-tab-group>
588
+ <mat-tab label="Geral">
589
+ <div class="tab-pad">
590
+ <div class="help">Configure o funcionamento e a aparência básica do passo a passo.</div>
591
+ <div class="pdx-grid">
592
+ <mat-form-field appearance="outline">
593
+ <mat-label>Orientação</mat-label>
594
+ <select matNativeControl [(ngModel)]="config.orientation" (ngModelChange)="markDirty()">
595
+ <option value="horizontal">Horizontal</option>
596
+ <option value="vertical">Vertical</option>
597
+ </select>
598
+ <mat-hint>Horizontal: cabeçalho no topo • Vertical: lista à esquerda</mat-hint>
599
+ </mat-form-field>
600
+ <mat-form-field appearance="outline">
601
+ <mat-label>Posição dos títulos</mat-label>
602
+ <select matNativeControl [(ngModel)]="config.headerPosition" (ngModelChange)="markDirty()">
603
+ <option value="top">Acima</option>
604
+ <option value="bottom">Abaixo</option>
605
+ </select>
606
+ <mat-hint>Válido na orientação horizontal</mat-hint>
607
+ </mat-form-field>
608
+ <mat-form-field appearance="outline">
609
+ <mat-label>Posição do rótulo</mat-label>
610
+ <select matNativeControl [(ngModel)]="config.labelPosition" (ngModelChange)="markDirty()" [disabled]="config.orientation !== 'horizontal'">
611
+ <option value="end">Ao lado</option>
612
+ <option value="bottom">Abaixo</option>
613
+ </select>
614
+ <mat-hint>Como o texto aparece no cabeçalho</mat-hint>
615
+ </mat-form-field>
616
+ <mat-form-field appearance="outline">
617
+ <mat-label>Cor</mat-label>
618
+ <select matNativeControl [(ngModel)]="config.color" (ngModelChange)="markDirty()">
619
+ <option [ngValue]="undefined">Padrão</option>
620
+ <option value="primary">Primária</option>
621
+ <option value="accent">Acento</option>
622
+ <option value="warn">Alerta</option>
623
+ </select>
624
+ <mat-hint>Aplicável a temas Material 2 (M2)</mat-hint>
625
+ </mat-form-field>
626
+ <mat-form-field appearance="outline">
627
+ <mat-label>Duração da animação</mat-label>
628
+ <input matInput [(ngModel)]="config.animationDuration" (ngModelChange)="markDirty()" placeholder="Ex.: 300ms" />
629
+ <mat-hint>Tempo da transição entre etapas</mat-hint>
630
+ </mat-form-field>
631
+ <mat-form-field appearance="outline">
632
+ <mat-label>Etapa selecionada</mat-label>
633
+ <input matInput type="number" [(ngModel)]="config.selectedIndex" (ngModelChange)="markDirty()" />
634
+ <mat-hint>Índice inicia em 0 (primeira etapa)</mat-hint>
635
+ </mat-form-field>
636
+ <mat-form-field appearance="outline">
637
+ <mat-label>Densidade</mat-label>
638
+ <select matNativeControl [(ngModel)]="config.density" (ngModelChange)="markDirty()">
639
+ <option [ngValue]="undefined">Padrão</option>
640
+ <option value="comfortable">Confortável</option>
641
+ <option value="compact">Compacta</option>
642
+ </select>
643
+ <mat-hint>Controla espaçamentos e alturas</mat-hint>
644
+ </mat-form-field>
645
+ <mat-form-field appearance="outline" class="w-full">
646
+ <mat-label>Classe do stepper (opcional)</mat-label>
647
+ <input matInput [(ngModel)]="config.stepperClass" (ngModelChange)="markDirty()" placeholder="Ex.: meu-stepper" />
648
+ <mat-hint>Permite estilizar com CSS escopado</mat-hint>
649
+ </mat-form-field>
650
+ <mat-form-field appearance="outline" class="w-full">
651
+ <mat-label>Classe do cabeçalho (opcional)</mat-label>
652
+ <input matInput [(ngModel)]="config.headerClass" (ngModelChange)="markDirty()" placeholder="Ex.: stepper-header" />
653
+ <mat-hint>Use para ajustes finos de aparência</mat-hint>
654
+ </mat-form-field>
655
+ <mat-form-field appearance="outline" class="w-full">
656
+ <mat-label>Classe do conteúdo (opcional)</mat-label>
657
+ <input matInput [(ngModel)]="config.contentClass" (ngModelChange)="markDirty()" placeholder="Ex.: stepper-content" />
658
+ <mat-hint>Aplica uma classe à área de conteúdo</mat-hint>
659
+ </mat-form-field>
660
+ </div>
661
+ <div class="pdx-toggles">
662
+ <mat-slide-toggle [(ngModel)]="config.linear" (ngModelChange)="markDirty()">Respeitar ordem (linear)</mat-slide-toggle>
663
+ <mat-slide-toggle [(ngModel)]="config.disableRipple" (ngModelChange)="markDirty()">Desativar efeitos de clique</mat-slide-toggle>
664
+ </div>
665
+ </div>
666
+ </mat-tab>
667
+
668
+ <mat-tab label="Etapas">
669
+ <div class="tab-pad">
670
+ <div class="help">Gerencie as etapas: nomes, mensagens e conteúdo. Arraste para reordenar.</div>
671
+ <div class="pdx-steps">
672
+ <div class="pdx-steps-header">
673
+ <div class="title">Etapas ({{ config.steps.length }})</div>
674
+ <button mat-button (click)="addStep()"><mat-icon [praxisIcon]="'add'"></mat-icon>Adicionar etapa</button>
675
+ </div>
676
+ <div cdkDropList (cdkDropListDropped)="drop($event)" class="pdx-step-list">
677
+ <div class="pdx-step-item" *ngFor="let s of config.steps; let i = index" cdkDrag [class.active]="activeIndex === i" (click)="setActive(i)">
678
+ <div class="drag-handle" cdkDragHandle><mat-icon [praxisIcon]="'drag_indicator'"></mat-icon></div>
679
+ <div class="pdx-fields">
680
+ <mat-form-field appearance="outline" style="min-width: 180px;">
681
+ <mat-label>Título da etapa</mat-label>
682
+ <input matInput [(ngModel)]="s.label" (ngModelChange)="markDirty()" />
683
+ <mat-hint>Como aparecerá no cabeçalho</mat-hint>
684
+ </mat-form-field>
685
+ <mat-form-field appearance="outline">
686
+ <mat-label>Identificador (opcional)</mat-label>
687
+ <input matInput [(ngModel)]="s.id" (ngModelChange)="markDirty()" />
688
+ <mat-hint>Útil para automações</mat-hint>
689
+ </mat-form-field>
690
+ <mat-form-field appearance="outline">
691
+ <mat-label>Texto alternativo (acessibilidade)</mat-label>
692
+ <input matInput [(ngModel)]="s.ariaLabel" (ngModelChange)="markDirty()" />
693
+ <mat-hint>Lido por leitores de tela</mat-hint>
694
+ </mat-form-field>
695
+ <mat-form-field appearance="outline">
696
+ <mat-label>ID que descreve (opcional)</mat-label>
697
+ <input matInput [(ngModel)]="s.ariaLabelledby" (ngModelChange)="markDirty()" />
698
+ <mat-hint>ID de um elemento que descreve a etapa</mat-hint>
699
+ </mat-form-field>
700
+ <mat-form-field appearance="outline">
701
+ <mat-label>Ícone/estado do marcador</mat-label>
702
+ <input matInput [(ngModel)]="s.state" (ngModelChange)="markDirty()" placeholder="Ex.: number, done, edit" />
703
+ <mat-hint>Controla o ícone do passo (quando aplicável)</mat-hint>
704
+ </mat-form-field>
705
+ <mat-form-field appearance="outline">
706
+ <mat-label>Mensagem de erro</mat-label>
707
+ <input matInput [(ngModel)]="s.errorMessage" (ngModelChange)="markDirty()" />
708
+ <mat-hint>Mostrada quando há erro na etapa</mat-hint>
709
+ </mat-form-field>
710
+ <div class="pdx-flag-group">
711
+ <mat-slide-toggle [(ngModel)]="s.optional" (ngModelChange)="markDirty()">Etapa opcional</mat-slide-toggle>
712
+ <mat-slide-toggle [(ngModel)]="s.editable" (ngModelChange)="markDirty()">Permitir voltar e editar</mat-slide-toggle>
713
+ <mat-slide-toggle [(ngModel)]="s.completed" (ngModelChange)="markDirty()">Marcar como concluída</mat-slide-toggle>
714
+ <mat-slide-toggle [(ngModel)]="s.hasError" (ngModelChange)="markDirty()">Marcar como com erro</mat-slide-toggle>
715
+ </div>
716
+ </div>
717
+ <div class="pdx-actions">
718
+ <button mat-icon-button color="warn" (click)="removeStep(i)" matTooltip="Remover">
719
+ <mat-icon [praxisIcon]="'delete'"></mat-icon>
720
+ </button>
721
+ </div>
722
+ </div>
723
+ </div>
724
+ </div>
725
+
726
+ <div class="pdx-active-step" *ngIf="activeStep as step; else noSteps">
727
+ <div class="hdr">
728
+ <div class="title">Editando: {{ step.label }}</div>
729
+ <mat-button-toggle-group [(ngModel)]="activeIndex" (ngModelChange)="onActiveIndexChange()" [hideSingleSelectionIndicator]="true">
730
+ <mat-button-toggle *ngFor="let s of config.steps; let i = index" [value]="i">{{ i + 1 }}</mat-button-toggle>
731
+ </mat-button-toggle-group>
732
+ </div>
733
+
734
+ <div class="pdx-content-editor">
735
+ <div class="section">
736
+ <div class="section-title">Formulário principal</div>
737
+ <div class="section-body">
738
+ <ng-container *ngIf="step.form; else addFormBtn">
739
+ <mat-card appearance="outlined" class="mini-card">
740
+ <mat-card-header>
741
+ <mat-icon mat-card-avatar [praxisIcon]="'dynamic_form'"></mat-icon>
742
+ <mat-card-title>Formulário principal</mat-card-title>
743
+ <mat-card-subtitle>{{ step.form.formId || step.id || '—' }}</mat-card-subtitle>
744
+ </mat-card-header>
745
+ <mat-card-actions align="end">
746
+ <button mat-button (click)="editMainForm()"><mat-icon [praxisIcon]="'tune'"></mat-icon> Editar</button>
747
+ <button mat-button color="warn" (click)="removeMainForm()"><mat-icon [praxisIcon]="'delete'"></mat-icon> Remover</button>
748
+ </mat-card-actions>
749
+ </mat-card>
750
+ </ng-container>
751
+ <ng-template #addFormBtn>
752
+ <button mat-stroked-button color="primary" (click)="addMainForm()"><mat-icon [praxisIcon]="'add'"></mat-icon> Adicionar formulário</button>
753
+ </ng-template>
754
+ </div>
755
+ </div>
756
+
757
+ <div class="section">
758
+ <div class="section-title">Conteúdos e componentes</div>
759
+ <div class="section-body">
760
+ <div class="cta-grid">
761
+ <div class="cta-card mat-elevation-z2">
762
+ <div class="cta-head">
763
+ <mat-icon [praxisIcon]="'dynamic_form'"></mat-icon>
764
+ <div class="cta-title">Formulário dinâmico</div>
765
+ </div>
766
+ <div class="cta-desc">Crie campos e validações personalizadas nesta etapa.</div>
767
+ <div class="cta-actions">
768
+ <button mat-flat-button color="primary" (click)="addMainForm()">
769
+ <mat-icon [praxisIcon]="'add'"></mat-icon>
770
+ Inserir formulário
771
+ </button>
772
+ </div>
773
+ </div>
774
+ <div class="cta-card mat-elevation-z2">
775
+ <div class="cta-head">
776
+ <mat-icon [praxisIcon]="'account_tree'"></mat-icon>
777
+ <div class="cta-title">Lista em árvore</div>
778
+ </div>
779
+ <div class="cta-desc">Exiba dados hierárquicos com seleção simples.</div>
780
+ <div class="cta-actions">
781
+ <button mat-stroked-button (click)="addTreeList()">
782
+ <mat-icon [praxisIcon]="'add'"></mat-icon>
783
+ Inserir árvore
784
+ </button>
785
+ </div>
786
+ </div>
787
+ <div class="cta-card mat-elevation-z2">
788
+ <div class="cta-head">
789
+ <mat-icon [praxisIcon]="'swap_horiz'"></mat-icon>
790
+ <div class="cta-title">Transferência de itens</div>
791
+ </div>
792
+ <div class="cta-desc">Mover itens entre listas (ideal para múltipla seleção).</div>
793
+ <div class="cta-actions">
794
+ <button mat-stroked-button (click)="addTransferListQuick()">
795
+ <mat-icon [praxisIcon]="'add'"></mat-icon>
796
+ Inserir transferência
797
+ </button>
798
+ </div>
799
+ </div>
800
+ </div>
801
+ <div cdkDropList (cdkDropListDropped)="dropWidget($event)" class="widget-list">
802
+ <div class="widget-item" *ngFor="let w of (step.widgets || []); let wi = index" cdkDrag>
803
+ <div class="drag-handle" cdkDragHandle><mat-icon [praxisIcon]="'drag_indicator'"></mat-icon></div>
804
+ <div class="info">
805
+ <div class="name">{{ displayWidgetName(w) }}</div>
806
+ <div class="sub">{{ w.id }}</div>
807
+ </div>
808
+ <div class="actions">
809
+ <button mat-button (click)="editWidget(w, wi)"><mat-icon [praxisIcon]="'tune'"></mat-icon> Editar</button>
810
+ <button mat-button color="warn" (click)="removeWidget(wi)"><mat-icon [praxisIcon]="'delete'"></mat-icon> Remover</button>
811
+ </div>
812
+ </div>
813
+ <div class="empty" *ngIf="!step.widgets || !step.widgets.length">Nenhum componente adicionado</div>
814
+ </div>
815
+ </div>
816
+ </div>
817
+ </div>
818
+ </div>
819
+ <ng-template #noSteps>
820
+ <div class="muted">Nenhuma etapa definida.</div>
821
+ </ng-template>
822
+ </div>
823
+ </mat-tab>
824
+
825
+ <mat-tab label="Navegação">
826
+ <div class="tab-pad">
827
+ <div class="help">Personalize os botões de avançar e voltar exibidos em cada etapa.</div>
828
+ <div class="pdx-grid">
829
+ <mat-form-field appearance="outline">
830
+ <mat-label>Estilo dos botões</mat-label>
831
+ <select matNativeControl [(ngModel)]="navigationCfg.variant" (ngModelChange)="markDirty()">
832
+ <option [ngValue]="undefined">Padrão (elevado)</option>
833
+ <option value="basic">Texto</option>
834
+ <option value="flat">Plano</option>
835
+ <option value="stroked">Contornado</option>
836
+ <option value="raised">Elevado</option>
837
+ </select>
838
+ </mat-form-field>
839
+ <mat-form-field appearance="outline">
840
+ <mat-label>Cor</mat-label>
841
+ <select matNativeControl [(ngModel)]="navigationCfg.color" (ngModelChange)="markDirty()">
842
+ <option [ngValue]="undefined">Padrão</option>
843
+ <option value="primary">Primária</option>
844
+ <option value="accent">Acento</option>
845
+ <option value="warn">Alerta</option>
846
+ </select>
847
+ </mat-form-field>
848
+ <mat-form-field appearance="outline">
849
+ <mat-label>Alinhamento</mat-label>
850
+ <select matNativeControl [(ngModel)]="navigationCfg.align" (ngModelChange)="markDirty()">
851
+ <option [ngValue]="undefined">Direita</option>
852
+ <option value="start">Esquerda</option>
853
+ <option value="center">Centro</option>
854
+ <option value="space-between">Espaçado</option>
855
+ <option value="end">Direita</option>
856
+ </select>
857
+ </mat-form-field>
858
+ </div>
859
+
860
+ <div class="pdx-toggles">
861
+ <mat-slide-toggle [(ngModel)]="navigationCfg.visible" (ngModelChange)="markDirty()">Mostrar navegação padrão</mat-slide-toggle>
862
+ </div>
863
+
864
+ <div class="pdx-grid">
865
+ <mat-form-field appearance="outline">
866
+ <mat-label>Texto do botão Voltar</mat-label>
867
+ <input matInput [(ngModel)]="navigationCfg.prevLabel" (ngModelChange)="markDirty()" placeholder="Ex.: Voltar" />
868
+ </mat-form-field>
869
+ <mat-form-field appearance="outline">
870
+ <mat-label>Texto do botão Próximo</mat-label>
871
+ <input matInput [(ngModel)]="navigationCfg.nextLabel" (ngModelChange)="markDirty()" placeholder="Ex.: Próximo" />
872
+ </mat-form-field>
873
+ </div>
874
+
875
+ <div class="icons-grid">
876
+ <div class="icon-item">
877
+ <div class="icon-head">Ícone de Voltar</div>
878
+ <button mat-stroked-button type="button" (click)="pickNavIcon('prevIcon')">
879
+ <mat-icon *ngIf="navigationCfg.prevIcon" [praxisIcon]="navigationCfg.prevIcon"></mat-icon>
880
+ <ng-container *ngIf="!navigationCfg.prevIcon">Escolher</ng-container>
881
+ </button>
882
+ </div>
883
+ <div class="icon-item">
884
+ <div class="icon-head">Ícone de Próximo</div>
885
+ <button mat-stroked-button type="button" (click)="pickNavIcon('nextIcon')">
886
+ <mat-icon *ngIf="navigationCfg.nextIcon" [praxisIcon]="navigationCfg.nextIcon"></mat-icon>
887
+ <ng-container *ngIf="!navigationCfg.nextIcon">Escolher</ng-container>
888
+ </button>
889
+ </div>
890
+ </div>
891
+ </div>
892
+ </mat-tab>
893
+
894
+ <mat-tab label="Aparência">
895
+ <div class="tab-pad">
896
+ <div class="help">Personalize cores, tipografia e ícones. Para ajustes comuns, use os campos abaixo; para maior controle, edite os tokens. Copie o SCSS e cole no arquivo de tema.</div>
897
+ <div class="quick-grid">
898
+ <mat-form-field appearance="outline">
899
+ <mat-label>Altura do cabeçalho (px)</mat-label>
900
+ <input matInput type="number" [ngModel]="headerHeightPx" (ngModelChange)="setHeaderHeightPx($event)" placeholder="Ex.: 48" />
901
+ <mat-hint>Altura do item do cabeçalho</mat-hint>
902
+ </mat-form-field>
903
+ <mat-form-field appearance="outline">
904
+ <mat-label>Tamanho do texto (px)</mat-label>
905
+ <input matInput type="number" [ngModel]="headerLabelTextSizePx" (ngModelChange)="setHeaderLabelTextSizePx($event)" placeholder="Ex.: 14" />
906
+ <mat-hint>Tamanho do título da etapa</mat-hint>
907
+ </mat-form-field>
908
+ <mat-form-field appearance="outline">
909
+ <mat-label>Peso do texto</mat-label>
910
+ <select matNativeControl [ngModel]="headerLabelTextWeight" (ngModelChange)="setHeaderLabelTextWeight($event)">
911
+ <option [ngValue]="undefined">Padrão</option>
912
+ <option value="400">Normal</option>
913
+ <option value="500">Médio</option>
914
+ <option value="600">Seminegrito</option>
915
+ <option value="700">Negrito</option>
916
+ </select>
917
+ </mat-form-field>
918
+ </div>
919
+ <div class="pdx-grid">
920
+ <mat-form-field appearance="outline" class="w-full">
921
+ <mat-label>Classe de tema (opcional)</mat-label>
922
+ <input matInput [(ngModel)]="appearance.themeClass" (ngModelChange)="onAppearanceChange()" placeholder="Ex.: theme-stepper-custom" />
923
+ <mat-hint>Aplica as cores apenas dentro desse seletor</mat-hint>
924
+ </mat-form-field>
925
+ <mat-form-field appearance="outline">
926
+ <mat-label>Conjunto de ícones</mat-label>
927
+ <select matNativeControl [(ngModel)]="appearance.iconsSet" (ngModelChange)="onAppearanceChange()">
928
+ <option [ngValue]="undefined">Material Icons (padrão)</option>
929
+ <option value="material-symbols-outlined">Material Symbols (Outlined)</option>
930
+ <option value="material-symbols-rounded">Material Symbols (Rounded)</option>
931
+ <option value="material-symbols-sharp">Material Symbols (Sharp)</option>
932
+ </select>
933
+ <mat-hint>Selecione se usar ícones do Material Symbols</mat-hint>
934
+ </mat-form-field>
935
+ </div>
936
+ <div class="icons-grid">
937
+ <div class="icon-item">
938
+ <div class="icon-head">Ícone do número</div>
939
+ <button mat-stroked-button type="button" (click)="pickIcon('number')">
940
+ <mat-icon *ngIf="icons.number; else pick" [praxisIcon]="icons.number"></mat-icon>
941
+ <ng-template #pick>Escolher</ng-template>
942
+ </button>
943
+ </div>
944
+ <div class="icon-item">
945
+ <div class="icon-head">Ícone concluído</div>
946
+ <button mat-stroked-button type="button" (click)="pickIcon('done')">
947
+ <mat-icon *ngIf="icons.done; else pick2" [praxisIcon]="icons.done"></mat-icon>
948
+ <ng-template #pick2>Escolher</ng-template>
949
+ </button>
950
+ </div>
951
+ <div class="icon-item">
952
+ <div class="icon-head">Ícone edição</div>
953
+ <button mat-stroked-button type="button" (click)="pickIcon('edit')">
954
+ <mat-icon *ngIf="icons.edit; else pick3" [praxisIcon]="icons.edit"></mat-icon>
955
+ <ng-template #pick3>Escolher</ng-template>
956
+ </button>
957
+ </div>
958
+ <div class="icon-item">
959
+ <div class="icon-head">Ícone de erro</div>
960
+ <button mat-stroked-button type="button" (click)="pickIcon('error')">
961
+ <mat-icon *ngIf="icons.error; else pick4" [praxisIcon]="icons.error"></mat-icon>
962
+ <ng-template #pick4>Escolher</ng-template>
963
+ </button>
964
+ </div>
965
+ </div>
966
+ <div class="icons-hint" *ngIf="symbolsLikelySelected.length && !appearance.iconsSet">
967
+ <div class="muted">
968
+ Dica: {{ symbolsLikelySelected.join(', ') }} são ícones do Material Symbols. Selecione um conjunto Symbols abaixo ou clique em
969
+ <button mat-button color="primary" type="button" (click)="setIconsSetToSymbols()">Usar Material Symbols (Outlined)</button>.
970
+ </div>
971
+ </div>
972
+ <div class="tokens-grid">
973
+ <div class="token-item" *ngFor="let key of stepperTokenKeys">
974
+ <mat-form-field appearance="outline" class="w-full">
975
+ <mat-label>{{ key }}</mat-label>
976
+ <input matInput [ngModel]="appearance.tokens?.[key]" (ngModelChange)="onTokenChange(key, $event)" placeholder="valor CSS ou var(--token)" />
977
+ </mat-form-field>
978
+ </div>
979
+ </div>
980
+ <div class="pdx-appearance-actions">
981
+ <button mat-stroked-button color="primary" type="button" (click)="applyPreset('neutral')">Preset: neutro</button>
982
+ <button mat-stroked-button color="primary" type="button" (click)="applyPreset('primary')">Preset: primário</button>
983
+ <button mat-stroked-button color="primary" type="button" (click)="applyPreset('high-contrast')">Preset: alto contraste</button>
984
+ <button mat-button type="button" (click)="clearTokens()">Limpar tokens</button>
985
+ </div>
986
+ <mat-card appearance="outlined" class="code-card">
987
+ <div class="code-head">Snippet SCSS</div>
988
+ <pre class="code"><code>{{ scssSnippet() }}</code></pre>
989
+ </mat-card>
990
+ </div>
991
+ </mat-tab>
992
+ </mat-tab-group>
993
+ </div>
994
+ `, isInline: true, styles: [".pdx-editor{display:grid;gap:16px}.pdx-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px}.pdx-toggles{display:flex;gap:12px;flex-wrap:wrap}.tab-pad{padding:12px 4px;display:grid;gap:12px}.help{color:#000000b3;font-size:13px}.quick-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin-bottom:6px}.icons-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin:8px 0}.icon-item{display:grid;gap:6px;align-content:start}.icons-hint{margin-top:4px}.pdx-steps{display:grid;gap:8px}.pdx-steps-header{display:flex;justify-content:space-between;align-items:center}.pdx-step-list{display:grid;gap:8px}.pdx-step-item{display:grid;grid-template-columns:24px 1fr auto;gap:8px;align-items:start;padding:8px;border:1px solid rgba(0,0,0,.12);border-radius:8px;cursor:pointer}.pdx-step-item.active{border-color:color-mix(in oklab,var(--md-sys-color-primary) 40%,rgba(0,0,0,.12));box-shadow:var(--mat-elevation-transition),var(--mat-elevation-level2, 0 2px 6px rgba(0,0,0,.12))}.drag-handle{display:flex;align-items:center;color:#0000008a}.pdx-fields{display:grid;gap:8px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}.pdx-flag-group{display:flex;gap:10px;align-items:center}.pdx-active-step{display:grid;gap:12px;padding-top:8px;border-top:1px dashed rgba(0,0,0,.12)}.pdx-active-step .hdr{display:flex;gap:12px;align-items:center;justify-content:space-between}.pdx-content-editor{display:grid;gap:16px}.section{display:grid;gap:8px}.section-title{font-weight:600;opacity:.85}.mini-card{display:block}.widget-list{display:grid;gap:8px}.widget-item{display:grid;grid-template-columns:24px 1fr auto;align-items:center;gap:8px;padding:8px;border:1px solid rgba(0,0,0,.12);border-radius:8px}.widget-item .info .name{font-weight:600}.widget-item .info .sub{font-size:12px;opacity:.7}.widget-item .actions{display:flex;gap:8px}.cta-row{display:flex;gap:8px;align-items:center}.spacer{flex:1 1 auto}.muted{color:#0009}.cta-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:12px;margin-bottom:8px}.cta-card{display:grid;gap:8px;padding:12px;border-radius:12px;border:1px solid var(--md-sys-color-outline-variant, rgba(0,0,0,.12));background:var(--md-sys-color-surface)}.cta-card .cta-head{display:flex;align-items:center;gap:8px}.cta-card .cta-title{font-weight:600}.cta-card .cta-desc{font-size:12px;opacity:.78}.cta-card .cta-actions{display:flex;gap:8px;align-items:center}.tokens-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;margin:8px 0}.token-item{display:contents}.pdx-appearance{margin-top:8px;display:grid;gap:8px}.pdx-appearance-actions{display:flex;gap:8px;flex-wrap:wrap}.code-card{margin-top:8px}.code-head{font-weight:600;margin-bottom:4px}.code{white-space:pre;overflow:auto}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.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: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i4.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: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i6.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i6.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i7.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i8.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i8.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i8.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i9.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatChipsModule }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i10.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i10.MatCardActions, selector: "mat-card-actions", inputs: ["align"], exportAs: ["matCardActions"] }, { kind: "directive", type: i10.MatCardAvatar, selector: "[mat-card-avatar], [matCardAvatar]" }, { kind: "component", type: i10.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i10.MatCardSubtitle, selector: "mat-card-subtitle, [mat-card-subtitle], [matCardSubtitle]" }, { kind: "directive", type: i10.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { kind: "ngmodule", type: MatButtonToggleModule }, { kind: "directive", type: i11.MatButtonToggleGroup, selector: "mat-button-toggle-group", inputs: ["appearance", "name", "vertical", "value", "multiple", "disabled", "disabledInteractive", "hideSingleSelectionIndicator", "hideMultipleSelectionIndicator"], outputs: ["valueChange", "change"], exportAs: ["matButtonToggleGroup"] }, { kind: "component", type: i11.MatButtonToggle, selector: "mat-button-toggle", inputs: ["aria-label", "aria-labelledby", "id", "name", "value", "tabIndex", "disableRipple", "appearance", "checked", "disabled", "disabledInteractive"], outputs: ["change"], exportAs: ["matButtonToggle"] }, { kind: "ngmodule", type: MatDialogModule }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i12.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i12.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }] });
995
+ }
996
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PraxisStepperConfigEditor, decorators: [{
997
+ type: Component,
998
+ args: [{ selector: 'praxis-stepper-config-editor', standalone: true, imports: [
999
+ CommonModule,
1000
+ FormsModule,
1001
+ MatFormFieldModule,
1002
+ MatInputModule,
1003
+ MatIconModule,
1004
+ PraxisIconDirective,
1005
+ MatButtonModule,
1006
+ MatSlideToggleModule,
1007
+ DragDropModule,
1008
+ MatTooltipModule,
1009
+ MatChipsModule,
1010
+ MatCardModule,
1011
+ MatButtonToggleModule,
1012
+ MatDialogModule,
1013
+ MatTabsModule,
1014
+ ], template: `
1015
+ <div class="pdx-editor">
1016
+ <mat-tab-group>
1017
+ <mat-tab label="Geral">
1018
+ <div class="tab-pad">
1019
+ <div class="help">Configure o funcionamento e a aparência básica do passo a passo.</div>
1020
+ <div class="pdx-grid">
1021
+ <mat-form-field appearance="outline">
1022
+ <mat-label>Orientação</mat-label>
1023
+ <select matNativeControl [(ngModel)]="config.orientation" (ngModelChange)="markDirty()">
1024
+ <option value="horizontal">Horizontal</option>
1025
+ <option value="vertical">Vertical</option>
1026
+ </select>
1027
+ <mat-hint>Horizontal: cabeçalho no topo • Vertical: lista à esquerda</mat-hint>
1028
+ </mat-form-field>
1029
+ <mat-form-field appearance="outline">
1030
+ <mat-label>Posição dos títulos</mat-label>
1031
+ <select matNativeControl [(ngModel)]="config.headerPosition" (ngModelChange)="markDirty()">
1032
+ <option value="top">Acima</option>
1033
+ <option value="bottom">Abaixo</option>
1034
+ </select>
1035
+ <mat-hint>Válido na orientação horizontal</mat-hint>
1036
+ </mat-form-field>
1037
+ <mat-form-field appearance="outline">
1038
+ <mat-label>Posição do rótulo</mat-label>
1039
+ <select matNativeControl [(ngModel)]="config.labelPosition" (ngModelChange)="markDirty()" [disabled]="config.orientation !== 'horizontal'">
1040
+ <option value="end">Ao lado</option>
1041
+ <option value="bottom">Abaixo</option>
1042
+ </select>
1043
+ <mat-hint>Como o texto aparece no cabeçalho</mat-hint>
1044
+ </mat-form-field>
1045
+ <mat-form-field appearance="outline">
1046
+ <mat-label>Cor</mat-label>
1047
+ <select matNativeControl [(ngModel)]="config.color" (ngModelChange)="markDirty()">
1048
+ <option [ngValue]="undefined">Padrão</option>
1049
+ <option value="primary">Primária</option>
1050
+ <option value="accent">Acento</option>
1051
+ <option value="warn">Alerta</option>
1052
+ </select>
1053
+ <mat-hint>Aplicável a temas Material 2 (M2)</mat-hint>
1054
+ </mat-form-field>
1055
+ <mat-form-field appearance="outline">
1056
+ <mat-label>Duração da animação</mat-label>
1057
+ <input matInput [(ngModel)]="config.animationDuration" (ngModelChange)="markDirty()" placeholder="Ex.: 300ms" />
1058
+ <mat-hint>Tempo da transição entre etapas</mat-hint>
1059
+ </mat-form-field>
1060
+ <mat-form-field appearance="outline">
1061
+ <mat-label>Etapa selecionada</mat-label>
1062
+ <input matInput type="number" [(ngModel)]="config.selectedIndex" (ngModelChange)="markDirty()" />
1063
+ <mat-hint>Índice inicia em 0 (primeira etapa)</mat-hint>
1064
+ </mat-form-field>
1065
+ <mat-form-field appearance="outline">
1066
+ <mat-label>Densidade</mat-label>
1067
+ <select matNativeControl [(ngModel)]="config.density" (ngModelChange)="markDirty()">
1068
+ <option [ngValue]="undefined">Padrão</option>
1069
+ <option value="comfortable">Confortável</option>
1070
+ <option value="compact">Compacta</option>
1071
+ </select>
1072
+ <mat-hint>Controla espaçamentos e alturas</mat-hint>
1073
+ </mat-form-field>
1074
+ <mat-form-field appearance="outline" class="w-full">
1075
+ <mat-label>Classe do stepper (opcional)</mat-label>
1076
+ <input matInput [(ngModel)]="config.stepperClass" (ngModelChange)="markDirty()" placeholder="Ex.: meu-stepper" />
1077
+ <mat-hint>Permite estilizar com CSS escopado</mat-hint>
1078
+ </mat-form-field>
1079
+ <mat-form-field appearance="outline" class="w-full">
1080
+ <mat-label>Classe do cabeçalho (opcional)</mat-label>
1081
+ <input matInput [(ngModel)]="config.headerClass" (ngModelChange)="markDirty()" placeholder="Ex.: stepper-header" />
1082
+ <mat-hint>Use para ajustes finos de aparência</mat-hint>
1083
+ </mat-form-field>
1084
+ <mat-form-field appearance="outline" class="w-full">
1085
+ <mat-label>Classe do conteúdo (opcional)</mat-label>
1086
+ <input matInput [(ngModel)]="config.contentClass" (ngModelChange)="markDirty()" placeholder="Ex.: stepper-content" />
1087
+ <mat-hint>Aplica uma classe à área de conteúdo</mat-hint>
1088
+ </mat-form-field>
1089
+ </div>
1090
+ <div class="pdx-toggles">
1091
+ <mat-slide-toggle [(ngModel)]="config.linear" (ngModelChange)="markDirty()">Respeitar ordem (linear)</mat-slide-toggle>
1092
+ <mat-slide-toggle [(ngModel)]="config.disableRipple" (ngModelChange)="markDirty()">Desativar efeitos de clique</mat-slide-toggle>
1093
+ </div>
1094
+ </div>
1095
+ </mat-tab>
1096
+
1097
+ <mat-tab label="Etapas">
1098
+ <div class="tab-pad">
1099
+ <div class="help">Gerencie as etapas: nomes, mensagens e conteúdo. Arraste para reordenar.</div>
1100
+ <div class="pdx-steps">
1101
+ <div class="pdx-steps-header">
1102
+ <div class="title">Etapas ({{ config.steps.length }})</div>
1103
+ <button mat-button (click)="addStep()"><mat-icon [praxisIcon]="'add'"></mat-icon>Adicionar etapa</button>
1104
+ </div>
1105
+ <div cdkDropList (cdkDropListDropped)="drop($event)" class="pdx-step-list">
1106
+ <div class="pdx-step-item" *ngFor="let s of config.steps; let i = index" cdkDrag [class.active]="activeIndex === i" (click)="setActive(i)">
1107
+ <div class="drag-handle" cdkDragHandle><mat-icon [praxisIcon]="'drag_indicator'"></mat-icon></div>
1108
+ <div class="pdx-fields">
1109
+ <mat-form-field appearance="outline" style="min-width: 180px;">
1110
+ <mat-label>Título da etapa</mat-label>
1111
+ <input matInput [(ngModel)]="s.label" (ngModelChange)="markDirty()" />
1112
+ <mat-hint>Como aparecerá no cabeçalho</mat-hint>
1113
+ </mat-form-field>
1114
+ <mat-form-field appearance="outline">
1115
+ <mat-label>Identificador (opcional)</mat-label>
1116
+ <input matInput [(ngModel)]="s.id" (ngModelChange)="markDirty()" />
1117
+ <mat-hint>Útil para automações</mat-hint>
1118
+ </mat-form-field>
1119
+ <mat-form-field appearance="outline">
1120
+ <mat-label>Texto alternativo (acessibilidade)</mat-label>
1121
+ <input matInput [(ngModel)]="s.ariaLabel" (ngModelChange)="markDirty()" />
1122
+ <mat-hint>Lido por leitores de tela</mat-hint>
1123
+ </mat-form-field>
1124
+ <mat-form-field appearance="outline">
1125
+ <mat-label>ID que descreve (opcional)</mat-label>
1126
+ <input matInput [(ngModel)]="s.ariaLabelledby" (ngModelChange)="markDirty()" />
1127
+ <mat-hint>ID de um elemento que descreve a etapa</mat-hint>
1128
+ </mat-form-field>
1129
+ <mat-form-field appearance="outline">
1130
+ <mat-label>Ícone/estado do marcador</mat-label>
1131
+ <input matInput [(ngModel)]="s.state" (ngModelChange)="markDirty()" placeholder="Ex.: number, done, edit" />
1132
+ <mat-hint>Controla o ícone do passo (quando aplicável)</mat-hint>
1133
+ </mat-form-field>
1134
+ <mat-form-field appearance="outline">
1135
+ <mat-label>Mensagem de erro</mat-label>
1136
+ <input matInput [(ngModel)]="s.errorMessage" (ngModelChange)="markDirty()" />
1137
+ <mat-hint>Mostrada quando há erro na etapa</mat-hint>
1138
+ </mat-form-field>
1139
+ <div class="pdx-flag-group">
1140
+ <mat-slide-toggle [(ngModel)]="s.optional" (ngModelChange)="markDirty()">Etapa opcional</mat-slide-toggle>
1141
+ <mat-slide-toggle [(ngModel)]="s.editable" (ngModelChange)="markDirty()">Permitir voltar e editar</mat-slide-toggle>
1142
+ <mat-slide-toggle [(ngModel)]="s.completed" (ngModelChange)="markDirty()">Marcar como concluída</mat-slide-toggle>
1143
+ <mat-slide-toggle [(ngModel)]="s.hasError" (ngModelChange)="markDirty()">Marcar como com erro</mat-slide-toggle>
1144
+ </div>
1145
+ </div>
1146
+ <div class="pdx-actions">
1147
+ <button mat-icon-button color="warn" (click)="removeStep(i)" matTooltip="Remover">
1148
+ <mat-icon [praxisIcon]="'delete'"></mat-icon>
1149
+ </button>
1150
+ </div>
1151
+ </div>
1152
+ </div>
1153
+ </div>
1154
+
1155
+ <div class="pdx-active-step" *ngIf="activeStep as step; else noSteps">
1156
+ <div class="hdr">
1157
+ <div class="title">Editando: {{ step.label }}</div>
1158
+ <mat-button-toggle-group [(ngModel)]="activeIndex" (ngModelChange)="onActiveIndexChange()" [hideSingleSelectionIndicator]="true">
1159
+ <mat-button-toggle *ngFor="let s of config.steps; let i = index" [value]="i">{{ i + 1 }}</mat-button-toggle>
1160
+ </mat-button-toggle-group>
1161
+ </div>
1162
+
1163
+ <div class="pdx-content-editor">
1164
+ <div class="section">
1165
+ <div class="section-title">Formulário principal</div>
1166
+ <div class="section-body">
1167
+ <ng-container *ngIf="step.form; else addFormBtn">
1168
+ <mat-card appearance="outlined" class="mini-card">
1169
+ <mat-card-header>
1170
+ <mat-icon mat-card-avatar [praxisIcon]="'dynamic_form'"></mat-icon>
1171
+ <mat-card-title>Formulário principal</mat-card-title>
1172
+ <mat-card-subtitle>{{ step.form.formId || step.id || '—' }}</mat-card-subtitle>
1173
+ </mat-card-header>
1174
+ <mat-card-actions align="end">
1175
+ <button mat-button (click)="editMainForm()"><mat-icon [praxisIcon]="'tune'"></mat-icon> Editar</button>
1176
+ <button mat-button color="warn" (click)="removeMainForm()"><mat-icon [praxisIcon]="'delete'"></mat-icon> Remover</button>
1177
+ </mat-card-actions>
1178
+ </mat-card>
1179
+ </ng-container>
1180
+ <ng-template #addFormBtn>
1181
+ <button mat-stroked-button color="primary" (click)="addMainForm()"><mat-icon [praxisIcon]="'add'"></mat-icon> Adicionar formulário</button>
1182
+ </ng-template>
1183
+ </div>
1184
+ </div>
1185
+
1186
+ <div class="section">
1187
+ <div class="section-title">Conteúdos e componentes</div>
1188
+ <div class="section-body">
1189
+ <div class="cta-grid">
1190
+ <div class="cta-card mat-elevation-z2">
1191
+ <div class="cta-head">
1192
+ <mat-icon [praxisIcon]="'dynamic_form'"></mat-icon>
1193
+ <div class="cta-title">Formulário dinâmico</div>
1194
+ </div>
1195
+ <div class="cta-desc">Crie campos e validações personalizadas nesta etapa.</div>
1196
+ <div class="cta-actions">
1197
+ <button mat-flat-button color="primary" (click)="addMainForm()">
1198
+ <mat-icon [praxisIcon]="'add'"></mat-icon>
1199
+ Inserir formulário
1200
+ </button>
1201
+ </div>
1202
+ </div>
1203
+ <div class="cta-card mat-elevation-z2">
1204
+ <div class="cta-head">
1205
+ <mat-icon [praxisIcon]="'account_tree'"></mat-icon>
1206
+ <div class="cta-title">Lista em árvore</div>
1207
+ </div>
1208
+ <div class="cta-desc">Exiba dados hierárquicos com seleção simples.</div>
1209
+ <div class="cta-actions">
1210
+ <button mat-stroked-button (click)="addTreeList()">
1211
+ <mat-icon [praxisIcon]="'add'"></mat-icon>
1212
+ Inserir árvore
1213
+ </button>
1214
+ </div>
1215
+ </div>
1216
+ <div class="cta-card mat-elevation-z2">
1217
+ <div class="cta-head">
1218
+ <mat-icon [praxisIcon]="'swap_horiz'"></mat-icon>
1219
+ <div class="cta-title">Transferência de itens</div>
1220
+ </div>
1221
+ <div class="cta-desc">Mover itens entre listas (ideal para múltipla seleção).</div>
1222
+ <div class="cta-actions">
1223
+ <button mat-stroked-button (click)="addTransferListQuick()">
1224
+ <mat-icon [praxisIcon]="'add'"></mat-icon>
1225
+ Inserir transferência
1226
+ </button>
1227
+ </div>
1228
+ </div>
1229
+ </div>
1230
+ <div cdkDropList (cdkDropListDropped)="dropWidget($event)" class="widget-list">
1231
+ <div class="widget-item" *ngFor="let w of (step.widgets || []); let wi = index" cdkDrag>
1232
+ <div class="drag-handle" cdkDragHandle><mat-icon [praxisIcon]="'drag_indicator'"></mat-icon></div>
1233
+ <div class="info">
1234
+ <div class="name">{{ displayWidgetName(w) }}</div>
1235
+ <div class="sub">{{ w.id }}</div>
1236
+ </div>
1237
+ <div class="actions">
1238
+ <button mat-button (click)="editWidget(w, wi)"><mat-icon [praxisIcon]="'tune'"></mat-icon> Editar</button>
1239
+ <button mat-button color="warn" (click)="removeWidget(wi)"><mat-icon [praxisIcon]="'delete'"></mat-icon> Remover</button>
1240
+ </div>
1241
+ </div>
1242
+ <div class="empty" *ngIf="!step.widgets || !step.widgets.length">Nenhum componente adicionado</div>
1243
+ </div>
1244
+ </div>
1245
+ </div>
1246
+ </div>
1247
+ </div>
1248
+ <ng-template #noSteps>
1249
+ <div class="muted">Nenhuma etapa definida.</div>
1250
+ </ng-template>
1251
+ </div>
1252
+ </mat-tab>
1253
+
1254
+ <mat-tab label="Navegação">
1255
+ <div class="tab-pad">
1256
+ <div class="help">Personalize os botões de avançar e voltar exibidos em cada etapa.</div>
1257
+ <div class="pdx-grid">
1258
+ <mat-form-field appearance="outline">
1259
+ <mat-label>Estilo dos botões</mat-label>
1260
+ <select matNativeControl [(ngModel)]="navigationCfg.variant" (ngModelChange)="markDirty()">
1261
+ <option [ngValue]="undefined">Padrão (elevado)</option>
1262
+ <option value="basic">Texto</option>
1263
+ <option value="flat">Plano</option>
1264
+ <option value="stroked">Contornado</option>
1265
+ <option value="raised">Elevado</option>
1266
+ </select>
1267
+ </mat-form-field>
1268
+ <mat-form-field appearance="outline">
1269
+ <mat-label>Cor</mat-label>
1270
+ <select matNativeControl [(ngModel)]="navigationCfg.color" (ngModelChange)="markDirty()">
1271
+ <option [ngValue]="undefined">Padrão</option>
1272
+ <option value="primary">Primária</option>
1273
+ <option value="accent">Acento</option>
1274
+ <option value="warn">Alerta</option>
1275
+ </select>
1276
+ </mat-form-field>
1277
+ <mat-form-field appearance="outline">
1278
+ <mat-label>Alinhamento</mat-label>
1279
+ <select matNativeControl [(ngModel)]="navigationCfg.align" (ngModelChange)="markDirty()">
1280
+ <option [ngValue]="undefined">Direita</option>
1281
+ <option value="start">Esquerda</option>
1282
+ <option value="center">Centro</option>
1283
+ <option value="space-between">Espaçado</option>
1284
+ <option value="end">Direita</option>
1285
+ </select>
1286
+ </mat-form-field>
1287
+ </div>
1288
+
1289
+ <div class="pdx-toggles">
1290
+ <mat-slide-toggle [(ngModel)]="navigationCfg.visible" (ngModelChange)="markDirty()">Mostrar navegação padrão</mat-slide-toggle>
1291
+ </div>
1292
+
1293
+ <div class="pdx-grid">
1294
+ <mat-form-field appearance="outline">
1295
+ <mat-label>Texto do botão Voltar</mat-label>
1296
+ <input matInput [(ngModel)]="navigationCfg.prevLabel" (ngModelChange)="markDirty()" placeholder="Ex.: Voltar" />
1297
+ </mat-form-field>
1298
+ <mat-form-field appearance="outline">
1299
+ <mat-label>Texto do botão Próximo</mat-label>
1300
+ <input matInput [(ngModel)]="navigationCfg.nextLabel" (ngModelChange)="markDirty()" placeholder="Ex.: Próximo" />
1301
+ </mat-form-field>
1302
+ </div>
1303
+
1304
+ <div class="icons-grid">
1305
+ <div class="icon-item">
1306
+ <div class="icon-head">Ícone de Voltar</div>
1307
+ <button mat-stroked-button type="button" (click)="pickNavIcon('prevIcon')">
1308
+ <mat-icon *ngIf="navigationCfg.prevIcon" [praxisIcon]="navigationCfg.prevIcon"></mat-icon>
1309
+ <ng-container *ngIf="!navigationCfg.prevIcon">Escolher</ng-container>
1310
+ </button>
1311
+ </div>
1312
+ <div class="icon-item">
1313
+ <div class="icon-head">Ícone de Próximo</div>
1314
+ <button mat-stroked-button type="button" (click)="pickNavIcon('nextIcon')">
1315
+ <mat-icon *ngIf="navigationCfg.nextIcon" [praxisIcon]="navigationCfg.nextIcon"></mat-icon>
1316
+ <ng-container *ngIf="!navigationCfg.nextIcon">Escolher</ng-container>
1317
+ </button>
1318
+ </div>
1319
+ </div>
1320
+ </div>
1321
+ </mat-tab>
1322
+
1323
+ <mat-tab label="Aparência">
1324
+ <div class="tab-pad">
1325
+ <div class="help">Personalize cores, tipografia e ícones. Para ajustes comuns, use os campos abaixo; para maior controle, edite os tokens. Copie o SCSS e cole no arquivo de tema.</div>
1326
+ <div class="quick-grid">
1327
+ <mat-form-field appearance="outline">
1328
+ <mat-label>Altura do cabeçalho (px)</mat-label>
1329
+ <input matInput type="number" [ngModel]="headerHeightPx" (ngModelChange)="setHeaderHeightPx($event)" placeholder="Ex.: 48" />
1330
+ <mat-hint>Altura do item do cabeçalho</mat-hint>
1331
+ </mat-form-field>
1332
+ <mat-form-field appearance="outline">
1333
+ <mat-label>Tamanho do texto (px)</mat-label>
1334
+ <input matInput type="number" [ngModel]="headerLabelTextSizePx" (ngModelChange)="setHeaderLabelTextSizePx($event)" placeholder="Ex.: 14" />
1335
+ <mat-hint>Tamanho do título da etapa</mat-hint>
1336
+ </mat-form-field>
1337
+ <mat-form-field appearance="outline">
1338
+ <mat-label>Peso do texto</mat-label>
1339
+ <select matNativeControl [ngModel]="headerLabelTextWeight" (ngModelChange)="setHeaderLabelTextWeight($event)">
1340
+ <option [ngValue]="undefined">Padrão</option>
1341
+ <option value="400">Normal</option>
1342
+ <option value="500">Médio</option>
1343
+ <option value="600">Seminegrito</option>
1344
+ <option value="700">Negrito</option>
1345
+ </select>
1346
+ </mat-form-field>
1347
+ </div>
1348
+ <div class="pdx-grid">
1349
+ <mat-form-field appearance="outline" class="w-full">
1350
+ <mat-label>Classe de tema (opcional)</mat-label>
1351
+ <input matInput [(ngModel)]="appearance.themeClass" (ngModelChange)="onAppearanceChange()" placeholder="Ex.: theme-stepper-custom" />
1352
+ <mat-hint>Aplica as cores apenas dentro desse seletor</mat-hint>
1353
+ </mat-form-field>
1354
+ <mat-form-field appearance="outline">
1355
+ <mat-label>Conjunto de ícones</mat-label>
1356
+ <select matNativeControl [(ngModel)]="appearance.iconsSet" (ngModelChange)="onAppearanceChange()">
1357
+ <option [ngValue]="undefined">Material Icons (padrão)</option>
1358
+ <option value="material-symbols-outlined">Material Symbols (Outlined)</option>
1359
+ <option value="material-symbols-rounded">Material Symbols (Rounded)</option>
1360
+ <option value="material-symbols-sharp">Material Symbols (Sharp)</option>
1361
+ </select>
1362
+ <mat-hint>Selecione se usar ícones do Material Symbols</mat-hint>
1363
+ </mat-form-field>
1364
+ </div>
1365
+ <div class="icons-grid">
1366
+ <div class="icon-item">
1367
+ <div class="icon-head">Ícone do número</div>
1368
+ <button mat-stroked-button type="button" (click)="pickIcon('number')">
1369
+ <mat-icon *ngIf="icons.number; else pick" [praxisIcon]="icons.number"></mat-icon>
1370
+ <ng-template #pick>Escolher</ng-template>
1371
+ </button>
1372
+ </div>
1373
+ <div class="icon-item">
1374
+ <div class="icon-head">Ícone concluído</div>
1375
+ <button mat-stroked-button type="button" (click)="pickIcon('done')">
1376
+ <mat-icon *ngIf="icons.done; else pick2" [praxisIcon]="icons.done"></mat-icon>
1377
+ <ng-template #pick2>Escolher</ng-template>
1378
+ </button>
1379
+ </div>
1380
+ <div class="icon-item">
1381
+ <div class="icon-head">Ícone edição</div>
1382
+ <button mat-stroked-button type="button" (click)="pickIcon('edit')">
1383
+ <mat-icon *ngIf="icons.edit; else pick3" [praxisIcon]="icons.edit"></mat-icon>
1384
+ <ng-template #pick3>Escolher</ng-template>
1385
+ </button>
1386
+ </div>
1387
+ <div class="icon-item">
1388
+ <div class="icon-head">Ícone de erro</div>
1389
+ <button mat-stroked-button type="button" (click)="pickIcon('error')">
1390
+ <mat-icon *ngIf="icons.error; else pick4" [praxisIcon]="icons.error"></mat-icon>
1391
+ <ng-template #pick4>Escolher</ng-template>
1392
+ </button>
1393
+ </div>
1394
+ </div>
1395
+ <div class="icons-hint" *ngIf="symbolsLikelySelected.length && !appearance.iconsSet">
1396
+ <div class="muted">
1397
+ Dica: {{ symbolsLikelySelected.join(', ') }} são ícones do Material Symbols. Selecione um conjunto Symbols abaixo ou clique em
1398
+ <button mat-button color="primary" type="button" (click)="setIconsSetToSymbols()">Usar Material Symbols (Outlined)</button>.
1399
+ </div>
1400
+ </div>
1401
+ <div class="tokens-grid">
1402
+ <div class="token-item" *ngFor="let key of stepperTokenKeys">
1403
+ <mat-form-field appearance="outline" class="w-full">
1404
+ <mat-label>{{ key }}</mat-label>
1405
+ <input matInput [ngModel]="appearance.tokens?.[key]" (ngModelChange)="onTokenChange(key, $event)" placeholder="valor CSS ou var(--token)" />
1406
+ </mat-form-field>
1407
+ </div>
1408
+ </div>
1409
+ <div class="pdx-appearance-actions">
1410
+ <button mat-stroked-button color="primary" type="button" (click)="applyPreset('neutral')">Preset: neutro</button>
1411
+ <button mat-stroked-button color="primary" type="button" (click)="applyPreset('primary')">Preset: primário</button>
1412
+ <button mat-stroked-button color="primary" type="button" (click)="applyPreset('high-contrast')">Preset: alto contraste</button>
1413
+ <button mat-button type="button" (click)="clearTokens()">Limpar tokens</button>
1414
+ </div>
1415
+ <mat-card appearance="outlined" class="code-card">
1416
+ <div class="code-head">Snippet SCSS</div>
1417
+ <pre class="code"><code>{{ scssSnippet() }}</code></pre>
1418
+ </mat-card>
1419
+ </div>
1420
+ </mat-tab>
1421
+ </mat-tab-group>
1422
+ </div>
1423
+ `, styles: [".pdx-editor{display:grid;gap:16px}.pdx-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px}.pdx-toggles{display:flex;gap:12px;flex-wrap:wrap}.tab-pad{padding:12px 4px;display:grid;gap:12px}.help{color:#000000b3;font-size:13px}.quick-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin-bottom:6px}.icons-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin:8px 0}.icon-item{display:grid;gap:6px;align-content:start}.icons-hint{margin-top:4px}.pdx-steps{display:grid;gap:8px}.pdx-steps-header{display:flex;justify-content:space-between;align-items:center}.pdx-step-list{display:grid;gap:8px}.pdx-step-item{display:grid;grid-template-columns:24px 1fr auto;gap:8px;align-items:start;padding:8px;border:1px solid rgba(0,0,0,.12);border-radius:8px;cursor:pointer}.pdx-step-item.active{border-color:color-mix(in oklab,var(--md-sys-color-primary) 40%,rgba(0,0,0,.12));box-shadow:var(--mat-elevation-transition),var(--mat-elevation-level2, 0 2px 6px rgba(0,0,0,.12))}.drag-handle{display:flex;align-items:center;color:#0000008a}.pdx-fields{display:grid;gap:8px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}.pdx-flag-group{display:flex;gap:10px;align-items:center}.pdx-active-step{display:grid;gap:12px;padding-top:8px;border-top:1px dashed rgba(0,0,0,.12)}.pdx-active-step .hdr{display:flex;gap:12px;align-items:center;justify-content:space-between}.pdx-content-editor{display:grid;gap:16px}.section{display:grid;gap:8px}.section-title{font-weight:600;opacity:.85}.mini-card{display:block}.widget-list{display:grid;gap:8px}.widget-item{display:grid;grid-template-columns:24px 1fr auto;align-items:center;gap:8px;padding:8px;border:1px solid rgba(0,0,0,.12);border-radius:8px}.widget-item .info .name{font-weight:600}.widget-item .info .sub{font-size:12px;opacity:.7}.widget-item .actions{display:flex;gap:8px}.cta-row{display:flex;gap:8px;align-items:center}.spacer{flex:1 1 auto}.muted{color:#0009}.cta-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:12px;margin-bottom:8px}.cta-card{display:grid;gap:8px;padding:12px;border-radius:12px;border:1px solid var(--md-sys-color-outline-variant, rgba(0,0,0,.12));background:var(--md-sys-color-surface)}.cta-card .cta-head{display:flex;align-items:center;gap:8px}.cta-card .cta-title{font-weight:600}.cta-card .cta-desc{font-size:12px;opacity:.78}.cta-card .cta-actions{display:flex;gap:8px;align-items:center}.tokens-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;margin:8px 0}.token-item{display:contents}.pdx-appearance{margin-top:8px;display:grid;gap:8px}.pdx-appearance-actions{display:flex;gap:8px;flex-wrap:wrap}.code-card{margin-top:8px}.code-head{font-weight:600;margin-bottom:4px}.code{white-space:pre;overflow:auto}\n"] }]
1424
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
1425
+ type: Inject,
1426
+ args: [SETTINGS_PANEL_DATA]
1427
+ }] }] });
1428
+
1429
+ class PraxisStepper {
1430
+ // Templates projetados
1431
+ stepLabelTpl;
1432
+ // Dados e estado
1433
+ _config = signal(null, ...(ngDevMode ? [{ debugName: "_config" }] : []));
1434
+ _selectedIndex = signal(0, ...(ngDevMode ? [{ debugName: "_selectedIndex" }] : []));
1435
+ _formGroups = new Map();
1436
+ _formValidity = new Map();
1437
+ // CTA models for empty state
1438
+ primaryCta = { label: 'Configurar Stepper', icon: 'tune', color: 'primary', action: () => this.openEditor() };
1439
+ secondaryCtas = [{ label: 'Adicionar etapa inicial', icon: 'add', action: () => this.addFirstStep() }];
1440
+ // API de entrada
1441
+ set config(c) {
1442
+ let next = null;
1443
+ if (typeof c === 'string') {
1444
+ try {
1445
+ next = JSON.parse(c);
1446
+ }
1447
+ catch {
1448
+ next = null;
1449
+ }
1450
+ }
1451
+ else {
1452
+ next = c;
1453
+ }
1454
+ this._config.set(next);
1455
+ if (next?.selectedIndex != null)
1456
+ this._selectedIndex.set(next.selectedIndex);
1457
+ }
1458
+ set selectedIndexInput(i) {
1459
+ if (i != null)
1460
+ this._selectedIndex.set(i);
1461
+ }
1462
+ // Suporte a two-way binding [(selectedIndex)] como no Material
1463
+ set selectedIndex(i) {
1464
+ if (i != null)
1465
+ this._selectedIndex.set(i);
1466
+ }
1467
+ disableRippleInput = false;
1468
+ editModeEnabled = false;
1469
+ // Paridade com API do Material
1470
+ labelPosition;
1471
+ color;
1472
+ /**
1473
+ * Validação remota opcional por etapa. Retorne ok=false com fieldErrors/formErrors para bloquear avanço.
1474
+ */
1475
+ serverValidate;
1476
+ // Emissões
1477
+ selectedIndexChange = new EventEmitter();
1478
+ widgetEvent = new EventEmitter();
1479
+ stepperContext;
1480
+ settings = inject(SettingsPanelService);
1481
+ animationDone = new EventEmitter();
1482
+ // Computed getters
1483
+ steps = computed(() => this._config()?.steps || [], ...(ngDevMode ? [{ debugName: "steps" }] : []));
1484
+ orientation = computed(() => this._config()?.orientation || 'horizontal', ...(ngDevMode ? [{ debugName: "orientation" }] : []));
1485
+ headerPosition = computed(() => this._config()?.headerPosition || 'top', ...(ngDevMode ? [{ debugName: "headerPosition" }] : []));
1486
+ // labelPosition: default 'end' no Material
1487
+ labelPos = computed(() => this._config()?.labelPosition ?? this.labelPosition ?? 'end', ...(ngDevMode ? [{ debugName: "labelPos" }] : []));
1488
+ linear = computed(() => !!this._config()?.linear, ...(ngDevMode ? [{ debugName: "linear" }] : []));
1489
+ disableRipple = computed(() => this._config()?.disableRipple ?? this.disableRippleInput, ...(ngDevMode ? [{ debugName: "disableRipple" }] : []));
1490
+ animationDuration = computed(() => this._config()?.animationDuration || '300ms', ...(ngDevMode ? [{ debugName: "animationDuration" }] : []));
1491
+ stepperColor = computed(() => this._config()?.color ?? this.color, ...(ngDevMode ? [{ debugName: "stepperColor" }] : []));
1492
+ icons = computed(() => this._config()?.appearance?.icons, ...(ngDevMode ? [{ debugName: "icons" }] : []));
1493
+ iconsSet = computed(() => this._config()?.appearance?.iconsSet, ...(ngDevMode ? [{ debugName: "iconsSet" }] : []));
1494
+ // Navigation helpers
1495
+ nav = computed(() => this._config()?.navigation || {}, ...(ngDevMode ? [{ debugName: "nav" }] : []));
1496
+ navVisible = () => this.nav()?.visible !== false; // default true
1497
+ navVariant = () => (this.nav()?.variant || 'raised');
1498
+ navColor = () => (this.nav()?.color || 'primary');
1499
+ navPrevLabel = () => this.nav()?.prevLabel || 'Voltar';
1500
+ navNextLabel = () => this.nav()?.nextLabel || 'Próximo';
1501
+ navPrevIcon = () => this.nav()?.prevIcon;
1502
+ navNextIcon = () => this.nav()?.nextIcon;
1503
+ navAlignClass = () => {
1504
+ const a = this.nav()?.align || 'end';
1505
+ switch (a) {
1506
+ case 'start': return 'nav-align-start';
1507
+ case 'center': return 'nav-align-center';
1508
+ case 'space-between': return 'nav-align-space-between';
1509
+ case 'end':
1510
+ default: return 'nav-align-end';
1511
+ }
1512
+ };
1513
+ isNextDisabled(i) {
1514
+ return this.linear() && this._formValidity.get(i) === false;
1515
+ }
1516
+ clampIndex(idx) {
1517
+ const len = this.steps().length;
1518
+ const max = Math.max(0, len - 1);
1519
+ if (Number.isNaN(idx))
1520
+ return 0;
1521
+ return Math.min(Math.max(0, idx), max);
1522
+ }
1523
+ selectedIndexComputed = computed(() => this.clampIndex(this._selectedIndex()), ...(ngDevMode ? [{ debugName: "selectedIndexComputed" }] : []));
1524
+ densityClass = () => {
1525
+ const d = this._config()?.density;
1526
+ if (d === 'compact')
1527
+ return 'density-compact';
1528
+ if (d === 'comfortable')
1529
+ return 'density-comfortable';
1530
+ return '';
1531
+ };
1532
+ // Handlers
1533
+ selectionChange = new EventEmitter();
1534
+ trackStep = (_, s) => s.id || _;
1535
+ onSelectionChange(ev) {
1536
+ const idx = this.clampIndex(ev?.selectedIndex ?? 0);
1537
+ this._selectedIndex.set(idx);
1538
+ this.selectedIndexChange.emit(idx);
1539
+ this.selectionChange.emit(ev);
1540
+ }
1541
+ // API programática básica
1542
+ goTo(index) {
1543
+ const clamped = this.clampIndex(index);
1544
+ this._selectedIndex.set(clamped);
1545
+ this.selectedIndexChange.emit(clamped);
1546
+ }
1547
+ next() {
1548
+ const curr = this._selectedIndex();
1549
+ const len = this.steps().length;
1550
+ if (curr < len - 1)
1551
+ this.goTo(curr + 1);
1552
+ }
1553
+ prev() {
1554
+ const curr = this._selectedIndex();
1555
+ if (curr > 0)
1556
+ this.goTo(curr - 1);
1557
+ }
1558
+ reset() { this.goTo(0); }
1559
+ // Alias para compatibilidade nominal com MatStepper
1560
+ previous() { this.prev(); }
1561
+ // Dynamic Form integration
1562
+ onFormReady(stepIndex, ev) {
1563
+ this._formGroups.set(stepIndex, ev.formGroup);
1564
+ this._formValidity.set(stepIndex, ev.formGroup.valid);
1565
+ }
1566
+ onFormValueChange(stepIndex, ev) {
1567
+ this._formValidity.set(stepIndex, ev.isValid);
1568
+ }
1569
+ formGroupFor(i) {
1570
+ return this._formGroups.get(i);
1571
+ }
1572
+ computedCompleted(i, step) {
1573
+ if (typeof step.completed === 'boolean')
1574
+ return step.completed;
1575
+ const v = this._formValidity.get(i);
1576
+ return v === true;
1577
+ }
1578
+ computedHasError(i, step) {
1579
+ if (typeof step.hasError === 'boolean')
1580
+ return step.hasError;
1581
+ const v = this._formValidity.get(i);
1582
+ return v === false;
1583
+ }
1584
+ onChildWidgetEvent(stepIndex, wd, e) {
1585
+ this.widgetEvent.emit({ stepIndex, stepId: this.steps()[stepIndex]?.id, sourceId: e.sourceId, output: e.output, payload: e.payload });
1586
+ }
1587
+ isStepEmpty(step) {
1588
+ const hasForm = !!step.form;
1589
+ const hasWidgets = Array.isArray(step.widgets) && step.widgets.length > 0;
1590
+ return !hasForm && !hasWidgets;
1591
+ }
1592
+ openEditor() {
1593
+ const cfg = this._config() || { steps: [], orientation: 'horizontal', headerPosition: 'top', linear: false };
1594
+ const ref = this.settings.open({
1595
+ id: 'praxis-stepper-editor',
1596
+ title: 'Configurar Stepper',
1597
+ content: { component: PraxisStepperConfigEditor, inputs: { config: cfg } },
1598
+ });
1599
+ const apply = (value) => {
1600
+ const nextCfg = value?.config || value;
1601
+ if (nextCfg) {
1602
+ // Deep clone to ensure change detection and avoid mutating editor instance
1603
+ const cloned = JSON.parse(JSON.stringify(nextCfg));
1604
+ this._config.set(cloned);
1605
+ // Clamp selection after changes
1606
+ const sel = typeof cloned.selectedIndex === 'number' ? cloned.selectedIndex : this._selectedIndex();
1607
+ this._selectedIndex.set(this.clampIndex(sel));
1608
+ }
1609
+ };
1610
+ ref.applied$.subscribe(apply);
1611
+ ref.saved$.subscribe(apply);
1612
+ }
1613
+ // CTA helpers
1614
+ addFirstStep() {
1615
+ const current = this._config() || { steps: [], orientation: 'horizontal', headerPosition: 'top', linear: false };
1616
+ if ((current.steps || []).length > 0)
1617
+ return;
1618
+ const next = {
1619
+ ...current,
1620
+ steps: [
1621
+ { id: 'step1', label: 'Etapa 1', description: 'Clique em Configurar para editar esta etapa.' },
1622
+ ],
1623
+ };
1624
+ this._config.set(next);
1625
+ // Seleciona a primeira etapa
1626
+ this._selectedIndex.set(0);
1627
+ }
1628
+ // Navegação com validação remota opcional
1629
+ async onNext(i) {
1630
+ // Em modo linear, bloquear quando inválido
1631
+ if (this.linear() && this._formValidity.get(i) === false) {
1632
+ return;
1633
+ }
1634
+ const step = this.steps()[i];
1635
+ const fg = this._formGroups.get(i);
1636
+ const data = fg?.getRawValue();
1637
+ if (this.serverValidate) {
1638
+ const res = await this.serverValidate({ step, stepIndex: i, formGroup: fg, formData: data });
1639
+ if (!res.ok) {
1640
+ this.applyServerErrors(fg, res.fieldErrors, res.formErrors);
1641
+ // Atualizar erro visual
1642
+ step.errorMessage = res.errorMessage || (res.formErrors && res.formErrors[0]) || 'Erro de validação';
1643
+ this._formValidity.set(i, false);
1644
+ return;
1645
+ }
1646
+ }
1647
+ this.next();
1648
+ }
1649
+ onPrev() { this.prev(); }
1650
+ onAnimationDone() { this.animationDone.emit(); }
1651
+ applyServerErrors(fg, fieldErrors, formErrors) {
1652
+ if (!fg)
1653
+ return;
1654
+ if (fieldErrors) {
1655
+ for (const [key, messages] of Object.entries(fieldErrors)) {
1656
+ const ctrl = fg.get(key);
1657
+ if (ctrl) {
1658
+ const prev = ctrl.errors || {};
1659
+ ctrl.setErrors({ ...prev, server: messages });
1660
+ ctrl.markAsTouched();
1661
+ }
1662
+ }
1663
+ }
1664
+ if (formErrors && formErrors.length) {
1665
+ const prev = fg.errors || {};
1666
+ fg.setErrors({ ...prev, server: formErrors });
1667
+ }
1668
+ fg.updateValueAndValidity({ emitEvent: true });
1669
+ }
1670
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PraxisStepper, deps: [], target: i0.ɵɵFactoryTarget.Component });
1671
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.4", type: PraxisStepper, isStandalone: true, selector: "praxis-stepper", inputs: { config: "config", selectedIndexInput: "selectedIndexInput", selectedIndex: "selectedIndex", disableRippleInput: "disableRippleInput", editModeEnabled: "editModeEnabled", labelPosition: "labelPosition", color: "color", serverValidate: "serverValidate", stepperContext: "stepperContext" }, outputs: { selectedIndexChange: "selectedIndexChange", widgetEvent: "widgetEvent", animationDone: "animationDone", selectionChange: "selectionChange" }, queries: [{ propertyName: "stepLabelTpl", first: true, predicate: ["stepLabelTpl"], descendants: true, read: TemplateRef }], ngImport: i0, template: `
1672
+ <ng-container *ngIf="steps().length > 0; else emptyState">
1673
+ <mat-stepper
1674
+ [linear]="linear()"
1675
+ [orientation]="orientation()"
1676
+ [headerPosition]="headerPosition()"
1677
+ [labelPosition]="labelPos()"
1678
+ [disableRipple]="disableRipple()"
1679
+ [color]="stepperColor()"
1680
+ [animationDuration]="animationDuration()"
1681
+ [selectedIndex]="selectedIndexComputed()"
1682
+ (selectionChange)="onSelectionChange($event)"
1683
+ (animationDone)="onAnimationDone()"
1684
+ class="praxis-stepper"
1685
+ [ngClass]="[densityClass(), _config()?.stepperClass || '']"
1686
+ >
1687
+ <!-- Projeção de ícones customizados do header: <ng-template matStepperIcon="done"> ... </ng-template> -->
1688
+ <ng-content></ng-content>
1689
+ <!-- Overrides de ícones via configuração (appearance.icons) -->
1690
+ <ng-template *ngIf="icons()?.number as icn" matStepperIcon="number">
1691
+ <mat-icon [praxisIcon]="icn"></mat-icon>
1692
+ </ng-template>
1693
+ <ng-template *ngIf="icons()?.done as icn" matStepperIcon="done">
1694
+ <mat-icon [praxisIcon]="icn"></mat-icon>
1695
+ </ng-template>
1696
+ <ng-template *ngIf="icons()?.edit as icn" matStepperIcon="edit">
1697
+ <mat-icon [praxisIcon]="icn"></mat-icon>
1698
+ </ng-template>
1699
+ <ng-template *ngIf="icons()?.error as icn" matStepperIcon="error">
1700
+ <mat-icon [praxisIcon]="icn"></mat-icon>
1701
+ </ng-template>
1702
+ <mat-step *ngFor="let step of steps(); let i = index; trackBy: trackStep"
1703
+ [stepControl]="$any(formGroupFor(i))"
1704
+ [id]="step.id || ''"
1705
+ [aria-label]="step.ariaLabel || ''"
1706
+ [aria-labelledby]="step.ariaLabelledby || ''"
1707
+ [label]="step.label"
1708
+ [optional]="step.optional || false"
1709
+ [editable]="step.editable ?? true"
1710
+ [completed]="computedCompleted(i, step)"
1711
+ [hasError]="computedHasError(i, step)"
1712
+ [errorMessage]="step.errorMessage || ''"
1713
+ [state]="step.state || step.stateIcon || ''"
1714
+ >
1715
+ <ng-template matStepLabel>
1716
+ <ng-container *ngIf="stepLabelTpl; else plainLabel"
1717
+ [ngTemplateOutlet]="stepLabelTpl"
1718
+ [ngTemplateOutletContext]="{ $implicit: step, index: i }"
1719
+ ></ng-container>
1720
+ <ng-template #plainLabel>{{ step.label }}</ng-template>
1721
+ </ng-template>
1722
+ <div class="pdx-step-content" [ngClass]="_config()?.contentClass || ''">
1723
+ <praxis-dynamic-form *ngIf="step.form as f"
1724
+ [resourcePath]="f.resourcePath"
1725
+ [resourceId]="$any(f.resourceId)"
1726
+ [mode]="f.mode || 'create'"
1727
+ [config]="f.config || { sections: [] }"
1728
+ [schemaSource]="f.schemaSource || 'resource'"
1729
+ [formId]="f.formId"
1730
+ (formReady)="onFormReady(i, $event)"
1731
+ (valueChange)="onFormValueChange(i, $event)"
1732
+ ></praxis-dynamic-form>
1733
+ <ng-container *ngFor="let wd of step.widgets || []"
1734
+ [dynamicWidgetLoader]="wd"
1735
+ [context]="stepperContext"
1736
+ [strictValidation]="true"
1737
+ [autoWireOutputs]="true"
1738
+ (widgetEvent)="onChildWidgetEvent(i, wd, $event)"></ng-container>
1739
+ <ng-container *ngIf="isStepEmpty(step)">
1740
+ <praxis-empty-state-card
1741
+ [inline]="true"
1742
+ icon="dashboard_customize"
1743
+ title="Sem conteúdo nesta etapa"
1744
+ description="Adicione um formulário ou widgets a esta etapa, ou abra o editor para configurar."
1745
+ [primaryAction]="{ label: 'Abrir editor', icon: 'tune', action: openEditor.bind(this) }"
1746
+ ></praxis-empty-state-card>
1747
+ </ng-container>
1748
+ </div>
1749
+ <div class="pdx-step-actions" *ngIf="navVisible()" [ngClass]="navAlignClass()">
1750
+ <!-- PREV button (hidden on first step) -->
1751
+ <ng-container *ngIf="selectedIndexComputed() > 0">
1752
+ <ng-container [ngSwitch]="navVariant()">
1753
+ <button *ngSwitchCase="'flat'" mat-flat-button [color]="navColor()" type="button" (click)="onPrev()">
1754
+ <mat-icon *ngIf="navPrevIcon() as ic" [praxisIcon]="ic"></mat-icon>
1755
+ {{ navPrevLabel() }}
1756
+ </button>
1757
+ <button *ngSwitchCase="'stroked'" mat-stroked-button [color]="navColor()" type="button" (click)="onPrev()">
1758
+ <mat-icon *ngIf="navPrevIcon() as ic" [praxisIcon]="ic"></mat-icon>
1759
+ {{ navPrevLabel() }}
1760
+ </button>
1761
+ <button *ngSwitchCase="'raised'" mat-raised-button [color]="navColor()" type="button" (click)="onPrev()">
1762
+ <mat-icon *ngIf="navPrevIcon() as ic" [praxisIcon]="ic"></mat-icon>
1763
+ {{ navPrevLabel() }}
1764
+ </button>
1765
+ <button *ngSwitchDefault mat-button [color]="navColor()" type="button" (click)="onPrev()">
1766
+ <mat-icon *ngIf="navPrevIcon() as ic" [praxisIcon]="ic"></mat-icon>
1767
+ {{ navPrevLabel() }}
1768
+ </button>
1769
+ </ng-container>
1770
+ </ng-container>
1771
+
1772
+ <!-- NEXT button -->
1773
+ <ng-container [ngSwitch]="navVariant()">
1774
+ <button *ngSwitchCase="'flat'" mat-flat-button [color]="navColor()" type="button" (click)="onNext(i)" [disabled]="isNextDisabled(i)">
1775
+ {{ navNextLabel() }}
1776
+ <mat-icon *ngIf="navNextIcon() as ic" [praxisIcon]="ic"></mat-icon>
1777
+ </button>
1778
+ <button *ngSwitchCase="'stroked'" mat-stroked-button [color]="navColor()" type="button" (click)="onNext(i)" [disabled]="isNextDisabled(i)">
1779
+ {{ navNextLabel() }}
1780
+ <mat-icon *ngIf="navNextIcon() as ic" [praxisIcon]="ic"></mat-icon>
1781
+ </button>
1782
+ <button *ngSwitchCase="'raised'" mat-raised-button [color]="navColor()" type="button" (click)="onNext(i)" [disabled]="isNextDisabled(i)">
1783
+ {{ navNextLabel() }}
1784
+ <mat-icon *ngIf="navNextIcon() as ic" [praxisIcon]="ic"></mat-icon>
1785
+ </button>
1786
+ <button *ngSwitchDefault mat-button [color]="navColor()" type="button" (click)="onNext(i)" [disabled]="isNextDisabled(i)">
1787
+ {{ navNextLabel() }}
1788
+ <mat-icon *ngIf="navNextIcon() as ic" [praxisIcon]="ic"></mat-icon>
1789
+ </button>
1790
+ </ng-container>
1791
+ </div>
1792
+ </mat-step>
1793
+ </mat-stepper>
1794
+ </ng-container>
1795
+ <ng-template #emptyState>
1796
+ <praxis-empty-state-card
1797
+ icon="view_timeline"
1798
+ [inline]="true"
1799
+ title="Stepper sem etapas"
1800
+ description="Adicione etapas para começar. Você pode configurar formulários ou widgets em cada etapa."
1801
+ [primaryAction]="primaryCta"
1802
+ [secondaryActions]="secondaryCtas"
1803
+ ></praxis-empty-state-card>
1804
+ </ng-template>
1805
+ <button
1806
+ *ngIf="editModeEnabled"
1807
+ mat-fab
1808
+ class="edit-fab"
1809
+ aria-label="Editar stepper"
1810
+ (click)="openEditor()"
1811
+ >
1812
+ <mat-icon fontIcon="edit"></mat-icon>
1813
+ </button>
1814
+ `, isInline: true, styles: [":host{display:block;position:relative}.praxis-stepper{width:100%}:host(.density-compact) .mat-step-header{min-height:36px}:host(.density-compact) .pdx-step-actions{padding-top:4px;gap:6px}:host(.density-comfortable) .mat-step-header{min-height:44px}.pdx-step-content{padding:8px 0}.pdx-step-actions{display:flex;gap:8px;padding-top:8px}.nav-align-start{justify-content:flex-start}.nav-align-center{justify-content:center}.nav-align-end{justify-content:flex-end}.nav-align-space-between{justify-content:space-between}.edit-fab{position:absolute;right:12px;bottom:12px;z-index:2}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1$1.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1$1.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1$1.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "ngmodule", type: MatStepperModule }, { kind: "component", type: i2$1.MatStep, selector: "mat-step", inputs: ["color"], exportAs: ["matStep"] }, { kind: "directive", type: i2$1.MatStepLabel, selector: "[matStepLabel]" }, { kind: "component", type: i2$1.MatStepper, selector: "mat-stepper, mat-vertical-stepper, mat-horizontal-stepper, [matStepper]", inputs: ["disableRipple", "color", "labelPosition", "headerPosition", "animationDuration"], outputs: ["animationDone"], exportAs: ["matStepper", "matVerticalStepper", "matHorizontalStepper"] }, { kind: "directive", type: i2$1.MatStepperIcon, selector: "ng-template[matStepperIcon]", inputs: ["matStepperIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i6.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i6.MatFabButton, selector: "button[mat-fab], a[mat-fab], button[matFab], a[matFab]", inputs: ["extended"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "component", type: PraxisDynamicForm, selector: "praxis-dynamic-form", inputs: ["resourcePath", "resourceId", "mode", "config", "schemaSource", "editModeEnabled", "formId", "layout", "backConfig", "hooks", "removeEmptyContainersOnSave", "reactiveValidation", "reactiveValidationDebounceMs", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "readonlyModeGlobal", "disabledModeGlobal", "presentationModeGlobal", "visibleGlobal", "customEndpoints"], outputs: ["formSubmit", "formCancel", "formReset", "configChange", "formReady", "valueChange", "syncCompleted", "initializationError", "editModeEnabledChange", "customAction", "actionConfirmation", "schemaStatusChange"] }, { kind: "directive", type: DynamicWidgetLoaderDirective, selector: "[dynamicWidgetLoader]", inputs: ["dynamicWidgetLoader", "context", "strictValidation", "autoWireOutputs"], outputs: ["widgetEvent"] }, { kind: "component", type: EmptyStateCardComponent, selector: "praxis-empty-state-card", inputs: ["icon", "title", "description", "primaryAction", "secondaryActions", "inline"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1815
+ }
1816
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PraxisStepper, decorators: [{
1817
+ type: Component,
1818
+ args: [{ selector: 'praxis-stepper', standalone: true, imports: [CommonModule, ReactiveFormsModule, MatStepperModule, MatButtonModule, MatIconModule, PraxisIconDirective, PraxisDynamicForm, DynamicWidgetLoaderDirective, EmptyStateCardComponent], template: `
1819
+ <ng-container *ngIf="steps().length > 0; else emptyState">
1820
+ <mat-stepper
1821
+ [linear]="linear()"
1822
+ [orientation]="orientation()"
1823
+ [headerPosition]="headerPosition()"
1824
+ [labelPosition]="labelPos()"
1825
+ [disableRipple]="disableRipple()"
1826
+ [color]="stepperColor()"
1827
+ [animationDuration]="animationDuration()"
1828
+ [selectedIndex]="selectedIndexComputed()"
1829
+ (selectionChange)="onSelectionChange($event)"
1830
+ (animationDone)="onAnimationDone()"
1831
+ class="praxis-stepper"
1832
+ [ngClass]="[densityClass(), _config()?.stepperClass || '']"
1833
+ >
1834
+ <!-- Projeção de ícones customizados do header: <ng-template matStepperIcon="done"> ... </ng-template> -->
1835
+ <ng-content></ng-content>
1836
+ <!-- Overrides de ícones via configuração (appearance.icons) -->
1837
+ <ng-template *ngIf="icons()?.number as icn" matStepperIcon="number">
1838
+ <mat-icon [praxisIcon]="icn"></mat-icon>
1839
+ </ng-template>
1840
+ <ng-template *ngIf="icons()?.done as icn" matStepperIcon="done">
1841
+ <mat-icon [praxisIcon]="icn"></mat-icon>
1842
+ </ng-template>
1843
+ <ng-template *ngIf="icons()?.edit as icn" matStepperIcon="edit">
1844
+ <mat-icon [praxisIcon]="icn"></mat-icon>
1845
+ </ng-template>
1846
+ <ng-template *ngIf="icons()?.error as icn" matStepperIcon="error">
1847
+ <mat-icon [praxisIcon]="icn"></mat-icon>
1848
+ </ng-template>
1849
+ <mat-step *ngFor="let step of steps(); let i = index; trackBy: trackStep"
1850
+ [stepControl]="$any(formGroupFor(i))"
1851
+ [id]="step.id || ''"
1852
+ [aria-label]="step.ariaLabel || ''"
1853
+ [aria-labelledby]="step.ariaLabelledby || ''"
1854
+ [label]="step.label"
1855
+ [optional]="step.optional || false"
1856
+ [editable]="step.editable ?? true"
1857
+ [completed]="computedCompleted(i, step)"
1858
+ [hasError]="computedHasError(i, step)"
1859
+ [errorMessage]="step.errorMessage || ''"
1860
+ [state]="step.state || step.stateIcon || ''"
1861
+ >
1862
+ <ng-template matStepLabel>
1863
+ <ng-container *ngIf="stepLabelTpl; else plainLabel"
1864
+ [ngTemplateOutlet]="stepLabelTpl"
1865
+ [ngTemplateOutletContext]="{ $implicit: step, index: i }"
1866
+ ></ng-container>
1867
+ <ng-template #plainLabel>{{ step.label }}</ng-template>
1868
+ </ng-template>
1869
+ <div class="pdx-step-content" [ngClass]="_config()?.contentClass || ''">
1870
+ <praxis-dynamic-form *ngIf="step.form as f"
1871
+ [resourcePath]="f.resourcePath"
1872
+ [resourceId]="$any(f.resourceId)"
1873
+ [mode]="f.mode || 'create'"
1874
+ [config]="f.config || { sections: [] }"
1875
+ [schemaSource]="f.schemaSource || 'resource'"
1876
+ [formId]="f.formId"
1877
+ (formReady)="onFormReady(i, $event)"
1878
+ (valueChange)="onFormValueChange(i, $event)"
1879
+ ></praxis-dynamic-form>
1880
+ <ng-container *ngFor="let wd of step.widgets || []"
1881
+ [dynamicWidgetLoader]="wd"
1882
+ [context]="stepperContext"
1883
+ [strictValidation]="true"
1884
+ [autoWireOutputs]="true"
1885
+ (widgetEvent)="onChildWidgetEvent(i, wd, $event)"></ng-container>
1886
+ <ng-container *ngIf="isStepEmpty(step)">
1887
+ <praxis-empty-state-card
1888
+ [inline]="true"
1889
+ icon="dashboard_customize"
1890
+ title="Sem conteúdo nesta etapa"
1891
+ description="Adicione um formulário ou widgets a esta etapa, ou abra o editor para configurar."
1892
+ [primaryAction]="{ label: 'Abrir editor', icon: 'tune', action: openEditor.bind(this) }"
1893
+ ></praxis-empty-state-card>
1894
+ </ng-container>
1895
+ </div>
1896
+ <div class="pdx-step-actions" *ngIf="navVisible()" [ngClass]="navAlignClass()">
1897
+ <!-- PREV button (hidden on first step) -->
1898
+ <ng-container *ngIf="selectedIndexComputed() > 0">
1899
+ <ng-container [ngSwitch]="navVariant()">
1900
+ <button *ngSwitchCase="'flat'" mat-flat-button [color]="navColor()" type="button" (click)="onPrev()">
1901
+ <mat-icon *ngIf="navPrevIcon() as ic" [praxisIcon]="ic"></mat-icon>
1902
+ {{ navPrevLabel() }}
1903
+ </button>
1904
+ <button *ngSwitchCase="'stroked'" mat-stroked-button [color]="navColor()" type="button" (click)="onPrev()">
1905
+ <mat-icon *ngIf="navPrevIcon() as ic" [praxisIcon]="ic"></mat-icon>
1906
+ {{ navPrevLabel() }}
1907
+ </button>
1908
+ <button *ngSwitchCase="'raised'" mat-raised-button [color]="navColor()" type="button" (click)="onPrev()">
1909
+ <mat-icon *ngIf="navPrevIcon() as ic" [praxisIcon]="ic"></mat-icon>
1910
+ {{ navPrevLabel() }}
1911
+ </button>
1912
+ <button *ngSwitchDefault mat-button [color]="navColor()" type="button" (click)="onPrev()">
1913
+ <mat-icon *ngIf="navPrevIcon() as ic" [praxisIcon]="ic"></mat-icon>
1914
+ {{ navPrevLabel() }}
1915
+ </button>
1916
+ </ng-container>
1917
+ </ng-container>
1918
+
1919
+ <!-- NEXT button -->
1920
+ <ng-container [ngSwitch]="navVariant()">
1921
+ <button *ngSwitchCase="'flat'" mat-flat-button [color]="navColor()" type="button" (click)="onNext(i)" [disabled]="isNextDisabled(i)">
1922
+ {{ navNextLabel() }}
1923
+ <mat-icon *ngIf="navNextIcon() as ic" [praxisIcon]="ic"></mat-icon>
1924
+ </button>
1925
+ <button *ngSwitchCase="'stroked'" mat-stroked-button [color]="navColor()" type="button" (click)="onNext(i)" [disabled]="isNextDisabled(i)">
1926
+ {{ navNextLabel() }}
1927
+ <mat-icon *ngIf="navNextIcon() as ic" [praxisIcon]="ic"></mat-icon>
1928
+ </button>
1929
+ <button *ngSwitchCase="'raised'" mat-raised-button [color]="navColor()" type="button" (click)="onNext(i)" [disabled]="isNextDisabled(i)">
1930
+ {{ navNextLabel() }}
1931
+ <mat-icon *ngIf="navNextIcon() as ic" [praxisIcon]="ic"></mat-icon>
1932
+ </button>
1933
+ <button *ngSwitchDefault mat-button [color]="navColor()" type="button" (click)="onNext(i)" [disabled]="isNextDisabled(i)">
1934
+ {{ navNextLabel() }}
1935
+ <mat-icon *ngIf="navNextIcon() as ic" [praxisIcon]="ic"></mat-icon>
1936
+ </button>
1937
+ </ng-container>
1938
+ </div>
1939
+ </mat-step>
1940
+ </mat-stepper>
1941
+ </ng-container>
1942
+ <ng-template #emptyState>
1943
+ <praxis-empty-state-card
1944
+ icon="view_timeline"
1945
+ [inline]="true"
1946
+ title="Stepper sem etapas"
1947
+ description="Adicione etapas para começar. Você pode configurar formulários ou widgets em cada etapa."
1948
+ [primaryAction]="primaryCta"
1949
+ [secondaryActions]="secondaryCtas"
1950
+ ></praxis-empty-state-card>
1951
+ </ng-template>
1952
+ <button
1953
+ *ngIf="editModeEnabled"
1954
+ mat-fab
1955
+ class="edit-fab"
1956
+ aria-label="Editar stepper"
1957
+ (click)="openEditor()"
1958
+ >
1959
+ <mat-icon fontIcon="edit"></mat-icon>
1960
+ </button>
1961
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;position:relative}.praxis-stepper{width:100%}:host(.density-compact) .mat-step-header{min-height:36px}:host(.density-compact) .pdx-step-actions{padding-top:4px;gap:6px}:host(.density-comfortable) .mat-step-header{min-height:44px}.pdx-step-content{padding:8px 0}.pdx-step-actions{display:flex;gap:8px;padding-top:8px}.nav-align-start{justify-content:flex-start}.nav-align-center{justify-content:center}.nav-align-end{justify-content:flex-end}.nav-align-space-between{justify-content:space-between}.edit-fab{position:absolute;right:12px;bottom:12px;z-index:2}\n"] }]
1962
+ }], propDecorators: { stepLabelTpl: [{
1963
+ type: ContentChild,
1964
+ args: ['stepLabelTpl', { read: TemplateRef }]
1965
+ }], config: [{
1966
+ type: Input
1967
+ }], selectedIndexInput: [{
1968
+ type: Input
1969
+ }], selectedIndex: [{
1970
+ type: Input
1971
+ }], disableRippleInput: [{
1972
+ type: Input
1973
+ }], editModeEnabled: [{
1974
+ type: Input
1975
+ }], labelPosition: [{
1976
+ type: Input
1977
+ }], color: [{
1978
+ type: Input
1979
+ }], serverValidate: [{
1980
+ type: Input
1981
+ }], selectedIndexChange: [{
1982
+ type: Output
1983
+ }], widgetEvent: [{
1984
+ type: Output
1985
+ }], stepperContext: [{
1986
+ type: Input
1987
+ }], animationDone: [{
1988
+ type: Output
1989
+ }], selectionChange: [{
1990
+ type: Output
1991
+ }] } });
1992
+
1993
+ const PRAXIS_STEPPER_COMPONENT_METADATA = {
1994
+ id: 'praxis-stepper',
1995
+ selector: 'praxis-stepper',
1996
+ component: PraxisStepper,
1997
+ friendlyName: 'Praxis Stepper',
1998
+ description: 'Stepper configurável (horizontal/vertical/linear) com passos e conteúdo dinâmico.',
1999
+ icon: 'format_list_numbered',
2000
+ inputs: [
2001
+ { name: 'config', type: 'StepperMetadata', label: 'Configuração', description: 'Configuração JSON com passos e opções' },
2002
+ { name: 'selectedIndex', type: 'number', label: '[(selectedIndex)] Índice selecionado' },
2003
+ { name: 'selectedIndexInput', type: 'number', label: 'Índice selecionado (legacy)' },
2004
+ { name: 'disableRippleInput', type: 'boolean', default: false, label: 'Desabilitar ripple' },
2005
+ { name: 'labelPosition', type: "'bottom' | 'end'", label: 'Posição do rótulo (horizontal)' },
2006
+ { name: 'color', type: 'ThemePalette', label: 'Cor (M2)' },
2007
+ { name: 'appearance', type: '{ themeClass?: string; tokens?: Record<string,string>; icons?: { number?: string; done?: string; edit?: string; error?: string }; iconsSet?: string }', label: 'Aparência (tokens, themeClass, ícones, conjunto)' },
2008
+ { name: 'serverValidate', type: '(args) => Promise<{ ok: boolean; fieldErrors?: Record<string,string[]>; formErrors?: string[] }>', label: 'Validação remota (opt-in)' },
2009
+ { name: 'density', type: "'default' | 'comfortable' | 'compact'", label: 'Densidade' },
2010
+ { name: 'stepperClass', type: 'string', label: 'Classe CSS (stepper)' },
2011
+ { name: 'headerClass', type: 'string', label: 'Classe CSS (header)' },
2012
+ { name: 'contentClass', type: 'string', label: 'Classe CSS (conteúdo)' },
2013
+ { name: 'navigation', type: "{ visible?: boolean; prevLabel?: string; nextLabel?: string; prevIcon?: string; nextIcon?: string; variant?: 'basic'|'flat'|'stroked'|'raised'; color?: ThemePalette; align?: 'start'|'center'|'end'|'space-between' }", label: 'Navegação (botões)'
2014
+ },
2015
+ ],
2016
+ outputs: [
2017
+ { name: 'selectionChange', type: 'any', label: 'Troca de seleção' },
2018
+ { name: 'selectedIndexChange', type: 'number', label: 'Índice selecionado (change)' },
2019
+ { name: 'animationDone', type: 'void', label: 'Animação concluída' },
2020
+ { name: 'widgetEvent', type: '{ stepId?: string; stepIndex?: number; sourceId: string; output?: string; payload?: any }', label: 'Evento interno' },
2021
+ ],
2022
+ tags: ['widget', 'container', 'stepper', 'configurable'],
2023
+ lib: '@praxisui/stepper',
2024
+ layoutHints: { recommendedCols: 8, recommendedRows: 6, minCols: 4, minRows: 4 },
2025
+ };
2026
+ function providePraxisStepperMetadata() {
2027
+ return {
2028
+ provide: ENVIRONMENT_INITIALIZER,
2029
+ multi: true,
2030
+ useFactory: (registry) => () => {
2031
+ registry.register(PRAXIS_STEPPER_COMPONENT_METADATA);
2032
+ },
2033
+ deps: [ComponentMetadataRegistry],
2034
+ };
2035
+ }
2036
+
2037
+ /*
2038
+ * Public API Surface of praxis-stepper
2039
+ */
2040
+
2041
+ /**
2042
+ * Generated bundle index. Do not edit.
2043
+ */
2044
+
2045
+ export { PRAXIS_STEPPER_COMPONENT_METADATA, PraxisStepper, PraxisStepperConfigEditor, providePraxisStepperMetadata };
2046
+ //# sourceMappingURL=praxisui-stepper.mjs.map