@praxisui/tabs
Version:
Configurable tabs (group and nav) for Praxis UI with metadata-driven content and runtime editor.
1 lines • 120 kB
Source Map (JSON)
{"version":3,"file":"praxisui-tabs.mjs","sources":["../../../projects/praxis-tabs/src/lib/praxis-tabs-config-editor.ts","../../../projects/praxis-tabs/src/lib/quick-setup/tabs-quick-setup.component.ts","../../../projects/praxis-tabs/src/lib/praxis-tabs.ts","../../../projects/praxis-tabs/src/lib/praxis-tabs.metadata.ts","../../../projects/praxis-tabs/src/public-api.ts","../../../projects/praxis-tabs/src/praxisui-tabs.ts"],"sourcesContent":["import {\n Component,\n Inject,\n} from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\nimport { MatTabsModule } from '@angular/material/tabs';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatInputModule } from '@angular/material/input';\nimport { MatIconModule } from '@angular/material/icon';\nimport { PraxisIconDirective } from '@praxisui/core';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatSlideToggleModule } from '@angular/material/slide-toggle';\nimport { DragDropModule, CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';\nimport { MatTooltipModule } from '@angular/material/tooltip';\nimport { BehaviorSubject } from 'rxjs';\nimport { SETTINGS_PANEL_DATA, SettingsValueProvider } from '@praxisui/settings-panel';\nimport { TabsMetadata } from './praxis-tabs';\nimport { ComponentMetadataRegistry, WidgetDefinition } from '@praxisui/core';\n\n@Component({\n selector: 'praxis-tabs-config-editor',\n standalone: true,\n imports: [\n CommonModule,\n FormsModule,\n MatTabsModule,\n MatFormFieldModule,\n MatInputModule,\n MatIconModule,\n PraxisIconDirective,\n MatButtonModule,\n MatSlideToggleModule,\n DragDropModule,\n MatTooltipModule,\n ],\n template: `\n <mat-tab-group>\n <mat-tab label=\"Comportamento\">\n <div style=\"padding: 12px; display:grid; gap: 12px;\">\n <div style=\"display:flex; gap: 10px; flex-wrap: wrap;\">\n <mat-slide-toggle [(ngModel)]=\"behavior.closeable\" (ngModelChange)=\"onAppearanceChange()\">closeable</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"behavior.lazyLoad\" (ngModelChange)=\"onAppearanceChange()\">lazyLoad (planejado)</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"behavior.reorderable\" (ngModelChange)=\"onAppearanceChange()\">reorderable (planejado)</mat-slide-toggle>\n </div>\n </div>\n </mat-tab>\n <mat-tab label=\"Grupo\">\n <div style=\"padding: 12px; display:grid; gap: 12px;\">\n <div style=\"display:grid; grid-template-columns: repeat(2, minmax(220px,1fr)); gap: 12px;\">\n <mat-form-field appearance=\"outline\"><mat-label>Alinhamento</mat-label>\n <select matNativeControl [(ngModel)]=\"group.alignTabs\" (ngModelChange)=\"onAppearanceChange()\">\n <option [ngValue]=\"undefined\">padrão</option>\n <option value=\"start\">start</option>\n <option value=\"center\">center</option>\n <option value=\"end\">end</option>\n </select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>Header</mat-label>\n <select matNativeControl [(ngModel)]=\"group.headerPosition\" (ngModelChange)=\"onAppearanceChange()\">\n <option [ngValue]=\"undefined\">above</option>\n <option value=\"above\">above</option>\n <option value=\"below\">below</option>\n </select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>selectedIndex</mat-label>\n <input matInput type=\"number\" [(ngModel)]=\"group.selectedIndex\" (ngModelChange)=\"onAppearanceChange()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>animationDuration</mat-label>\n <input matInput [(ngModel)]=\"group.animationDuration\" (ngModelChange)=\"onAppearanceChange()\" placeholder=\"500ms\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>contentTabIndex</mat-label>\n <input matInput type=\"number\" [(ngModel)]=\"group.contentTabIndex\" (ngModelChange)=\"onAppearanceChange()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>color (M2)</mat-label>\n <select matNativeControl [(ngModel)]=\"group.color\" (ngModelChange)=\"onAppearanceChange()\">\n <option [ngValue]=\"undefined\">(nenhum)</option>\n <option value=\"primary\">primary</option>\n <option value=\"accent\">accent</option>\n <option value=\"warn\">warn</option>\n </select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>backgroundColor (M2)</mat-label>\n <select matNativeControl [(ngModel)]=\"group.backgroundColor\" (ngModelChange)=\"onAppearanceChange()\">\n <option [ngValue]=\"undefined\">(nenhum)</option>\n <option value=\"primary\">primary</option>\n <option value=\"accent\">accent</option>\n <option value=\"warn\">warn</option>\n </select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>aria-label</mat-label>\n <input matInput [(ngModel)]=\"group.ariaLabel\" (ngModelChange)=\"onAppearanceChange()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>aria-labelledby</mat-label>\n <input matInput [(ngModel)]=\"group.ariaLabelledby\" (ngModelChange)=\"onAppearanceChange()\" />\n </mat-form-field>\n </div>\n <div style=\"display:flex; gap: 10px; flex-wrap: wrap;\">\n <mat-slide-toggle [(ngModel)]=\"group.dynamicHeight\" (ngModelChange)=\"onAppearanceChange()\">dynamicHeight</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"group.fitInkBarToContent\" (ngModelChange)=\"onAppearanceChange()\">fitInkBarToContent</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"group.disablePagination\" (ngModelChange)=\"onAppearanceChange()\">disablePagination</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"group.disableRipple\" (ngModelChange)=\"onAppearanceChange()\">disableRipple</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"group.preserveContent\" (ngModelChange)=\"onAppearanceChange()\">preserveContent</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"group.stretchTabs\" (ngModelChange)=\"onAppearanceChange()\">stretchTabs</mat-slide-toggle>\n </div>\n </div>\n </mat-tab>\n\n <mat-tab label=\"Navegação\">\n <div style=\"padding: 12px; display:grid; gap: 12px;\">\n <div style=\"display:grid; grid-template-columns: repeat(2, minmax(220px,1fr)); gap: 12px;\">\n <mat-form-field appearance=\"outline\"><mat-label>selectedIndex</mat-label>\n <input matInput type=\"number\" [(ngModel)]=\"nav.selectedIndex\" (ngModelChange)=\"onAppearanceChange()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>animationDuration</mat-label>\n <input matInput [(ngModel)]=\"nav.animationDuration\" (ngModelChange)=\"onAppearanceChange()\" placeholder=\"500ms\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>color (M2)</mat-label>\n <select matNativeControl [(ngModel)]=\"nav.color\" (ngModelChange)=\"onAppearanceChange()\">\n <option [ngValue]=\"undefined\">(nenhum)</option>\n <option value=\"primary\">primary</option>\n <option value=\"accent\">accent</option>\n <option value=\"warn\">warn</option>\n </select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>backgroundColor (M2)</mat-label>\n <select matNativeControl [(ngModel)]=\"nav.backgroundColor\" (ngModelChange)=\"onAppearanceChange()\">\n <option [ngValue]=\"undefined\">(nenhum)</option>\n <option value=\"primary\">primary</option>\n <option value=\"accent\">accent</option>\n <option value=\"warn\">warn</option>\n </select>\n </mat-form-field>\n </div>\n <div style=\"display:flex; gap: 10px; flex-wrap: wrap;\">\n <mat-slide-toggle [(ngModel)]=\"nav.fitInkBarToContent\" (ngModelChange)=\"onAppearanceChange()\">fitInkBarToContent</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"nav.disablePagination\" (ngModelChange)=\"onAppearanceChange()\">disablePagination</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"nav.disableRipple\" (ngModelChange)=\"onAppearanceChange()\">disableRipple</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"nav.stretchTabs\" (ngModelChange)=\"onAppearanceChange()\">stretchTabs</mat-slide-toggle>\n </div>\n </div>\n </mat-tab>\n <mat-tab label=\"JSON\">\n <div style=\"padding: 12px; display: grid; gap: 12px;\">\n <div class=\"json-editor-toolbar\" style=\"display:flex; gap:8px;\">\n <button mat-button (click)=\"formatJson()\" [disabled]=\"!isValid\"> \n <mat-icon [praxisIcon]=\"'format_align_left'\"></mat-icon>Formatar\n </button>\n <button mat-button (click)=\"reset()\"> \n <mat-icon [praxisIcon]=\"'restart_alt'\"></mat-icon>Resetar\n </button>\n </div>\n\n <mat-form-field appearance=\"outline\" class=\"json-textarea-field\" style=\"width:100%\">\n <mat-label>Configuração JSON</mat-label>\n <textarea\n matInput\n [(ngModel)]=\"jsonText\"\n (ngModelChange)=\"onJsonTextChange($event)\"\n rows=\"22\"\n spellcheck=\"false\"\n style=\"font-family: monospace\"\n ></textarea>\n <mat-hint *ngIf=\"isValid\">JSON válido</mat-hint>\n <mat-error *ngIf=\"!isValid && jsonText\">JSON inválido: {{ errorMsg }}</mat-error>\n </mat-form-field>\n </div>\n </mat-tab>\n\n <mat-tab label=\"Estilo\">\n <div style=\"padding: 12px; display: grid; gap: 16px;\">\n <div style=\"display:flex; gap:8px; align-items:center; flex-wrap: wrap;\">\n <span style=\"opacity:.75\">Presets:</span>\n <button mat-button color=\"primary\" (click)=\"applyPreset('primary')\">\n <mat-icon [praxisIcon]=\"'palette'\"></mat-icon>\n Primário\n </button>\n <button mat-button (click)=\"applyPreset('neutral')\">\n <mat-icon [praxisIcon]=\"'contrast'\"></mat-icon>\n Neutro\n </button>\n <button mat-button (click)=\"applyPreset('high-contrast')\">\n <mat-icon [praxisIcon]=\"'visibility'\"></mat-icon>\n Alto contraste\n </button>\n <button mat-button (click)=\"clearTokens()\">\n <mat-icon [praxisIcon]=\"'backspace'\"></mat-icon>\n Limpar tokens\n </button>\n </div>\n\n <div style=\"display:grid; gap:8px; grid-template-columns: repeat(2, minmax(220px, 1fr));\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Classe de tema (opcional)</mat-label>\n <input matInput [(ngModel)]=\"appearance.themeClass\" (ngModelChange)=\"onAppearanceChange()\" placeholder=\"ex.: tabs-accented\" />\n </mat-form-field>\n\n <mat-form-field appearance=\"outline\">\n <mat-label>Densidade</mat-label>\n <select matNativeControl [(ngModel)]=\"appearance.density\" (ngModelChange)=\"onAppearanceChange()\">\n <option [ngValue]=\"undefined\">padrão</option>\n <option value=\"compact\">compact</option>\n <option value=\"comfortable\">comfortable</option>\n <option value=\"spacious\">spacious</option>\n </select>\n </mat-form-field>\n </div>\n\n <div>\n <h3 style=\"margin: 6px 0 8px;\">Tokens (M3 aproximados)</h3>\n <div style=\"display:grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); gap: 10px;\">\n <ng-container *ngFor=\"let t of tokenList\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ t.label }}</mat-label>\n <input matInput placeholder=\"var(--mat-sys-primary) / #RRGGBB\" [ngModel]=\"appearance.tokens?.[t.key]\" (ngModelChange)=\"onTokenChange(t.key, $event)\" />\n </mat-form-field>\n </ng-container>\n </div>\n <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>\n </div>\n\n <div>\n <h3 style=\"margin: 6px 0 8px;\">CSS Personalizado</h3>\n <mat-form-field appearance=\"outline\" class=\"json-textarea-field\" style=\"width:100%\">\n <mat-label>CSS a ser injetado no componente</mat-label>\n <textarea matInput rows=\"10\" [(ngModel)]=\"appearance.customCss\" (ngModelChange)=\"onAppearanceChange()\" placeholder=\".praxis-tabs-root .mdc-tab__text-label { font-weight: 600; }\"></textarea>\n </mat-form-field>\n </div>\n\n <div>\n <h3 style=\"margin: 6px 0 8px;\">Snippet SCSS (para uso em styles.scss)</h3>\n <pre style=\"white-space: pre-wrap; background: rgba(0,0,0,0.2); padding: 8px; border-radius: 6px;\">\n@use '@angular/material' as mat;\n{{ scssSnippet() }}\n </pre>\n </div>\n </div>\n </mat-tab>\n\n <mat-tab label=\"Abas\">\n <div style=\"padding:12px; display:grid; gap:12px;\">\n <button mat-stroked-button color=\"primary\" (click)=\"addTab()\"><mat-icon [praxisIcon]=\"'add'\"></mat-icon>Adicionar aba</button>\n <div *ngIf=\"editedConfig.tabs?.length; else noTabs\" style=\"display:grid; gap:10px;\">\n <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;\">\n <div style=\"display:flex; align-items:center; gap:8px;\">\n <strong style=\"flex:1\">#{{ i+1 }}</strong>\n <button mat-icon-button (click)=\"moveTab(i, -1)\" [disabled]=\"i===0\"><mat-icon [praxisIcon]=\"'arrow_upward'\"></mat-icon></button>\n <button mat-icon-button (click)=\"moveTab(i, 1)\" [disabled]=\"i===editedConfig.tabs!.length-1\"><mat-icon [praxisIcon]=\"'arrow_downward'\"></mat-icon></button>\n <button mat-icon-button color=\"warn\" (click)=\"removeTab(i)\"><mat-icon [praxisIcon]=\"'delete'\"></mat-icon></button>\n </div>\n <div style=\"display:grid; grid-template-columns: repeat(2, minmax(220px,1fr)); gap: 10px;\">\n <mat-form-field appearance=\"outline\"><mat-label>ID</mat-label>\n <input matInput [(ngModel)]=\"t.id\" (ngModelChange)=\"onAppearanceChange()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>Texto</mat-label>\n <input matInput [(ngModel)]=\"t.textLabel\" (ngModelChange)=\"onAppearanceChange()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>Label class</mat-label>\n <input matInput [(ngModel)]=\"t.labelClass\" (ngModelChange)=\"onAppearanceChange()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>Body class</mat-label>\n <input matInput [(ngModel)]=\"t.bodyClass\" (ngModelChange)=\"onAppearanceChange()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>aria-label</mat-label>\n <input matInput [(ngModel)]=\"t.ariaLabel\" (ngModelChange)=\"onAppearanceChange()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>aria-labelledby</mat-label>\n <input matInput [(ngModel)]=\"t.ariaLabelledby\" (ngModelChange)=\"onAppearanceChange()\" />\n </mat-form-field>\n </div>\n <mat-slide-toggle [(ngModel)]=\"t.disabled\" (ngModelChange)=\"onAppearanceChange()\">disabled</mat-slide-toggle>\n\n <!-- Widgets (componentes dinâmicos) -->\n <div style=\"border-top:1px solid var(--mat-sys-outline-variant); padding-top:8px; display:grid; gap:8px;\">\n <div style=\"display:flex; gap:8px; align-items:center; flex-wrap:wrap;\">\n <mat-form-field appearance=\"outline\" style=\"min-width:260px;\">\n <mat-label>Adicionar componente</mat-label>\n <select matNativeControl [(ngModel)]=\"selectedTabWidgetId[i]\">\n <option [ngValue]=\"''\">(selecione)</option>\n <option *ngFor=\"let c of componentOptions\" [ngValue]=\"c.id\">{{ c.friendlyName || c.id }}</option>\n </select>\n </mat-form-field>\n <button mat-stroked-button (click)=\"addWidgetToTab(i)\"><mat-icon [praxisIcon]=\"'add'\"></mat-icon>Adicionar</button>\n <span style=\"flex:1\"></span>\n <mat-form-field appearance=\"outline\" style=\"width:240px;\">\n <mat-label>resourcePath</mat-label>\n <input matInput [(ngModel)]=\"quickResourcePathTab[i]\" placeholder=\"ex.: usuarios\" />\n </mat-form-field>\n <button mat-button (click)=\"addPresetToTab(i, 'form')\"><mat-icon [praxisIcon]=\"'description'\"></mat-icon>Form</button>\n <button mat-button (click)=\"addPresetToTab(i, 'table')\"><mat-icon [praxisIcon]=\"'grid_on'\"></mat-icon>Tabela</button>\n <button mat-button (click)=\"addPresetToTab(i, 'crud')\"><mat-icon [praxisIcon]=\"'dynamic_form'\"></mat-icon>CRUD</button>\n </div>\n\n <div *ngIf=\"t.widgets?.length\" style=\"display:grid; gap:8px;\" cdkDropList [cdkDropListData]=\"t.widgets || []\" (cdkDropListDropped)=\"onTabWidgetDrop(i, $event)\">\n <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;\">\n <div style=\"display:flex; align-items:center; gap:8px;\">\n <button mat-icon-button cdkDragHandle matTooltip=\"Arrastar para reordenar\" aria-label=\"Arrastar para reordenar\">\n <mat-icon [praxisIcon]=\"'drag_indicator'\"></mat-icon>\n </button>\n <strong style=\"flex:1\">{{ getCompName(w.id) }}</strong>\n <button mat-icon-button color=\"warn\" (click)=\"removeWidgetFromTab(i, wi)\" matTooltip=\"Remover componente\" aria-label=\"Remover componente\"><mat-icon [praxisIcon]=\"'delete'\"></mat-icon></button>\n </div>\n <div style=\"display:grid; grid-template-columns: 1fr 1fr; gap:8px;\">\n <mat-form-field appearance=\"outline\">\n <mat-label>inputs (JSON)</mat-label>\n <textarea matInput rows=\"4\" [ngModel]=\"stringify(w.inputs)\" (ngModelChange)=\"updateWidgetInputsTab(i, wi, $event)\"></textarea>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>outputs (JSON)</mat-label>\n <textarea matInput rows=\"4\" [ngModel]=\"stringify(w.outputs)\" (ngModelChange)=\"updateWidgetOutputsTab(i, wi, $event)\"></textarea>\n </mat-form-field>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n <ng-template #noTabs><em>Nenhuma aba definida.</em></ng-template>\n </div>\n </mat-tab>\n\n <mat-tab label=\"Acessibilidade\">\n <div style=\"padding: 12px; display:grid; gap: 12px;\">\n <div style=\"display:flex; gap: 10px; flex-wrap: wrap;\">\n <mat-slide-toggle [(ngModel)]=\"accessibility.highContrast\" (ngModelChange)=\"onAppearanceChange()\">highContrast</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"accessibility.reduceMotion\" (ngModelChange)=\"onAppearanceChange()\">reduceMotion</mat-slide-toggle>\n </div>\n </div>\n </mat-tab>\n\n <mat-tab label=\"Links\">\n <div style=\"padding:12px; display:grid; gap:12px;\">\n <button mat-stroked-button color=\"primary\" (click)=\"addLink()\"><mat-icon [praxisIcon]=\"'add_link'\"></mat-icon>Adicionar link</button>\n <div *ngIf=\"nav.links?.length; else noLinks\" style=\"display:grid; gap:10px;\">\n <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;\">\n <div style=\"display:flex; align-items:center; gap:8px;\">\n <strong style=\"flex:1\">#{{ i+1 }}</strong>\n <button mat-icon-button (click)=\"moveLink(i, -1)\" [disabled]=\"i===0\"><mat-icon [praxisIcon]=\"'arrow_upward'\"></mat-icon></button>\n <button mat-icon-button (click)=\"moveLink(i, 1)\" [disabled]=\"i===nav.links!.length-1\"><mat-icon [praxisIcon]=\"'arrow_downward'\"></mat-icon></button>\n <button mat-icon-button color=\"warn\" (click)=\"removeLink(i)\"><mat-icon [praxisIcon]=\"'delete'\"></mat-icon></button>\n </div>\n <div style=\"display:grid; grid-template-columns: repeat(2, minmax(220px,1fr)); gap: 10px;\">\n <mat-form-field appearance=\"outline\"><mat-label>ID</mat-label>\n <input matInput [(ngModel)]=\"l.id\" (ngModelChange)=\"onAppearanceChange()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>Label</mat-label>\n <input matInput [(ngModel)]=\"l.label\" (ngModelChange)=\"onAppearanceChange()\" />\n </mat-form-field>\n </div>\n <div style=\"display:flex; gap:10px; flex-wrap:wrap;\">\n <mat-slide-toggle [(ngModel)]=\"l.active\" (ngModelChange)=\"onAppearanceChange()\">active</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"l.disabled\" (ngModelChange)=\"onAppearanceChange()\">disabled</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"l.disableRipple\" (ngModelChange)=\"onAppearanceChange()\">disableRipple</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"l.fitInkBarToContent\" (ngModelChange)=\"onAppearanceChange()\">fitInkBarToContent</mat-slide-toggle>\n </div>\n\n <!-- Widgets para Links -->\n <div style=\"border-top:1px solid var(--mat-sys-outline-variant); padding-top:8px; display:grid; gap:8px;\">\n <div style=\"display:flex; gap:8px; align-items:center; flex-wrap:wrap;\">\n <mat-form-field appearance=\"outline\" style=\"min-width:260px;\">\n <mat-label>Adicionar componente</mat-label>\n <select matNativeControl [(ngModel)]=\"selectedLinkWidgetId[i]\">\n <option [ngValue]=\"''\">(selecione)</option>\n <option *ngFor=\"let c of componentOptions\" [ngValue]=\"c.id\">{{ c.friendlyName || c.id }}</option>\n </select>\n </mat-form-field>\n <button mat-stroked-button (click)=\"addWidgetToLink(i)\"><mat-icon [praxisIcon]=\"'add'\"></mat-icon>Adicionar</button>\n <span style=\"flex:1\"></span>\n <mat-form-field appearance=\"outline\" style=\"width:240px;\">\n <mat-label>resourcePath</mat-label>\n <input matInput [(ngModel)]=\"quickResourcePathLink[i]\" placeholder=\"ex.: usuarios\" />\n </mat-form-field>\n <button mat-button (click)=\"addPresetToLink(i, 'form')\"><mat-icon [praxisIcon]=\"'description'\"></mat-icon>Form</button>\n <button mat-button (click)=\"addPresetToLink(i, 'table')\"><mat-icon [praxisIcon]=\"'grid_on'\"></mat-icon>Tabela</button>\n <button mat-button (click)=\"addPresetToLink(i, 'crud')\"><mat-icon [praxisIcon]=\"'dynamic_form'\"></mat-icon>CRUD</button>\n </div>\n\n <div *ngIf=\"l.widgets?.length\" style=\"display:grid; gap:8px;\" cdkDropList [cdkDropListData]=\"l.widgets || []\" (cdkDropListDropped)=\"onLinkWidgetDrop(i, $event)\">\n <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;\">\n <div style=\"display:flex; align-items:center; gap:8px;\">\n <button mat-icon-button cdkDragHandle matTooltip=\"Arrastar para reordenar\" aria-label=\"Arrastar para reordenar\">\n <mat-icon [praxisIcon]=\"'drag_indicator'\"></mat-icon>\n </button>\n <strong style=\"flex:1\">{{ getCompName(w.id) }}</strong>\n <button mat-icon-button color=\"warn\" (click)=\"removeWidgetFromLink(i, wi)\" matTooltip=\"Remover componente\" aria-label=\"Remover componente\"><mat-icon [praxisIcon]=\"'delete'\"></mat-icon></button>\n </div>\n <div style=\"display:grid; grid-template-columns: 1fr 1fr; gap:8px;\">\n <mat-form-field appearance=\"outline\">\n <mat-label>inputs (JSON)</mat-label>\n <textarea matInput rows=\"4\" [ngModel]=\"stringify(w.inputs)\" (ngModelChange)=\"updateWidgetInputsLink(i, wi, $event)\"></textarea>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>outputs (JSON)</mat-label>\n <textarea matInput rows=\"4\" [ngModel]=\"stringify(w.outputs)\" (ngModelChange)=\"updateWidgetOutputsLink(i, wi, $event)\"></textarea>\n </mat-form-field>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n <ng-template #noLinks><em>Nenhum link definido.</em></ng-template>\n </div>\n </mat-tab>\n </mat-tab-group>\n `,\n})\nexport class PraxisTabsConfigEditor implements SettingsValueProvider {\n editedConfig: TabsMetadata;\n private initialConfig: TabsMetadata;\n jsonText = '';\n isValid = true;\n errorMsg = '';\n componentOptions: Array<{ id: string; friendlyName?: string }> = [];\n selectedTabWidgetId: Record<number, string> = {};\n selectedLinkWidgetId: Record<number, string> = {};\n quickResourcePathTab: Record<number, string> = {};\n quickResourcePathLink: Record<number, string> = {};\n\n // Simplified token list we support at runtime\n tokenList = [\n { key: 'active-indicator-color', label: 'Indicador ativo' },\n { key: 'active-label-text-color', label: 'Texto ativo' },\n { key: 'active-hover-indicator-color', label: 'Indicador ativo (hover)' },\n { key: 'active-hover-label-text-color', label: 'Texto ativo (hover)' },\n { key: 'active-focus-indicator-color', label: 'Indicador ativo (focus)' },\n { key: 'active-focus-label-text-color', label: 'Texto ativo (focus)' },\n { key: 'inactive-label-text-color', label: 'Texto inativo' },\n { key: 'inactive-hover-label-text-color', label: 'Texto inativo (hover)' },\n { key: 'inactive-focus-label-text-color', label: 'Texto inativo (focus)' },\n { key: 'pagination-icon-color', label: 'Ícones de paginação' },\n { key: 'divider-color', label: 'Divisor/linha' },\n { key: 'background-color', label: 'Fundo do header' },\n ] as const;\n\n private presets: Record<string, Record<string, string>> = {\n primary: {\n 'active-indicator-color': 'var(--mat-sys-primary)',\n 'active-label-text-color': 'var(--mat-sys-primary)',\n 'inactive-label-text-color': 'rgba(var(--mat-sys-on-surface-rgb), 0.72)',\n 'inactive-hover-label-text-color': 'var(--mat-sys-on-surface)',\n 'divider-color': 'rgba(255, 255, 255, 0.12)',\n 'background-color': 'var(--mat-sys-surface-container)',\n 'pagination-icon-color': 'var(--mat-sys-on-surface)'\n },\n neutral: {\n 'active-indicator-color': 'var(--mat-sys-on-surface)',\n 'active-label-text-color': 'var(--mat-sys-on-surface)',\n 'inactive-label-text-color': 'rgba(var(--mat-sys-on-surface-rgb), 0.56)',\n 'inactive-hover-label-text-color': 'var(--mat-sys-on-surface)',\n 'divider-color': 'rgba(255, 255, 255, 0.10)',\n 'background-color': 'var(--mat-sys-surface-container)',\n 'pagination-icon-color': 'var(--mat-sys-on-surface)'\n },\n 'high-contrast': {\n 'active-indicator-color': '#FFD54F',\n 'active-label-text-color': '#FFFFFF',\n 'inactive-label-text-color': '#BDBDBD',\n 'inactive-hover-label-text-color': '#EEEEEE',\n 'divider-color': '#FFFFFF',\n 'background-color': 'var(--mat-sys-surface)',\n 'pagination-icon-color': '#FFFFFF'\n }\n };\n\n isDirty$ = new BehaviorSubject<boolean>(false);\n isValid$ = new BehaviorSubject<boolean>(true);\n isBusy$ = new BehaviorSubject<boolean>(false);\n\n constructor(@Inject(SETTINGS_PANEL_DATA) data: any, private registry: ComponentMetadataRegistry) {\n const cfg: TabsMetadata = data?.config || data || ({} as TabsMetadata);\n this.initialConfig = structuredClone(cfg);\n this.editedConfig = structuredClone(cfg);\n this.isValid$.next(true);\n this.jsonText = this.stringify(this.editedConfig);\n this.updateDirty();\n this.componentOptions = this.registry.getAll().map((m) => ({ id: m.id, friendlyName: m.friendlyName }));\n }\n\n private updateDirty(): void {\n this.isDirty$.next(\n JSON.stringify(this.initialConfig) !== JSON.stringify(this.editedConfig),\n );\n }\n\n onJsonTextChange(text: string): void {\n try {\n const parsed = JSON.parse(text) as TabsMetadata;\n this.errorMsg = '';\n this.isValid = true;\n this.isValid$.next(true);\n this.editedConfig = parsed;\n this.updateDirty();\n } catch (e: any) {\n this.isValid = false;\n this.isValid$.next(false);\n this.errorMsg = e?.message || 'Erro de sintaxe JSON';\n }\n }\n\n // stringify helper is public to be used in the template\n\n formatJson(): void {\n if (!this.isValid) return;\n this.jsonText = this.stringify(this.editedConfig);\n }\n\n getSettingsValue(): any {\n return { config: this.editedConfig };\n }\n\n onSave(): any {\n return { config: this.editedConfig };\n }\n\n reset(): void {\n this.editedConfig = structuredClone(this.initialConfig);\n this.jsonText = this.stringify(this.editedConfig);\n this.isValid = true;\n this.isValid$.next(true);\n this.updateDirty();\n }\n\n // Appearance helpers\n get appearance(): NonNullable<TabsMetadata['appearance']> {\n const ap = (this.editedConfig.appearance ??= {} as any);\n (ap.tokens ??= {} as any);\n return ap as any;\n }\n\n onAppearanceChange(): void {\n this.updateDirty();\n this.jsonText = this.stringify(this.editedConfig);\n }\n\n onTokenChange(key: string, value: string): void {\n const ap = this.appearance;\n const tokens = (ap.tokens = ap.tokens || {});\n if (!value) {\n delete tokens[key as keyof typeof tokens];\n } else {\n (tokens as any)[key] = value;\n }\n this.onAppearanceChange();\n }\n\n clearTokens(): void {\n const ap = this.appearance;\n ap.tokens = {} as any;\n this.onAppearanceChange();\n }\n\n applyPreset(id: 'primary' | 'neutral' | 'high-contrast'): void {\n const preset = this.presets[id];\n if (!preset) return;\n const ap = this.appearance;\n const tokens = (ap.tokens = ap.tokens || {});\n for (const [k, v] of Object.entries(preset)) {\n (tokens as any)[k] = v;\n }\n this.onAppearanceChange();\n }\n\n scssSnippet(): string {\n const tokens = this.editedConfig.appearance?.tokens || {};\n const entries = Object.entries(tokens).filter(([, v]) => !!v);\n const selector = this.editedConfig.appearance?.themeClass\n ? `.${this.editedConfig.appearance.themeClass}`\n : ':root';\n const map = entries.map(([k, v]) => ` ${k}: ${v},`).join('\\n');\n return `${selector} {\\n @include mat.tabs-overrides((\\n${map}\\n ));\\n}`;\n }\n\n // Group/Nav helpers and list editors\n get group(): any {\n if (!this.editedConfig.group) this.editedConfig.group = {} as any;\n return this.editedConfig.group as any;\n }\n\n get nav(): any {\n const nav = (this.editedConfig.nav ??= { links: [] } as any);\n if (!nav.links) nav.links = [] as any;\n return nav as any;\n }\n\n get behavior(): any {\n if (!this.editedConfig.behavior) this.editedConfig.behavior = {} as any;\n // Default to lazy load to improve UX/perf with heavy widgets (table/form)\n if (typeof (this.editedConfig.behavior as any).lazyLoad === 'undefined') {\n (this.editedConfig.behavior as any).lazyLoad = true;\n }\n return this.editedConfig.behavior as any;\n }\n\n get accessibility(): any {\n if (!this.editedConfig.accessibility) this.editedConfig.accessibility = {} as any;\n return this.editedConfig.accessibility as any;\n }\n\n addTab(): void {\n if (!this.editedConfig.tabs) this.editedConfig.tabs = [];\n this.editedConfig.tabs.push({ id: `tab${(this.editedConfig.tabs.length + 1)}`, textLabel: 'Nova aba' } as any);\n this.onAppearanceChange();\n }\n\n removeTab(index: number): void {\n this.editedConfig.tabs?.splice(index, 1);\n this.onAppearanceChange();\n }\n\n moveTab(index: number, delta: number): void {\n if (!this.editedConfig.tabs) return;\n const i2 = index + delta;\n if (i2 < 0 || i2 >= this.editedConfig.tabs.length) return;\n const [item] = this.editedConfig.tabs.splice(index, 1);\n this.editedConfig.tabs.splice(i2, 0, item);\n this.onAppearanceChange();\n }\n // ===== Widgets (Tabs) =====\n addWidgetToTab(tabIndex: number): void {\n const id = (this.selectedTabWidgetId[tabIndex] || '').trim();\n if (!id) return;\n const t = this.editedConfig.tabs![tabIndex] as any;\n t.widgets = t.widgets || [];\n t.widgets.push({ id } as WidgetDefinition);\n this.onAppearanceChange();\n }\n removeWidgetFromTab(tabIndex: number, widgetIndex: number): void {\n const t = this.editedConfig.tabs![tabIndex] as any;\n if (!t.widgets) return;\n t.widgets.splice(widgetIndex, 1);\n this.onAppearanceChange();\n }\n updateWidgetInputsTab(tabIndex: number, widgetIndex: number, text: string): void {\n const t = this.editedConfig.tabs![tabIndex] as any;\n try { const obj = text ? JSON.parse(text) : undefined; t.widgets[widgetIndex].inputs = obj; this.onAppearanceChange(); } catch {}\n }\n updateWidgetOutputsTab(tabIndex: number, widgetIndex: number, text: string): void {\n const t = this.editedConfig.tabs![tabIndex] as any;\n try { const obj = text ? JSON.parse(text) : undefined; t.widgets[widgetIndex].outputs = obj; this.onAppearanceChange(); } catch {}\n }\n addPresetToTab(tabIndex: number, type: 'form' | 'table' | 'crud'): void {\n const rp = (this.quickResourcePathTab[tabIndex] || '').trim();\n if (!rp) return;\n const t = this.editedConfig.tabs![tabIndex] as any;\n t.widgets = t.widgets || [];\n if (type === 'form') {\n t.widgets.push({ id: 'praxis-dynamic-form', inputs: { resourcePath: rp } });\n } else if (type === 'table') {\n t.widgets.push({ id: 'praxis-table', inputs: { resourcePath: rp } });\n } else {\n t.widgets.push({ id: 'praxis-crud', inputs: { metadata: { resource: { path: rp } } } });\n }\n this.onAppearanceChange();\n }\n\n addLink(): void {\n if (!this.nav.links) this.nav.links = [];\n this.nav.links.push({ id: `link${this.nav.links.length + 1}`, label: 'Novo link' });\n this.onAppearanceChange();\n }\n\n removeLink(index: number): void {\n if (!this.nav.links) return;\n this.nav.links.splice(index, 1);\n this.onAppearanceChange();\n }\n\n moveLink(index: number, delta: number): void {\n if (!this.nav.links) return;\n const i2 = index + delta;\n if (i2 < 0 || i2 >= this.nav.links.length) return;\n const [item] = this.nav.links.splice(index, 1);\n this.nav.links.splice(i2, 0, item);\n this.onAppearanceChange();\n }\n // ===== Widgets (Links) =====\n addWidgetToLink(linkIndex: number): void {\n const id = (this.selectedLinkWidgetId[linkIndex] || '').trim();\n if (!id) return;\n const l = this.nav.links[linkIndex] as any;\n l.widgets = l.widgets || [];\n l.widgets.push({ id } as WidgetDefinition);\n this.onAppearanceChange();\n }\n removeWidgetFromLink(linkIndex: number, widgetIndex: number): void {\n const l = this.nav.links[linkIndex] as any;\n if (!l.widgets) return;\n l.widgets.splice(widgetIndex, 1);\n this.onAppearanceChange();\n }\n updateWidgetInputsLink(linkIndex: number, widgetIndex: number, text: string): void {\n const l = this.nav.links[linkIndex] as any;\n try { const obj = text ? JSON.parse(text) : undefined; l.widgets[widgetIndex].inputs = obj; this.onAppearanceChange(); } catch {}\n }\n updateWidgetOutputsLink(linkIndex: number, widgetIndex: number, text: string): void {\n const l = this.nav.links[linkIndex] as any;\n try { const obj = text ? JSON.parse(text) : undefined; l.widgets[widgetIndex].outputs = obj; this.onAppearanceChange(); } catch {}\n }\n addPresetToLink(linkIndex: number, type: 'form' | 'table' | 'crud'): void {\n const rp = (this.quickResourcePathLink[linkIndex] || '').trim();\n if (!rp) return;\n const l = this.nav.links[linkIndex] as any;\n l.widgets = l.widgets || [];\n if (type === 'form') {\n l.widgets.push({ id: 'praxis-dynamic-form', inputs: { resourcePath: rp } });\n } else if (type === 'table') {\n l.widgets.push({ id: 'praxis-table', inputs: { resourcePath: rp } });\n } else {\n l.widgets.push({ id: 'praxis-crud', inputs: { metadata: { resource: { path: rp } } } });\n }\n this.onAppearanceChange();\n }\n\n stringify(v: any): string { try { return v ? JSON.stringify(v, null, 2) : ''; } catch { return ''; } }\n getCompName(id: string): string { const c = this.componentOptions.find(o => o.id === id); return c?.friendlyName || id; }\n\n // Reorder handlers (drag & drop)\n onTabWidgetDrop(tabIndex: number, event: CdkDragDrop<WidgetDefinition[]>): void {\n const t = this.editedConfig.tabs?.[tabIndex] as any;\n if (!t?.widgets) return;\n moveItemInArray(t.widgets, event.previousIndex, event.currentIndex);\n this.onAppearanceChange();\n }\n\n onLinkWidgetDrop(linkIndex: number, event: CdkDragDrop<WidgetDefinition[]>): void {\n const l = this.nav.links?.[linkIndex] as any;\n if (!l?.widgets) return;\n moveItemInArray(l.widgets, event.previousIndex, event.currentIndex);\n this.onAppearanceChange();\n }\n}\n","import { Component, Inject } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatInputModule } from '@angular/material/input';\nimport { MatIconModule } from '@angular/material/icon';\nimport { PraxisIconDirective } from '@praxisui/core';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatSlideToggleModule } from '@angular/material/slide-toggle';\nimport { BehaviorSubject } from 'rxjs';\nimport { SETTINGS_PANEL_DATA, SettingsValueProvider } from '@praxisui/settings-panel';\nimport { TabsMetadata } from '../praxis-tabs';\n\ninterface QuickSetupModel {\n mode: 'group' | 'nav';\n labels: string[];\n dynamicHeight?: boolean;\n stretchTabs?: boolean;\n}\n\n@Component({\n selector: 'praxis-tabs-quick-setup',\n standalone: true,\n imports: [\n CommonModule,\n FormsModule,\n MatFormFieldModule,\n MatInputModule,\n MatIconModule,\n PraxisIconDirective,\n MatButtonModule,\n MatSlideToggleModule,\n ],\n template: `\n <div style=\"display:grid; gap: 12px; padding: 8px;\">\n <div style=\"display:flex; gap: 12px; align-items:center; flex-wrap:wrap;\">\n <button mat-stroked-button [color]=\"model.mode==='group' ? 'primary' : undefined\" (click)=\"setMode('group')\">\n <mat-icon [praxisIcon]=\"'tab'\"></mat-icon>\n Abas (TabGroup)\n </button>\n <button mat-stroked-button [color]=\"model.mode==='nav' ? 'primary' : undefined\" (click)=\"setMode('nav')\">\n <mat-icon [praxisIcon]=\"'segment'\"></mat-icon>\n Navegação (TabNav)\n </button>\n </div>\n\n <div style=\"display:grid; grid-template-columns: 1fr auto; gap: 8px; align-items:start;\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Adicionar label</mat-label>\n <input matInput [(ngModel)]=\"newLabel\" (keyup.enter)=\"addLabel()\" placeholder=\"Ex.: Dados Gerais\" />\n </mat-form-field>\n <button mat-flat-button color=\"primary\" (click)=\"addLabel()\">\n <mat-icon [praxisIcon]=\"'add'\"></mat-icon>\n Adicionar\n </button>\n </div>\n\n <div *ngIf=\"model.labels.length; else emptyLabels\" style=\"display:flex; flex-wrap:wrap; gap: 8px;\">\n <div *ngFor=\"let l of model.labels; let i = index\" class=\"chip\">\n <span>{{ l }}</span>\n <button mat-icon-button (click)=\"removeLabel(i)\"><mat-icon [praxisIcon]=\"'close'\"></mat-icon></button>\n </div>\n </div>\n <ng-template #emptyLabels>\n <em>Adicione 1 ou mais labels para criar as abas.</em>\n </ng-template>\n\n <div style=\"display:flex; gap: 12px; flex-wrap:wrap;\">\n <mat-slide-toggle [(ngModel)]=\"model.dynamicHeight\">dynamicHeight</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"model.stretchTabs\">stretchTabs</mat-slide-toggle>\n </div>\n </div>\n `,\n styles: [`\n .chip { display:inline-flex; align-items:center; gap:6px; padding:4px 8px; border: 1px solid var(--mat-sys-outline-variant); border-radius: 16px; }\n `],\n})\nexport class TabsQuickSetupComponent implements SettingsValueProvider {\n model: QuickSetupModel = { mode: 'group', labels: [], dynamicHeight: true, stretchTabs: true };\n private initial: QuickSetupModel = { ...this.model };\n newLabel = '';\n\n isDirty$ = new BehaviorSubject<boolean>(false);\n isValid$ = new BehaviorSubject<boolean>(false);\n isBusy$ = new BehaviorSubject<boolean>(false);\n\n constructor(@Inject(SETTINGS_PANEL_DATA) data: any) {\n // Optionally seed from existing config\n const cfg = (data?.config as TabsMetadata | undefined) || undefined;\n if (cfg) {\n if (cfg.tabs?.length) {\n this.model.mode = 'group';\n this.model.labels = cfg.tabs.map((t) => t.textLabel || 'Aba');\n this.model.dynamicHeight = cfg.group?.dynamicHeight ?? true;\n this.model.stretchTabs = cfg.group?.stretchTabs ?? true;\n } else if (cfg.nav?.links?.length) {\n this.model.mode = 'nav';\n this.model.labels = cfg.nav.links.map((l) => l.label || 'Link');\n this.model.stretchTabs = cfg.nav.stretchTabs ?? true;\n }\n this.initial = { ...this.model, labels: [...this.model.labels] };\n }\n this.updateState();\n }\n\n private updateState(): void {\n const dirty = JSON.stringify(this.model) !== JSON.stringify(this.initial);\n this.isDirty$.next(dirty);\n this.isValid$.next(this.model.labels.length > 0);\n }\n\n setMode(mode: 'group' | 'nav'): void {\n this.model.mode = mode;\n this.updateState();\n }\n\n addLabel(): void {\n const l = (this.newLabel || '').trim();\n if (!l) return;\n this.model.labels.push(l);\n this.newLabel = '';\n this.updateState();\n }\n\n removeLabel(i: number): void {\n this.model.labels.splice(i, 1);\n this.updateState();\n }\n\n private buildConfig(): TabsMetadata {\n if (this.model.mode === 'group') {\n return {\n group: {\n dynamicHeight: !!this.model.dynamicHeight,\n stretchTabs: !!this.model.stretchTabs,\n selectedIndex: 0,\n },\n tabs: this.model.labels.map((label, idx) => ({ id: `tab${idx + 1}`, textLabel: label })),\n };\n }\n return {\n nav: {\n selectedIndex: 0,\n stretchTabs: !!this.model.stretchTabs,\n links: this.model.labels.map((label, idx) => ({ id: `link${idx + 1}`, label })),\n },\n } as TabsMetadata;\n }\n\n getSettingsValue(): any {\n return { config: this.buildConfig() };\n }\n\n onSave(): any {\n return { config: this.buildConfig() };\n }\n}\n","import {\n Component,\n Input,\n Output,\n EventEmitter,\n ChangeDetectionStrategy,\n OnChanges,\n SimpleChanges,\n OnInit,\n inject,\n signal,\n computed,\n} from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { MatTabsModule, MatTabChangeEvent } from '@angular/material/tabs';\nimport { MatIconModule } from '@angular/material/icon';\nimport { PraxisIconDirective } from '@praxisui/core';\nimport { MatButtonModule } from '@angular/material/button';\nimport { FormGroup, ReactiveFormsModule } from '@angular/forms';\nimport { DynamicFieldLoaderDirective } from '@praxisui/dynamic-fields';\nimport { SettingsPanelService } from '@praxisui/settings-panel';\nimport { CONFIG_STORAGE, ConfigStorage } from '@praxisui/core';\nimport { PraxisTabsConfigEditor } from './praxis-tabs-config-editor';\nimport { produce } from 'immer';\nimport { DragDropModule, CdkDragDrop } from '@angular/cdk/drag-drop';\nimport { EmptyStateCardComponent, DynamicWidgetLoaderDirective, WidgetDefinition } from '@praxisui/core';\nimport { TabsQuickSetupComponent } from './quick-setup/tabs-quick-setup.component';\nimport { MatSnackBar } from '@angular/material/snack-bar';\n\n// =====================\n// Metadata interfaces\n// =====================\nexport interface TabsMetadata {\n meta?: any;\n appearance?: TabsAppearanceConfig;\n behavior?: TabsBehaviorConfig;\n events?: TabsEventConfig;\n accessibility?: TabsAccessibilityConfig;\n group?: TabGroupMetadata;\n tabs?: TabMetadata[];\n nav?: TabNavMetadata;\n}\n\nexport interface TabsAppearanceConfig {\n density?: 'compact' | 'comfortable' | 'spacious';\n themeClass?: string;\n customCss?: string;\n /** Optional runtime tokens mapped to CSS for per-instance styling */\n tokens?: Partial<TabsStyleTokens>;\n}\n\nexport interface TabsBehaviorConfig {\n lazyLoad?: boolean;\n closeable?: boolean;\n reorderable?: boolean;\n}\n\nexport interface TabsEventConfig {\n // Placeholder for docs parity; events are component outputs\n}\n\nexport interface TabsAccessibilityConfig {\n ariaLabels?: { [key: string]: string };\n keyboardNavigation?: boolean;\n highContrast?: boolean;\n reduceMotion?: boolean;\n}\n\nexport interface TabGroupMetadata {\n alignTabs?: 'start' | 'center' | 'end';\n animationDuration?: string;\n ariaLabel?: string;\n ariaLabelledby?: string;\n color?: 'primary' | 'accent' | 'warn';\n contentTabIndex?: number;\n disablePagination?: boolean;\n disableRipple?: boolean;\n dynamicHeight?: boolean;\n fitInkBarToContent?: boolean;\n headerPosition?: 'above' | 'below';\n preserveContent?: boolean;\n selectedIndex?: number;\n stretchTabs?: boolean;\n backgroundColor?: 'primary' | 'accent' | 'warn' | undefined;\n}\n\nexport interface TabMetadata {\n id?: string;\n ariaLabel?: string;\n ariaLabelledby?: string;\n textLabel?: string;\n labelClass?: string | string[];\n bodyClass?: string | string[];\n disabled?: boolean;\n content?: any[]; // DynamicFieldMetadata[]\n widgets?: WidgetDefinition[];\n isActive?: boolean;\n origin?: number;\n position?: number;\n}\n\nexport interface TabNavMetadata {\n animationDuration?: string;\n color?: 'primary' | 'accent' | 'warn';\n backgroundColor?: 'primary' | 'accent' | 'warn' | undefined;\n disablePagination?: boolean;\n disableRipple?: boolean;\n fitInkBarToContent?: boolean;\n selectedIndex?: number;\n stretchTabs?: boolean;\n links: TabLinkMetadata[];\n}\n\nexport interface TabLinkMetadata {\n id?: string;\n label: string;\n active?: boolean;\n disabled?: boolean;\n disableRipple?: boolean;\n fitInkBarToContent?: boolean;\n content?: any[]; // DynamicFieldMetadata[]\n widgets?: WidgetDefinition[];\n}\n\n// A minimal set of style tokens mapped to CSS at runtime.\nexport interface TabsStyleTokens {\n 'active-indicator-color': string;\n 'active-focus-indicator-color': string;\n 'active-hover-indicator-color': string;\n 'active-label-text-color': string;\n 'active-focus-label-text-color': string;\n 'active-hover-label-text-color': string;\n 'inactive-label-text-color': string;\n 'inactive-hover-label-text-color': string;\n 'inactive-focus-label-te