@praxisui/tabs 0.0.1

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,2192 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Inject, Component, inject, EventEmitter, signal, Output, Input, ChangeDetectionStrategy, ENVIRONMENT_INITIALIZER } from '@angular/core';
3
+ import * as i1$1 from '@angular/common';
4
+ import { CommonModule } from '@angular/common';
5
+ import * as i3$1 from '@angular/material/tabs';
6
+ import { MatTabsModule } from '@angular/material/tabs';
7
+ import * as i7 from '@angular/material/icon';
8
+ import { MatIconModule } from '@angular/material/icon';
9
+ import * as i1 from '@praxisui/core';
10
+ import { PraxisIconDirective, CONFIG_STORAGE, EmptyStateCardComponent, DynamicWidgetLoaderDirective, ComponentMetadataRegistry } from '@praxisui/core';
11
+ import * as i5$1 from '@angular/material/button';
12
+ import { MatButtonModule } from '@angular/material/button';
13
+ import * as i3 from '@angular/forms';
14
+ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
15
+ import { DynamicFieldLoaderDirective } from '@praxisui/dynamic-fields';
16
+ import { SETTINGS_PANEL_DATA, SettingsPanelService } from '@praxisui/settings-panel';
17
+ import * as i5 from '@angular/material/form-field';
18
+ import { MatFormFieldModule } from '@angular/material/form-field';
19
+ import * as i6 from '@angular/material/input';
20
+ import { MatInputModule } from '@angular/material/input';
21
+ import * as i9 from '@angular/material/slide-toggle';
22
+ import { MatSlideToggleModule } from '@angular/material/slide-toggle';
23
+ import * as i10 from '@angular/cdk/drag-drop';
24
+ import { moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop';
25
+ import * as i11 from '@angular/material/tooltip';
26
+ import { MatTooltipModule } from '@angular/material/tooltip';
27
+ import { BehaviorSubject } from 'rxjs';
28
+ import { produce } from 'immer';
29
+ import { MatSnackBar } from '@angular/material/snack-bar';
30
+
31
+ class PraxisTabsConfigEditor {
32
+ registry;
33
+ editedConfig;
34
+ initialConfig;
35
+ jsonText = '';
36
+ isValid = true;
37
+ errorMsg = '';
38
+ componentOptions = [];
39
+ selectedTabWidgetId = {};
40
+ selectedLinkWidgetId = {};
41
+ quickResourcePathTab = {};
42
+ quickResourcePathLink = {};
43
+ // Simplified token list we support at runtime
44
+ tokenList = [
45
+ { key: 'active-indicator-color', label: 'Indicador ativo' },
46
+ { key: 'active-label-text-color', label: 'Texto ativo' },
47
+ { key: 'active-hover-indicator-color', label: 'Indicador ativo (hover)' },
48
+ { key: 'active-hover-label-text-color', label: 'Texto ativo (hover)' },
49
+ { key: 'active-focus-indicator-color', label: 'Indicador ativo (focus)' },
50
+ { key: 'active-focus-label-text-color', label: 'Texto ativo (focus)' },
51
+ { key: 'inactive-label-text-color', label: 'Texto inativo' },
52
+ { key: 'inactive-hover-label-text-color', label: 'Texto inativo (hover)' },
53
+ { key: 'inactive-focus-label-text-color', label: 'Texto inativo (focus)' },
54
+ { key: 'pagination-icon-color', label: 'Ícones de paginação' },
55
+ { key: 'divider-color', label: 'Divisor/linha' },
56
+ { key: 'background-color', label: 'Fundo do header' },
57
+ ];
58
+ presets = {
59
+ primary: {
60
+ 'active-indicator-color': 'var(--mat-sys-primary)',
61
+ 'active-label-text-color': 'var(--mat-sys-primary)',
62
+ 'inactive-label-text-color': 'rgba(var(--mat-sys-on-surface-rgb), 0.72)',
63
+ 'inactive-hover-label-text-color': 'var(--mat-sys-on-surface)',
64
+ 'divider-color': 'rgba(255, 255, 255, 0.12)',
65
+ 'background-color': 'var(--mat-sys-surface-container)',
66
+ 'pagination-icon-color': 'var(--mat-sys-on-surface)'
67
+ },
68
+ neutral: {
69
+ 'active-indicator-color': 'var(--mat-sys-on-surface)',
70
+ 'active-label-text-color': 'var(--mat-sys-on-surface)',
71
+ 'inactive-label-text-color': 'rgba(var(--mat-sys-on-surface-rgb), 0.56)',
72
+ 'inactive-hover-label-text-color': 'var(--mat-sys-on-surface)',
73
+ 'divider-color': 'rgba(255, 255, 255, 0.10)',
74
+ 'background-color': 'var(--mat-sys-surface-container)',
75
+ 'pagination-icon-color': 'var(--mat-sys-on-surface)'
76
+ },
77
+ 'high-contrast': {
78
+ 'active-indicator-color': '#FFD54F',
79
+ 'active-label-text-color': '#FFFFFF',
80
+ 'inactive-label-text-color': '#BDBDBD',
81
+ 'inactive-hover-label-text-color': '#EEEEEE',
82
+ 'divider-color': '#FFFFFF',
83
+ 'background-color': 'var(--mat-sys-surface)',
84
+ 'pagination-icon-color': '#FFFFFF'
85
+ }
86
+ };
87
+ isDirty$ = new BehaviorSubject(false);
88
+ isValid$ = new BehaviorSubject(true);
89
+ isBusy$ = new BehaviorSubject(false);
90
+ constructor(data, registry) {
91
+ this.registry = registry;
92
+ const cfg = data?.config || data || {};
93
+ this.initialConfig = structuredClone(cfg);
94
+ this.editedConfig = structuredClone(cfg);
95
+ this.isValid$.next(true);
96
+ this.jsonText = this.stringify(this.editedConfig);
97
+ this.updateDirty();
98
+ this.componentOptions = this.registry.getAll().map((m) => ({ id: m.id, friendlyName: m.friendlyName }));
99
+ }
100
+ updateDirty() {
101
+ this.isDirty$.next(JSON.stringify(this.initialConfig) !== JSON.stringify(this.editedConfig));
102
+ }
103
+ onJsonTextChange(text) {
104
+ try {
105
+ const parsed = JSON.parse(text);
106
+ this.errorMsg = '';
107
+ this.isValid = true;
108
+ this.isValid$.next(true);
109
+ this.editedConfig = parsed;
110
+ this.updateDirty();
111
+ }
112
+ catch (e) {
113
+ this.isValid = false;
114
+ this.isValid$.next(false);
115
+ this.errorMsg = e?.message || 'Erro de sintaxe JSON';
116
+ }
117
+ }
118
+ // stringify helper is public to be used in the template
119
+ formatJson() {
120
+ if (!this.isValid)
121
+ return;
122
+ this.jsonText = this.stringify(this.editedConfig);
123
+ }
124
+ getSettingsValue() {
125
+ return { config: this.editedConfig };
126
+ }
127
+ onSave() {
128
+ return { config: this.editedConfig };
129
+ }
130
+ reset() {
131
+ this.editedConfig = structuredClone(this.initialConfig);
132
+ this.jsonText = this.stringify(this.editedConfig);
133
+ this.isValid = true;
134
+ this.isValid$.next(true);
135
+ this.updateDirty();
136
+ }
137
+ // Appearance helpers
138
+ get appearance() {
139
+ const ap = (this.editedConfig.appearance ??= {});
140
+ (ap.tokens ??= {});
141
+ return ap;
142
+ }
143
+ onAppearanceChange() {
144
+ this.updateDirty();
145
+ this.jsonText = this.stringify(this.editedConfig);
146
+ }
147
+ onTokenChange(key, value) {
148
+ const ap = this.appearance;
149
+ const tokens = (ap.tokens = ap.tokens || {});
150
+ if (!value) {
151
+ delete tokens[key];
152
+ }
153
+ else {
154
+ tokens[key] = value;
155
+ }
156
+ this.onAppearanceChange();
157
+ }
158
+ clearTokens() {
159
+ const ap = this.appearance;
160
+ ap.tokens = {};
161
+ this.onAppearanceChange();
162
+ }
163
+ applyPreset(id) {
164
+ const preset = this.presets[id];
165
+ if (!preset)
166
+ return;
167
+ const ap = this.appearance;
168
+ const tokens = (ap.tokens = ap.tokens || {});
169
+ for (const [k, v] of Object.entries(preset)) {
170
+ tokens[k] = v;
171
+ }
172
+ this.onAppearanceChange();
173
+ }
174
+ scssSnippet() {
175
+ const tokens = this.editedConfig.appearance?.tokens || {};
176
+ const entries = Object.entries(tokens).filter(([, v]) => !!v);
177
+ const selector = this.editedConfig.appearance?.themeClass
178
+ ? `.${this.editedConfig.appearance.themeClass}`
179
+ : ':root';
180
+ const map = entries.map(([k, v]) => ` ${k}: ${v},`).join('\n');
181
+ return `${selector} {\n @include mat.tabs-overrides((\n${map}\n ));\n}`;
182
+ }
183
+ // Group/Nav helpers and list editors
184
+ get group() {
185
+ if (!this.editedConfig.group)
186
+ this.editedConfig.group = {};
187
+ return this.editedConfig.group;
188
+ }
189
+ get nav() {
190
+ const nav = (this.editedConfig.nav ??= { links: [] });
191
+ if (!nav.links)
192
+ nav.links = [];
193
+ return nav;
194
+ }
195
+ get behavior() {
196
+ if (!this.editedConfig.behavior)
197
+ this.editedConfig.behavior = {};
198
+ // Default to lazy load to improve UX/perf with heavy widgets (table/form)
199
+ if (typeof this.editedConfig.behavior.lazyLoad === 'undefined') {
200
+ this.editedConfig.behavior.lazyLoad = true;
201
+ }
202
+ return this.editedConfig.behavior;
203
+ }
204
+ get accessibility() {
205
+ if (!this.editedConfig.accessibility)
206
+ this.editedConfig.accessibility = {};
207
+ return this.editedConfig.accessibility;
208
+ }
209
+ addTab() {
210
+ if (!this.editedConfig.tabs)
211
+ this.editedConfig.tabs = [];
212
+ this.editedConfig.tabs.push({ id: `tab${(this.editedConfig.tabs.length + 1)}`, textLabel: 'Nova aba' });
213
+ this.onAppearanceChange();
214
+ }
215
+ removeTab(index) {
216
+ this.editedConfig.tabs?.splice(index, 1);
217
+ this.onAppearanceChange();
218
+ }
219
+ moveTab(index, delta) {
220
+ if (!this.editedConfig.tabs)
221
+ return;
222
+ const i2 = index + delta;
223
+ if (i2 < 0 || i2 >= this.editedConfig.tabs.length)
224
+ return;
225
+ const [item] = this.editedConfig.tabs.splice(index, 1);
226
+ this.editedConfig.tabs.splice(i2, 0, item);
227
+ this.onAppearanceChange();
228
+ }
229
+ // ===== Widgets (Tabs) =====
230
+ addWidgetToTab(tabIndex) {
231
+ const id = (this.selectedTabWidgetId[tabIndex] || '').trim();
232
+ if (!id)
233
+ return;
234
+ const t = this.editedConfig.tabs[tabIndex];
235
+ t.widgets = t.widgets || [];
236
+ t.widgets.push({ id });
237
+ this.onAppearanceChange();
238
+ }
239
+ removeWidgetFromTab(tabIndex, widgetIndex) {
240
+ const t = this.editedConfig.tabs[tabIndex];
241
+ if (!t.widgets)
242
+ return;
243
+ t.widgets.splice(widgetIndex, 1);
244
+ this.onAppearanceChange();
245
+ }
246
+ updateWidgetInputsTab(tabIndex, widgetIndex, text) {
247
+ const t = this.editedConfig.tabs[tabIndex];
248
+ try {
249
+ const obj = text ? JSON.parse(text) : undefined;
250
+ t.widgets[widgetIndex].inputs = obj;
251
+ this.onAppearanceChange();
252
+ }
253
+ catch { }
254
+ }
255
+ updateWidgetOutputsTab(tabIndex, widgetIndex, text) {
256
+ const t = this.editedConfig.tabs[tabIndex];
257
+ try {
258
+ const obj = text ? JSON.parse(text) : undefined;
259
+ t.widgets[widgetIndex].outputs = obj;
260
+ this.onAppearanceChange();
261
+ }
262
+ catch { }
263
+ }
264
+ addPresetToTab(tabIndex, type) {
265
+ const rp = (this.quickResourcePathTab[tabIndex] || '').trim();
266
+ if (!rp)
267
+ return;
268
+ const t = this.editedConfig.tabs[tabIndex];
269
+ t.widgets = t.widgets || [];
270
+ if (type === 'form') {
271
+ t.widgets.push({ id: 'praxis-dynamic-form', inputs: { resourcePath: rp } });
272
+ }
273
+ else if (type === 'table') {
274
+ t.widgets.push({ id: 'praxis-table', inputs: { resourcePath: rp } });
275
+ }
276
+ else {
277
+ t.widgets.push({ id: 'praxis-crud', inputs: { metadata: { resource: { path: rp } } } });
278
+ }
279
+ this.onAppearanceChange();
280
+ }
281
+ addLink() {
282
+ if (!this.nav.links)
283
+ this.nav.links = [];
284
+ this.nav.links.push({ id: `link${this.nav.links.length + 1}`, label: 'Novo link' });
285
+ this.onAppearanceChange();
286
+ }
287
+ removeLink(index) {
288
+ if (!this.nav.links)
289
+ return;
290
+ this.nav.links.splice(index, 1);
291
+ this.onAppearanceChange();
292
+ }
293
+ moveLink(index, delta) {
294
+ if (!this.nav.links)
295
+ return;
296
+ const i2 = index + delta;
297
+ if (i2 < 0 || i2 >= this.nav.links.length)
298
+ return;
299
+ const [item] = this.nav.links.splice(index, 1);
300
+ this.nav.links.splice(i2, 0, item);
301
+ this.onAppearanceChange();
302
+ }
303
+ // ===== Widgets (Links) =====
304
+ addWidgetToLink(linkIndex) {
305
+ const id = (this.selectedLinkWidgetId[linkIndex] || '').trim();
306
+ if (!id)
307
+ return;
308
+ const l = this.nav.links[linkIndex];
309
+ l.widgets = l.widgets || [];
310
+ l.widgets.push({ id });
311
+ this.onAppearanceChange();
312
+ }
313
+ removeWidgetFromLink(linkIndex, widgetIndex) {
314
+ const l = this.nav.links[linkIndex];
315
+ if (!l.widgets)
316
+ return;
317
+ l.widgets.splice(widgetIndex, 1);
318
+ this.onAppearanceChange();
319
+ }
320
+ updateWidgetInputsLink(linkIndex, widgetIndex, text) {
321
+ const l = this.nav.links[linkIndex];
322
+ try {
323
+ const obj = text ? JSON.parse(text) : undefined;
324
+ l.widgets[widgetIndex].inputs = obj;
325
+ this.onAppearanceChange();
326
+ }
327
+ catch { }
328
+ }
329
+ updateWidgetOutputsLink(linkIndex, widgetIndex, text) {
330
+ const l = this.nav.links[linkIndex];
331
+ try {
332
+ const obj = text ? JSON.parse(text) : undefined;
333
+ l.widgets[widgetIndex].outputs = obj;
334
+ this.onAppearanceChange();
335
+ }
336
+ catch { }
337
+ }
338
+ addPresetToLink(linkIndex, type) {
339
+ const rp = (this.quickResourcePathLink[linkIndex] || '').trim();
340
+ if (!rp)
341
+ return;
342
+ const l = this.nav.links[linkIndex];
343
+ l.widgets = l.widgets || [];
344
+ if (type === 'form') {
345
+ l.widgets.push({ id: 'praxis-dynamic-form', inputs: { resourcePath: rp } });
346
+ }
347
+ else if (type === 'table') {
348
+ l.widgets.push({ id: 'praxis-table', inputs: { resourcePath: rp } });
349
+ }
350
+ else {
351
+ l.widgets.push({ id: 'praxis-crud', inputs: { metadata: { resource: { path: rp } } } });
352
+ }
353
+ this.onAppearanceChange();
354
+ }
355
+ stringify(v) { try {
356
+ return v ? JSON.stringify(v, null, 2) : '';
357
+ }
358
+ catch {
359
+ return '';
360
+ } }
361
+ getCompName(id) { const c = this.componentOptions.find(o => o.id === id); return c?.friendlyName || id; }
362
+ // Reorder handlers (drag & drop)
363
+ onTabWidgetDrop(tabIndex, event) {
364
+ const t = this.editedConfig.tabs?.[tabIndex];
365
+ if (!t?.widgets)
366
+ return;
367
+ moveItemInArray(t.widgets, event.previousIndex, event.currentIndex);
368
+ this.onAppearanceChange();
369
+ }
370
+ onLinkWidgetDrop(linkIndex, event) {
371
+ const l = this.nav.links?.[linkIndex];
372
+ if (!l?.widgets)
373
+ return;
374
+ moveItemInArray(l.widgets, event.previousIndex, event.currentIndex);
375
+ this.onAppearanceChange();
376
+ }
377
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PraxisTabsConfigEditor, deps: [{ token: SETTINGS_PANEL_DATA }, { token: i1.ComponentMetadataRegistry }], target: i0.ɵɵFactoryTarget.Component });
378
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.4", type: PraxisTabsConfigEditor, isStandalone: true, selector: "praxis-tabs-config-editor", ngImport: i0, template: `
379
+ <mat-tab-group>
380
+ <mat-tab label="Comportamento">
381
+ <div style="padding: 12px; display:grid; gap: 12px;">
382
+ <div style="display:flex; gap: 10px; flex-wrap: wrap;">
383
+ <mat-slide-toggle [(ngModel)]="behavior.closeable" (ngModelChange)="onAppearanceChange()">closeable</mat-slide-toggle>
384
+ <mat-slide-toggle [(ngModel)]="behavior.lazyLoad" (ngModelChange)="onAppearanceChange()">lazyLoad (planejado)</mat-slide-toggle>
385
+ <mat-slide-toggle [(ngModel)]="behavior.reorderable" (ngModelChange)="onAppearanceChange()">reorderable (planejado)</mat-slide-toggle>
386
+ </div>
387
+ </div>
388
+ </mat-tab>
389
+ <mat-tab label="Grupo">
390
+ <div style="padding: 12px; display:grid; gap: 12px;">
391
+ <div style="display:grid; grid-template-columns: repeat(2, minmax(220px,1fr)); gap: 12px;">
392
+ <mat-form-field appearance="outline"><mat-label>Alinhamento</mat-label>
393
+ <select matNativeControl [(ngModel)]="group.alignTabs" (ngModelChange)="onAppearanceChange()">
394
+ <option [ngValue]="undefined">padrão</option>
395
+ <option value="start">start</option>
396
+ <option value="center">center</option>
397
+ <option value="end">end</option>
398
+ </select>
399
+ </mat-form-field>
400
+ <mat-form-field appearance="outline"><mat-label>Header</mat-label>
401
+ <select matNativeControl [(ngModel)]="group.headerPosition" (ngModelChange)="onAppearanceChange()">
402
+ <option [ngValue]="undefined">above</option>
403
+ <option value="above">above</option>
404
+ <option value="below">below</option>
405
+ </select>
406
+ </mat-form-field>
407
+ <mat-form-field appearance="outline"><mat-label>selectedIndex</mat-label>
408
+ <input matInput type="number" [(ngModel)]="group.selectedIndex" (ngModelChange)="onAppearanceChange()" />
409
+ </mat-form-field>
410
+ <mat-form-field appearance="outline"><mat-label>animationDuration</mat-label>
411
+ <input matInput [(ngModel)]="group.animationDuration" (ngModelChange)="onAppearanceChange()" placeholder="500ms" />
412
+ </mat-form-field>
413
+ <mat-form-field appearance="outline"><mat-label>contentTabIndex</mat-label>
414
+ <input matInput type="number" [(ngModel)]="group.contentTabIndex" (ngModelChange)="onAppearanceChange()" />
415
+ </mat-form-field>
416
+ <mat-form-field appearance="outline"><mat-label>color (M2)</mat-label>
417
+ <select matNativeControl [(ngModel)]="group.color" (ngModelChange)="onAppearanceChange()">
418
+ <option [ngValue]="undefined">(nenhum)</option>
419
+ <option value="primary">primary</option>
420
+ <option value="accent">accent</option>
421
+ <option value="warn">warn</option>
422
+ </select>
423
+ </mat-form-field>
424
+ <mat-form-field appearance="outline"><mat-label>backgroundColor (M2)</mat-label>
425
+ <select matNativeControl [(ngModel)]="group.backgroundColor" (ngModelChange)="onAppearanceChange()">
426
+ <option [ngValue]="undefined">(nenhum)</option>
427
+ <option value="primary">primary</option>
428
+ <option value="accent">accent</option>
429
+ <option value="warn">warn</option>
430
+ </select>
431
+ </mat-form-field>
432
+ <mat-form-field appearance="outline"><mat-label>aria-label</mat-label>
433
+ <input matInput [(ngModel)]="group.ariaLabel" (ngModelChange)="onAppearanceChange()" />
434
+ </mat-form-field>
435
+ <mat-form-field appearance="outline"><mat-label>aria-labelledby</mat-label>
436
+ <input matInput [(ngModel)]="group.ariaLabelledby" (ngModelChange)="onAppearanceChange()" />
437
+ </mat-form-field>
438
+ </div>
439
+ <div style="display:flex; gap: 10px; flex-wrap: wrap;">
440
+ <mat-slide-toggle [(ngModel)]="group.dynamicHeight" (ngModelChange)="onAppearanceChange()">dynamicHeight</mat-slide-toggle>
441
+ <mat-slide-toggle [(ngModel)]="group.fitInkBarToContent" (ngModelChange)="onAppearanceChange()">fitInkBarToContent</mat-slide-toggle>
442
+ <mat-slide-toggle [(ngModel)]="group.disablePagination" (ngModelChange)="onAppearanceChange()">disablePagination</mat-slide-toggle>
443
+ <mat-slide-toggle [(ngModel)]="group.disableRipple" (ngModelChange)="onAppearanceChange()">disableRipple</mat-slide-toggle>
444
+ <mat-slide-toggle [(ngModel)]="group.preserveContent" (ngModelChange)="onAppearanceChange()">preserveContent</mat-slide-toggle>
445
+ <mat-slide-toggle [(ngModel)]="group.stretchTabs" (ngModelChange)="onAppearanceChange()">stretchTabs</mat-slide-toggle>
446
+ </div>
447
+ </div>
448
+ </mat-tab>
449
+
450
+ <mat-tab label="Navegação">
451
+ <div style="padding: 12px; display:grid; gap: 12px;">
452
+ <div style="display:grid; grid-template-columns: repeat(2, minmax(220px,1fr)); gap: 12px;">
453
+ <mat-form-field appearance="outline"><mat-label>selectedIndex</mat-label>
454
+ <input matInput type="number" [(ngModel)]="nav.selectedIndex" (ngModelChange)="onAppearanceChange()" />
455
+ </mat-form-field>
456
+ <mat-form-field appearance="outline"><mat-label>animationDuration</mat-label>
457
+ <input matInput [(ngModel)]="nav.animationDuration" (ngModelChange)="onAppearanceChange()" placeholder="500ms" />
458
+ </mat-form-field>
459
+ <mat-form-field appearance="outline"><mat-label>color (M2)</mat-label>
460
+ <select matNativeControl [(ngModel)]="nav.color" (ngModelChange)="onAppearanceChange()">
461
+ <option [ngValue]="undefined">(nenhum)</option>
462
+ <option value="primary">primary</option>
463
+ <option value="accent">accent</option>
464
+ <option value="warn">warn</option>
465
+ </select>
466
+ </mat-form-field>
467
+ <mat-form-field appearance="outline"><mat-label>backgroundColor (M2)</mat-label>
468
+ <select matNativeControl [(ngModel)]="nav.backgroundColor" (ngModelChange)="onAppearanceChange()">
469
+ <option [ngValue]="undefined">(nenhum)</option>
470
+ <option value="primary">primary</option>
471
+ <option value="accent">accent</option>
472
+ <option value="warn">warn</option>
473
+ </select>
474
+ </mat-form-field>
475
+ </div>
476
+ <div style="display:flex; gap: 10px; flex-wrap: wrap;">
477
+ <mat-slide-toggle [(ngModel)]="nav.fitInkBarToContent" (ngModelChange)="onAppearanceChange()">fitInkBarToContent</mat-slide-toggle>
478
+ <mat-slide-toggle [(ngModel)]="nav.disablePagination" (ngModelChange)="onAppearanceChange()">disablePagination</mat-slide-toggle>
479
+ <mat-slide-toggle [(ngModel)]="nav.disableRipple" (ngModelChange)="onAppearanceChange()">disableRipple</mat-slide-toggle>
480
+ <mat-slide-toggle [(ngModel)]="nav.stretchTabs" (ngModelChange)="onAppearanceChange()">stretchTabs</mat-slide-toggle>
481
+ </div>
482
+ </div>
483
+ </mat-tab>
484
+ <mat-tab label="JSON">
485
+ <div style="padding: 12px; display: grid; gap: 12px;">
486
+ <div class="json-editor-toolbar" style="display:flex; gap:8px;">
487
+ <button mat-button (click)="formatJson()" [disabled]="!isValid">
488
+ <mat-icon [praxisIcon]="'format_align_left'"></mat-icon>Formatar
489
+ </button>
490
+ <button mat-button (click)="reset()">
491
+ <mat-icon [praxisIcon]="'restart_alt'"></mat-icon>Resetar
492
+ </button>
493
+ </div>
494
+
495
+ <mat-form-field appearance="outline" class="json-textarea-field" style="width:100%">
496
+ <mat-label>Configuração JSON</mat-label>
497
+ <textarea
498
+ matInput
499
+ [(ngModel)]="jsonText"
500
+ (ngModelChange)="onJsonTextChange($event)"
501
+ rows="22"
502
+ spellcheck="false"
503
+ style="font-family: monospace"
504
+ ></textarea>
505
+ <mat-hint *ngIf="isValid">JSON válido</mat-hint>
506
+ <mat-error *ngIf="!isValid && jsonText">JSON inválido: {{ errorMsg }}</mat-error>
507
+ </mat-form-field>
508
+ </div>
509
+ </mat-tab>
510
+
511
+ <mat-tab label="Estilo">
512
+ <div style="padding: 12px; display: grid; gap: 16px;">
513
+ <div style="display:flex; gap:8px; align-items:center; flex-wrap: wrap;">
514
+ <span style="opacity:.75">Presets:</span>
515
+ <button mat-button color="primary" (click)="applyPreset('primary')">
516
+ <mat-icon [praxisIcon]="'palette'"></mat-icon>
517
+ Primário
518
+ </button>
519
+ <button mat-button (click)="applyPreset('neutral')">
520
+ <mat-icon [praxisIcon]="'contrast'"></mat-icon>
521
+ Neutro
522
+ </button>
523
+ <button mat-button (click)="applyPreset('high-contrast')">
524
+ <mat-icon [praxisIcon]="'visibility'"></mat-icon>
525
+ Alto contraste
526
+ </button>
527
+ <button mat-button (click)="clearTokens()">
528
+ <mat-icon [praxisIcon]="'backspace'"></mat-icon>
529
+ Limpar tokens
530
+ </button>
531
+ </div>
532
+
533
+ <div style="display:grid; gap:8px; grid-template-columns: repeat(2, minmax(220px, 1fr));">
534
+ <mat-form-field appearance="outline">
535
+ <mat-label>Classe de tema (opcional)</mat-label>
536
+ <input matInput [(ngModel)]="appearance.themeClass" (ngModelChange)="onAppearanceChange()" placeholder="ex.: tabs-accented" />
537
+ </mat-form-field>
538
+
539
+ <mat-form-field appearance="outline">
540
+ <mat-label>Densidade</mat-label>
541
+ <select matNativeControl [(ngModel)]="appearance.density" (ngModelChange)="onAppearanceChange()">
542
+ <option [ngValue]="undefined">padrão</option>
543
+ <option value="compact">compact</option>
544
+ <option value="comfortable">comfortable</option>
545
+ <option value="spacious">spacious</option>
546
+ </select>
547
+ </mat-form-field>
548
+ </div>
549
+
550
+ <div>
551
+ <h3 style="margin: 6px 0 8px;">Tokens (M3 aproximados)</h3>
552
+ <div style="display:grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); gap: 10px;">
553
+ <ng-container *ngFor="let t of tokenList">
554
+ <mat-form-field appearance="outline">
555
+ <mat-label>{{ t.label }}</mat-label>
556
+ <input matInput placeholder="var(--mat-sys-primary) / #RRGGBB" [ngModel]="appearance.tokens?.[t.key]" (ngModelChange)="onTokenChange(t.key, $event)" />
557
+ </mat-form-field>
558
+ </ng-container>
559
+ </div>
560
+ <p style="margin:8px 0 0; color: var(--mat-sys-on-surface-variant); font-size: 12px;">Dica: valores aceitam CSS vars (ex.: <code>var(--mat-sys-primary)</code>) ou cores hex/rgb.</p>
561
+ </div>
562
+
563
+ <div>
564
+ <h3 style="margin: 6px 0 8px;">CSS Personalizado</h3>
565
+ <mat-form-field appearance="outline" class="json-textarea-field" style="width:100%">
566
+ <mat-label>CSS a ser injetado no componente</mat-label>
567
+ <textarea matInput rows="10" [(ngModel)]="appearance.customCss" (ngModelChange)="onAppearanceChange()" placeholder=".praxis-tabs-root .mdc-tab__text-label { font-weight: 600; }"></textarea>
568
+ </mat-form-field>
569
+ </div>
570
+
571
+ <div>
572
+ <h3 style="margin: 6px 0 8px;">Snippet SCSS (para uso em styles.scss)</h3>
573
+ <pre style="white-space: pre-wrap; background: rgba(0,0,0,0.2); padding: 8px; border-radius: 6px;">
574
+ @use '@angular/material' as mat;
575
+ {{ scssSnippet() }}
576
+ </pre>
577
+ </div>
578
+ </div>
579
+ </mat-tab>
580
+
581
+ <mat-tab label="Abas">
582
+ <div style="padding:12px; display:grid; gap:12px;">
583
+ <button mat-stroked-button color="primary" (click)="addTab()"><mat-icon [praxisIcon]="'add'"></mat-icon>Adicionar aba</button>
584
+ <div *ngIf="editedConfig.tabs?.length; else noTabs" style="display:grid; gap:10px;">
585
+ <div *ngFor="let t of editedConfig.tabs; let i = index" style="border:1px solid var(--mat-sys-outline-variant); padding:10px; border-radius:8px; display:grid; gap:8px;">
586
+ <div style="display:flex; align-items:center; gap:8px;">
587
+ <strong style="flex:1">#{{ i+1 }}</strong>
588
+ <button mat-icon-button (click)="moveTab(i, -1)" [disabled]="i===0"><mat-icon [praxisIcon]="'arrow_upward'"></mat-icon></button>
589
+ <button mat-icon-button (click)="moveTab(i, 1)" [disabled]="i===editedConfig.tabs!.length-1"><mat-icon [praxisIcon]="'arrow_downward'"></mat-icon></button>
590
+ <button mat-icon-button color="warn" (click)="removeTab(i)"><mat-icon [praxisIcon]="'delete'"></mat-icon></button>
591
+ </div>
592
+ <div style="display:grid; grid-template-columns: repeat(2, minmax(220px,1fr)); gap: 10px;">
593
+ <mat-form-field appearance="outline"><mat-label>ID</mat-label>
594
+ <input matInput [(ngModel)]="t.id" (ngModelChange)="onAppearanceChange()" />
595
+ </mat-form-field>
596
+ <mat-form-field appearance="outline"><mat-label>Texto</mat-label>
597
+ <input matInput [(ngModel)]="t.textLabel" (ngModelChange)="onAppearanceChange()" />
598
+ </mat-form-field>
599
+ <mat-form-field appearance="outline"><mat-label>Label class</mat-label>
600
+ <input matInput [(ngModel)]="t.labelClass" (ngModelChange)="onAppearanceChange()" />
601
+ </mat-form-field>
602
+ <mat-form-field appearance="outline"><mat-label>Body class</mat-label>
603
+ <input matInput [(ngModel)]="t.bodyClass" (ngModelChange)="onAppearanceChange()" />
604
+ </mat-form-field>
605
+ <mat-form-field appearance="outline"><mat-label>aria-label</mat-label>
606
+ <input matInput [(ngModel)]="t.ariaLabel" (ngModelChange)="onAppearanceChange()" />
607
+ </mat-form-field>
608
+ <mat-form-field appearance="outline"><mat-label>aria-labelledby</mat-label>
609
+ <input matInput [(ngModel)]="t.ariaLabelledby" (ngModelChange)="onAppearanceChange()" />
610
+ </mat-form-field>
611
+ </div>
612
+ <mat-slide-toggle [(ngModel)]="t.disabled" (ngModelChange)="onAppearanceChange()">disabled</mat-slide-toggle>
613
+
614
+ <!-- Widgets (componentes dinâmicos) -->
615
+ <div style="border-top:1px solid var(--mat-sys-outline-variant); padding-top:8px; display:grid; gap:8px;">
616
+ <div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
617
+ <mat-form-field appearance="outline" style="min-width:260px;">
618
+ <mat-label>Adicionar componente</mat-label>
619
+ <select matNativeControl [(ngModel)]="selectedTabWidgetId[i]">
620
+ <option [ngValue]="''">(selecione)</option>
621
+ <option *ngFor="let c of componentOptions" [ngValue]="c.id">{{ c.friendlyName || c.id }}</option>
622
+ </select>
623
+ </mat-form-field>
624
+ <button mat-stroked-button (click)="addWidgetToTab(i)"><mat-icon [praxisIcon]="'add'"></mat-icon>Adicionar</button>
625
+ <span style="flex:1"></span>
626
+ <mat-form-field appearance="outline" style="width:240px;">
627
+ <mat-label>resourcePath</mat-label>
628
+ <input matInput [(ngModel)]="quickResourcePathTab[i]" placeholder="ex.: usuarios" />
629
+ </mat-form-field>
630
+ <button mat-button (click)="addPresetToTab(i, 'form')"><mat-icon [praxisIcon]="'description'"></mat-icon>Form</button>
631
+ <button mat-button (click)="addPresetToTab(i, 'table')"><mat-icon [praxisIcon]="'grid_on'"></mat-icon>Tabela</button>
632
+ <button mat-button (click)="addPresetToTab(i, 'crud')"><mat-icon [praxisIcon]="'dynamic_form'"></mat-icon>CRUD</button>
633
+ </div>
634
+
635
+ <div *ngIf="t.widgets?.length" style="display:grid; gap:8px;" cdkDropList [cdkDropListData]="t.widgets || []" (cdkDropListDropped)="onTabWidgetDrop(i, $event)">
636
+ <div *ngFor="let w of t.widgets; let wi = index" cdkDrag style="border:1px dashed var(--mat-sys-outline-variant); padding:8px; border-radius:6px;">
637
+ <div style="display:flex; align-items:center; gap:8px;">
638
+ <button mat-icon-button cdkDragHandle matTooltip="Arrastar para reordenar" aria-label="Arrastar para reordenar">
639
+ <mat-icon [praxisIcon]="'drag_indicator'"></mat-icon>
640
+ </button>
641
+ <strong style="flex:1">{{ getCompName(w.id) }}</strong>
642
+ <button mat-icon-button color="warn" (click)="removeWidgetFromTab(i, wi)" matTooltip="Remover componente" aria-label="Remover componente"><mat-icon [praxisIcon]="'delete'"></mat-icon></button>
643
+ </div>
644
+ <div style="display:grid; grid-template-columns: 1fr 1fr; gap:8px;">
645
+ <mat-form-field appearance="outline">
646
+ <mat-label>inputs (JSON)</mat-label>
647
+ <textarea matInput rows="4" [ngModel]="stringify(w.inputs)" (ngModelChange)="updateWidgetInputsTab(i, wi, $event)"></textarea>
648
+ </mat-form-field>
649
+ <mat-form-field appearance="outline">
650
+ <mat-label>outputs (JSON)</mat-label>
651
+ <textarea matInput rows="4" [ngModel]="stringify(w.outputs)" (ngModelChange)="updateWidgetOutputsTab(i, wi, $event)"></textarea>
652
+ </mat-form-field>
653
+ </div>
654
+ </div>
655
+ </div>
656
+ </div>
657
+ </div>
658
+ </div>
659
+ <ng-template #noTabs><em>Nenhuma aba definida.</em></ng-template>
660
+ </div>
661
+ </mat-tab>
662
+
663
+ <mat-tab label="Acessibilidade">
664
+ <div style="padding: 12px; display:grid; gap: 12px;">
665
+ <div style="display:flex; gap: 10px; flex-wrap: wrap;">
666
+ <mat-slide-toggle [(ngModel)]="accessibility.highContrast" (ngModelChange)="onAppearanceChange()">highContrast</mat-slide-toggle>
667
+ <mat-slide-toggle [(ngModel)]="accessibility.reduceMotion" (ngModelChange)="onAppearanceChange()">reduceMotion</mat-slide-toggle>
668
+ </div>
669
+ </div>
670
+ </mat-tab>
671
+
672
+ <mat-tab label="Links">
673
+ <div style="padding:12px; display:grid; gap:12px;">
674
+ <button mat-stroked-button color="primary" (click)="addLink()"><mat-icon [praxisIcon]="'add_link'"></mat-icon>Adicionar link</button>
675
+ <div *ngIf="nav.links?.length; else noLinks" style="display:grid; gap:10px;">
676
+ <div *ngFor="let l of nav.links; let i = index" style="border:1px solid var(--mat-sys-outline-variant); padding:10px; border-radius:8px; display:grid; gap:8px;">
677
+ <div style="display:flex; align-items:center; gap:8px;">
678
+ <strong style="flex:1">#{{ i+1 }}</strong>
679
+ <button mat-icon-button (click)="moveLink(i, -1)" [disabled]="i===0"><mat-icon [praxisIcon]="'arrow_upward'"></mat-icon></button>
680
+ <button mat-icon-button (click)="moveLink(i, 1)" [disabled]="i===nav.links!.length-1"><mat-icon [praxisIcon]="'arrow_downward'"></mat-icon></button>
681
+ <button mat-icon-button color="warn" (click)="removeLink(i)"><mat-icon [praxisIcon]="'delete'"></mat-icon></button>
682
+ </div>
683
+ <div style="display:grid; grid-template-columns: repeat(2, minmax(220px,1fr)); gap: 10px;">
684
+ <mat-form-field appearance="outline"><mat-label>ID</mat-label>
685
+ <input matInput [(ngModel)]="l.id" (ngModelChange)="onAppearanceChange()" />
686
+ </mat-form-field>
687
+ <mat-form-field appearance="outline"><mat-label>Label</mat-label>
688
+ <input matInput [(ngModel)]="l.label" (ngModelChange)="onAppearanceChange()" />
689
+ </mat-form-field>
690
+ </div>
691
+ <div style="display:flex; gap:10px; flex-wrap:wrap;">
692
+ <mat-slide-toggle [(ngModel)]="l.active" (ngModelChange)="onAppearanceChange()">active</mat-slide-toggle>
693
+ <mat-slide-toggle [(ngModel)]="l.disabled" (ngModelChange)="onAppearanceChange()">disabled</mat-slide-toggle>
694
+ <mat-slide-toggle [(ngModel)]="l.disableRipple" (ngModelChange)="onAppearanceChange()">disableRipple</mat-slide-toggle>
695
+ <mat-slide-toggle [(ngModel)]="l.fitInkBarToContent" (ngModelChange)="onAppearanceChange()">fitInkBarToContent</mat-slide-toggle>
696
+ </div>
697
+
698
+ <!-- Widgets para Links -->
699
+ <div style="border-top:1px solid var(--mat-sys-outline-variant); padding-top:8px; display:grid; gap:8px;">
700
+ <div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
701
+ <mat-form-field appearance="outline" style="min-width:260px;">
702
+ <mat-label>Adicionar componente</mat-label>
703
+ <select matNativeControl [(ngModel)]="selectedLinkWidgetId[i]">
704
+ <option [ngValue]="''">(selecione)</option>
705
+ <option *ngFor="let c of componentOptions" [ngValue]="c.id">{{ c.friendlyName || c.id }}</option>
706
+ </select>
707
+ </mat-form-field>
708
+ <button mat-stroked-button (click)="addWidgetToLink(i)"><mat-icon [praxisIcon]="'add'"></mat-icon>Adicionar</button>
709
+ <span style="flex:1"></span>
710
+ <mat-form-field appearance="outline" style="width:240px;">
711
+ <mat-label>resourcePath</mat-label>
712
+ <input matInput [(ngModel)]="quickResourcePathLink[i]" placeholder="ex.: usuarios" />
713
+ </mat-form-field>
714
+ <button mat-button (click)="addPresetToLink(i, 'form')"><mat-icon [praxisIcon]="'description'"></mat-icon>Form</button>
715
+ <button mat-button (click)="addPresetToLink(i, 'table')"><mat-icon [praxisIcon]="'grid_on'"></mat-icon>Tabela</button>
716
+ <button mat-button (click)="addPresetToLink(i, 'crud')"><mat-icon [praxisIcon]="'dynamic_form'"></mat-icon>CRUD</button>
717
+ </div>
718
+
719
+ <div *ngIf="l.widgets?.length" style="display:grid; gap:8px;" cdkDropList [cdkDropListData]="l.widgets || []" (cdkDropListDropped)="onLinkWidgetDrop(i, $event)">
720
+ <div *ngFor="let w of l.widgets; let wi = index" cdkDrag style="border:1px dashed var(--mat-sys-outline-variant); padding:8px; border-radius:6px;">
721
+ <div style="display:flex; align-items:center; gap:8px;">
722
+ <button mat-icon-button cdkDragHandle matTooltip="Arrastar para reordenar" aria-label="Arrastar para reordenar">
723
+ <mat-icon [praxisIcon]="'drag_indicator'"></mat-icon>
724
+ </button>
725
+ <strong style="flex:1">{{ getCompName(w.id) }}</strong>
726
+ <button mat-icon-button color="warn" (click)="removeWidgetFromLink(i, wi)" matTooltip="Remover componente" aria-label="Remover componente"><mat-icon [praxisIcon]="'delete'"></mat-icon></button>
727
+ </div>
728
+ <div style="display:grid; grid-template-columns: 1fr 1fr; gap:8px;">
729
+ <mat-form-field appearance="outline">
730
+ <mat-label>inputs (JSON)</mat-label>
731
+ <textarea matInput rows="4" [ngModel]="stringify(w.inputs)" (ngModelChange)="updateWidgetInputsLink(i, wi, $event)"></textarea>
732
+ </mat-form-field>
733
+ <mat-form-field appearance="outline">
734
+ <mat-label>outputs (JSON)</mat-label>
735
+ <textarea matInput rows="4" [ngModel]="stringify(w.outputs)" (ngModelChange)="updateWidgetOutputsLink(i, wi, $event)"></textarea>
736
+ </mat-form-field>
737
+ </div>
738
+ </div>
739
+ </div>
740
+ </div>
741
+ </div>
742
+ </div>
743
+ <ng-template #noLinks><em>Nenhum link definido.</em></ng-template>
744
+ </div>
745
+ </mat-tab>
746
+ </mat-tab-group>
747
+ `, isInline: true, 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: i3.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3.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: i3.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i3.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i3$1.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i3$1.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"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "directive", type: i5.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i5.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.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: i7.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: i5$1.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: i5$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i9.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: i10.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: i10.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: i10.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i11.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] });
748
+ }
749
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PraxisTabsConfigEditor, decorators: [{
750
+ type: Component,
751
+ args: [{
752
+ selector: 'praxis-tabs-config-editor',
753
+ standalone: true,
754
+ imports: [
755
+ CommonModule,
756
+ FormsModule,
757
+ MatTabsModule,
758
+ MatFormFieldModule,
759
+ MatInputModule,
760
+ MatIconModule,
761
+ PraxisIconDirective,
762
+ MatButtonModule,
763
+ MatSlideToggleModule,
764
+ DragDropModule,
765
+ MatTooltipModule,
766
+ ],
767
+ template: `
768
+ <mat-tab-group>
769
+ <mat-tab label="Comportamento">
770
+ <div style="padding: 12px; display:grid; gap: 12px;">
771
+ <div style="display:flex; gap: 10px; flex-wrap: wrap;">
772
+ <mat-slide-toggle [(ngModel)]="behavior.closeable" (ngModelChange)="onAppearanceChange()">closeable</mat-slide-toggle>
773
+ <mat-slide-toggle [(ngModel)]="behavior.lazyLoad" (ngModelChange)="onAppearanceChange()">lazyLoad (planejado)</mat-slide-toggle>
774
+ <mat-slide-toggle [(ngModel)]="behavior.reorderable" (ngModelChange)="onAppearanceChange()">reorderable (planejado)</mat-slide-toggle>
775
+ </div>
776
+ </div>
777
+ </mat-tab>
778
+ <mat-tab label="Grupo">
779
+ <div style="padding: 12px; display:grid; gap: 12px;">
780
+ <div style="display:grid; grid-template-columns: repeat(2, minmax(220px,1fr)); gap: 12px;">
781
+ <mat-form-field appearance="outline"><mat-label>Alinhamento</mat-label>
782
+ <select matNativeControl [(ngModel)]="group.alignTabs" (ngModelChange)="onAppearanceChange()">
783
+ <option [ngValue]="undefined">padrão</option>
784
+ <option value="start">start</option>
785
+ <option value="center">center</option>
786
+ <option value="end">end</option>
787
+ </select>
788
+ </mat-form-field>
789
+ <mat-form-field appearance="outline"><mat-label>Header</mat-label>
790
+ <select matNativeControl [(ngModel)]="group.headerPosition" (ngModelChange)="onAppearanceChange()">
791
+ <option [ngValue]="undefined">above</option>
792
+ <option value="above">above</option>
793
+ <option value="below">below</option>
794
+ </select>
795
+ </mat-form-field>
796
+ <mat-form-field appearance="outline"><mat-label>selectedIndex</mat-label>
797
+ <input matInput type="number" [(ngModel)]="group.selectedIndex" (ngModelChange)="onAppearanceChange()" />
798
+ </mat-form-field>
799
+ <mat-form-field appearance="outline"><mat-label>animationDuration</mat-label>
800
+ <input matInput [(ngModel)]="group.animationDuration" (ngModelChange)="onAppearanceChange()" placeholder="500ms" />
801
+ </mat-form-field>
802
+ <mat-form-field appearance="outline"><mat-label>contentTabIndex</mat-label>
803
+ <input matInput type="number" [(ngModel)]="group.contentTabIndex" (ngModelChange)="onAppearanceChange()" />
804
+ </mat-form-field>
805
+ <mat-form-field appearance="outline"><mat-label>color (M2)</mat-label>
806
+ <select matNativeControl [(ngModel)]="group.color" (ngModelChange)="onAppearanceChange()">
807
+ <option [ngValue]="undefined">(nenhum)</option>
808
+ <option value="primary">primary</option>
809
+ <option value="accent">accent</option>
810
+ <option value="warn">warn</option>
811
+ </select>
812
+ </mat-form-field>
813
+ <mat-form-field appearance="outline"><mat-label>backgroundColor (M2)</mat-label>
814
+ <select matNativeControl [(ngModel)]="group.backgroundColor" (ngModelChange)="onAppearanceChange()">
815
+ <option [ngValue]="undefined">(nenhum)</option>
816
+ <option value="primary">primary</option>
817
+ <option value="accent">accent</option>
818
+ <option value="warn">warn</option>
819
+ </select>
820
+ </mat-form-field>
821
+ <mat-form-field appearance="outline"><mat-label>aria-label</mat-label>
822
+ <input matInput [(ngModel)]="group.ariaLabel" (ngModelChange)="onAppearanceChange()" />
823
+ </mat-form-field>
824
+ <mat-form-field appearance="outline"><mat-label>aria-labelledby</mat-label>
825
+ <input matInput [(ngModel)]="group.ariaLabelledby" (ngModelChange)="onAppearanceChange()" />
826
+ </mat-form-field>
827
+ </div>
828
+ <div style="display:flex; gap: 10px; flex-wrap: wrap;">
829
+ <mat-slide-toggle [(ngModel)]="group.dynamicHeight" (ngModelChange)="onAppearanceChange()">dynamicHeight</mat-slide-toggle>
830
+ <mat-slide-toggle [(ngModel)]="group.fitInkBarToContent" (ngModelChange)="onAppearanceChange()">fitInkBarToContent</mat-slide-toggle>
831
+ <mat-slide-toggle [(ngModel)]="group.disablePagination" (ngModelChange)="onAppearanceChange()">disablePagination</mat-slide-toggle>
832
+ <mat-slide-toggle [(ngModel)]="group.disableRipple" (ngModelChange)="onAppearanceChange()">disableRipple</mat-slide-toggle>
833
+ <mat-slide-toggle [(ngModel)]="group.preserveContent" (ngModelChange)="onAppearanceChange()">preserveContent</mat-slide-toggle>
834
+ <mat-slide-toggle [(ngModel)]="group.stretchTabs" (ngModelChange)="onAppearanceChange()">stretchTabs</mat-slide-toggle>
835
+ </div>
836
+ </div>
837
+ </mat-tab>
838
+
839
+ <mat-tab label="Navegação">
840
+ <div style="padding: 12px; display:grid; gap: 12px;">
841
+ <div style="display:grid; grid-template-columns: repeat(2, minmax(220px,1fr)); gap: 12px;">
842
+ <mat-form-field appearance="outline"><mat-label>selectedIndex</mat-label>
843
+ <input matInput type="number" [(ngModel)]="nav.selectedIndex" (ngModelChange)="onAppearanceChange()" />
844
+ </mat-form-field>
845
+ <mat-form-field appearance="outline"><mat-label>animationDuration</mat-label>
846
+ <input matInput [(ngModel)]="nav.animationDuration" (ngModelChange)="onAppearanceChange()" placeholder="500ms" />
847
+ </mat-form-field>
848
+ <mat-form-field appearance="outline"><mat-label>color (M2)</mat-label>
849
+ <select matNativeControl [(ngModel)]="nav.color" (ngModelChange)="onAppearanceChange()">
850
+ <option [ngValue]="undefined">(nenhum)</option>
851
+ <option value="primary">primary</option>
852
+ <option value="accent">accent</option>
853
+ <option value="warn">warn</option>
854
+ </select>
855
+ </mat-form-field>
856
+ <mat-form-field appearance="outline"><mat-label>backgroundColor (M2)</mat-label>
857
+ <select matNativeControl [(ngModel)]="nav.backgroundColor" (ngModelChange)="onAppearanceChange()">
858
+ <option [ngValue]="undefined">(nenhum)</option>
859
+ <option value="primary">primary</option>
860
+ <option value="accent">accent</option>
861
+ <option value="warn">warn</option>
862
+ </select>
863
+ </mat-form-field>
864
+ </div>
865
+ <div style="display:flex; gap: 10px; flex-wrap: wrap;">
866
+ <mat-slide-toggle [(ngModel)]="nav.fitInkBarToContent" (ngModelChange)="onAppearanceChange()">fitInkBarToContent</mat-slide-toggle>
867
+ <mat-slide-toggle [(ngModel)]="nav.disablePagination" (ngModelChange)="onAppearanceChange()">disablePagination</mat-slide-toggle>
868
+ <mat-slide-toggle [(ngModel)]="nav.disableRipple" (ngModelChange)="onAppearanceChange()">disableRipple</mat-slide-toggle>
869
+ <mat-slide-toggle [(ngModel)]="nav.stretchTabs" (ngModelChange)="onAppearanceChange()">stretchTabs</mat-slide-toggle>
870
+ </div>
871
+ </div>
872
+ </mat-tab>
873
+ <mat-tab label="JSON">
874
+ <div style="padding: 12px; display: grid; gap: 12px;">
875
+ <div class="json-editor-toolbar" style="display:flex; gap:8px;">
876
+ <button mat-button (click)="formatJson()" [disabled]="!isValid">
877
+ <mat-icon [praxisIcon]="'format_align_left'"></mat-icon>Formatar
878
+ </button>
879
+ <button mat-button (click)="reset()">
880
+ <mat-icon [praxisIcon]="'restart_alt'"></mat-icon>Resetar
881
+ </button>
882
+ </div>
883
+
884
+ <mat-form-field appearance="outline" class="json-textarea-field" style="width:100%">
885
+ <mat-label>Configuração JSON</mat-label>
886
+ <textarea
887
+ matInput
888
+ [(ngModel)]="jsonText"
889
+ (ngModelChange)="onJsonTextChange($event)"
890
+ rows="22"
891
+ spellcheck="false"
892
+ style="font-family: monospace"
893
+ ></textarea>
894
+ <mat-hint *ngIf="isValid">JSON válido</mat-hint>
895
+ <mat-error *ngIf="!isValid && jsonText">JSON inválido: {{ errorMsg }}</mat-error>
896
+ </mat-form-field>
897
+ </div>
898
+ </mat-tab>
899
+
900
+ <mat-tab label="Estilo">
901
+ <div style="padding: 12px; display: grid; gap: 16px;">
902
+ <div style="display:flex; gap:8px; align-items:center; flex-wrap: wrap;">
903
+ <span style="opacity:.75">Presets:</span>
904
+ <button mat-button color="primary" (click)="applyPreset('primary')">
905
+ <mat-icon [praxisIcon]="'palette'"></mat-icon>
906
+ Primário
907
+ </button>
908
+ <button mat-button (click)="applyPreset('neutral')">
909
+ <mat-icon [praxisIcon]="'contrast'"></mat-icon>
910
+ Neutro
911
+ </button>
912
+ <button mat-button (click)="applyPreset('high-contrast')">
913
+ <mat-icon [praxisIcon]="'visibility'"></mat-icon>
914
+ Alto contraste
915
+ </button>
916
+ <button mat-button (click)="clearTokens()">
917
+ <mat-icon [praxisIcon]="'backspace'"></mat-icon>
918
+ Limpar tokens
919
+ </button>
920
+ </div>
921
+
922
+ <div style="display:grid; gap:8px; grid-template-columns: repeat(2, minmax(220px, 1fr));">
923
+ <mat-form-field appearance="outline">
924
+ <mat-label>Classe de tema (opcional)</mat-label>
925
+ <input matInput [(ngModel)]="appearance.themeClass" (ngModelChange)="onAppearanceChange()" placeholder="ex.: tabs-accented" />
926
+ </mat-form-field>
927
+
928
+ <mat-form-field appearance="outline">
929
+ <mat-label>Densidade</mat-label>
930
+ <select matNativeControl [(ngModel)]="appearance.density" (ngModelChange)="onAppearanceChange()">
931
+ <option [ngValue]="undefined">padrão</option>
932
+ <option value="compact">compact</option>
933
+ <option value="comfortable">comfortable</option>
934
+ <option value="spacious">spacious</option>
935
+ </select>
936
+ </mat-form-field>
937
+ </div>
938
+
939
+ <div>
940
+ <h3 style="margin: 6px 0 8px;">Tokens (M3 aproximados)</h3>
941
+ <div style="display:grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); gap: 10px;">
942
+ <ng-container *ngFor="let t of tokenList">
943
+ <mat-form-field appearance="outline">
944
+ <mat-label>{{ t.label }}</mat-label>
945
+ <input matInput placeholder="var(--mat-sys-primary) / #RRGGBB" [ngModel]="appearance.tokens?.[t.key]" (ngModelChange)="onTokenChange(t.key, $event)" />
946
+ </mat-form-field>
947
+ </ng-container>
948
+ </div>
949
+ <p style="margin:8px 0 0; color: var(--mat-sys-on-surface-variant); font-size: 12px;">Dica: valores aceitam CSS vars (ex.: <code>var(--mat-sys-primary)</code>) ou cores hex/rgb.</p>
950
+ </div>
951
+
952
+ <div>
953
+ <h3 style="margin: 6px 0 8px;">CSS Personalizado</h3>
954
+ <mat-form-field appearance="outline" class="json-textarea-field" style="width:100%">
955
+ <mat-label>CSS a ser injetado no componente</mat-label>
956
+ <textarea matInput rows="10" [(ngModel)]="appearance.customCss" (ngModelChange)="onAppearanceChange()" placeholder=".praxis-tabs-root .mdc-tab__text-label { font-weight: 600; }"></textarea>
957
+ </mat-form-field>
958
+ </div>
959
+
960
+ <div>
961
+ <h3 style="margin: 6px 0 8px;">Snippet SCSS (para uso em styles.scss)</h3>
962
+ <pre style="white-space: pre-wrap; background: rgba(0,0,0,0.2); padding: 8px; border-radius: 6px;">
963
+ @use '@angular/material' as mat;
964
+ {{ scssSnippet() }}
965
+ </pre>
966
+ </div>
967
+ </div>
968
+ </mat-tab>
969
+
970
+ <mat-tab label="Abas">
971
+ <div style="padding:12px; display:grid; gap:12px;">
972
+ <button mat-stroked-button color="primary" (click)="addTab()"><mat-icon [praxisIcon]="'add'"></mat-icon>Adicionar aba</button>
973
+ <div *ngIf="editedConfig.tabs?.length; else noTabs" style="display:grid; gap:10px;">
974
+ <div *ngFor="let t of editedConfig.tabs; let i = index" style="border:1px solid var(--mat-sys-outline-variant); padding:10px; border-radius:8px; display:grid; gap:8px;">
975
+ <div style="display:flex; align-items:center; gap:8px;">
976
+ <strong style="flex:1">#{{ i+1 }}</strong>
977
+ <button mat-icon-button (click)="moveTab(i, -1)" [disabled]="i===0"><mat-icon [praxisIcon]="'arrow_upward'"></mat-icon></button>
978
+ <button mat-icon-button (click)="moveTab(i, 1)" [disabled]="i===editedConfig.tabs!.length-1"><mat-icon [praxisIcon]="'arrow_downward'"></mat-icon></button>
979
+ <button mat-icon-button color="warn" (click)="removeTab(i)"><mat-icon [praxisIcon]="'delete'"></mat-icon></button>
980
+ </div>
981
+ <div style="display:grid; grid-template-columns: repeat(2, minmax(220px,1fr)); gap: 10px;">
982
+ <mat-form-field appearance="outline"><mat-label>ID</mat-label>
983
+ <input matInput [(ngModel)]="t.id" (ngModelChange)="onAppearanceChange()" />
984
+ </mat-form-field>
985
+ <mat-form-field appearance="outline"><mat-label>Texto</mat-label>
986
+ <input matInput [(ngModel)]="t.textLabel" (ngModelChange)="onAppearanceChange()" />
987
+ </mat-form-field>
988
+ <mat-form-field appearance="outline"><mat-label>Label class</mat-label>
989
+ <input matInput [(ngModel)]="t.labelClass" (ngModelChange)="onAppearanceChange()" />
990
+ </mat-form-field>
991
+ <mat-form-field appearance="outline"><mat-label>Body class</mat-label>
992
+ <input matInput [(ngModel)]="t.bodyClass" (ngModelChange)="onAppearanceChange()" />
993
+ </mat-form-field>
994
+ <mat-form-field appearance="outline"><mat-label>aria-label</mat-label>
995
+ <input matInput [(ngModel)]="t.ariaLabel" (ngModelChange)="onAppearanceChange()" />
996
+ </mat-form-field>
997
+ <mat-form-field appearance="outline"><mat-label>aria-labelledby</mat-label>
998
+ <input matInput [(ngModel)]="t.ariaLabelledby" (ngModelChange)="onAppearanceChange()" />
999
+ </mat-form-field>
1000
+ </div>
1001
+ <mat-slide-toggle [(ngModel)]="t.disabled" (ngModelChange)="onAppearanceChange()">disabled</mat-slide-toggle>
1002
+
1003
+ <!-- Widgets (componentes dinâmicos) -->
1004
+ <div style="border-top:1px solid var(--mat-sys-outline-variant); padding-top:8px; display:grid; gap:8px;">
1005
+ <div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
1006
+ <mat-form-field appearance="outline" style="min-width:260px;">
1007
+ <mat-label>Adicionar componente</mat-label>
1008
+ <select matNativeControl [(ngModel)]="selectedTabWidgetId[i]">
1009
+ <option [ngValue]="''">(selecione)</option>
1010
+ <option *ngFor="let c of componentOptions" [ngValue]="c.id">{{ c.friendlyName || c.id }}</option>
1011
+ </select>
1012
+ </mat-form-field>
1013
+ <button mat-stroked-button (click)="addWidgetToTab(i)"><mat-icon [praxisIcon]="'add'"></mat-icon>Adicionar</button>
1014
+ <span style="flex:1"></span>
1015
+ <mat-form-field appearance="outline" style="width:240px;">
1016
+ <mat-label>resourcePath</mat-label>
1017
+ <input matInput [(ngModel)]="quickResourcePathTab[i]" placeholder="ex.: usuarios" />
1018
+ </mat-form-field>
1019
+ <button mat-button (click)="addPresetToTab(i, 'form')"><mat-icon [praxisIcon]="'description'"></mat-icon>Form</button>
1020
+ <button mat-button (click)="addPresetToTab(i, 'table')"><mat-icon [praxisIcon]="'grid_on'"></mat-icon>Tabela</button>
1021
+ <button mat-button (click)="addPresetToTab(i, 'crud')"><mat-icon [praxisIcon]="'dynamic_form'"></mat-icon>CRUD</button>
1022
+ </div>
1023
+
1024
+ <div *ngIf="t.widgets?.length" style="display:grid; gap:8px;" cdkDropList [cdkDropListData]="t.widgets || []" (cdkDropListDropped)="onTabWidgetDrop(i, $event)">
1025
+ <div *ngFor="let w of t.widgets; let wi = index" cdkDrag style="border:1px dashed var(--mat-sys-outline-variant); padding:8px; border-radius:6px;">
1026
+ <div style="display:flex; align-items:center; gap:8px;">
1027
+ <button mat-icon-button cdkDragHandle matTooltip="Arrastar para reordenar" aria-label="Arrastar para reordenar">
1028
+ <mat-icon [praxisIcon]="'drag_indicator'"></mat-icon>
1029
+ </button>
1030
+ <strong style="flex:1">{{ getCompName(w.id) }}</strong>
1031
+ <button mat-icon-button color="warn" (click)="removeWidgetFromTab(i, wi)" matTooltip="Remover componente" aria-label="Remover componente"><mat-icon [praxisIcon]="'delete'"></mat-icon></button>
1032
+ </div>
1033
+ <div style="display:grid; grid-template-columns: 1fr 1fr; gap:8px;">
1034
+ <mat-form-field appearance="outline">
1035
+ <mat-label>inputs (JSON)</mat-label>
1036
+ <textarea matInput rows="4" [ngModel]="stringify(w.inputs)" (ngModelChange)="updateWidgetInputsTab(i, wi, $event)"></textarea>
1037
+ </mat-form-field>
1038
+ <mat-form-field appearance="outline">
1039
+ <mat-label>outputs (JSON)</mat-label>
1040
+ <textarea matInput rows="4" [ngModel]="stringify(w.outputs)" (ngModelChange)="updateWidgetOutputsTab(i, wi, $event)"></textarea>
1041
+ </mat-form-field>
1042
+ </div>
1043
+ </div>
1044
+ </div>
1045
+ </div>
1046
+ </div>
1047
+ </div>
1048
+ <ng-template #noTabs><em>Nenhuma aba definida.</em></ng-template>
1049
+ </div>
1050
+ </mat-tab>
1051
+
1052
+ <mat-tab label="Acessibilidade">
1053
+ <div style="padding: 12px; display:grid; gap: 12px;">
1054
+ <div style="display:flex; gap: 10px; flex-wrap: wrap;">
1055
+ <mat-slide-toggle [(ngModel)]="accessibility.highContrast" (ngModelChange)="onAppearanceChange()">highContrast</mat-slide-toggle>
1056
+ <mat-slide-toggle [(ngModel)]="accessibility.reduceMotion" (ngModelChange)="onAppearanceChange()">reduceMotion</mat-slide-toggle>
1057
+ </div>
1058
+ </div>
1059
+ </mat-tab>
1060
+
1061
+ <mat-tab label="Links">
1062
+ <div style="padding:12px; display:grid; gap:12px;">
1063
+ <button mat-stroked-button color="primary" (click)="addLink()"><mat-icon [praxisIcon]="'add_link'"></mat-icon>Adicionar link</button>
1064
+ <div *ngIf="nav.links?.length; else noLinks" style="display:grid; gap:10px;">
1065
+ <div *ngFor="let l of nav.links; let i = index" style="border:1px solid var(--mat-sys-outline-variant); padding:10px; border-radius:8px; display:grid; gap:8px;">
1066
+ <div style="display:flex; align-items:center; gap:8px;">
1067
+ <strong style="flex:1">#{{ i+1 }}</strong>
1068
+ <button mat-icon-button (click)="moveLink(i, -1)" [disabled]="i===0"><mat-icon [praxisIcon]="'arrow_upward'"></mat-icon></button>
1069
+ <button mat-icon-button (click)="moveLink(i, 1)" [disabled]="i===nav.links!.length-1"><mat-icon [praxisIcon]="'arrow_downward'"></mat-icon></button>
1070
+ <button mat-icon-button color="warn" (click)="removeLink(i)"><mat-icon [praxisIcon]="'delete'"></mat-icon></button>
1071
+ </div>
1072
+ <div style="display:grid; grid-template-columns: repeat(2, minmax(220px,1fr)); gap: 10px;">
1073
+ <mat-form-field appearance="outline"><mat-label>ID</mat-label>
1074
+ <input matInput [(ngModel)]="l.id" (ngModelChange)="onAppearanceChange()" />
1075
+ </mat-form-field>
1076
+ <mat-form-field appearance="outline"><mat-label>Label</mat-label>
1077
+ <input matInput [(ngModel)]="l.label" (ngModelChange)="onAppearanceChange()" />
1078
+ </mat-form-field>
1079
+ </div>
1080
+ <div style="display:flex; gap:10px; flex-wrap:wrap;">
1081
+ <mat-slide-toggle [(ngModel)]="l.active" (ngModelChange)="onAppearanceChange()">active</mat-slide-toggle>
1082
+ <mat-slide-toggle [(ngModel)]="l.disabled" (ngModelChange)="onAppearanceChange()">disabled</mat-slide-toggle>
1083
+ <mat-slide-toggle [(ngModel)]="l.disableRipple" (ngModelChange)="onAppearanceChange()">disableRipple</mat-slide-toggle>
1084
+ <mat-slide-toggle [(ngModel)]="l.fitInkBarToContent" (ngModelChange)="onAppearanceChange()">fitInkBarToContent</mat-slide-toggle>
1085
+ </div>
1086
+
1087
+ <!-- Widgets para Links -->
1088
+ <div style="border-top:1px solid var(--mat-sys-outline-variant); padding-top:8px; display:grid; gap:8px;">
1089
+ <div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
1090
+ <mat-form-field appearance="outline" style="min-width:260px;">
1091
+ <mat-label>Adicionar componente</mat-label>
1092
+ <select matNativeControl [(ngModel)]="selectedLinkWidgetId[i]">
1093
+ <option [ngValue]="''">(selecione)</option>
1094
+ <option *ngFor="let c of componentOptions" [ngValue]="c.id">{{ c.friendlyName || c.id }}</option>
1095
+ </select>
1096
+ </mat-form-field>
1097
+ <button mat-stroked-button (click)="addWidgetToLink(i)"><mat-icon [praxisIcon]="'add'"></mat-icon>Adicionar</button>
1098
+ <span style="flex:1"></span>
1099
+ <mat-form-field appearance="outline" style="width:240px;">
1100
+ <mat-label>resourcePath</mat-label>
1101
+ <input matInput [(ngModel)]="quickResourcePathLink[i]" placeholder="ex.: usuarios" />
1102
+ </mat-form-field>
1103
+ <button mat-button (click)="addPresetToLink(i, 'form')"><mat-icon [praxisIcon]="'description'"></mat-icon>Form</button>
1104
+ <button mat-button (click)="addPresetToLink(i, 'table')"><mat-icon [praxisIcon]="'grid_on'"></mat-icon>Tabela</button>
1105
+ <button mat-button (click)="addPresetToLink(i, 'crud')"><mat-icon [praxisIcon]="'dynamic_form'"></mat-icon>CRUD</button>
1106
+ </div>
1107
+
1108
+ <div *ngIf="l.widgets?.length" style="display:grid; gap:8px;" cdkDropList [cdkDropListData]="l.widgets || []" (cdkDropListDropped)="onLinkWidgetDrop(i, $event)">
1109
+ <div *ngFor="let w of l.widgets; let wi = index" cdkDrag style="border:1px dashed var(--mat-sys-outline-variant); padding:8px; border-radius:6px;">
1110
+ <div style="display:flex; align-items:center; gap:8px;">
1111
+ <button mat-icon-button cdkDragHandle matTooltip="Arrastar para reordenar" aria-label="Arrastar para reordenar">
1112
+ <mat-icon [praxisIcon]="'drag_indicator'"></mat-icon>
1113
+ </button>
1114
+ <strong style="flex:1">{{ getCompName(w.id) }}</strong>
1115
+ <button mat-icon-button color="warn" (click)="removeWidgetFromLink(i, wi)" matTooltip="Remover componente" aria-label="Remover componente"><mat-icon [praxisIcon]="'delete'"></mat-icon></button>
1116
+ </div>
1117
+ <div style="display:grid; grid-template-columns: 1fr 1fr; gap:8px;">
1118
+ <mat-form-field appearance="outline">
1119
+ <mat-label>inputs (JSON)</mat-label>
1120
+ <textarea matInput rows="4" [ngModel]="stringify(w.inputs)" (ngModelChange)="updateWidgetInputsLink(i, wi, $event)"></textarea>
1121
+ </mat-form-field>
1122
+ <mat-form-field appearance="outline">
1123
+ <mat-label>outputs (JSON)</mat-label>
1124
+ <textarea matInput rows="4" [ngModel]="stringify(w.outputs)" (ngModelChange)="updateWidgetOutputsLink(i, wi, $event)"></textarea>
1125
+ </mat-form-field>
1126
+ </div>
1127
+ </div>
1128
+ </div>
1129
+ </div>
1130
+ </div>
1131
+ </div>
1132
+ <ng-template #noLinks><em>Nenhum link definido.</em></ng-template>
1133
+ </div>
1134
+ </mat-tab>
1135
+ </mat-tab-group>
1136
+ `,
1137
+ }]
1138
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
1139
+ type: Inject,
1140
+ args: [SETTINGS_PANEL_DATA]
1141
+ }] }, { type: i1.ComponentMetadataRegistry }] });
1142
+
1143
+ class TabsQuickSetupComponent {
1144
+ model = { mode: 'group', labels: [], dynamicHeight: true, stretchTabs: true };
1145
+ initial = { ...this.model };
1146
+ newLabel = '';
1147
+ isDirty$ = new BehaviorSubject(false);
1148
+ isValid$ = new BehaviorSubject(false);
1149
+ isBusy$ = new BehaviorSubject(false);
1150
+ constructor(data) {
1151
+ // Optionally seed from existing config
1152
+ const cfg = data?.config || undefined;
1153
+ if (cfg) {
1154
+ if (cfg.tabs?.length) {
1155
+ this.model.mode = 'group';
1156
+ this.model.labels = cfg.tabs.map((t) => t.textLabel || 'Aba');
1157
+ this.model.dynamicHeight = cfg.group?.dynamicHeight ?? true;
1158
+ this.model.stretchTabs = cfg.group?.stretchTabs ?? true;
1159
+ }
1160
+ else if (cfg.nav?.links?.length) {
1161
+ this.model.mode = 'nav';
1162
+ this.model.labels = cfg.nav.links.map((l) => l.label || 'Link');
1163
+ this.model.stretchTabs = cfg.nav.stretchTabs ?? true;
1164
+ }
1165
+ this.initial = { ...this.model, labels: [...this.model.labels] };
1166
+ }
1167
+ this.updateState();
1168
+ }
1169
+ updateState() {
1170
+ const dirty = JSON.stringify(this.model) !== JSON.stringify(this.initial);
1171
+ this.isDirty$.next(dirty);
1172
+ this.isValid$.next(this.model.labels.length > 0);
1173
+ }
1174
+ setMode(mode) {
1175
+ this.model.mode = mode;
1176
+ this.updateState();
1177
+ }
1178
+ addLabel() {
1179
+ const l = (this.newLabel || '').trim();
1180
+ if (!l)
1181
+ return;
1182
+ this.model.labels.push(l);
1183
+ this.newLabel = '';
1184
+ this.updateState();
1185
+ }
1186
+ removeLabel(i) {
1187
+ this.model.labels.splice(i, 1);
1188
+ this.updateState();
1189
+ }
1190
+ buildConfig() {
1191
+ if (this.model.mode === 'group') {
1192
+ return {
1193
+ group: {
1194
+ dynamicHeight: !!this.model.dynamicHeight,
1195
+ stretchTabs: !!this.model.stretchTabs,
1196
+ selectedIndex: 0,
1197
+ },
1198
+ tabs: this.model.labels.map((label, idx) => ({ id: `tab${idx + 1}`, textLabel: label })),
1199
+ };
1200
+ }
1201
+ return {
1202
+ nav: {
1203
+ selectedIndex: 0,
1204
+ stretchTabs: !!this.model.stretchTabs,
1205
+ links: this.model.labels.map((label, idx) => ({ id: `link${idx + 1}`, label })),
1206
+ },
1207
+ };
1208
+ }
1209
+ getSettingsValue() {
1210
+ return { config: this.buildConfig() };
1211
+ }
1212
+ onSave() {
1213
+ return { config: this.buildConfig() };
1214
+ }
1215
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: TabsQuickSetupComponent, deps: [{ token: SETTINGS_PANEL_DATA }], target: i0.ɵɵFactoryTarget.Component });
1216
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.4", type: TabsQuickSetupComponent, isStandalone: true, selector: "praxis-tabs-quick-setup", ngImport: i0, template: `
1217
+ <div style="display:grid; gap: 12px; padding: 8px;">
1218
+ <div style="display:flex; gap: 12px; align-items:center; flex-wrap:wrap;">
1219
+ <button mat-stroked-button [color]="model.mode==='group' ? 'primary' : undefined" (click)="setMode('group')">
1220
+ <mat-icon [praxisIcon]="'tab'"></mat-icon>
1221
+ Abas (TabGroup)
1222
+ </button>
1223
+ <button mat-stroked-button [color]="model.mode==='nav' ? 'primary' : undefined" (click)="setMode('nav')">
1224
+ <mat-icon [praxisIcon]="'segment'"></mat-icon>
1225
+ Navegação (TabNav)
1226
+ </button>
1227
+ </div>
1228
+
1229
+ <div style="display:grid; grid-template-columns: 1fr auto; gap: 8px; align-items:start;">
1230
+ <mat-form-field appearance="outline">
1231
+ <mat-label>Adicionar label</mat-label>
1232
+ <input matInput [(ngModel)]="newLabel" (keyup.enter)="addLabel()" placeholder="Ex.: Dados Gerais" />
1233
+ </mat-form-field>
1234
+ <button mat-flat-button color="primary" (click)="addLabel()">
1235
+ <mat-icon [praxisIcon]="'add'"></mat-icon>
1236
+ Adicionar
1237
+ </button>
1238
+ </div>
1239
+
1240
+ <div *ngIf="model.labels.length; else emptyLabels" style="display:flex; flex-wrap:wrap; gap: 8px;">
1241
+ <div *ngFor="let l of model.labels; let i = index" class="chip">
1242
+ <span>{{ l }}</span>
1243
+ <button mat-icon-button (click)="removeLabel(i)"><mat-icon [praxisIcon]="'close'"></mat-icon></button>
1244
+ </div>
1245
+ </div>
1246
+ <ng-template #emptyLabels>
1247
+ <em>Adicione 1 ou mais labels para criar as abas.</em>
1248
+ </ng-template>
1249
+
1250
+ <div style="display:flex; gap: 12px; flex-wrap:wrap;">
1251
+ <mat-slide-toggle [(ngModel)]="model.dynamicHeight">dynamicHeight</mat-slide-toggle>
1252
+ <mat-slide-toggle [(ngModel)]="model.stretchTabs">stretchTabs</mat-slide-toggle>
1253
+ </div>
1254
+ </div>
1255
+ `, isInline: true, styles: [".chip{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border:1px solid var(--mat-sys-outline-variant);border-radius:16px}\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: i3.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: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.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: i7.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: i5$1.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: i5$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i9.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"] }] });
1256
+ }
1257
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: TabsQuickSetupComponent, decorators: [{
1258
+ type: Component,
1259
+ args: [{ selector: 'praxis-tabs-quick-setup', standalone: true, imports: [
1260
+ CommonModule,
1261
+ FormsModule,
1262
+ MatFormFieldModule,
1263
+ MatInputModule,
1264
+ MatIconModule,
1265
+ PraxisIconDirective,
1266
+ MatButtonModule,
1267
+ MatSlideToggleModule,
1268
+ ], template: `
1269
+ <div style="display:grid; gap: 12px; padding: 8px;">
1270
+ <div style="display:flex; gap: 12px; align-items:center; flex-wrap:wrap;">
1271
+ <button mat-stroked-button [color]="model.mode==='group' ? 'primary' : undefined" (click)="setMode('group')">
1272
+ <mat-icon [praxisIcon]="'tab'"></mat-icon>
1273
+ Abas (TabGroup)
1274
+ </button>
1275
+ <button mat-stroked-button [color]="model.mode==='nav' ? 'primary' : undefined" (click)="setMode('nav')">
1276
+ <mat-icon [praxisIcon]="'segment'"></mat-icon>
1277
+ Navegação (TabNav)
1278
+ </button>
1279
+ </div>
1280
+
1281
+ <div style="display:grid; grid-template-columns: 1fr auto; gap: 8px; align-items:start;">
1282
+ <mat-form-field appearance="outline">
1283
+ <mat-label>Adicionar label</mat-label>
1284
+ <input matInput [(ngModel)]="newLabel" (keyup.enter)="addLabel()" placeholder="Ex.: Dados Gerais" />
1285
+ </mat-form-field>
1286
+ <button mat-flat-button color="primary" (click)="addLabel()">
1287
+ <mat-icon [praxisIcon]="'add'"></mat-icon>
1288
+ Adicionar
1289
+ </button>
1290
+ </div>
1291
+
1292
+ <div *ngIf="model.labels.length; else emptyLabels" style="display:flex; flex-wrap:wrap; gap: 8px;">
1293
+ <div *ngFor="let l of model.labels; let i = index" class="chip">
1294
+ <span>{{ l }}</span>
1295
+ <button mat-icon-button (click)="removeLabel(i)"><mat-icon [praxisIcon]="'close'"></mat-icon></button>
1296
+ </div>
1297
+ </div>
1298
+ <ng-template #emptyLabels>
1299
+ <em>Adicione 1 ou mais labels para criar as abas.</em>
1300
+ </ng-template>
1301
+
1302
+ <div style="display:flex; gap: 12px; flex-wrap:wrap;">
1303
+ <mat-slide-toggle [(ngModel)]="model.dynamicHeight">dynamicHeight</mat-slide-toggle>
1304
+ <mat-slide-toggle [(ngModel)]="model.stretchTabs">stretchTabs</mat-slide-toggle>
1305
+ </div>
1306
+ </div>
1307
+ `, styles: [".chip{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border:1px solid var(--mat-sys-outline-variant);border-radius:16px}\n"] }]
1308
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
1309
+ type: Inject,
1310
+ args: [SETTINGS_PANEL_DATA]
1311
+ }] }] });
1312
+
1313
+ class PraxisTabs {
1314
+ settings = inject(SettingsPanelService);
1315
+ storage = inject(CONFIG_STORAGE);
1316
+ snack = inject(MatSnackBar);
1317
+ config = null;
1318
+ tabsId;
1319
+ editModeEnabled = false;
1320
+ form = null;
1321
+ context = null;
1322
+ animationDone = new EventEmitter();
1323
+ focusChange = new EventEmitter();
1324
+ selectedIndexChange = new EventEmitter();
1325
+ selectedTabChange = new EventEmitter();
1326
+ indexFocused = new EventEmitter();
1327
+ selectFocusedIndex = new EventEmitter();
1328
+ widgetEvent = new EventEmitter();
1329
+ // Signals to manage local state for selection in Nav mode and Group mode
1330
+ currentNavIndex = signal(0, ...(ngDevMode ? [{ debugName: "currentNavIndex" }] : []));
1331
+ selectedIndexSignal = signal(0, ...(ngDevMode ? [{ debugName: "selectedIndexSignal" }] : []));
1332
+ groupLoaded = new Set();
1333
+ navLoaded = new Set();
1334
+ ngOnInit() {
1335
+ // Load stored config if tabsId provided
1336
+ if (this.tabsId) {
1337
+ const stored = this.storage.loadConfig(this.storageKey());
1338
+ if (stored) {
1339
+ this.config = stored;
1340
+ }
1341
+ }
1342
+ // Initialize selected indices from config
1343
+ this.selectedIndexSignal.set(this.config?.group?.selectedIndex ?? 0);
1344
+ this.currentNavIndex.set(this.config?.nav?.selectedIndex ?? 0);
1345
+ // Mark initial selections as loaded for lazy mode
1346
+ this.groupLoaded.add(this.selectedIndexSignal() ?? 0);
1347
+ this.navLoaded.add(this.currentNavIndex() ?? 0);
1348
+ }
1349
+ ngOnChanges(changes) {
1350
+ if (changes['config'] && this.config) {
1351
+ // Reset loaded caches on config change and seed with current selections
1352
+ this.groupLoaded.clear();
1353
+ this.navLoaded.clear();
1354
+ // Keep indices in sync when config input changes
1355
+ this.selectedIndexSignal.set(this.config?.group?.selectedIndex ?? 0);
1356
+ this.currentNavIndex.set(this.config?.nav?.selectedIndex ?? 0);
1357
+ this.groupLoaded.add(this.selectedIndexSignal() ?? 0);
1358
+ this.navLoaded.add(this.currentNavIndex() ?? 0);
1359
+ // Persist when tabsId provided
1360
+ if (this.tabsId) {
1361
+ this.storage.saveConfig(this.storageKey(), this.config);
1362
+ }
1363
+ }
1364
+ }
1365
+ isNavMode() {
1366
+ return !!this.config?.nav && !!this.config?.nav?.links?.length;
1367
+ }
1368
+ effectiveAnimationDuration() {
1369
+ const reduce = this.config?.accessibility?.reduceMotion;
1370
+ if (reduce)
1371
+ return '0ms';
1372
+ return (this.config?.group?.animationDuration || this.config?.nav?.animationDuration || '500ms');
1373
+ }
1374
+ getNavActive(i) {
1375
+ return this.currentNavIndex() === i;
1376
+ }
1377
+ onNavClick(i) {
1378
+ if (!this.config?.nav)
1379
+ return;
1380
+ this.currentNavIndex.set(i);
1381
+ // Lazy: mark as loaded
1382
+ this.navLoaded.add(i);
1383
+ // Emit as index change for consumers to track
1384
+ this.selectedIndexChange.emit(i);
1385
+ }
1386
+ onNavDrop(event) {
1387
+ if (!this.config?.nav?.links)
1388
+ return;
1389
+ const prev = event.previousIndex;
1390
+ const curr = event.currentIndex;
1391
+ if (prev === curr)
1392
+ return;
1393
+ const selectedId = this.config.nav.links[this.currentNavIndex()]?.id;
1394
+ this.config = produce(this.config, (draft) => {
1395
+ const arr = draft.nav.links;
1396
+ const [moved] = arr.splice(prev, 1);
1397
+ arr.splice(curr, 0, moved);
1398
+ });
1399
+ // Update selected index based on id
1400
+ if (selectedId) {
1401
+ const idx = this.config.nav.links.findIndex((l) => l.id === selectedId);
1402
+ if (idx >= 0)
1403
+ this.currentNavIndex.set(idx);
1404
+ }
1405
+ else {
1406
+ // If no id, try to preserve selection around movement
1407
+ if (this.currentNavIndex() === prev)
1408
+ this.currentNavIndex.set(curr);
1409
+ }
1410
+ // Reset lazy cache for nav except current
1411
+ const currIdx = this.currentNavIndex();
1412
+ this.navLoaded.clear();
1413
+ this.navLoaded.add(currIdx);
1414
+ }
1415
+ onSelectedIndexChange(index) {
1416
+ this.selectedIndexSignal.set(index);
1417
+ // Update config immutably
1418
+ if (this.config) {
1419
+ this.config = produce(this.config, (draft) => {
1420
+ if (!draft.group) {
1421
+ draft.group = { selectedIndex: index };
1422
+ }
1423
+ else {
1424
+ draft.group.selectedIndex = index;
1425
+ }
1426
+ });
1427
+ if (this.tabsId) {
1428
+ this.storage.saveConfig(this.storageKey(), this.config);
1429
+ }
1430
+ }
1431
+ // Lazy: mark as loaded
1432
+ this.groupLoaded.add(index);
1433
+ this.selectedIndexChange.emit(index);
1434
+ }
1435
+ closeTab(index) {
1436
+ if (!this.config?.tabs)
1437
+ return;
1438
+ this.config = produce(this.config, (draft) => {
1439
+ draft.tabs.splice(index, 1);
1440
+ });
1441
+ if (this.tabsId)
1442
+ this.storage.saveConfig(this.storageKey(), this.config);
1443
+ }
1444
+ moveTab(index, delta) {
1445
+ if (!this.config?.tabs)
1446
+ return;
1447
+ const to = index + delta;
1448
+ if (to < 0 || to >= this.config.tabs.length)
1449
+ return;
1450
+ const wasSelected = this.selectedIndexSignal() === index;
1451
+ this.config = produce(this.config, (draft) => {
1452
+ const arr = draft.tabs;
1453
+ const [m] = arr.splice(index, 1);
1454
+ arr.splice(to, 0, m);
1455
+ if (!draft.group)
1456
+ draft.group = { selectedIndex: to };
1457
+ else if (wasSelected)
1458
+ draft.group.selectedIndex = to;
1459
+ else if (draft.group.selectedIndex != null) {
1460
+ // Adjust selection if it lies between moved indices
1461
+ const sel = draft.group.selectedIndex;
1462
+ if (index < sel && to >= sel)
1463
+ draft.group.selectedIndex = sel - 1;
1464
+ else if (index > sel && to <= sel)
1465
+ draft.group.selectedIndex = sel + 1;
1466
+ }
1467
+ });
1468
+ this.selectedIndexSignal.set(this.config.group?.selectedIndex ?? this.selectedIndexSignal());
1469
+ // Reset lazy cache for group except current
1470
+ const sel = this.selectedIndexSignal() ?? 0;
1471
+ this.groupLoaded.clear();
1472
+ this.groupLoaded.add(sel);
1473
+ }
1474
+ openEditor() {
1475
+ const ref = this.settings.open({
1476
+ id: `praxis-tabs-editor:${this.tabsId || 'default'}`,
1477
+ title: 'Configurar Tabs',
1478
+ content: {
1479
+ component: PraxisTabsConfigEditor,
1480
+ inputs: { config: this.config, tabsId: this.tabsId },
1481
+ },
1482
+ });
1483
+ // Essential preview: apply updates without closing
1484
+ ref.applied$.subscribe((value) => {
1485
+ const nextCfg = value?.config || value;
1486
+ if (nextCfg) {
1487
+ this.config = produce(this.config || {}, () => nextCfg);
1488
+ if (this.tabsId)
1489
+ this.storage.saveConfig(this.storageKey(), this.config);
1490
+ this.selectedIndexSignal.set(this.config?.group?.selectedIndex ?? 0);
1491
+ this.currentNavIndex.set(this.config?.nav?.selectedIndex ?? 0);
1492
+ }
1493
+ });
1494
+ ref.saved$.subscribe((value) => {
1495
+ const nextCfg = value?.config || value;
1496
+ if (nextCfg) {
1497
+ this.config = produce(this.config || {}, () => nextCfg);
1498
+ if (this.tabsId)
1499
+ this.storage.saveConfig(this.storageKey(), this.config);
1500
+ // Sync indices
1501
+ this.selectedIndexSignal.set(this.config?.group?.selectedIndex ?? 0);
1502
+ this.currentNavIndex.set(this.config?.nav?.selectedIndex ?? 0);
1503
+ }
1504
+ });
1505
+ }
1506
+ addEmptyTab() {
1507
+ const next = produce(this.config || {}, (draft) => {
1508
+ if (!draft.group)
1509
+ draft.group = { selectedIndex: 0 };
1510
+ if (!draft.tabs)
1511
+ draft.tabs = [];
1512
+ draft.tabs.push({ id: `tab${(draft.tabs.length || 0) + 1}`, textLabel: 'Nova aba' });
1513
+ draft.group.selectedIndex = (draft.tabs.length || 1) - 1;
1514
+ });
1515
+ this.config = next;
1516
+ if (this.tabsId)
1517
+ this.storage.saveConfig(this.storageKey(), this.config);
1518
+ this.selectedIndexSignal.set(this.config?.group?.selectedIndex ?? 0);
1519
+ }
1520
+ /** Clears persisted tabs configuration to fallback to defaults */
1521
+ resetPreferences() {
1522
+ if (!this.tabsId)
1523
+ return;
1524
+ try {
1525
+ this.storage.clearConfig(this.storageKey());
1526
+ }
1527
+ catch { }
1528
+ try {
1529
+ this.snack.open('Preferências de Tabs redefinidas', undefined, { duration: 2000 });
1530
+ }
1531
+ catch { }
1532
+ // keep current in-memory config; caller may choose to reload
1533
+ }
1534
+ openQuickSetup() {
1535
+ const ref = this.settings.open({
1536
+ id: `praxis-tabs-quick-setup:${this.tabsId || 'default'}`,
1537
+ title: 'Criar abas rapidamente',
1538
+ content: {
1539
+ component: TabsQuickSetupComponent,
1540
+ inputs: { config: this.config },
1541
+ },
1542
+ });
1543
+ const apply = (value) => {
1544
+ const nextCfg = value?.config || value;
1545
+ if (nextCfg) {
1546
+ this.config = produce(this.config || {}, () => nextCfg);
1547
+ if (this.tabsId)
1548
+ this.storage.saveConfig(this.storageKey(), this.config);
1549
+ this.selectedIndexSignal.set(this.config?.group?.selectedIndex ?? 0);
1550
+ this.currentNavIndex.set(this.config?.nav?.selectedIndex ?? 0);
1551
+ }
1552
+ };
1553
+ ref.applied$.subscribe(apply);
1554
+ ref.saved$.subscribe(apply);
1555
+ }
1556
+ storageKey() {
1557
+ return `tabs:${this.tabsId}`;
1558
+ }
1559
+ // =====================
1560
+ // Lazy load helpers
1561
+ // =====================
1562
+ isLazy() {
1563
+ return !!this.config?.behavior?.lazyLoad;
1564
+ }
1565
+ groupContentReady(index) {
1566
+ return !this.isLazy() || this.groupLoaded.has(index) || this.selectedIndexSignal() === index;
1567
+ }
1568
+ navContentReady(index) {
1569
+ return !this.isLazy() || this.navLoaded.has(index) || this.currentNavIndex() === index;
1570
+ }
1571
+ isEmptyGlobal() {
1572
+ const hasTabs = !!(this.config?.tabs && this.config.tabs.length > 0);
1573
+ const hasLinks = !!(this.config?.nav?.links && this.config.nav.links.length > 0);
1574
+ return !(hasTabs || hasLinks);
1575
+ }
1576
+ emitWidgetEvent(loc, ev) {
1577
+ this.widgetEvent.emit({ ...loc, ...ev });
1578
+ }
1579
+ styleCss() {
1580
+ const t = this.config?.appearance?.tokens;
1581
+ if (!t)
1582
+ return null;
1583
+ const scope = `.praxis-tabs-root[data-tabs-id="${(this.tabsId || 'default').replace(/"/g, '')}"]`;
1584
+ const rules = [];
1585
+ const push = (selector, decls) => {
1586
+ rules.push(`${scope} ${selector}{${decls.join('')}}`);
1587
+ };
1588
+ // Active indicator underline
1589
+ if (t['active-indicator-color']) {
1590
+ push(' .mat-mdc-tab .mdc-tab-indicator__content--underline', [`background-color:${t['active-indicator-color']}!important;`]);
1591
+ }
1592
+ // Active label
1593
+ if (t['active-label-text-color']) {
1594
+ push(' .mdc-tab--active .mdc-tab__text-label', [
1595
+ `color:${t['active-label-text-color']}!important;`,
1596
+ ]);
1597
+ }
1598
+ if (t['active-hover-label-text-color']) {
1599
+ push(' .mdc-tab--active:hover .mdc-tab__text-label', [
1600
+ `color:${t['active-hover-label-text-color']}!important;`,
1601
+ ]);
1602
+ }
1603
+ if (t['active-focus-label-text-color']) {
1604
+ push(' .mdc-tab--active:focus .mdc-tab__text-label', [
1605
+ `color:${t['active-focus-label-text-color']}!important;`,
1606
+ ]);
1607
+ }
1608
+ if (t['active-focus-indicator-color']) {
1609
+ push(' .mdc-tab--active:focus .mdc-tab-indicator__content--underline', [
1610
+ `background-color:${t['active-focus-indicator-color']}!important;`,
1611
+ ]);
1612
+ }
1613
+ if (t['active-hover-indicator-color']) {
1614
+ push(' .mdc-tab--active:hover .mdc-tab-indicator__content--underline', [
1615
+ `background-color:${t['active-hover-indicator-color']}!important;`,
1616
+ ]);
1617
+ }
1618
+ // Inactive label
1619
+ if (t['inactive-label-text-color']) {
1620
+ push(' .mdc-tab:not(.mdc-tab--active) .mdc-tab__text-label', [
1621
+ `color:${t['inactive-label-text-color']}!important;`,
1622
+ ]);
1623
+ }
1624
+ if (t['inactive-hover-label-text-color']) {
1625
+ push(' .mdc-tab:not(.mdc-tab--active):hover .mdc-tab__text-label', [
1626
+ `color:${t['inactive-hover-label-text-color']}!important;`,
1627
+ ]);
1628
+ }
1629
+ if (t['inactive-focus-label-text-color']) {
1630
+ push(' .mdc-tab:not(.mdc-tab--active):focus .mdc-tab__text-label', [
1631
+ `color:${t['inactive-focus-label-text-color']}!important;`,
1632
+ ]);
1633
+ }
1634
+ // Pagination chevrons
1635
+ if (t['pagination-icon-color']) {
1636
+ push(' .mat-mdc-tab-header-pagination-chevron', [
1637
+ `color:${t['pagination-icon-color']}!important;`,
1638
+ ]);
1639
+ }
1640
+ // Divider / background
1641
+ const containerDecls = [];
1642
+ if (t['divider-color']) {
1643
+ containerDecls.push(`border-bottom-color:${t['divider-color']}!important;`);
1644
+ }
1645
+ if (containerDecls.length) {
1646
+ push(' .mat-mdc-tab-header', containerDecls);
1647
+ }
1648
+ if (t['background-color']) {
1649
+ push(' .mat-mdc-tab-header', [`background:${t['background-color']}!important;`]);
1650
+ }
1651
+ return rules.length ? rules.join('\n') : null;
1652
+ }
1653
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PraxisTabs, deps: [], target: i0.ɵɵFactoryTarget.Component });
1654
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.4", type: PraxisTabs, isStandalone: true, selector: "praxis-tabs", inputs: { config: "config", tabsId: "tabsId", editModeEnabled: "editModeEnabled", form: "form", context: "context" }, outputs: { animationDone: "animationDone", focusChange: "focusChange", selectedIndexChange: "selectedIndexChange", selectedTabChange: "selectedTabChange", indexFocused: "indexFocused", selectFocusedIndex: "selectFocusedIndex", widgetEvent: "widgetEvent" }, usesOnChanges: true, ngImport: i0, template: `
1655
+ <div
1656
+ class="praxis-tabs-root"
1657
+ [class.density-compact]="config?.appearance?.density === 'compact'"
1658
+ [class.density-comfortable]="config?.appearance?.density === 'comfortable'"
1659
+ [class.density-spacious]="config?.appearance?.density === 'spacious'"
1660
+ [class.high-contrast]="config?.accessibility?.highContrast"
1661
+ [class.reduce-motion]="config?.accessibility?.reduceMotion"
1662
+ [ngClass]="config?.appearance?.themeClass || ''"
1663
+ [attr.data-tabs-id]="tabsId || 'default'"
1664
+ >
1665
+ <!-- Inline custom CSS, if provided -->
1666
+ <style *ngIf="config?.appearance?.customCss" [innerHTML]="config?.appearance?.customCss"></style>
1667
+ <!-- Runtime style derived from tokens -->
1668
+ <style *ngIf="styleCss() as s" [innerHTML]="s"></style>
1669
+
1670
+ <!-- Empty state (global) -->
1671
+ <ng-container *ngIf="isEmptyGlobal(); else notEmpty">
1672
+ <praxis-empty-state-card
1673
+ icon="tab"
1674
+ [title]="'Nenhuma aba configurada'"
1675
+ [description]="'Crie rapidamente suas abas ou abra o editor completo.'"
1676
+ [primaryAction]="{ label: 'Criar abas rapidamente', icon: 'add', action: openQuickSetup.bind(this) }"
1677
+ [secondaryActions]="[
1678
+ { label: 'Adicionar aba vazia', icon: 'tab', action: addEmptyTab.bind(this) },
1679
+ { label: 'Abrir editor completo', icon: 'tune', action: openEditor.bind(this) }
1680
+ ]"
1681
+ ></praxis-empty-state-card>
1682
+ </ng-container>
1683
+
1684
+ <ng-template #notEmpty>
1685
+ <!-- Tab Nav variant -->
1686
+ <ng-container *ngIf="isNavMode(); else groupMode">
1687
+ <nav
1688
+ mat-tab-nav-bar
1689
+ cdkDropList
1690
+ cdkDropListOrientation="horizontal"
1691
+ [cdkDropListDisabled]="!config?.behavior?.reorderable"
1692
+ (cdkDropListDropped)="onNavDrop($event)"
1693
+ [disablePagination]="config?.nav?.disablePagination"
1694
+ [fitInkBarToContent]="config?.nav?.fitInkBarToContent"
1695
+ [mat-stretch-tabs]="config?.nav?.stretchTabs"
1696
+ [color]="config?.nav?.color"
1697
+ [backgroundColor]="config?.nav?.backgroundColor"
1698
+ [selectedIndex]="currentNavIndex()"
1699
+ [attr.aria-label]="config?.group?.ariaLabel || null"
1700
+ [attr.aria-labelledby]="config?.group?.ariaLabelledby || null"
1701
+ [animationDuration]="effectiveAnimationDuration()"
1702
+ (indexFocused)="indexFocused.emit($event)"
1703
+ (selectFocusedIndex)="selectFocusedIndex.emit($event)"
1704
+ >
1705
+ <a
1706
+ mat-tab-link
1707
+ *ngFor="let link of config?.nav?.links; let i = index"
1708
+ cdkDrag
1709
+ [cdkDragDisabled]="!config?.behavior?.reorderable"
1710
+ cdkDragLockAxis="x"
1711
+ [active]="getNavActive(i)"
1712
+ [disabled]="link.disabled"
1713
+ [disableRipple]="config?.nav?.disableRipple || link.disableRipple"
1714
+ [fitInkBarToContent]="link.fitInkBarToContent || false"
1715
+ [id]="link.id || ''"
1716
+ (click)="onNavClick(i)"
1717
+ >
1718
+ <span *ngIf="config?.behavior?.reorderable" class="drag-handle" cdkDragHandle>
1719
+ <mat-icon fontIcon="drag_indicator"></mat-icon>
1720
+ </span>
1721
+ {{ link.label }}
1722
+ </a>
1723
+ </nav>
1724
+
1725
+ <div class="praxis-tabnav-content" *ngIf="currentNavIndex() >= 0">
1726
+ <ng-container
1727
+ *ngIf="config?.nav?.links?.[currentNavIndex()] as l"
1728
+ >
1729
+ <ng-container *ngIf="(l.content?.length || l.widgets?.length) && navContentReady(currentNavIndex()); else emptyNav">
1730
+ <ng-container *ngIf="l.content && form">
1731
+ <ng-container
1732
+ dynamicFieldLoader
1733
+ [fields]="l.content || []"
1734
+ [formGroup]="form!"
1735
+ ></ng-container>
1736
+ </ng-container>
1737
+ <ng-container *ngIf="l.widgets?.length">
1738
+ <ng-container *ngFor="let w of l.widgets; let wi = index">
1739
+ <ng-container
1740
+ [dynamicWidgetLoader]="w"
1741
+ [context]="context || {}"
1742
+ (widgetEvent)="emitWidgetEvent({ linkId: l.id, linkIndex: currentNavIndex() }, $event)"
1743
+ ></ng-container>
1744
+ </ng-container>
1745
+ </ng-container>
1746
+ </ng-container>
1747
+ <ng-template #emptyNav>
1748
+ <praxis-empty-state-card
1749
+ [inline]="true"
1750
+ icon="add_to_queue"
1751
+ [title]="'Sem conteúdo neste link'"
1752
+ [description]="'Adicione conteúdo ou use o editor para configurar.'"
1753
+ [primaryAction]="{ label: 'Abrir editor', icon: 'tune', action: openEditor.bind(this) }"
1754
+ ></praxis-empty-state-card>
1755
+ </ng-template>
1756
+ </ng-container>
1757
+ </div>
1758
+ </ng-container>
1759
+
1760
+ <!-- Tab Group variant -->
1761
+ <ng-template #groupMode>
1762
+ <mat-tab-group
1763
+ [dynamicHeight]="config?.group?.dynamicHeight"
1764
+ [disableRipple]="config?.group?.disableRipple"
1765
+ [disablePagination]="config?.group?.disablePagination"
1766
+ [fitInkBarToContent]="config?.group?.fitInkBarToContent"
1767
+ [headerPosition]="config?.group?.headerPosition ?? 'above'"
1768
+ [preserveContent]="config?.group?.preserveContent"
1769
+ [selectedIndex]="selectedIndexSignal()"
1770
+ [mat-stretch-tabs]="config?.group?.stretchTabs"
1771
+ [mat-align-tabs]="config?.group?.alignTabs || 'start'"
1772
+ [color]="config?.group?.color"
1773
+ [backgroundColor]="config?.group?.backgroundColor"
1774
+ [animationDuration]="effectiveAnimationDuration()"
1775
+ [attr.aria-label]="config?.group?.ariaLabel || null"
1776
+ [attr.aria-labelledby]="config?.group?.ariaLabelledby || null"
1777
+ (animationDone)="animationDone.emit()"
1778
+ (focusChange)="focusChange.emit($event)"
1779
+ (selectedIndexChange)="onSelectedIndexChange($event)"
1780
+ (selectedTabChange)="selectedTabChange.emit($event)"
1781
+ class="praxis-tabs-group"
1782
+ >
1783
+ <mat-tab
1784
+ *ngFor="let t of config?.tabs; let i = index"
1785
+ [disabled]="t.disabled"
1786
+ [labelClass]="t.labelClass ?? ''"
1787
+ [bodyClass]="t.bodyClass ?? ''"
1788
+ [id]="t.id || ''"
1789
+ [attr.aria-label]="t.ariaLabel || null"
1790
+ [attr.aria-labelledby]="t.ariaLabelledby || null"
1791
+ >
1792
+ <ng-template mat-tab-label>
1793
+ <span>{{ t.textLabel }}</span>
1794
+ <button
1795
+ *ngIf="config?.behavior?.closeable"
1796
+ mat-icon-button
1797
+ type="button"
1798
+ (click)="closeTab(i); $event.stopPropagation()"
1799
+ aria-label="Fechar aba"
1800
+ >
1801
+ <mat-icon fontIcon="close"></mat-icon>
1802
+ </button>
1803
+ <ng-container *ngIf="config?.behavior?.reorderable">
1804
+ <button mat-icon-button type="button" (click)="moveTab(i, -1); $event.stopPropagation()" [disabled]="i===0" aria-label="Mover aba para esquerda">
1805
+ <mat-icon fontIcon="arrow_back"></mat-icon>
1806
+ </button>
1807
+ <button mat-icon-button type="button" (click)="moveTab(i, 1); $event.stopPropagation()" [disabled]="i===(config?.tabs?.length||1)-1" aria-label="Mover aba para direita">
1808
+ <mat-icon fontIcon="arrow_forward"></mat-icon>
1809
+ </button>
1810
+ </ng-container>
1811
+ </ng-template>
1812
+
1813
+ <ng-template matTabContent>
1814
+ <ng-container *ngIf="(t.content?.length || t.widgets?.length) && groupContentReady(i); else emptyTab">
1815
+ <ng-container *ngIf="t.content && form">
1816
+ <ng-container
1817
+ dynamicFieldLoader
1818
+ [fields]="t.content || []"
1819
+ [formGroup]="form!"
1820
+ ></ng-container>
1821
+ </ng-container>
1822
+ <ng-container *ngIf="t.widgets?.length">
1823
+ <ng-container *ngFor="let w of t.widgets; let wi = index">
1824
+ <ng-container
1825
+ [dynamicWidgetLoader]="w"
1826
+ [context]="context || {}"
1827
+ (widgetEvent)="emitWidgetEvent({ tabId: t.id, tabIndex: i }, $event)"
1828
+ ></ng-container>
1829
+ </ng-container>
1830
+ </ng-container>
1831
+ </ng-container>
1832
+ <ng-template #emptyTab>
1833
+ <praxis-empty-state-card
1834
+ [inline]="true"
1835
+ icon="dashboard_customize"
1836
+ [title]="'Sem conteúdo nesta aba'"
1837
+ [description]="'Adicione conteúdo ou use o editor para configurar.'"
1838
+ [primaryAction]="{ label: 'Abrir editor', icon: 'tune', action: openEditor.bind(this) }"
1839
+ ></praxis-empty-state-card>
1840
+ </ng-template>
1841
+ </ng-template>
1842
+ </mat-tab>
1843
+ </mat-tab-group>
1844
+ </ng-template>
1845
+ </ng-template>
1846
+
1847
+ <!-- Edit button -->
1848
+ <button
1849
+ *ngIf="editModeEnabled"
1850
+ mat-fab
1851
+ class="edit-fab"
1852
+ aria-label="Editar tabs"
1853
+ (click)="openEditor()"
1854
+ >
1855
+ <mat-icon fontIcon="edit"></mat-icon>
1856
+ </button>
1857
+ <button
1858
+ *ngIf="editModeEnabled && tabsId"
1859
+ mat-mini-fab
1860
+ class="edit-fab"
1861
+ style="right: 56px"
1862
+ aria-label="Redefinir preferências de tabs"
1863
+ (click)="resetPreferences()"
1864
+ matTooltip="Redefinir preferências de tabs"
1865
+ >
1866
+ <mat-icon [praxisIcon]="'restart_alt'"></mat-icon>
1867
+ </button>
1868
+ </div>
1869
+ `, isInline: true, styles: [".praxis-tabs-root{position:relative;display:block}.praxis-tabs-group.align-start .mat-mdc-tab-header{justify-content:flex-start}.praxis-tabs-group.align-center .mat-mdc-tab-header{justify-content:center}.praxis-tabs-group.align-end .mat-mdc-tab-header{justify-content:flex-end}.density-compact .mat-mdc-tab-body-content{padding:8px}.density-comfortable .mat-mdc-tab-body-content{padding:16px}.density-spacious .mat-mdc-tab-body-content{padding:24px}.edit-fab{position:absolute;right:12px;bottom:12px;z-index:2}.tab-empty{padding:16px;color:#0000008a;font-style:italic}.high-contrast{filter:contrast(1.2)}.reduce-motion{--mat-animation-duration: 0ms}.drag-handle{display:inline-flex;align-items:center;vertical-align:middle;margin-right:4px;cursor:grab}:host-context(.pdx-gridster-item) .praxis-tabs-root{display:flex;flex-direction:column;height:100%;min-height:0}:host-context(.pdx-gridster-item) .praxis-tabs-group,:host-context(.pdx-gridster-item) .mat-mdc-tab-group{flex:1 1 auto;min-height:0}:host-context(.pdx-gridster-item) .mat-mdc-tab-body-wrapper,:host-context(.pdx-gridster-item) .mat-mdc-tab-body-content{height:100%;min-height:0}:host-context(.pdx-gridster-item) .mat-mdc-tab-body-content{overflow:auto}:host-context(.pdx-gridster-item) .praxis-tabnav-content{flex:1 1 auto;min-height:0;overflow:auto}\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: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i3.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "directive", type: i3$1.MatTabContent, selector: "[matTabContent]" }, { kind: "directive", type: i3$1.MatTabLabel, selector: "[mat-tab-label], [matTabLabel]" }, { kind: "component", type: i3$1.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i3$1.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"] }, { kind: "component", type: i3$1.MatTabNav, selector: "[mat-tab-nav-bar]", inputs: ["fitInkBarToContent", "mat-stretch-tabs", "animationDuration", "backgroundColor", "disableRipple", "color", "tabPanel"], exportAs: ["matTabNavBar", "matTabNav"] }, { kind: "component", type: i3$1.MatTabLink, selector: "[mat-tab-link], [matTabLink]", inputs: ["active", "disabled", "disableRipple", "tabIndex", "id"], exportAs: ["matTabLink"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i7.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: i5$1.MatMiniFabButton, selector: "button[mat-mini-fab], a[mat-mini-fab], button[matMiniFab], a[matMiniFab]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i5$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i5$1.MatFabButton, selector: "button[mat-fab], a[mat-fab], button[matFab], a[matFab]", inputs: ["extended"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i10.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: i10.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: i10.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "component", type: EmptyStateCardComponent, selector: "praxis-empty-state-card", inputs: ["icon", "title", "description", "primaryAction", "secondaryActions", "inline"] }, { kind: "directive", type: DynamicFieldLoaderDirective, selector: "[dynamicFieldLoader]", inputs: ["fields", "formGroup", "enableExternalControlBinding", "itemTemplate", "readonlyMode", "disabledMode", "presentationMode", "visible", "canvasMode"], outputs: ["componentsCreated", "fieldCreated", "fieldDestroyed", "canvasMouseEnter", "canvasMouseLeave", "canvasClick"] }, { kind: "directive", type: DynamicWidgetLoaderDirective, selector: "[dynamicWidgetLoader]", inputs: ["dynamicWidgetLoader", "context", "strictValidation", "autoWireOutputs"], outputs: ["widgetEvent"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1870
+ }
1871
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PraxisTabs, decorators: [{
1872
+ type: Component,
1873
+ args: [{ selector: 'praxis-tabs', standalone: true, imports: [
1874
+ CommonModule,
1875
+ ReactiveFormsModule,
1876
+ MatTabsModule,
1877
+ MatIconModule,
1878
+ PraxisIconDirective,
1879
+ MatButtonModule,
1880
+ DragDropModule,
1881
+ EmptyStateCardComponent,
1882
+ DynamicFieldLoaderDirective,
1883
+ DynamicWidgetLoaderDirective,
1884
+ ], template: `
1885
+ <div
1886
+ class="praxis-tabs-root"
1887
+ [class.density-compact]="config?.appearance?.density === 'compact'"
1888
+ [class.density-comfortable]="config?.appearance?.density === 'comfortable'"
1889
+ [class.density-spacious]="config?.appearance?.density === 'spacious'"
1890
+ [class.high-contrast]="config?.accessibility?.highContrast"
1891
+ [class.reduce-motion]="config?.accessibility?.reduceMotion"
1892
+ [ngClass]="config?.appearance?.themeClass || ''"
1893
+ [attr.data-tabs-id]="tabsId || 'default'"
1894
+ >
1895
+ <!-- Inline custom CSS, if provided -->
1896
+ <style *ngIf="config?.appearance?.customCss" [innerHTML]="config?.appearance?.customCss"></style>
1897
+ <!-- Runtime style derived from tokens -->
1898
+ <style *ngIf="styleCss() as s" [innerHTML]="s"></style>
1899
+
1900
+ <!-- Empty state (global) -->
1901
+ <ng-container *ngIf="isEmptyGlobal(); else notEmpty">
1902
+ <praxis-empty-state-card
1903
+ icon="tab"
1904
+ [title]="'Nenhuma aba configurada'"
1905
+ [description]="'Crie rapidamente suas abas ou abra o editor completo.'"
1906
+ [primaryAction]="{ label: 'Criar abas rapidamente', icon: 'add', action: openQuickSetup.bind(this) }"
1907
+ [secondaryActions]="[
1908
+ { label: 'Adicionar aba vazia', icon: 'tab', action: addEmptyTab.bind(this) },
1909
+ { label: 'Abrir editor completo', icon: 'tune', action: openEditor.bind(this) }
1910
+ ]"
1911
+ ></praxis-empty-state-card>
1912
+ </ng-container>
1913
+
1914
+ <ng-template #notEmpty>
1915
+ <!-- Tab Nav variant -->
1916
+ <ng-container *ngIf="isNavMode(); else groupMode">
1917
+ <nav
1918
+ mat-tab-nav-bar
1919
+ cdkDropList
1920
+ cdkDropListOrientation="horizontal"
1921
+ [cdkDropListDisabled]="!config?.behavior?.reorderable"
1922
+ (cdkDropListDropped)="onNavDrop($event)"
1923
+ [disablePagination]="config?.nav?.disablePagination"
1924
+ [fitInkBarToContent]="config?.nav?.fitInkBarToContent"
1925
+ [mat-stretch-tabs]="config?.nav?.stretchTabs"
1926
+ [color]="config?.nav?.color"
1927
+ [backgroundColor]="config?.nav?.backgroundColor"
1928
+ [selectedIndex]="currentNavIndex()"
1929
+ [attr.aria-label]="config?.group?.ariaLabel || null"
1930
+ [attr.aria-labelledby]="config?.group?.ariaLabelledby || null"
1931
+ [animationDuration]="effectiveAnimationDuration()"
1932
+ (indexFocused)="indexFocused.emit($event)"
1933
+ (selectFocusedIndex)="selectFocusedIndex.emit($event)"
1934
+ >
1935
+ <a
1936
+ mat-tab-link
1937
+ *ngFor="let link of config?.nav?.links; let i = index"
1938
+ cdkDrag
1939
+ [cdkDragDisabled]="!config?.behavior?.reorderable"
1940
+ cdkDragLockAxis="x"
1941
+ [active]="getNavActive(i)"
1942
+ [disabled]="link.disabled"
1943
+ [disableRipple]="config?.nav?.disableRipple || link.disableRipple"
1944
+ [fitInkBarToContent]="link.fitInkBarToContent || false"
1945
+ [id]="link.id || ''"
1946
+ (click)="onNavClick(i)"
1947
+ >
1948
+ <span *ngIf="config?.behavior?.reorderable" class="drag-handle" cdkDragHandle>
1949
+ <mat-icon fontIcon="drag_indicator"></mat-icon>
1950
+ </span>
1951
+ {{ link.label }}
1952
+ </a>
1953
+ </nav>
1954
+
1955
+ <div class="praxis-tabnav-content" *ngIf="currentNavIndex() >= 0">
1956
+ <ng-container
1957
+ *ngIf="config?.nav?.links?.[currentNavIndex()] as l"
1958
+ >
1959
+ <ng-container *ngIf="(l.content?.length || l.widgets?.length) && navContentReady(currentNavIndex()); else emptyNav">
1960
+ <ng-container *ngIf="l.content && form">
1961
+ <ng-container
1962
+ dynamicFieldLoader
1963
+ [fields]="l.content || []"
1964
+ [formGroup]="form!"
1965
+ ></ng-container>
1966
+ </ng-container>
1967
+ <ng-container *ngIf="l.widgets?.length">
1968
+ <ng-container *ngFor="let w of l.widgets; let wi = index">
1969
+ <ng-container
1970
+ [dynamicWidgetLoader]="w"
1971
+ [context]="context || {}"
1972
+ (widgetEvent)="emitWidgetEvent({ linkId: l.id, linkIndex: currentNavIndex() }, $event)"
1973
+ ></ng-container>
1974
+ </ng-container>
1975
+ </ng-container>
1976
+ </ng-container>
1977
+ <ng-template #emptyNav>
1978
+ <praxis-empty-state-card
1979
+ [inline]="true"
1980
+ icon="add_to_queue"
1981
+ [title]="'Sem conteúdo neste link'"
1982
+ [description]="'Adicione conteúdo ou use o editor para configurar.'"
1983
+ [primaryAction]="{ label: 'Abrir editor', icon: 'tune', action: openEditor.bind(this) }"
1984
+ ></praxis-empty-state-card>
1985
+ </ng-template>
1986
+ </ng-container>
1987
+ </div>
1988
+ </ng-container>
1989
+
1990
+ <!-- Tab Group variant -->
1991
+ <ng-template #groupMode>
1992
+ <mat-tab-group
1993
+ [dynamicHeight]="config?.group?.dynamicHeight"
1994
+ [disableRipple]="config?.group?.disableRipple"
1995
+ [disablePagination]="config?.group?.disablePagination"
1996
+ [fitInkBarToContent]="config?.group?.fitInkBarToContent"
1997
+ [headerPosition]="config?.group?.headerPosition ?? 'above'"
1998
+ [preserveContent]="config?.group?.preserveContent"
1999
+ [selectedIndex]="selectedIndexSignal()"
2000
+ [mat-stretch-tabs]="config?.group?.stretchTabs"
2001
+ [mat-align-tabs]="config?.group?.alignTabs || 'start'"
2002
+ [color]="config?.group?.color"
2003
+ [backgroundColor]="config?.group?.backgroundColor"
2004
+ [animationDuration]="effectiveAnimationDuration()"
2005
+ [attr.aria-label]="config?.group?.ariaLabel || null"
2006
+ [attr.aria-labelledby]="config?.group?.ariaLabelledby || null"
2007
+ (animationDone)="animationDone.emit()"
2008
+ (focusChange)="focusChange.emit($event)"
2009
+ (selectedIndexChange)="onSelectedIndexChange($event)"
2010
+ (selectedTabChange)="selectedTabChange.emit($event)"
2011
+ class="praxis-tabs-group"
2012
+ >
2013
+ <mat-tab
2014
+ *ngFor="let t of config?.tabs; let i = index"
2015
+ [disabled]="t.disabled"
2016
+ [labelClass]="t.labelClass ?? ''"
2017
+ [bodyClass]="t.bodyClass ?? ''"
2018
+ [id]="t.id || ''"
2019
+ [attr.aria-label]="t.ariaLabel || null"
2020
+ [attr.aria-labelledby]="t.ariaLabelledby || null"
2021
+ >
2022
+ <ng-template mat-tab-label>
2023
+ <span>{{ t.textLabel }}</span>
2024
+ <button
2025
+ *ngIf="config?.behavior?.closeable"
2026
+ mat-icon-button
2027
+ type="button"
2028
+ (click)="closeTab(i); $event.stopPropagation()"
2029
+ aria-label="Fechar aba"
2030
+ >
2031
+ <mat-icon fontIcon="close"></mat-icon>
2032
+ </button>
2033
+ <ng-container *ngIf="config?.behavior?.reorderable">
2034
+ <button mat-icon-button type="button" (click)="moveTab(i, -1); $event.stopPropagation()" [disabled]="i===0" aria-label="Mover aba para esquerda">
2035
+ <mat-icon fontIcon="arrow_back"></mat-icon>
2036
+ </button>
2037
+ <button mat-icon-button type="button" (click)="moveTab(i, 1); $event.stopPropagation()" [disabled]="i===(config?.tabs?.length||1)-1" aria-label="Mover aba para direita">
2038
+ <mat-icon fontIcon="arrow_forward"></mat-icon>
2039
+ </button>
2040
+ </ng-container>
2041
+ </ng-template>
2042
+
2043
+ <ng-template matTabContent>
2044
+ <ng-container *ngIf="(t.content?.length || t.widgets?.length) && groupContentReady(i); else emptyTab">
2045
+ <ng-container *ngIf="t.content && form">
2046
+ <ng-container
2047
+ dynamicFieldLoader
2048
+ [fields]="t.content || []"
2049
+ [formGroup]="form!"
2050
+ ></ng-container>
2051
+ </ng-container>
2052
+ <ng-container *ngIf="t.widgets?.length">
2053
+ <ng-container *ngFor="let w of t.widgets; let wi = index">
2054
+ <ng-container
2055
+ [dynamicWidgetLoader]="w"
2056
+ [context]="context || {}"
2057
+ (widgetEvent)="emitWidgetEvent({ tabId: t.id, tabIndex: i }, $event)"
2058
+ ></ng-container>
2059
+ </ng-container>
2060
+ </ng-container>
2061
+ </ng-container>
2062
+ <ng-template #emptyTab>
2063
+ <praxis-empty-state-card
2064
+ [inline]="true"
2065
+ icon="dashboard_customize"
2066
+ [title]="'Sem conteúdo nesta aba'"
2067
+ [description]="'Adicione conteúdo ou use o editor para configurar.'"
2068
+ [primaryAction]="{ label: 'Abrir editor', icon: 'tune', action: openEditor.bind(this) }"
2069
+ ></praxis-empty-state-card>
2070
+ </ng-template>
2071
+ </ng-template>
2072
+ </mat-tab>
2073
+ </mat-tab-group>
2074
+ </ng-template>
2075
+ </ng-template>
2076
+
2077
+ <!-- Edit button -->
2078
+ <button
2079
+ *ngIf="editModeEnabled"
2080
+ mat-fab
2081
+ class="edit-fab"
2082
+ aria-label="Editar tabs"
2083
+ (click)="openEditor()"
2084
+ >
2085
+ <mat-icon fontIcon="edit"></mat-icon>
2086
+ </button>
2087
+ <button
2088
+ *ngIf="editModeEnabled && tabsId"
2089
+ mat-mini-fab
2090
+ class="edit-fab"
2091
+ style="right: 56px"
2092
+ aria-label="Redefinir preferências de tabs"
2093
+ (click)="resetPreferences()"
2094
+ matTooltip="Redefinir preferências de tabs"
2095
+ >
2096
+ <mat-icon [praxisIcon]="'restart_alt'"></mat-icon>
2097
+ </button>
2098
+ </div>
2099
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".praxis-tabs-root{position:relative;display:block}.praxis-tabs-group.align-start .mat-mdc-tab-header{justify-content:flex-start}.praxis-tabs-group.align-center .mat-mdc-tab-header{justify-content:center}.praxis-tabs-group.align-end .mat-mdc-tab-header{justify-content:flex-end}.density-compact .mat-mdc-tab-body-content{padding:8px}.density-comfortable .mat-mdc-tab-body-content{padding:16px}.density-spacious .mat-mdc-tab-body-content{padding:24px}.edit-fab{position:absolute;right:12px;bottom:12px;z-index:2}.tab-empty{padding:16px;color:#0000008a;font-style:italic}.high-contrast{filter:contrast(1.2)}.reduce-motion{--mat-animation-duration: 0ms}.drag-handle{display:inline-flex;align-items:center;vertical-align:middle;margin-right:4px;cursor:grab}:host-context(.pdx-gridster-item) .praxis-tabs-root{display:flex;flex-direction:column;height:100%;min-height:0}:host-context(.pdx-gridster-item) .praxis-tabs-group,:host-context(.pdx-gridster-item) .mat-mdc-tab-group{flex:1 1 auto;min-height:0}:host-context(.pdx-gridster-item) .mat-mdc-tab-body-wrapper,:host-context(.pdx-gridster-item) .mat-mdc-tab-body-content{height:100%;min-height:0}:host-context(.pdx-gridster-item) .mat-mdc-tab-body-content{overflow:auto}:host-context(.pdx-gridster-item) .praxis-tabnav-content{flex:1 1 auto;min-height:0;overflow:auto}\n"] }]
2100
+ }], propDecorators: { config: [{
2101
+ type: Input
2102
+ }], tabsId: [{
2103
+ type: Input
2104
+ }], editModeEnabled: [{
2105
+ type: Input
2106
+ }], form: [{
2107
+ type: Input
2108
+ }], context: [{
2109
+ type: Input
2110
+ }], animationDone: [{
2111
+ type: Output
2112
+ }], focusChange: [{
2113
+ type: Output
2114
+ }], selectedIndexChange: [{
2115
+ type: Output
2116
+ }], selectedTabChange: [{
2117
+ type: Output
2118
+ }], indexFocused: [{
2119
+ type: Output
2120
+ }], selectFocusedIndex: [{
2121
+ type: Output
2122
+ }], widgetEvent: [{
2123
+ type: Output
2124
+ }] } });
2125
+
2126
+ const PRAXIS_TABS_COMPONENT_METADATA = {
2127
+ id: 'praxis-tabs',
2128
+ selector: 'praxis-tabs',
2129
+ component: PraxisTabs,
2130
+ friendlyName: 'Praxis Tabs',
2131
+ description: 'Abas dinâmicas baseadas em metadata, suportando MatTabGroup e TabNav.',
2132
+ icon: 'tab',
2133
+ inputs: [
2134
+ { name: 'config', type: 'TabsMetadata', label: 'Configuração', description: 'Configuração JSON (tabs/nav e widgets internos)' },
2135
+ {
2136
+ name: 'tabsId',
2137
+ type: 'string',
2138
+ label: 'ID das Tabs',
2139
+ description: 'Identificador para persistência local',
2140
+ },
2141
+ {
2142
+ name: 'editModeEnabled',
2143
+ type: 'boolean',
2144
+ default: false,
2145
+ label: 'Modo de edição',
2146
+ description: 'Habilita editor via Settings Panel',
2147
+ },
2148
+ {
2149
+ name: 'form',
2150
+ type: 'FormGroup',
2151
+ label: 'FormGroup',
2152
+ description: 'FormGroup para conteúdo com campos dinâmicos',
2153
+ },
2154
+ ],
2155
+ outputs: [
2156
+ { name: 'selectedIndexChange', type: 'number', label: 'Índice selecionado' },
2157
+ { name: 'selectedTabChange', type: 'MatTabChangeEvent', label: 'Troca de aba' },
2158
+ { name: 'animationDone', type: 'void' },
2159
+ { name: 'focusChange', type: 'MatTabChangeEvent', label: 'Foco alterado' },
2160
+ { name: 'indexFocused', type: 'number', label: 'Índice focado' },
2161
+ { name: 'selectFocusedIndex', type: 'number', label: 'Selecionar foco' },
2162
+ {
2163
+ name: 'widgetEvent',
2164
+ type: "{ tabId?: string; tabIndex?: number; linkId?: string; linkIndex?: number; sourceId: string; output?: string; payload?: any }",
2165
+ label: 'Evento interno',
2166
+ description: 'Eventos reemitidos de componentes dinâmicos dentro da aba/link',
2167
+ },
2168
+ ],
2169
+ tags: ['widget', 'tabs', 'configurable', 'stable'],
2170
+ lib: '@praxisui/tabs',
2171
+ };
2172
+ function providePraxisTabsMetadata() {
2173
+ return {
2174
+ provide: ENVIRONMENT_INITIALIZER,
2175
+ multi: true,
2176
+ useFactory: (registry) => () => {
2177
+ registry.register(PRAXIS_TABS_COMPONENT_METADATA);
2178
+ },
2179
+ deps: [ComponentMetadataRegistry],
2180
+ };
2181
+ }
2182
+
2183
+ /*
2184
+ * Public API Surface of praxis-tabs
2185
+ */
2186
+
2187
+ /**
2188
+ * Generated bundle index. Do not edit.
2189
+ */
2190
+
2191
+ export { PRAXIS_TABS_COMPONENT_METADATA, PraxisTabs, PraxisTabsConfigEditor, providePraxisTabsMetadata };
2192
+ //# sourceMappingURL=praxisui-tabs.mjs.map