UNPKG

@praxisui/page-builder

Version:

Page and widget builder utilities for Praxis UI (grid, dynamic widgets, editors).

597 lines (589 loc) 84.8 kB
import * as i3$2 from '@angular/common'; import { CommonModule } from '@angular/common'; import * as i0 from '@angular/core'; import { Inject, ChangeDetectionStrategy, Component, EventEmitter, Output, Input, signal, computed, effect } from '@angular/core'; import * as i2 from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button'; import * as i1 from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; import * as i3 from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon'; import * as i2$1 from '@praxisui/core'; import { PraxisIconDirective } from '@praxisui/core'; import * as i3$1 from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip'; import * as i6 from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field'; import * as i7 from '@angular/material/input'; import { MatInputModule } from '@angular/material/input'; import * as i4 from '@angular/forms'; import { FormsModule } from '@angular/forms'; import * as i9 from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select'; import * as i12 from '@angular/material/list'; import { MatListModule } from '@angular/material/list'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatMenuModule } from '@angular/material/menu'; import * as i3$3 from '@angular/material/snack-bar'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatTabsModule } from '@angular/material/tabs'; import * as i8 from '@angular/cdk/scrolling'; import { ScrollingModule } from '@angular/cdk/scrolling'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatCheckboxModule } from '@angular/material/checkbox'; const PLACEHOLDER = 1; class ConfirmDialogComponent { data; ref; constructor(data, ref) { this.data = data; this.ref = ref; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: ConfirmDialogComponent, deps: [{ token: MAT_DIALOG_DATA }, { token: i1.MatDialogRef }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.4", type: ConfirmDialogComponent, isStandalone: true, selector: "praxis-confirm-dialog", ngImport: i0, template: ` <h2 mat-dialog-title> <mat-icon aria-hidden="true" style="vertical-align: middle; margin-right: 8px;" [praxisIcon]="data.icon || 'warning'"></mat-icon> {{ data.title || 'Confirmar ação' }} </h2> <div mat-dialog-content> <p>{{ data.message || 'Tem certeza que deseja prosseguir?' }}</p> </div> <div mat-dialog-actions align="end"> <button mat-button mat-dialog-close>{{ data.cancelLabel || 'Cancelar' }}</button> <button mat-raised-button color="warn" [mat-dialog-close]="true">{{ data.confirmLabel || 'Excluir' }}</button> </div> `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: MatDialogModule }, { kind: "directive", type: i1.MatDialogClose, selector: "[mat-dialog-close], [matDialogClose]", inputs: ["aria-label", "type", "mat-dialog-close", "matDialogClose"], exportAs: ["matDialogClose"] }, { kind: "directive", type: i1.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i1.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i1.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.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: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: ConfirmDialogComponent, decorators: [{ type: Component, args: [{ selector: 'praxis-confirm-dialog', standalone: true, imports: [CommonModule, MatDialogModule, MatButtonModule, MatIconModule, PraxisIconDirective], template: ` <h2 mat-dialog-title> <mat-icon aria-hidden="true" style="vertical-align: middle; margin-right: 8px;" [praxisIcon]="data.icon || 'warning'"></mat-icon> {{ data.title || 'Confirmar ação' }} </h2> <div mat-dialog-content> <p>{{ data.message || 'Tem certeza que deseja prosseguir?' }}</p> </div> <div mat-dialog-actions align="end"> <button mat-button mat-dialog-close>{{ data.cancelLabel || 'Cancelar' }}</button> <button mat-raised-button color="warn" [mat-dialog-close]="true">{{ data.confirmLabel || 'Excluir' }}</button> </div> `, changeDetection: ChangeDetectionStrategy.OnPush, }] }], ctorParameters: () => [{ type: undefined, decorators: [{ type: Inject, args: [MAT_DIALOG_DATA] }] }, { type: i1.MatDialogRef }] }); class TileToolbarComponent { selected = false; widgetType = null; remove = new EventEmitter(); settings = new EventEmitter(); static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: TileToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.4", type: TileToolbarComponent, isStandalone: true, selector: "praxis-tile-toolbar", inputs: { selected: "selected", widgetType: "widgetType" }, outputs: { remove: "remove", settings: "settings" }, ngImport: i0, template: ` <div class="pdx-tile-toolbar" [class.always-visible]="selected" role="toolbar" aria-label="Ações do widget"> <button mat-mini-fab (click)="settings.emit()" [matTooltip]="'Configurar widget'" aria-label="Configurar widget" > <mat-icon [praxisIcon]="'settings'"></mat-icon> </button> <button mat-mini-fab color="primary" class="pdx-drag-handle" [matTooltip]="'Arrastar tile'" aria-label="Arrastar tile" > <mat-icon [praxisIcon]="'drag_indicator'"></mat-icon> </button> <button mat-mini-fab color="warn" (click)="remove.emit()" [matTooltip]="'Excluir widget'" aria-label="Excluir widget" > <mat-icon [praxisIcon]="'delete'"></mat-icon> </button> </div> `, isInline: true, styles: [":host{position:absolute;inset:0;pointer-events:none}.pdx-tile-toolbar{position:absolute;right:6px;top:-18px;display:flex;gap:6px;z-index:5;opacity:0;transition:opacity .12s ease-in-out;pointer-events:auto;background:color-mix(in oklab,var(--md-sys-color-surface) 92%,transparent);-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);border-radius:999px;padding:4px;box-shadow:var(--mat-elevation-level3, 0 4px 8px rgba(0,0,0,.2))}.pdx-tile-toolbar.always-visible{opacity:1}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatMiniFabButton, selector: "button[mat-mini-fab], a[mat-mini-fab], button[matMiniFab], a[matMiniFab]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: TileToolbarComponent, decorators: [{ type: Component, args: [{ selector: 'praxis-tile-toolbar', standalone: true, imports: [CommonModule, MatButtonModule, MatIconModule, MatTooltipModule, PraxisIconDirective], template: ` <div class="pdx-tile-toolbar" [class.always-visible]="selected" role="toolbar" aria-label="Ações do widget"> <button mat-mini-fab (click)="settings.emit()" [matTooltip]="'Configurar widget'" aria-label="Configurar widget" > <mat-icon [praxisIcon]="'settings'"></mat-icon> </button> <button mat-mini-fab color="primary" class="pdx-drag-handle" [matTooltip]="'Arrastar tile'" aria-label="Arrastar tile" > <mat-icon [praxisIcon]="'drag_indicator'"></mat-icon> </button> <button mat-mini-fab color="warn" (click)="remove.emit()" [matTooltip]="'Excluir widget'" aria-label="Excluir widget" > <mat-icon [praxisIcon]="'delete'"></mat-icon> </button> </div> `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{position:absolute;inset:0;pointer-events:none}.pdx-tile-toolbar{position:absolute;right:6px;top:-18px;display:flex;gap:6px;z-index:5;opacity:0;transition:opacity .12s ease-in-out;pointer-events:auto;background:color-mix(in oklab,var(--md-sys-color-surface) 92%,transparent);-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);border-radius:999px;padding:4px;box-shadow:var(--mat-elevation-level3, 0 4px 8px rgba(0,0,0,.2))}.pdx-tile-toolbar.always-visible{opacity:1}\n"] }] }], propDecorators: { selected: [{ type: Input }], widgetType: [{ type: Input }], remove: [{ type: Output }], settings: [{ type: Output }] } }); class FloatingToolbarComponent { visible = false; canUndo = false; canRedo = false; add = new EventEmitter(); undo = new EventEmitter(); redo = new EventEmitter(); settings = new EventEmitter(); preview = new EventEmitter(); connections = new EventEmitter(); connectionsEdit = new EventEmitter(); connectionsVisual = new EventEmitter(); save = new EventEmitter(); static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: FloatingToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.4", type: FloatingToolbarComponent, isStandalone: true, selector: "praxis-floating-toolbar", inputs: { visible: "visible", canUndo: "canUndo", canRedo: "canRedo" }, outputs: { add: "add", undo: "undo", redo: "redo", settings: "settings", preview: "preview", connections: "connections", connectionsEdit: "connectionsEdit", connectionsVisual: "connectionsVisual", save: "save" }, ngImport: i0, template: ` <div class="pdx-floating-toolbar" *ngIf="visible"> <button mat-fab color="primary" class="pdx-fab" [matTooltip]="'Ações rápidas'"> <mat-icon [praxisIcon]="'build'"></mat-icon> </button> <div class="pdx-actions"> <button mat-mini-fab color="primary" (click)="add.emit()" [matTooltip]="'Inserir componente'" aria-label="Inserir componente"> <mat-icon [praxisIcon]="'add'"></mat-icon> </button> <button mat-mini-fab (click)="connections.emit()" [matTooltip]="'Visualizar conexões (Diagrama)'" aria-label="Visualizar conexões (Diagrama)"> <mat-icon [praxisIcon]="'device_hub'"></mat-icon> </button> <button mat-mini-fab (click)="connectionsVisual.emit()" [matTooltip]="'Editor Visual (beta)'" aria-label="Editor Visual (beta)"> <mat-icon [praxisIcon]="'schema'"></mat-icon> </button> <button mat-mini-fab (click)="connectionsEdit.emit()" [matTooltip]="'Editar conexões (Builder)'" aria-label="Editar conexões (Builder)"> <mat-icon [praxisIcon]="'tune'"></mat-icon> </button> <button mat-mini-fab color="accent" (click)="save.emit()" [matTooltip]="'Salvar página'" aria-label="Salvar página"> <mat-icon [praxisIcon]="'save'"></mat-icon> </button> <button mat-mini-fab (click)="undo.emit()" [disabled]="!canUndo" [matTooltip]="'Desfazer'" aria-label="Desfazer"> <mat-icon [praxisIcon]="'undo'"></mat-icon> </button> <button mat-mini-fab (click)="redo.emit()" [disabled]="!canRedo" [matTooltip]="'Refazer'" aria-label="Refazer"> <mat-icon [praxisIcon]="'redo'"></mat-icon> </button> <button mat-mini-fab (click)="settings.emit()" [matTooltip]="'Configurações da página'" aria-label="Configurações da página"> <mat-icon [praxisIcon]="'settings'"></mat-icon> </button> <button mat-mini-fab (click)="preview.emit()" [matTooltip]="'Pré-visualizar'" aria-label="Pré-visualizar"> <mat-icon [praxisIcon]="'visibility'"></mat-icon> </button> </div> </div> `, isInline: true, styles: [":host{position:absolute;inset:0;pointer-events:none}.pdx-floating-toolbar{position:absolute;right:16px;bottom:16px;display:flex;gap:8px;align-items:center;pointer-events:auto}.pdx-actions{display:flex;gap:8px}.pdx-fab{box-shadow:var(--mat-elevation-transition),var(--mat-elevation-level6, 0 6px 12px rgba(0,0,0,.24))}@media (max-width: 720px){.pdx-actions{gap:6px}.pdx-floating-toolbar{right:12px;bottom:12px}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatMiniFabButton, selector: "button[mat-mini-fab], a[mat-mini-fab], button[matMiniFab], a[matMiniFab]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatFabButton, selector: "button[mat-fab], a[mat-fab], button[matFab], a[matFab]", inputs: ["extended"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: FloatingToolbarComponent, decorators: [{ type: Component, args: [{ selector: 'praxis-floating-toolbar', standalone: true, imports: [CommonModule, MatButtonModule, MatIconModule, MatTooltipModule, PraxisIconDirective], template: ` <div class="pdx-floating-toolbar" *ngIf="visible"> <button mat-fab color="primary" class="pdx-fab" [matTooltip]="'Ações rápidas'"> <mat-icon [praxisIcon]="'build'"></mat-icon> </button> <div class="pdx-actions"> <button mat-mini-fab color="primary" (click)="add.emit()" [matTooltip]="'Inserir componente'" aria-label="Inserir componente"> <mat-icon [praxisIcon]="'add'"></mat-icon> </button> <button mat-mini-fab (click)="connections.emit()" [matTooltip]="'Visualizar conexões (Diagrama)'" aria-label="Visualizar conexões (Diagrama)"> <mat-icon [praxisIcon]="'device_hub'"></mat-icon> </button> <button mat-mini-fab (click)="connectionsVisual.emit()" [matTooltip]="'Editor Visual (beta)'" aria-label="Editor Visual (beta)"> <mat-icon [praxisIcon]="'schema'"></mat-icon> </button> <button mat-mini-fab (click)="connectionsEdit.emit()" [matTooltip]="'Editar conexões (Builder)'" aria-label="Editar conexões (Builder)"> <mat-icon [praxisIcon]="'tune'"></mat-icon> </button> <button mat-mini-fab color="accent" (click)="save.emit()" [matTooltip]="'Salvar página'" aria-label="Salvar página"> <mat-icon [praxisIcon]="'save'"></mat-icon> </button> <button mat-mini-fab (click)="undo.emit()" [disabled]="!canUndo" [matTooltip]="'Desfazer'" aria-label="Desfazer"> <mat-icon [praxisIcon]="'undo'"></mat-icon> </button> <button mat-mini-fab (click)="redo.emit()" [disabled]="!canRedo" [matTooltip]="'Refazer'" aria-label="Refazer"> <mat-icon [praxisIcon]="'redo'"></mat-icon> </button> <button mat-mini-fab (click)="settings.emit()" [matTooltip]="'Configurações da página'" aria-label="Configurações da página"> <mat-icon [praxisIcon]="'settings'"></mat-icon> </button> <button mat-mini-fab (click)="preview.emit()" [matTooltip]="'Pré-visualizar'" aria-label="Pré-visualizar"> <mat-icon [praxisIcon]="'visibility'"></mat-icon> </button> </div> </div> `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{position:absolute;inset:0;pointer-events:none}.pdx-floating-toolbar{position:absolute;right:16px;bottom:16px;display:flex;gap:8px;align-items:center;pointer-events:auto}.pdx-actions{display:flex;gap:8px}.pdx-fab{box-shadow:var(--mat-elevation-transition),var(--mat-elevation-level6, 0 6px 12px rgba(0,0,0,.24))}@media (max-width: 720px){.pdx-actions{gap:6px}.pdx-floating-toolbar{right:12px;bottom:12px}}\n"] }] }], propDecorators: { visible: [{ type: Input }], canUndo: [{ type: Input }], canRedo: [{ type: Input }], add: [{ type: Output }], undo: [{ type: Output }], redo: [{ type: Output }], settings: [{ type: Output }], preview: [{ type: Output }], connections: [{ type: Output }], connectionsEdit: [{ type: Output }], connectionsVisual: [{ type: Output }], save: [{ type: Output }] } }); class ComponentPaletteDialogComponent { dialogRef; registry; data; query = ''; all = signal([], ...(ngDevMode ? [{ debugName: "all" }] : [])); filtered = computed(() => this.applyFilters(), ...(ngDevMode ? [{ debugName: "filtered" }] : [])); density = computed(() => { const n = this.filtered().length; if (n <= 4) return 'roomy'; if (n > 12) return 'dense'; return 'normal'; }, ...(ngDevMode ? [{ debugName: "density" }] : [])); constructor(dialogRef, registry, data) { this.dialogRef = dialogRef; this.registry = registry; this.data = data; } ngOnInit() { this.all.set(this.registry.getAll()); } select(id) { this.dialogRef.close(id); } trackById = (_, m) => m.id; getPreview(m) { return (m.description || m.selector || m.friendlyName || m.id || '').toString(); } // Create effect in injection context (field initializer is allowed) _adjustSizeEffect = effect(() => { const n = this.filtered().length; let width = '720px'; if (n <= 4) width = '560px'; else if (n <= 8) width = '680px'; const container = this.dialogRef._containerInstance; if (container && container._config) { container._config.width = width; } }, ...(ngDevMode ? [{ debugName: "_adjustSizeEffect" }] : [])); applyFilters() { const list = (this.all() || []); const q = (this.query || '').toLowerCase(); const allowIds = new Set((this.data?.allowedWidgetIds || []).map((s) => s.toLowerCase())); const allowTags = new Set((this.data?.allowedWidgetTags || []).map((s) => s.toLowerCase())); return list.filter((m) => { if (q) { const hay = `${m.id} ${m.friendlyName || ''} ${m.description || ''} ${m.selector || ''}`.toLowerCase(); if (!hay.includes(q)) return false; } if (allowIds.size > 0 && !allowIds.has((m.id || '').toLowerCase())) return false; if (allowTags.size > 0 && !m.tags?.some(t => allowTags.has(t.toLowerCase()))) return false; if (typeof this.data?.predicate === 'function' && !this.data.predicate(m)) return false; return true; }); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: ComponentPaletteDialogComponent, deps: [{ token: i1.MatDialogRef }, { token: i2$1.ComponentMetadataRegistry }, { token: MAT_DIALOG_DATA }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.4", type: ComponentPaletteDialogComponent, isStandalone: true, selector: "praxis-component-palette-dialog", ngImport: i0, template: ` <h2 mat-dialog-title> <div class="dlg-title"> <mat-icon class="dlg-icon" [praxisIcon]="'widgets'"></mat-icon> <div class="dlg-texts"> <div class="dlg-head">{{ data?.title || 'Inserir componente' }}</div> <div class="dlg-sub">Escolha um componente para adicionar à página · {{ filtered().length }} disponíveis</div> </div> </div> </h2> <div mat-dialog-content class="pdx-palette-content"> <div class="pdx-grid" [ngClass]="density()" *ngIf="filtered().length; else emptyState"> <div *ngFor="let m of filtered(); trackBy: trackById" class="pdx-card mat-elevation-z2" tabindex="0" role="button" [attr.aria-label]="'Adicionar ' + (m.friendlyName || m.id)" (click)="select(m.id)" (keydown.enter)="select(m.id)" (keydown.space)="select(m.id)" > <div class="pdx-card-icon"> <mat-icon [praxisIcon]="m.icon || 'widgets'"></mat-icon> </div> <div class="pdx-card-body"> <div class="pdx-card-title">{{ m.friendlyName || m.id }}</div> <div class="pdx-card-desc">{{ m.description || m.selector }}</div> </div> </div> </div> <ng-template #emptyState> <div class="pdx-empty"> <mat-icon [praxisIcon]="'sentiment_dissatisfied'"></mat-icon> <div>Nenhum componente disponível</div> </div> </ng-template> </div> <div mat-dialog-actions align="end"> <button mat-stroked-button color="primary" mat-dialog-close>Cancelar</button> </div> `, isInline: true, styles: [":host{display:block}h2[mat-dialog-title]{margin:0;padding:12px 24px 10px;border-bottom:1px solid var(--md-sys-color-outline-variant, rgba(0,0,0,.12))}.dlg-title{display:flex;align-items:center;gap:10px}.dlg-icon{color:var(--md-sys-color-primary, #3f51b5)}.dlg-head{font-size:18px;font-weight:600;line-height:1.2}.dlg-sub{font-size:12px;opacity:.85;color:var(--md-sys-color-on-surface-variant, rgba(0,0,0,.62))}.pdx-palette-content{min-width:480px;max-width:920px;padding-top:8px}.pdx-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px}.pdx-grid.dense{grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px}.pdx-grid.roomy{grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px}.pdx-card{position:relative;display:grid;grid-template-columns:44px 1fr;grid-template-rows:auto auto;gap:10px;padding:14px;border:1px solid transparent;border-radius:14px;cursor:pointer;outline:none;min-height:92px;background:linear-gradient(var(--md-sys-color-surface),var(--md-sys-color-surface)) padding-box,linear-gradient(135deg,color-mix(in oklab,var(--md-sys-color-primary, #3f51b5) 55%,transparent),color-mix(in oklab,var(--md-sys-color-secondary, #ff4081) 45%,transparent)) border-box;transition:box-shadow .2s ease,background .25s ease,transform .06s ease}.pdx-grid.dense .pdx-card{padding:10px;border-radius:12px;min-height:84px}.pdx-grid.roomy .pdx-card{padding:16px;border-radius:16px;min-height:100px}.pdx-card:focus,.pdx-card:hover{box-shadow:var(--mat-elevation-transition),var(--mat-elevation-level6, 0 6px 12px rgba(0,0,0,.22));background:linear-gradient(var(--md-sys-color-surface),var(--md-sys-color-surface)) padding-box,linear-gradient(135deg,color-mix(in oklab,var(--md-sys-color-primary) 70%,transparent),color-mix(in oklab,var(--md-sys-color-secondary) 55%,transparent)) border-box}.pdx-card:active{transform:translateY(1px)}.pdx-card-icon{grid-row:span 2;display:flex;align-items:center;justify-content:center;color:color-mix(in oklab,var(--md-sys-color-primary) 70%,#666)}.pdx-card-title{font-weight:600;line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pdx-card-desc{color:#000000b3;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;word-break:break-word}.pdx-empty{padding:24px;display:grid;place-items:center;color:#0009;gap:8px}@media (max-width: 600px){.pdx-palette-content{min-width:320px}.pdx-grid{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i3$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i3$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: MatDialogModule }, { kind: "directive", type: i1.MatDialogClose, selector: "[mat-dialog-close], [matDialogClose]", inputs: ["aria-label", "type", "mat-dialog-close", "matDialogClose"], exportAs: ["matDialogClose"] }, { kind: "directive", type: i1.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i1.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i1.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "ngmodule", type: MatInputModule }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.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: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: ComponentPaletteDialogComponent, decorators: [{ type: Component, args: [{ selector: 'praxis-component-palette-dialog', standalone: true, imports: [CommonModule, FormsModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatIconModule, MatButtonModule, PraxisIconDirective], template: ` <h2 mat-dialog-title> <div class="dlg-title"> <mat-icon class="dlg-icon" [praxisIcon]="'widgets'"></mat-icon> <div class="dlg-texts"> <div class="dlg-head">{{ data?.title || 'Inserir componente' }}</div> <div class="dlg-sub">Escolha um componente para adicionar à página · {{ filtered().length }} disponíveis</div> </div> </div> </h2> <div mat-dialog-content class="pdx-palette-content"> <div class="pdx-grid" [ngClass]="density()" *ngIf="filtered().length; else emptyState"> <div *ngFor="let m of filtered(); trackBy: trackById" class="pdx-card mat-elevation-z2" tabindex="0" role="button" [attr.aria-label]="'Adicionar ' + (m.friendlyName || m.id)" (click)="select(m.id)" (keydown.enter)="select(m.id)" (keydown.space)="select(m.id)" > <div class="pdx-card-icon"> <mat-icon [praxisIcon]="m.icon || 'widgets'"></mat-icon> </div> <div class="pdx-card-body"> <div class="pdx-card-title">{{ m.friendlyName || m.id }}</div> <div class="pdx-card-desc">{{ m.description || m.selector }}</div> </div> </div> </div> <ng-template #emptyState> <div class="pdx-empty"> <mat-icon [praxisIcon]="'sentiment_dissatisfied'"></mat-icon> <div>Nenhum componente disponível</div> </div> </ng-template> </div> <div mat-dialog-actions align="end"> <button mat-stroked-button color="primary" mat-dialog-close>Cancelar</button> </div> `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block}h2[mat-dialog-title]{margin:0;padding:12px 24px 10px;border-bottom:1px solid var(--md-sys-color-outline-variant, rgba(0,0,0,.12))}.dlg-title{display:flex;align-items:center;gap:10px}.dlg-icon{color:var(--md-sys-color-primary, #3f51b5)}.dlg-head{font-size:18px;font-weight:600;line-height:1.2}.dlg-sub{font-size:12px;opacity:.85;color:var(--md-sys-color-on-surface-variant, rgba(0,0,0,.62))}.pdx-palette-content{min-width:480px;max-width:920px;padding-top:8px}.pdx-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px}.pdx-grid.dense{grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px}.pdx-grid.roomy{grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px}.pdx-card{position:relative;display:grid;grid-template-columns:44px 1fr;grid-template-rows:auto auto;gap:10px;padding:14px;border:1px solid transparent;border-radius:14px;cursor:pointer;outline:none;min-height:92px;background:linear-gradient(var(--md-sys-color-surface),var(--md-sys-color-surface)) padding-box,linear-gradient(135deg,color-mix(in oklab,var(--md-sys-color-primary, #3f51b5) 55%,transparent),color-mix(in oklab,var(--md-sys-color-secondary, #ff4081) 45%,transparent)) border-box;transition:box-shadow .2s ease,background .25s ease,transform .06s ease}.pdx-grid.dense .pdx-card{padding:10px;border-radius:12px;min-height:84px}.pdx-grid.roomy .pdx-card{padding:16px;border-radius:16px;min-height:100px}.pdx-card:focus,.pdx-card:hover{box-shadow:var(--mat-elevation-transition),var(--mat-elevation-level6, 0 6px 12px rgba(0,0,0,.22));background:linear-gradient(var(--md-sys-color-surface),var(--md-sys-color-surface)) padding-box,linear-gradient(135deg,color-mix(in oklab,var(--md-sys-color-primary) 70%,transparent),color-mix(in oklab,var(--md-sys-color-secondary) 55%,transparent)) border-box}.pdx-card:active{transform:translateY(1px)}.pdx-card-icon{grid-row:span 2;display:flex;align-items:center;justify-content:center;color:color-mix(in oklab,var(--md-sys-color-primary) 70%,#666)}.pdx-card-title{font-weight:600;line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pdx-card-desc{color:#000000b3;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;word-break:break-word}.pdx-empty{padding:24px;display:grid;place-items:center;color:#0009;gap:8px}@media (max-width: 600px){.pdx-palette-content{min-width:320px}.pdx-grid{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}}\n"] }] }], ctorParameters: () => [{ type: i1.MatDialogRef }, { type: i2$1.ComponentMetadataRegistry }, { type: undefined, decorators: [{ type: Inject, args: [MAT_DIALOG_DATA] }] }] }); class ConnectionBuilderComponent { dialog; registry; snack; page; widgets; pageChange = new EventEmitter(); // State originalSnapshot = ''; showOnlyIssues = false; showFriendly = true; filterText = ''; groupBy = 'none'; sortBy = 'from'; // Move braces-containing placeholder into TS (AGENTS.md policy) mapPlaceholder = 'payload | payload.id | ${payload.id}'; // Signals connections = signal([], ...(ngDevMode ? [{ debugName: "connections" }] : [])); selectedIndex = signal(-1, ...(ngDevMode ? [{ debugName: "selectedIndex" }] : [])); // Derived lists filteredConnections = computed(() => this.applyFilters(this.connections()), ...(ngDevMode ? [{ debugName: "filteredConnections" }] : [])); groupedConnections = computed(() => this.applyGrouping(this.filteredConnections()), ...(ngDevMode ? [{ debugName: "groupedConnections" }] : [])); constructor(dialog, registry, snack) { this.dialog = dialog; this.registry = registry; this.snack = snack; } ngOnInit() { const p = this.parsePage(this.page); const ws = this.widgets || p?.widgets || []; this.widgets = ws; const conns = [...(p?.connections || [])]; this.connections.set(conns); this.originalSnapshot = JSON.stringify(conns); } ngOnChanges(changes) { if (changes['page'] || changes['widgets']) { const p = this.parsePage(this.page); this.widgets = this.widgets || p?.widgets || []; const next = [...(p?.connections || [])]; this.connections.set(next); this.originalSnapshot = JSON.stringify(next); } } // UI helpers isExpanded(i) { return i === this.selectedIndex(); } toggleExpanded(i, ev) { if (ev) ev.stopPropagation(); this.selectedIndex.set(this.selectedIndex() === i ? -1 : i); } setSortBy(v) { this.sortBy = v; this.connections.set(this.sortList([...this.connections()])); } onGroupByChange() { } toggleShowOnlyIssues() { this.showOnlyIssues = !this.showOnlyIssues; } toggleFriendly() { this.showFriendly = !this.showFriendly; } trackByIndex = (i) => i; // Labels fromLabel(c) { return `${c.from.widget}.${c.from.output}`; } toLabel(c) { return `${c.to.widget}.${c.to.input}`; } fromFriendly(c) { return `${this.widgetFriendlyNameForKey(c.from.widget)}.${c.from.output}`; } toFriendly(c) { return `${this.widgetFriendlyNameForKey(c.to.widget)}.${c.to.input}`; } outputDescription(c) { return this.registry.get(this.widgetTypeByKey(c.from.widget))?.outputs?.find(o => o.name === c.from.output)?.description || ''; } inputDescription(c) { return this.registry.get(this.widgetTypeByKey(c.to.widget))?.inputs?.find(i => i.name === c.to.input)?.description || ''; } widgetFriendlyNameForKey(key) { const id = this.widgetTypeByKey(key); return this.registry.get(id)?.friendlyName || id; } componentIconForKey(key) { const id = this.widgetTypeByKey(key); return this.registry.get(id)?.icon || 'widgets'; } widgetTypeByKey(key) { return this.widgets?.find(w => w.key === key)?.definition?.id || key; } applyFilters(list) { const q = (this.filterText || '').toLowerCase(); const arr = list.filter((c) => { if (this.showOnlyIssues && this.connectionStatus(c) === 'ok') return false; if (!q) return true; const hay = `${c.from.widget}.${c.from.output} ${c.to.widget}.${c.to.input} ${c.map || ''}`.toLowerCase(); return hay.includes(q); }); return this.sortList(arr); } applyGrouping(list) { switch (this.groupBy) { case 'from': return this.groupByKey(list, c => `${c.from.widget}.${c.from.output}`); case 'to': return this.groupByKey(list, c => `${c.to.widget}.${c.to.input}`); case 'event': return this.groupByKey(list, c => `${c.from.output}`); default: return [{ label: 'Todas', list }]; } } groupByKey(list, keyFn) { const map = new Map(); for (const c of list) { const k = keyFn(c); const arr = map.get(k) || []; arr.push(c); map.set(k, arr); } return Array.from(map.entries()).map(([k, v]) => ({ label: k, list: v })); } sortList(list) { return [...list].sort((a, b) => { if (this.sortBy === 'from') return this.fromLabel(a).localeCompare(this.fromLabel(b)); return this.toLabel(a).localeCompare(this.toLabel(b)); }); } // Editing createNew() { const fromKey = this.widgets?.[0]?.key || ''; const toKey = this.widgets?.[1]?.key || ''; const conn = { from: { widget: fromKey, output: 'submit' }, to: { widget: toKey, input: 'context' } }; this.connections.set([conn, ...this.connections()]); } startEdit(index, _c) { this.selectedIndex.set(index); } startEditByConn(c) { const i = this.connections().indexOf(c); if (i >= 0) this.startEdit(i, c); } duplicateConnection(index) { const next = [...this.connections()]; const c = next[index]; if (!c) return; next.splice(index + 1, 0, { ...c }); this.connections.set(next); } removeConnection(index) { const next = [...this.connections()]; if (index < 0 || index >= next.length) return; next.splice(index, 1); this.connections.set(next); } // Validation connectionStatus(c) { if (!c.from?.widget || !c.from?.output || !c.to?.widget || !c.to?.input) return 'err'; const wFrom = this.widgetTypeByKey(c.from.widget); const wTo = this.widgetTypeByKey(c.to.widget); const outOk = !!this.registry.get(wFrom)?.outputs?.some(o => o.name === c.from.output); const inOk = !!this.registry.get(wTo)?.inputs?.some(i => i.name === c.to.input); if (!outOk || !inOk) return 'warn'; return 'ok'; } // Persist/apply onSave() { const p = this.parsePage(this.page) || {}; const next = { ...p, connections: this.connections() }; this.page = next; this.pageChange.emit(next); this.snack.open('Conexões salvas', undefined, { duration: 1000 }); this.originalSnapshot = JSON.stringify(this.connections()); } // Diagram helpers openDiagramFor(_c) { import('./praxisui-page-builder-connection-graph.component-C6x--6--.mjs').then(m => { this.dialog.open(m.ConnectionGraphComponent, { width: '95vw', height: '80vh', maxWidth: '100vw', panelClass: 'praxis-conn-graph-dialog' }); }); } openDiagramFullscreen() { import('./praxisui-page-builder-connection-graph.component-C6x--6--.mjs').then(m => { this.dialog.open(m.ConnectionGraphComponent, { width: '95vw', height: '95vh', maxWidth: '100vw', panelClass: 'praxis-conn-graph-dialog', autoFocus: false, restoreFocus: false }); }); } parsePage(input) { if (!input) return undefined; if (typeof input === 'string') { try { return JSON.parse(input); } catch { return undefined; } } return input; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: ConnectionBuilderComponent, deps: [{ token: i1.MatDialog }, { token: i2$1.ComponentMetadataRegistry }, { token: i3$3.MatSnackBar }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.4", type: ConnectionBuilderComponent, isStandalone: true, selector: "praxis-connection-builder", inputs: { page: "page", widgets: "widgets" }, outputs: { pageChange: "pageChange" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"pdx-conn-root\" role=\"region\" aria-label=\"Construtor de Conex\u00F5es\">\n <div class=\"pdx-conn-head\">\n <span class=\"pdx-conn-title\">Conex\u00F5es</span>\n <span class=\"pdx-conn-count\" aria-label=\"Conex\u00F5es filtradas e total\">{{ filteredConnections().length }} / {{ connections().length }}</span>\n <mat-form-field appearance=\"outline\" class=\"pdx-conn-search\">\n <mat-label>Buscar</mat-label>\n <input matInput [(ngModel)]=\"filterText\" placeholder=\"Origem, Destino, Input, Map...\" />\n <button mat-icon-button matSuffix *ngIf=\"filterText\" (click)=\"filterText='';\" aria-label=\"Limpar busca\"><mat-icon [praxisIcon]=\"'close'\"></mat-icon></button>\n </mat-form-field>\n <span class=\"pdx-spacer\"></span>\n <button mat-button (click)=\"toggleShowOnlyIssues()\" [color]=\"showOnlyIssues ? 'primary': undefined\" aria-label=\"Somente avisos/erros\"><mat-icon [praxisIcon]=\"'report_problem'\"></mat-icon> Issues</button>\n <mat-form-field appearance=\"outline\" style=\"width: 160px;\">\n <mat-label>Ordenar por</mat-label>\n <mat-select [(ngModel)]=\"sortBy\" (ngModelChange)=\"setSortBy($event)\">\n <mat-option value=\"from\">Origem</mat-option>\n <mat-option value=\"to\">Destino</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" style=\"width: 170px;\">\n <mat-label>Agrupar por</mat-label>\n <mat-select [(ngModel)]=\"groupBy\" (ngModelChange)=\"onGroupByChange()\">\n <mat-option value=\"none\">Nenhum</mat-option>\n <mat-option value=\"from\">Origem</mat-option>\n <mat-option value=\"to\">Destino</mat-option>\n <mat-option value=\"event\">Evento</mat-option>\n </mat-select>\n </mat-form-field>\n <button mat-icon-button [color]=\"showFriendly ? 'primary' : undefined\" (click)=\"toggleFriendly()\" aria-label=\"Alternar nome amig\u00E1vel/t\u00E9cnico\"><mat-icon [praxisIcon]=\"'translate'\"></mat-icon></button>\n <button mat-stroked-button (click)=\"createNew()\" aria-label=\"Nova conex\u00E3o\"><mat-icon [praxisIcon]=\"'add'\"></mat-icon> Nova Conex\u00E3o</button>\n <button mat-flat-button color=\"accent\" class=\"pdx-diagram-cta\" (click)=\"openDiagramFullscreen()\" aria-label=\"Abrir diagrama em tela cheia\" matTooltip=\"Visualizar conex\u00F5es em grafo\"><mat-icon [praxisIcon]=\"'schema'\"></mat-icon><span>Diagrama</span></button>\n </div>\n\n <div class=\"pdx-conn-grid\">\n <!-- Left: read-only list -->\n <div class=\"pdx-conn-list\" role=\"list\" aria-label=\"Lista de conex\u00F5es\" cdkScrollable>\n <mat-list *ngIf=\"filteredConnections().length; else noConns\">\n <ng-container *ngIf=\"groupBy!=='none'; else flatList\">\n <div class=\"group-block\" *ngFor=\"let g of groupedConnections(); let gi = index\">\n <div class=\"group-header\">\n <span class=\"group-title\">{{ g.label }}</span>\n <span class=\"group-count\">{{ g.list.length }}</span>\n </div>\n <mat-divider></mat-divider>\n <div class=\"group-list\">\n <ng-container *ngFor=\"let c of g.list; let i = index\">\n <mat-list-item role=\"listitem\" (click)=\"startEdit(i, c)\" [class.selected]=\"selectedIndex()===i\">\n <div class=\"card-head\" matListItemTitle [matTooltip]=\"showFriendly ? (c.from.widget + '.' + c.from.output + ' \u2192 ' + (c.to.widget + '.' + c.to.input)) : ''\">\n <mat-icon class=\"comp-icon from\" [matTooltip]=\"widgetFriendlyNameForKey(c.from.widget)\">{{ componentIconForKey(c.from.widget) }}</mat-icon>\n <span class=\"pdx-badge from\" aria-hidden=\"true\"></span>\n <span class=\"from\">{{ showFriendly ? fromFriendly(c) : fromLabel(c) }}</span>\n <span class=\"arrow\">\u2192</span>\n <mat-icon class=\"comp-icon to\" [matTooltip]=\"widgetFriendlyNameForKey(c.to.widget)\">{{ componentIconForKey(c.to.widget) }}</mat-icon>\n <span class=\"pdx-badge to\" aria-hidden=\"true\"></span>\n <span class=\"to\">{{ showFriendly ? toFriendly(c) : toLabel(c) }}</span>\n <span class=\"spacer\"></span>\n <span class=\"status-pill\" [class.ok]=\"connectionStatus(c)==='ok'\" [class.warn]=\"connectionStatus(c)==='warn'\" [class.err]=\"connectionStatus(c)==='err'\">\n <mat-icon *ngIf=\"connectionStatus(c)==='ok'\">check_circle</mat-icon>\n <mat-icon *ngIf=\"connectionStatus(c)==='warn'\">warning</mat-icon>\n <mat-icon *ngIf=\"connectionStatus(c)==='err'\">error</mat-icon>\n </span>\n <button mat-icon-button class=\"expand-btn\" [attr.aria-expanded]=\"isExpanded(i)\" (click)=\"toggleExpanded(i, $event)\" [matTooltip]=\"isExpanded(i) ? 'Recolher detalhes' : 'Expandir detalhes'\">\n <mat-icon>{{ isExpanded(i) ? 'expand_less' : 'expand_more' }}</mat-icon>\n </button>\n </div>\n <div class=\"meta\" *ngIf=\"showFriendly\" matListItemLine>\n <span class=\"hint\">{{ outputDescription(c) }}</span>\n <span class=\"sep\" *ngIf=\"outputDescription(c) && inputDescription(c)\">\u2022</span>\n <span class=\"hint\">{{ inputDescription(c) }}</span>\n </div>\n <div class=\"map-chip\" *ngIf=\"c.map\" [matTooltip]=\"c.map || ''\" matListItemLine (click)=\"startEditByConn(c)\" title=\"Clique para editar Map\">\n <span class=\"pdx-badge map\" aria-hidden=\"true\"></span>\n <mat-icon class=\"map-icon\" inline>bolt</mat-icon>\n <span class=\"mono\">{{ c.map }}</span>\n </div>\n <div class=\"card-actions\" matListItemLine (click)=\"$event.stopPropagation()\">\n <span class=\"action-group\">\n <button mat-icon-button (click)=\"startEdit(i, c)\" matTooltip=\"Editar\"><mat-icon>settings</mat-icon></button>\n <button mat-icon-button (click)=\"duplicateConnection(i)\" matTooltip=\"Duplicar\"><mat-icon>content_copy</mat-icon></button>\n </span>\n <span class=\"action-group\">\n <button mat-stroked-button color=\"primary\" (click)=\"openDiagramFor(c)\" matTooltip=\"Ver rela\u00E7\u00E3o no diagrama\"><mat-icon>schema</mat-icon><span>Diagrama</span></button>\n </span>\n <span class=\"spacer\"></span>\n <span class=\"action-group\">\n <button mat-icon-button color=\"warn\" (click)=\"removeConnection(i)\" matTooltip=\"Remover\"><mat-icon>delete</mat-icon></button>\n </span>\n </div>\n <div class=\"card-details\" matListItemLine [class.expanded]=\"isExpanded(i)\" [style.maxHeight]=\"isExpanded(i) ? '240px' : '0px'\" [style.opacity]=\"isExpanded(i) ? 1 : 0\" [style.pointerEvents]=\"isExpanded(i) ? 'auto' : 'none'\">\n <div class=\"stepper\">\n <div class=\"step from-step\">\n <div class=\"dot from\"></div>\n <div class=\"content\">\n <div class=\"label\">De</div>\n <div class=\"value\">{{ showFriendly ? fromFriendly(c) : fromLabel(c) }}</div>\n </div>\n </div>\n <div class=\"step to-step\">\n <div class=\"dot to\"></div>\n <div class=\"content\">\n <div class=\"label\">Para</div>\n <div class=\"value\">{{ showFriendly ? toFriendly(c) : toLabel(c) }}</div>\n </div>\n </div>\n <div class=\"step map-step\" *ngIf=\"c.map\">\n <div class=\"dot map\"></div>\n <div class=\"content\">\n <div class=\"label\">Map</div>\n <div class=\"value mono\">{{ c.map }}</div>\n </div>\n </div>\n </div>\n </div>\n </mat-list-item>\n </ng-container>\n </div>\n </div>\n </ng-container>\n <ng-template #flatList>\n <ng-container *ngFor=\"let c of filteredConnections(); let i = index\">\n <mat-list-item role=\"listitem\" (click)=\"startEdit(i, c)\" [class.selected]=\"selectedIndex()===i\">\n <div class=\"card-head\" matListItemTitle