UNPKG

@praxisui/tabs

Version:

Configurable tabs (group and nav) for Praxis UI with metadata-driven content and runtime editor.

829 lines (813 loc) 126 kB
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