@praxisui/tabs
Version:
Configurable tabs (group and nav) for Praxis UI with metadata-driven content and runtime editor.
829 lines (813 loc) • 126 kB
JavaScript
import * as i0 from '@angular/core';
import { Inject, Component, inject, EventEmitter, signal, Output, Input, ChangeDetectionStrategy, ENVIRONMENT_INITIALIZER } from '@angular/core';
import * as i1$1 from '@angular/common';
import { CommonModule } from '@angular/common';
import * as i3$1 from '@angular/material/tabs';
import { MatTabsModule } from '@angular/material/tabs';
import * as i7 from '@angular/material/icon';
import { MatIconModule } from '@angular/material/icon';
import * as i1 from '@praxisui/core';
import { PraxisIconDirective, CONFIG_STORAGE, EmptyStateCardComponent, DynamicWidgetLoaderDirective, ComponentMetadataRegistry } from '@praxisui/core';
import * as i5$1 from '@angular/material/button';
import { MatButtonModule } from '@angular/material/button';
import * as i3 from '@angular/forms';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { DynamicFieldLoaderDirective } from '@praxisui/dynamic-fields';
import { SETTINGS_PANEL_DATA, SettingsPanelService } from '@praxisui/settings-panel';
import * as i5 from '@angular/material/form-field';
import { MatFormFieldModule } from '@angular/material/form-field';
import * as i6 from '@angular/material/input';
import { MatInputModule } from '@angular/material/input';
import * as i9 from '@angular/material/slide-toggle';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import * as i10 from '@angular/cdk/drag-drop';
import { moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop';
import * as i11 from '@angular/material/tooltip';
import { MatTooltipModule } from '@angular/material/tooltip';
import { BehaviorSubject } from 'rxjs';
import { produce } from 'immer';
import { MatSnackBar } from '@angular/material/snack-bar';
class PraxisTabsConfigEditor {
registry;
editedConfig;
initialConfig;
jsonText = '';
isValid = true;
errorMsg = '';
componentOptions = [];
selectedTabWidgetId = {};
selectedLinkWidgetId = {};
quickResourcePathTab = {};
quickResourcePathLink = {};
// Simplified token list we support at runtime
tokenList = [
{ key: 'active-indicator-color', label: 'Indicador ativo' },
{ key: 'active-label-text-color', label: 'Texto ativo' },
{ key: 'active-hover-indicator-color', label: 'Indicador ativo (hover)' },
{ key: 'active-hover-label-text-color', label: 'Texto ativo (hover)' },
{ key: 'active-focus-indicator-color', label: 'Indicador ativo (focus)' },
{ key: 'active-focus-label-text-color', label: 'Texto ativo (focus)' },
{ key: 'inactive-label-text-color', label: 'Texto inativo' },
{ key: 'inactive-hover-label-text-color', label: 'Texto inativo (hover)' },
{ key: 'inactive-focus-label-text-color', label: 'Texto inativo (focus)' },
{ key: 'pagination-icon-color', label: 'Ícones de paginação' },
{ key: 'divider-color', label: 'Divisor/linha' },
{ key: 'background-color', label: 'Fundo do header' },
];
presets = {
primary: {
'active-indicator-color': 'var(--mat-sys-primary)',
'active-label-text-color': 'var(--mat-sys-primary)',
'inactive-label-text-color': 'rgba(var(--mat-sys-on-surface-rgb), 0.72)',
'inactive-hover-label-text-color': 'var(--mat-sys-on-surface)',
'divider-color': 'rgba(255, 255, 255, 0.12)',
'background-color': 'var(--mat-sys-surface-container)',
'pagination-icon-color': 'var(--mat-sys-on-surface)'
},
neutral: {
'active-indicator-color': 'var(--mat-sys-on-surface)',
'active-label-text-color': 'var(--mat-sys-on-surface)',
'inactive-label-text-color': 'rgba(var(--mat-sys-on-surface-rgb), 0.56)',
'inactive-hover-label-text-color': 'var(--mat-sys-on-surface)',
'divider-color': 'rgba(255, 255, 255, 0.10)',
'background-color': 'var(--mat-sys-surface-container)',
'pagination-icon-color': 'var(--mat-sys-on-surface)'
},
'high-contrast': {
'active-indicator-color': '#FFD54F',
'active-label-text-color': '#FFFFFF',
'inactive-label-text-color': '#BDBDBD',
'inactive-hover-label-text-color': '#EEEEEE',
'divider-color': '#FFFFFF',
'background-color': 'var(--mat-sys-surface)',
'pagination-icon-color': '#FFFFFF'
}
};
isDirty$ = new BehaviorSubject(false);
isValid$ = new BehaviorSubject(true);
isBusy$ = new BehaviorSubject(false);
constructor(data, registry) {
this.registry = registry;
const cfg = data?.config || data || {};
this.initialConfig = structuredClone(cfg);
this.editedConfig = structuredClone(cfg);
this.isValid$.next(true);
this.jsonText = this.stringify(this.editedConfig);
this.updateDirty();
this.componentOptions = this.registry.getAll().map((m) => ({ id: m.id, friendlyName: m.friendlyName }));
}
updateDirty() {
this.isDirty$.next(JSON.stringify(this.initialConfig) !== JSON.stringify(this.editedConfig));
}
onJsonTextChange(text) {
try {
const parsed = JSON.parse(text);
this.errorMsg = '';
this.isValid = true;
this.isValid$.next(true);
this.editedConfig = parsed;
this.updateDirty();
}
catch (e) {
this.isValid = false;
this.isValid$.next(false);
this.errorMsg = e?.message || 'Erro de sintaxe JSON';
}
}
// stringify helper is public to be used in the template
formatJson() {
if (!this.isValid)
return;
this.jsonText = this.stringify(this.editedConfig);
}
getSettingsValue() {
return { config: this.editedConfig };
}
onSave() {
return { config: this.editedConfig };
}
reset() {
this.editedConfig = structuredClone(this.initialConfig);
this.jsonText = this.stringify(this.editedConfig);
this.isValid = true;
this.isValid$.next(true);
this.updateDirty();
}
// Appearance helpers
get appearance() {
const ap = (this.editedConfig.appearance ??= {});
(ap.tokens ??= {});
return ap;
}
onAppearanceChange() {
this.updateDirty();
this.jsonText = this.stringify(this.editedConfig);
}
onTokenChange(key, value) {
const ap = this.appearance;
const tokens = (ap.tokens = ap.tokens || {});
if (!value) {
delete tokens[key];
}
else {
tokens[key] = value;
}
this.onAppearanceChange();
}
clearTokens() {
const ap = this.appearance;
ap.tokens = {};
this.onAppearanceChange();
}
applyPreset(id) {
const preset = this.presets[id];
if (!preset)
return;
const ap = this.appearance;
const tokens = (ap.tokens = ap.tokens || {});
for (const [k, v] of Object.entries(preset)) {
tokens[k] = v;
}
this.onAppearanceChange();
}
scssSnippet() {
const tokens = this.editedConfig.appearance?.tokens || {};
const entries = Object.entries(tokens).filter(([, v]) => !!v);
const selector = this.editedConfig.appearance?.themeClass
? `.${this.editedConfig.appearance.themeClass}`
: ':root';
const map = entries.map(([k, v]) => ` ${k}: ${v},`).join('\n');
return `${selector} {\n @include mat.tabs-overrides((\n${map}\n ));\n}`;
}
// Group/Nav helpers and list editors
get group() {
if (!this.editedConfig.group)
this.editedConfig.group = {};
return this.editedConfig.group;
}
get nav() {
const nav = (this.editedConfig.nav ??= { links: [] });
if (!nav.links)
nav.links = [];
return nav;
}
get behavior() {
if (!this.editedConfig.behavior)
this.editedConfig.behavior = {};
// Default to lazy load to improve UX/perf with heavy widgets (table/form)
if (typeof this.editedConfig.behavior.lazyLoad === 'undefined') {
this.editedConfig.behavior.lazyLoad = true;
}
return this.editedConfig.behavior;
}
get accessibility() {
if (!this.editedConfig.accessibility)
this.editedConfig.accessibility = {};
return this.editedConfig.accessibility;
}
addTab() {
if (!this.editedConfig.tabs)
this.editedConfig.tabs = [];
this.editedConfig.tabs.push({ id: `tab${(this.editedConfig.tabs.length + 1)}`, textLabel: 'Nova aba' });
this.onAppearanceChange();
}
removeTab(index) {
this.editedConfig.tabs?.splice(index, 1);
this.onAppearanceChange();
}
moveTab(index, delta) {
if (!this.editedConfig.tabs)
return;
const i2 = index + delta;
if (i2 < 0 || i2 >= this.editedConfig.tabs.length)
return;
const [item] = this.editedConfig.tabs.splice(index, 1);
this.editedConfig.tabs.splice(i2, 0, item);
this.onAppearanceChange();
}
// ===== Widgets (Tabs) =====
addWidgetToTab(tabIndex) {
const id = (this.selectedTabWidgetId[tabIndex] || '').trim();
if (!id)
return;
const t = this.editedConfig.tabs[tabIndex];
t.widgets = t.widgets || [];
t.widgets.push({ id });
this.onAppearanceChange();
}
removeWidgetFromTab(tabIndex, widgetIndex) {
const t = this.editedConfig.tabs[tabIndex];
if (!t.widgets)
return;
t.widgets.splice(widgetIndex, 1);
this.onAppearanceChange();
}
updateWidgetInputsTab(tabIndex, widgetIndex, text) {
const t = this.editedConfig.tabs[tabIndex];
try {
const obj = text ? JSON.parse(text) : undefined;
t.widgets[widgetIndex].inputs = obj;
this.onAppearanceChange();
}
catch { }
}
updateWidgetOutputsTab(tabIndex, widgetIndex, text) {
const t = this.editedConfig.tabs[tabIndex];
try {
const obj = text ? JSON.parse(text) : undefined;
t.widgets[widgetIndex].outputs = obj;
this.onAppearanceChange();
}
catch { }
}
addPresetToTab(tabIndex, type) {
const rp = (this.quickResourcePathTab[tabIndex] || '').trim();
if (!rp)
return;
const t = this.editedConfig.tabs[tabIndex];
t.widgets = t.widgets || [];
if (type === 'form') {
t.widgets.push({ id: 'praxis-dynamic-form', inputs: { resourcePath: rp } });
}
else if (type === 'table') {
t.widgets.push({ id: 'praxis-table', inputs: { resourcePath: rp } });
}
else {
t.widgets.push({ id: 'praxis-crud', inputs: { metadata: { resource: { path: rp } } } });
}
this.onAppearanceChange();
}
addLink() {
if (!this.nav.links)
this.nav.links = [];
this.nav.links.push({ id: `link${this.nav.links.length + 1}`, label: 'Novo link' });
this.onAppearanceChange();
}
removeLink(index) {
if (!this.nav.links)
return;
this.nav.links.splice(index, 1);
this.onAppearanceChange();
}
moveLink(index, delta) {
if (!this.nav.links)
return;
const i2 = index + delta;
if (i2 < 0 || i2 >= this.nav.links.length)
return;
const [item] = this.nav.links.splice(index, 1);
this.nav.links.splice(i2, 0, item);
this.onAppearanceChange();
}
// ===== Widgets (Links) =====
addWidgetToLink(linkIndex) {
const id = (this.selectedLinkWidgetId[linkIndex] || '').trim();
if (!id)
return;
const l = this.nav.links[linkIndex];
l.widgets = l.widgets || [];
l.widgets.push({ id });
this.onAppearanceChange();
}
removeWidgetFromLink(linkIndex, widgetIndex) {
const l = this.nav.links[linkIndex];
if (!l.widgets)
return;
l.widgets.splice(widgetIndex, 1);
this.onAppearanceChange();
}
updateWidgetInputsLink(linkIndex, widgetIndex, text) {
const l = this.nav.links[linkIndex];
try {
const obj = text ? JSON.parse(text) : undefined;
l.widgets[widgetIndex].inputs = obj;
this.onAppearanceChange();
}
catch { }
}
updateWidgetOutputsLink(linkIndex, widgetIndex, text) {
const l = this.nav.links[linkIndex];
try {
const obj = text ? JSON.parse(text) : undefined;
l.widgets[widgetIndex].outputs = obj;
this.onAppearanceChange();
}
catch { }
}
addPresetToLink(linkIndex, type) {
const rp = (this.quickResourcePathLink[linkIndex] || '').trim();
if (!rp)
return;
const l = this.nav.links[linkIndex];
l.widgets = l.widgets || [];
if (type === 'form') {
l.widgets.push({ id: 'praxis-dynamic-form', inputs: { resourcePath: rp } });
}
else if (type === 'table') {
l.widgets.push({ id: 'praxis-table', inputs: { resourcePath: rp } });
}
else {
l.widgets.push({ id: 'praxis-crud', inputs: { metadata: { resource: { path: rp } } } });
}
this.onAppearanceChange();
}
stringify(v) { try {
return v ? JSON.stringify(v, null, 2) : '';
}
catch {
return '';
} }
getCompName(id) { const c = this.componentOptions.find(o => o.id === id); return c?.friendlyName || id; }
// Reorder handlers (drag & drop)
onTabWidgetDrop(tabIndex, event) {
const t = this.editedConfig.tabs?.[tabIndex];
if (!t?.widgets)
return;
moveItemInArray(t.widgets, event.previousIndex, event.currentIndex);
this.onAppearanceChange();
}
onLinkWidgetDrop(linkIndex, event) {
const l = this.nav.links?.[linkIndex];
if (!l?.widgets)
return;
moveItemInArray(l.widgets, event.previousIndex, event.currentIndex);
this.onAppearanceChange();
}
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 });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.4", type: PraxisTabsConfigEditor, isStandalone: true, selector: "praxis-tabs-config-editor", ngImport: i0, template: `
<mat-tab-group>
<mat-tab label="Comportamento">
<div style="padding: 12px; display:grid; gap: 12px;">
<div style="display:flex; gap: 10px; flex-wrap: wrap;">
<mat-slide-toggle [(ngModel)]="behavior.closeable" (ngModelChange)="onAppearanceChange()">closeable</mat-slide-toggle>
<mat-slide-toggle [(ngModel)]="behavior.lazyLoad" (ngModelChange)="onAppearanceChange()">lazyLoad (planejado)</mat-slide-toggle>
<mat-slide-toggle [(ngModel)]="behavior.reorderable" (ngModelChange)="onAppearanceChange()">reorderable (planejado)</mat-slide-toggle>
</div>
</div>
</mat-tab>
<mat-tab label="Grupo">
<div style="padding: 12px; display:grid; gap: 12px;">
<div style="display:grid; grid-template-columns: repeat(2, minmax(220px,1fr)); gap: 12px;">
<mat-form-field appearance="outline"><mat-label>Alinhamento</mat-label>
<select matNativeControl [(ngModel)]="group.alignTabs" (ngModelChange)="onAppearanceChange()">
<option [ngValue]="undefined">padrão</option>
<option value="start">start</option>
<option value="center">center</option>
<option value="end">end</option>
</select>
</mat-form-field>
<mat-form-field appearance="outline"><mat-label>Header</mat-label>
<select matNativeControl [(ngModel)]="group.headerPosition" (ngModelChange)="onAppearanceChange()">
<option [ngValue]="undefined">above</option>
<option value="above">above</option>
<option value="below">below</option>
</select>
</mat-form-field>
<mat-form-field appearance="outline"><mat-label>selectedIndex</mat-label>
<input matInput type="number" [(ngModel)]="group.selectedIndex" (ngModelChange)="onAppearanceChange()" />
</mat-form-field>
<mat-form-field appearance="outline"><mat-label>animationDuration</mat-label>
<input matInput [(ngModel)]="group.animationDuration" (ngModelChange)="onAppearanceChange()" placeholder="500ms" />
</mat-form-field>
<mat-form-field appearance="outline"><mat-label>contentTabIndex</mat-label>
<input matInput type="number" [(ngModel)]="group.contentTabIndex" (ngModelChange)="onAppearanceChange()" />
</mat-form-field>
<mat-form-field appearance="outline"><mat-label>color (M2)</mat-label>
<select matNativeControl [(ngModel)]="group.color" (ngModelChange)="onAppearanceChange()">
<option [ngValue]="undefined">(nenhum)</option>
<option value="primary">primary</option>
<option value="accent">accent</option>
<option value="warn">warn</option>
</select>
</mat-form-field>
<mat-form-field appearance="outline"><mat-label>backgroundColor (M2)</mat-label>
<select matNativeControl [(ngModel)]="group.backgroundColor" (ngModelChange)="onAppearanceChange()">
<option [ngValue]="undefined">(nenhum)</option>
<option value="primary">primary</option>
<option value="accent">accent</option>
<option value="warn">warn</option>
</select>
</mat-form-field>
<mat-form-field appearance="outline"><mat-label>aria-label</mat-label>
<input matInput [(ngModel)]="group.ariaLabel" (ngModelChange)="onAppearanceChange()" />
</mat-form-field>
<mat-form-field appearance="outline"><mat-label>aria-labelledby</mat-label>
<input matInput [(ngModel)]="group.ariaLabelledby" (ngModelChange)="onAppearanceChange()" />
</mat-form-field>
</div>
<div style="display:flex; gap: 10px; flex-wrap: wrap;">
<mat-slide-toggle [(ngModel)]="group.dynamicHeight" (ngModelChange)="onAppearanceChange()">dynamicHeight</mat-slide-toggle>
<mat-slide-toggle [(ngModel)]="group.fitInkBarToContent" (ngModelChange)="onAppearanceChange()">fitInkBarToContent</mat-slide-toggle>
<mat-slide-toggle [(ngModel)]="group.disablePagination" (ngModelChange)="onAppearanceChange()">disablePagination</mat-slide-toggle>
<mat-slide-toggle [(ngModel)]="group.disableRipple" (ngModelChange)="onAppearanceChange()">disableRipple</mat-slide-toggle>
<mat-slide-toggle [(ngModel)]="group.preserveContent" (ngModelChange)="onAppearanceChange()">preserveContent</mat-slide-toggle>
<mat-slide-toggle [(ngModel)]="group.stretchTabs" (ngModelChange)="onAppearanceChange()">stretchTabs</mat-slide-toggle>
</div>
</div>
</mat-tab>
<mat-tab label="Navegação">
<div style="padding: 12px; display:grid; gap: 12px;">
<div style="display:grid; grid-template-columns: repeat(2, minmax(220px,1fr)); gap: 12px;">
<mat-form-field appearance="outline"><mat-label>selectedIndex</mat-label>
<input matInput type="number" [(ngModel)]="nav.selectedIndex" (ngModelChange)="onAppearanceChange()" />
</mat-form-field>
<mat-form-field appearance="outline"><mat-label>animationDuration</mat-label>
<input matInput [(ngModel)]="nav.animationDuration" (ngModelChange)="onAppearanceChange()" placeholder="500ms" />
</mat-form-field>
<mat-form-field appearance="outline"><mat-label>color (M2)</mat-label>
<select matNativeControl [(ngModel)]="nav.color" (ngModelChange)="onAppearanceChange()">
<option [ngValue]="undefined">(nenhum)</option>
<option value="primary">primary</option>
<option value="accent">accent</option>
<option value="warn">warn</option>
</select>
</mat-form-field>
<mat-form-field appearance="outline"><mat-label>backgroundColor (M2)</mat-label>
<select matNativeControl [(ngModel)]="nav.backgroundColor" (ngModelChange)="onAppearanceChange()">
<option [ngValue]="undefined">(nenhum)</option>
<option value="primary">primary</option>
<option value="accent">accent</option>
<option value="warn">warn</option>
</select>
</mat-form-field>
</div>
<div style="display:flex; gap: 10px; flex-wrap: wrap;">
<mat-slide-toggle [(ngModel)]="nav.fitInkBarToContent" (ngModelChange)="onAppearanceChange()">fitInkBarToContent</mat-slide-toggle>
<mat-slide-toggle [(ngModel)]="nav.disablePagination" (ngModelChange)="onAppearanceChange()">disablePagination</mat-slide-toggle>
<mat-slide-toggle [(ngModel)]="nav.disableRipple" (ngModelChange)="onAppearanceChange()">disableRipple</mat-slide-toggle>
<mat-slide-toggle [(ngModel)]="nav.stretchTabs" (ngModelChange)="onAppearanceChange()">stretchTabs</mat-slide-toggle>
</div>
</div>
</mat-tab>
<mat-tab label="JSON">
<div style="padding: 12px; display: grid; gap: 12px;">
<div class="json-editor-toolbar" style="display:flex; gap:8px;">
<button mat-button (click)="formatJson()" [disabled]="!isValid">
<mat-icon [praxisIcon]="'format_align_left'"></mat-icon>Formatar
</button>
<button mat-button (click)="reset()">
<mat-icon [praxisIcon]="'restart_alt'"></mat-icon>Resetar
</button>
</div>
<mat-form-field appearance="outline" class="json-textarea-field" style="width:100%">
<mat-label>Configuração JSON</mat-label>
<textarea
matInput
[(ngModel)]="jsonText"
(ngModelChange)="onJsonTextChange($event)"
rows="22"
spellcheck="false"
style="font-family: monospace"
></textarea>
<mat-hint *ngIf="isValid">JSON válido</mat-hint>
<mat-error *ngIf="!isValid && jsonText">JSON inválido: {{ errorMsg }}</mat-error>
</mat-form-field>
</div>
</mat-tab>
<mat-tab label="Estilo">
<div style="padding: 12px; display: grid; gap: 16px;">
<div style="display:flex; gap:8px; align-items:center; flex-wrap: wrap;">
<span style="opacity:.75">Presets:</span>
<button mat-button color="primary" (click)="applyPreset('primary')">
<mat-icon [praxisIcon]="'palette'"></mat-icon>
Primário
</button>
<button mat-button (click)="applyPreset('neutral')">
<mat-icon [praxisIcon]="'contrast'"></mat-icon>
Neutro
</button>
<button mat-button (click)="applyPreset('high-contrast')">
<mat-icon [praxisIcon]="'visibility'"></mat-icon>
Alto contraste
</button>
<button mat-button (click)="clearTokens()">
<mat-icon [praxisIcon]="'backspace'"></mat-icon>
Limpar tokens
</button>
</div>
<div style="display:grid; gap:8px; grid-template-columns: repeat(2, minmax(220px, 1fr));">
<mat-form-field appearance="outline">
<mat-label>Classe de tema (opcional)</mat-label>
<input matInput [(ngModel)]="appearance.themeClass" (ngModelChange)="onAppearanceChange()" placeholder="ex.: tabs-accented" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Densidade</mat-label>
<select matNativeControl [(ngModel)]="appearance.density" (ngModelChange)="onAppearanceChange()">
<option [ngValue]="undefined">padrão</option>
<option value="compact">compact</option>
<option value="comfortable">comfortable</option>
<option value="spacious">spacious</option>
</select>
</mat-form-field>
</div>
<div>
<h3 style="margin: 6px 0 8px;">Tokens (M3 aproximados)</h3>
<div style="display:grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); gap: 10px;">
<ng-container *ngFor="let t of tokenList">
<mat-form-field appearance="outline">
<mat-label>{{ t.label }}</mat-label>
<input matInput placeholder="var(--mat-sys-primary) / #RRGGBB" [ngModel]="appearance.tokens?.[t.key]" (ngModelChange)="onTokenChange(t.key, $event)" />
</mat-form-field>
</ng-container>
</div>
<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>
</div>
<div>
<h3 style="margin: 6px 0 8px;">CSS Personalizado</h3>
<mat-form-field appearance="outline" class="json-textarea-field" style="width:100%">
<mat-label>CSS a ser injetado no componente</mat-label>
<textarea matInput rows="10" [(ngModel)]="appearance.customCss" (ngModelChange)="onAppearanceChange()" placeholder=".praxis-tabs-root .mdc-tab__text-label { font-weight: 600; }"></textarea>
</mat-form-field>
</div>
<div>
<h3 style="margin: 6px 0 8px;">Snippet SCSS (para uso em styles.scss)</h3>
<pre style="white-space: pre-wrap; background: rgba(0,0,0,0.2); padding: 8px; border-radius: 6px;">
@use '@angular/material' as mat;
{{ scssSnippet() }}
</pre>
</div>
</div>
</mat-tab>
<mat-tab label="Abas">
<div style="padding:12px; display:grid; gap:12px;">
<button mat-stroked-button color="primary" (click)="addTab()"><mat-icon [praxisIcon]="'add'"></mat-icon>Adicionar aba</button>
<div *ngIf="editedConfig.tabs?.length; else noTabs" style="display:grid; gap:10px;">
<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;">
<div style="display:flex; align-items:center; gap:8px;">
<strong style="flex:1">#{{ i+1 }}</strong>
<button mat-icon-button (click)="moveTab(i, -1)" [disabled]="i===0"><mat-icon [praxisIcon]="'arrow_upward'"></mat-icon></button>
<button mat-icon-button (click)="moveTab(i, 1)" [disabled]="i===editedConfig.tabs!.length-1"><mat-icon [praxisIcon]="'arrow_downward'"></mat-icon></button>
<button mat-icon-button color="warn" (click)="removeTab(i)"><mat-icon [praxisIcon]="'delete'"></mat-icon></button>
</div>
<div style="display:grid; grid-template-columns: repeat(2, minmax(220px,1fr)); gap: 10px;">
<mat-form-field appearance="outline"><mat-label>ID</mat-label>
<input matInput [(ngModel)]="t.id" (ngModelChange)="onAppearanceChange()" />
</mat-form-field>
<mat-form-field appearance="outline"><mat-label>Texto</mat-label>
<input matInput [(ngModel)]="t.textLabel" (ngModelChange)="onAppearanceChange()" />
</mat-form-field>
<mat-form-field appearance="outline"><mat-label>Label class</mat-label>
<input matInput [(ngModel)]="t.labelClass" (ngModelChange)="onAppearanceChange()" />
</mat-form-field>
<mat-form-field appearance="outline"><mat-label>Body class</mat-label>
<input matInput [(ngModel)]="t.bodyClass" (ngModelChange)="onAppearanceChange()" />
</mat-form-field>
<mat-form-field appearance="outline"><mat-label>aria-label</mat-label>
<input matInput [(ngModel)]="t.ariaLabel" (ngModelChange)="onAppearanceChange()" />
</mat-form-field>
<mat-form-field appearance="outline"><mat-label>aria-labelledby</mat-label>
<input matInput [(ngModel)]="t.ariaLabelledby" (ngModelChange)="onAppearanceChange()" />
</mat-form-field>
</div>
<mat-slide-toggle [(ngModel)]="t.disabled" (ngModelChange)="onAppearanceChange()">disabled</mat-slide-toggle>
<!-- Widgets (componentes dinâmicos) -->
<div style="border-top:1px solid var(--mat-sys-outline-variant); padding-top:8px; display:grid; gap:8px;">
<div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
<mat-form-field appearance="outline" style="min-width:260px;">
<mat-label>Adicionar componente</mat-label>
<select matNativeControl [(ngModel)]="selectedTabWidgetId[i]">
<option [ngValue]="''">(selecione)</option>
<option *ngFor="let c of componentOptions" [ngValue]="c.id">{{ c.friendlyName || c.id }}</option>
</select>
</mat-form-field>
<button mat-stroked-button (click)="addWidgetToTab(i)"><mat-icon [praxisIcon]="'add'"></mat-icon>Adicionar</button>
<span style="flex:1"></span>
<mat-form-field appearance="outline" style="width:240px;">
<mat-label>resourcePath</mat-label>
<input matInput [(ngModel)]="quickResourcePathTab[i]" placeholder="ex.: usuarios" />
</mat-form-field>
<button mat-button (click)="addPresetToTab(i, 'form')"><mat-icon [praxisIcon]="'description'"></mat-icon>Form</button>
<button mat-button (click)="addPresetToTab(i, 'table')"><mat-icon [praxisIcon]="'grid_on'"></mat-icon>Tabela</button>
<button mat-button (click)="addPresetToTab(i, 'crud')"><mat-icon [praxisIcon]="'dynamic_form'"></mat-icon>CRUD</button>
</div>
<div *ngIf="t.widgets?.length" style="display:grid; gap:8px;" cdkDropList [cdkDropListData]="t.widgets || []" (cdkDropListDropped)="onTabWidgetDrop(i, $event)">
<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;">
<div style="display:flex; align-items:center; gap:8px;">
<button mat-icon-button cdkDragHandle matTooltip="Arrastar para reordenar" aria-label="Arrastar para reordenar">
<mat-icon [praxisIcon]="'drag_indicator'"></mat-icon>
</button>
<strong style="flex:1">{{ getCompName(w.id) }}</strong>
<button mat-icon-button color="warn" (click)="removeWidgetFromTab(i, wi)" matTooltip="Remover componente" aria-label="Remover componente"><mat-icon [praxisIcon]="'delete'"></mat-icon></button>
</div>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:8px;">
<mat-form-field appearance="outline">
<mat-label>inputs (JSON)</mat-label>
<textarea matInput rows="4" [ngModel]="stringify(w.inputs)" (ngModelChange)="updateWidgetInputsTab(i, wi, $event)"></textarea>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>outputs (JSON)</mat-label>
<textarea matInput rows="4" [ngModel]="stringify(w.outputs)" (ngModelChange)="updateWidgetOutputsTab(i, wi, $event)"></textarea>
</mat-form-field>
</div>
</div>
</div>
</div>
</div>
</div>
<ng-template #noTabs><em>Nenhuma aba definida.</em></ng-template>
</div>
</mat-tab>
<mat-tab label="Acessibilidade">
<div style="padding: 12px; display:grid; gap: 12px;">
<div style="display:flex; gap: 10px; flex-wrap: wrap;">
<mat-slide-toggle [(ngModel)]="accessibility.highContrast" (ngModelChange)="onAppearanceChange()">highContrast</mat-slide-toggle>
<mat-slide-toggle [(ngModel)]="accessibility.reduceMotion" (ngModelChange)="onAppearanceChange()">reduceMotion</mat-slide-toggle>
</div>
</div>
</mat-tab>
<mat-tab label="Links">
<div style="padding:12px; display:grid; gap:12px;">
<button mat-stroked-button color="primary" (click)="addLink()"><mat-icon [praxisIcon]="'add_link'"></mat-icon>Adicionar link</button>
<div *ngIf="nav.links?.length; else noLinks" style="display:grid; gap:10px;">
<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;">
<div style="display:flex; align-items:center; gap:8px;">
<strong style="flex:1">#{{ i+1 }}</strong>
<button mat-icon-button (click)="moveLink(i, -1)" [disabled]="i===0"><mat-icon [praxisIcon]="'arrow_upward'"></mat-icon></button>
<button mat-icon-button (click)="moveLink(i, 1)" [disabled]="i===nav.links!.length-1"><mat-icon [praxisIcon]="'arrow_downward'"></mat-icon></button>
<button mat-icon-button color="warn" (click)="removeLink(i)"><mat-icon [praxisIcon]="'delete'"></mat-icon></button>
</div>
<div style="display:grid; grid-template-columns: repeat(2, minmax(220px,1fr)); gap: 10px;">
<mat-form-field appearance="outline"><mat-label>ID</mat-label>
<input matInput [(ngModel)]="l.id" (ngModelChange)="onAppearanceChange()" />
</mat-form-field>
<mat-form-field appearance="outline"><mat-label>Label</mat-label>
<input matInput [(ngModel)]="l.label" (ngModelChange)="onAppearanceChange()" />
</mat-form-field>
</div>
<div style="display:flex; gap:10px; flex-wrap:wrap;">
<mat-slide-toggle [(ngModel)]="l.active" (ngModelChange)="onAppearanceChange()">active</mat-slide-toggle>
<mat-slide-toggle [(ngModel)]="l.disabled" (ngModelChange)="onAppearanceChange()">disabled</mat-slide-toggle>
<mat-slide-toggle [(ngModel)]="l.disableRipple" (ngModelChange)="onAppearanceChange()">disableRipple</mat-slide-toggle>
<mat-slide-toggle [(ngModel)]="l.fitInkBarToContent" (ngModelChange)="onAppearanceChange()">fitInkBarToContent</mat-slide-toggle>
</div>
<!-- Widgets para Links -->
<div style="border-top:1px solid var(--mat-sys-outline-variant); padding-top:8px; display:grid; gap:8px;">
<div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
<mat-form-field appearance="outline" style="min-width:260px;">
<mat-label>Adicionar componente</mat-label>
<select matNativeControl [(ngModel)]="selectedLinkWidgetId[i]">
<option [ngValue]="''">(selecione)</option>
<option *ngFor="let c of componentOptions" [ngValue]="c.id">{{ c.friendlyName || c.id }}</option>
</select>
</mat-form-field>
<button mat-stroked-button (click)="addWidgetToLink(i)"><mat-icon [praxisIcon]="'add'"></mat-icon>Adicionar</button>
<span style="flex:1"></span>
<mat-form-field appearance="outline" style="width:240px;">
<mat-label>resourcePath</mat-label>
<input matInput [(ngModel)]="quickResourcePathLink[i]" placeholder="ex.: usuarios" />
</mat-form-field>
<button mat-button (click)="addPresetToLink(i, 'form')"><mat-icon [praxisIcon]="'description'"></mat-icon>Form</button>
<button mat-button (click)="addPresetToLink(i, 'table')"><mat-icon [praxisIcon]="'grid_on'"></mat-icon>Tabela</button>
<button mat-button (click)="addPresetToLink(i, 'crud')"><mat-icon [praxisIcon]="'dynamic_form'"></mat-icon>CRUD</button>
</div>
<div *ngIf="l.widgets?.length" style="display:grid; gap:8px;" cdkDropList [cdkDropListData]="l.widgets || []" (cdkDropListDropped)="onLinkWidgetDrop(i, $event)">
<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;">
<div style="display:flex; align-items:center; gap:8px;">
<button mat-icon-button cdkDragHandle matTooltip="Arrastar para reordenar" aria-label="Arrastar para reordenar">
<mat-icon [praxisIcon]="'drag_indicator'"></mat-icon>
</button>
<strong style="flex:1">{{ getCompName(w.id) }}</strong>
<button mat-icon-button color="warn" (click)="removeWidgetFromLink(i, wi)" matTooltip="Remover componente" aria-label="Remover componente"><mat-icon [praxisIcon]="'delete'"></mat-icon></button>
</div>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:8px;">
<mat-form-field appearance="outline">
<mat-label>inputs (JSON)</mat-label>
<textarea matInput rows="4" [ngModel]="stringify(w.inputs)" (ngModelChange)="updateWidgetInputsLink(i, wi, $event)"></textarea>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>outputs (JSON)</mat-label>
<textarea matInput rows="4" [ngModel]="stringify(w.outputs)" (ngModelChange)="updateWidgetOutputsLink(i, wi, $event)"></textarea>
</mat-form-field>
</div>
</div>
</div>
</div>
</div>
</div>
<ng-template #noLinks><em>Nenhum link definido.</em></ng-template>
</div>
</mat-tab>
</mat-tab-group>
`, 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"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PraxisTabsConfigEditor, decorators: [{
type: Component,
args: [{
selector: 'praxis-tabs-config-editor',
standalone: true,
imports: [
CommonModule,
FormsModule,
MatTabsModule,
MatFormFieldModule,
MatInputModule,
MatIconModule,
PraxisIconDirective,
MatButtonModule,
MatSlideToggleModule,
DragDropModule,
MatTooltipModule,
],
template: `
<mat-tab-group>
<mat-tab label="Comportamento">
<div style="padding: 12px; display:grid; gap: 12px;">
<div style="display:flex; gap: 10px; flex-wrap: wrap;">
<mat-slide-toggle [(ngModel)]="behavior.closeable" (ngModelChange)="onAppearanceChange()">closeable</mat-slide-toggle>
<mat-slide-toggle [(ngModel)]="behavior.lazyLoad" (ngModelChange)="onAppearanceChange()">lazyLoad (planejado)</mat-slide-toggle>
<mat-slide-toggle [(ngModel)]="behavior.reorderable" (ngModelChange)="onAppearanceChange()">reorderable (planejado)</mat-slide-toggle>
</div>
</div>
</mat-tab>
<mat-tab label="Grupo">
<div style="padding: 12px; display:grid; gap: 12px;">
<div style="display:grid; grid-template-columns: repeat(2, minmax(220px,1fr)); gap: 12px;">
<mat-form-field appearance="outline"><mat-label>Alinhamento</mat-label>
<select matNativeControl [(ngModel)]="group.alignTabs" (ngModelChange)="onAppearanceChange()">
<option [ngValue]="undefined">padrão</option>
<option value="start">start</option>
<option value="center">center</option>
<option value="end">end</option>
</select>
</mat-form-field>
<mat-form-field appearance="outline"><mat-label>Header</mat-label>
<select matNativeControl [(ngModel)]="group.headerPosition" (ngModelChange)="onAppearanceChange()">
<option [ngValue]="undefined">above</option>
<option value="above">above</option>
<option value="below">below</option>
</select>
</mat-form-field>
<mat-form-field appearance="outline"><mat-label>selectedIndex</mat-label>
<input matInput type="number" [(ngModel)]="group.selectedIndex" (ngModelChange)="onAppearanceChange()" />
</mat-form-field>
<mat-form-field appearance="outline"><mat-label>animationDuration</mat-label>
<input matInput [(ngModel)]="group.animationDuration" (ngModelChange)="onAppearanceChange()" placeholder="500ms" />
</mat-form-field>
<mat-form-field appearance="outline"><mat-label>contentTabIndex</mat-label>
<input matInput type="number" [(ngModel)]="group.contentTabIndex" (ngModelChange)="onAppearanceChange()" />
</mat-form-field>
<mat-form-field appearance="outline"><mat-label>color (M2)</mat-label>
<select matNativeControl [(ngModel)]="group.color" (ngModelChange)="onAppearanceChange()">
<option [ngValue]="undefined">(nenhum)</option>
<option value="primary">primary</option>
<option value="accent">accent</option>
<option value="warn">warn</option>
</select>
</mat-form-field>
<mat-form-field appearance="outline"><mat-label>backgroundColor (M2)</mat-label>
<select matNativeControl [(ngModel)]="group.backgroundColor" (ngModelChange)="onAppearanceChange()">
<option [ngValue]="undefined">(nenhum)</option>
<option value="primary">primary</option>
<option value="accent">accent</option>
<option value="warn">warn</option>
</select>
</mat-form-field>
<mat-form-field appearance="outline"><mat-label>aria-label</mat-label>
<input matInput [(ngModel)]="group.ariaLabel" (ngModelChange)="onAppearanceChange()" />
</mat-form-field>
<mat-form-field appearance="outline"><mat-label>aria-labelledby</mat-label>
<input matInput [(ngModel)]="group.ariaLabelledby" (ngModelChange)="onAppearanceChange()" />
</mat-form-field>
</div>
<div style="display:flex; gap: 10px; flex-wrap: wrap;">
<mat-slide-toggle [(ngModel)]="group.dynamicHeight" (ngModelChange)="onAppearanceChan