@praxisui/page-builder 3.0.0-beta.3 → 3.0.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,7 +19,7 @@ import * as i4 from '@angular/material/input';
19
19
  import { MatInputModule } from '@angular/material/input';
20
20
  import * as i1$2 from '@angular/forms';
21
21
  import { FormsModule, FormGroup, FormArray, FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
22
- import * as i9 from '@angular/material/select';
22
+ import * as i6 from '@angular/material/select';
23
23
  import { MatSelectModule } from '@angular/material/select';
24
24
  import * as i12 from '@angular/material/list';
25
25
  import { MatListModule } from '@angular/material/list';
@@ -27,7 +27,7 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete';
27
27
  import { MatMenuModule } from '@angular/material/menu';
28
28
  import * as i3$1 from '@angular/material/snack-bar';
29
29
  import { MatSnackBarModule } from '@angular/material/snack-bar';
30
- import * as i9$1 from '@angular/material/tabs';
30
+ import * as i9 from '@angular/material/tabs';
31
31
  import { MatTabsModule } from '@angular/material/tabs';
32
32
  import * as i8 from '@angular/cdk/scrolling';
33
33
  import { ScrollingModule } from '@angular/cdk/scrolling';
@@ -514,16 +514,121 @@ class ConnectionBuilderComponent {
514
514
  toggleShowOnlyIssues() { this.showOnlyIssues = !this.showOnlyIssues; }
515
515
  toggleFriendly() { this.showFriendly = !this.showFriendly; }
516
516
  trackByIndex = (i) => i;
517
+ isWidgetSource(from) {
518
+ return 'widget' in from;
519
+ }
520
+ isWidgetTarget(to) {
521
+ return 'widget' in to;
522
+ }
517
523
  // Labels
518
- fromLabel(c) { return `${c.from.widget}.${c.from.output}`; }
519
- toLabel(c) { return `${c.to.widget}.${c.to.input}`; }
520
- fromFriendly(c) { return `${this.widgetFriendlyNameForKey(c.from.widget)}.${c.from.output}`; }
521
- toFriendly(c) { return `${this.widgetFriendlyNameForKey(c.to.widget)}.${c.to.input}`; }
522
- outputDescription(c) { return this.registry.get(this.widgetTypeByKey(c.from.widget))?.outputs?.find(o => o.name === c.from.output)?.description || ''; }
523
- inputDescription(c) { return this.registry.get(this.widgetTypeByKey(c.to.widget))?.inputs?.find(i => i.name === c.to.input)?.description || ''; }
524
+ fromLabel(c) {
525
+ return this.isWidgetSource(c.from) ? `${c.from.widget}.${c.from.output}` : `state:${c.from.state}`;
526
+ }
527
+ toLabel(c) {
528
+ return this.isWidgetTarget(c.to) ? `${c.to.widget}.${c.to.input}` : `state:${c.to.state}`;
529
+ }
530
+ fromFriendly(c) {
531
+ return this.isWidgetSource(c.from) ? `${this.widgetFriendlyNameForKey(c.from.widget)}.${c.from.output}` : `state:${c.from.state}`;
532
+ }
533
+ toFriendly(c) {
534
+ return this.isWidgetTarget(c.to) ? `${this.widgetFriendlyNameForKey(c.to.widget)}.${c.to.input}` : `state:${c.to.state}`;
535
+ }
536
+ outputDescription(c) {
537
+ if (!this.isWidgetSource(c.from))
538
+ return '';
539
+ const from = c.from;
540
+ return this.registry.get(this.widgetTypeByKey(from.widget))?.outputs?.find(o => o.name === from.output)?.description || '';
541
+ }
542
+ inputDescription(c) {
543
+ if (!this.isWidgetTarget(c.to))
544
+ return '';
545
+ const to = c.to;
546
+ return this.registry.get(this.widgetTypeByKey(to.widget))?.inputs?.find(i => i.name === to.input)?.description || '';
547
+ }
548
+ fromTooltip(c) { return this.showFriendly ? `${this.fromFriendly(c)} → ${this.toFriendly(c)}` : ''; }
549
+ fromIcon(c) { return this.isWidgetSource(c.from) ? this.componentIconForKey(c.from.widget) : 'account_tree'; }
550
+ toIcon(c) { return this.isWidgetTarget(c.to) ? this.componentIconForKey(c.to.widget) : 'account_tree'; }
551
+ fromIconTooltip(c) { return this.isWidgetSource(c.from) ? this.widgetFriendlyNameForKey(c.from.widget) : this.fromLabel(c); }
552
+ toIconTooltip(c) { return this.isWidgetTarget(c.to) ? this.widgetFriendlyNameForKey(c.to.widget) : this.toLabel(c); }
524
553
  widgetFriendlyNameForKey(key) { const id = this.widgetTypeByKey(key); return this.registry.get(id)?.friendlyName || id; }
525
554
  componentIconForKey(key) { const id = this.widgetTypeByKey(key); return this.registry.get(id)?.icon || 'widgets'; }
526
555
  widgetTypeByKey(key) { return this.widgets?.find(w => w.key === key)?.definition?.id || key; }
556
+ selectedConnection() {
557
+ const index = this.selectedIndex();
558
+ return index >= 0 ? this.connections()[index] : undefined;
559
+ }
560
+ selectedSourcePrimaryValue() {
561
+ const selected = this.selectedConnection();
562
+ if (!selected)
563
+ return '';
564
+ return this.isWidgetSource(selected.from) ? selected.from.widget : selected.from.state;
565
+ }
566
+ setSelectedSourcePrimaryValue(value) {
567
+ const selected = this.selectedConnection();
568
+ if (!selected)
569
+ return;
570
+ if (this.isWidgetSource(selected.from))
571
+ selected.from.widget = value;
572
+ else
573
+ selected.from.state = value;
574
+ this.connections.set([...this.connections()]);
575
+ }
576
+ selectedSourceSecondaryValue() {
577
+ const selected = this.selectedConnection();
578
+ return selected && this.isWidgetSource(selected.from) ? selected.from.output : '';
579
+ }
580
+ setSelectedSourceSecondaryValue(value) {
581
+ const selected = this.selectedConnection();
582
+ if (!selected || !this.isWidgetSource(selected.from))
583
+ return;
584
+ selected.from.output = value;
585
+ this.connections.set([...this.connections()]);
586
+ }
587
+ selectedTargetPrimaryValue() {
588
+ const selected = this.selectedConnection();
589
+ if (!selected)
590
+ return '';
591
+ return this.isWidgetTarget(selected.to) ? selected.to.widget : selected.to.state;
592
+ }
593
+ setSelectedTargetPrimaryValue(value) {
594
+ const selected = this.selectedConnection();
595
+ if (!selected)
596
+ return;
597
+ if (this.isWidgetTarget(selected.to))
598
+ selected.to.widget = value;
599
+ else
600
+ selected.to.state = value;
601
+ this.connections.set([...this.connections()]);
602
+ }
603
+ selectedTargetSecondaryValue() {
604
+ const selected = this.selectedConnection();
605
+ return selected && this.isWidgetTarget(selected.to) ? selected.to.input : '';
606
+ }
607
+ setSelectedTargetSecondaryValue(value) {
608
+ const selected = this.selectedConnection();
609
+ if (!selected || !this.isWidgetTarget(selected.to))
610
+ return;
611
+ selected.to.input = value;
612
+ this.connections.set([...this.connections()]);
613
+ }
614
+ selectedMapValue() {
615
+ return this.selectedConnection()?.map || '';
616
+ }
617
+ setSelectedMapValue(value) {
618
+ const selected = this.selectedConnection();
619
+ if (!selected)
620
+ return;
621
+ selected.map = value;
622
+ this.connections.set([...this.connections()]);
623
+ }
624
+ showSelectedSourceSecondary() {
625
+ const selected = this.selectedConnection();
626
+ return !!selected && this.isWidgetSource(selected.from);
627
+ }
628
+ showSelectedTargetSecondary() {
629
+ const selected = this.selectedConnection();
630
+ return !!selected && this.isWidgetTarget(selected.to);
631
+ }
527
632
  applyFilters(list) {
528
633
  const q = (this.filterText || '').toLowerCase();
529
634
  const arr = list.filter((c) => {
@@ -531,16 +636,16 @@ class ConnectionBuilderComponent {
531
636
  return false;
532
637
  if (!q)
533
638
  return true;
534
- const hay = `${c.from.widget}.${c.from.output} ${c.to.widget}.${c.to.input} ${c.map || ''}`.toLowerCase();
639
+ const hay = `${this.fromLabel(c)} ${this.toLabel(c)} ${c.map || ''}`.toLowerCase();
535
640
  return hay.includes(q);
536
641
  });
537
642
  return this.sortList(arr);
538
643
  }
539
644
  applyGrouping(list) {
540
645
  switch (this.groupBy) {
541
- case 'from': return this.groupByKey(list, c => `${c.from.widget}.${c.from.output}`);
542
- case 'to': return this.groupByKey(list, c => `${c.to.widget}.${c.to.input}`);
543
- case 'event': return this.groupByKey(list, c => `${c.from.output}`);
646
+ case 'from': return this.groupByKey(list, c => this.fromLabel(c));
647
+ case 'to': return this.groupByKey(list, c => this.toLabel(c));
648
+ case 'event': return this.groupByKey(list, c => this.isWidgetSource(c.from) ? c.from.output : c.from.state);
544
649
  default: return [{ label: 'Todas', list }];
545
650
  }
546
651
  }
@@ -577,14 +682,30 @@ class ConnectionBuilderComponent {
577
682
  return; next.splice(index, 1); this.connections.set(next); }
578
683
  // Validation
579
684
  connectionStatus(c) {
580
- if (!c.from?.widget || !c.from?.output || !c.to?.widget || !c.to?.input)
685
+ if (this.isWidgetSource(c.from)) {
686
+ const from = c.from;
687
+ if (!from.widget || !from.output)
688
+ return 'err';
689
+ const wFrom = this.widgetTypeByKey(from.widget);
690
+ const outOk = !!this.registry.get(wFrom)?.outputs?.some(o => o.name === from.output);
691
+ if (!outOk)
692
+ return 'warn';
693
+ }
694
+ else if (!c.from.state) {
581
695
  return 'err';
582
- const wFrom = this.widgetTypeByKey(c.from.widget);
583
- const wTo = this.widgetTypeByKey(c.to.widget);
584
- const outOk = !!this.registry.get(wFrom)?.outputs?.some(o => o.name === c.from.output);
585
- const inOk = !!this.registry.get(wTo)?.inputs?.some(i => i.name === c.to.input);
586
- if (!outOk || !inOk)
587
- return 'warn';
696
+ }
697
+ if (this.isWidgetTarget(c.to)) {
698
+ const to = c.to;
699
+ if (!to.widget || !to.input)
700
+ return 'err';
701
+ const wTo = this.widgetTypeByKey(to.widget);
702
+ const inOk = !!this.registry.get(wTo)?.inputs?.some(i => i.name === to.input);
703
+ if (!inOk)
704
+ return 'warn';
705
+ }
706
+ else if (!c.to.state) {
707
+ return 'err';
708
+ }
588
709
  return 'ok';
589
710
  }
590
711
  // Persist/apply
@@ -621,11 +742,11 @@ class ConnectionBuilderComponent {
621
742
  return input;
622
743
  }
623
744
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ConnectionBuilderComponent, deps: [{ token: i1.MatDialog }, { token: i1$1.ComponentMetadataRegistry }, { token: i3$1.MatSnackBar }], target: i0.ɵɵFactoryTarget.Component });
624
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", 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> Alertas</button>\n <mat-form-field appearance=\"outline\" class=\"pdx-conn-select\">\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\" class=\"pdx-conn-group\">\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 [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 </ng-template>\n </mat-list>\n <ng-template #noConns>\n <div class=\"pdx-empty-list\" role=\"status\" aria-live=\"polite\">\n <mat-icon>link_off</mat-icon>\n <div>Nenhuma conex\u00E3o definida. Use \"Nova conex\u00E3o\".</div>\n </div>\n </ng-template>\n </div>\n\n <!-- Right: editor form -->\n <div class=\"pdx-conn-editor\" role=\"form\" aria-label=\"Editor de conex\u00E3o\">\n <ng-container *ngIf=\"selectedIndex() >= 0; else emptyEditor\">\n <div class=\"form-grid\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Origem (widget.key)</mat-label>\n <input matInput [(ngModel)]=\"connections()[selectedIndex()].from.widget\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Evento (output)</mat-label>\n <input matInput [(ngModel)]=\"connections()[selectedIndex()].from.output\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Destino (widget.key)</mat-label>\n <input matInput [(ngModel)]=\"connections()[selectedIndex()].to.widget\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Input</mat-label>\n <input matInput [(ngModel)]=\"connections()[selectedIndex()].to.input\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"full-span\">\n <mat-label>Map (opcional)</mat-label>\n <input matInput [(ngModel)]=\"connections()[selectedIndex()].map\" [placeholder]=\"mapPlaceholder\" />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"Ex.: payload.id ou ${payload.id}.\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <div class=\"full-span\">\n <button mat-flat-button color=\"primary\" (click)=\"onSave()\"><mat-icon [praxisIcon]=\"'save'\"></mat-icon>Salvar</button>\n </div>\n </div>\n </ng-container>\n <ng-template #emptyEditor>\n <div class=\"pdx-empty-editor\">Selecione uma conex\u00E3o para editar.</div>\n </ng-template>\n </div>\n </div>\n</div>\n", styles: [".pdx-conn-root{display:grid;gap:12px;color:var(--md-sys-color-on-surface)}.pdx-conn-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.pdx-conn-title{font-weight:600}.pdx-conn-count{color:var(--md-sys-color-on-surface-variant)}.pdx-conn-search{width:260px}.pdx-conn-select{width:160px}.pdx-conn-group{width:170px}.pdx-spacer{flex:1}.pdx-diagram-cta span{margin-left:6px}.pdx-conn-grid{display:grid;grid-template-columns:1.4fr 1fr;gap:12px}.pdx-conn-list{height:540px;overflow:auto;padding:8px 0;border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;background:var(--md-sys-color-surface)}.pdx-conn-list .mat-mdc-list-item.mdc-list-item{height:auto;padding:10px 8px 8px}.pdx-conn-list .mat-mdc-list-item.selected{background:var(--md-sys-color-primary-container)}.card-head{display:flex;align-items:center;gap:6px}.card-head .spacer{flex:1}.card-head .comp-icon{opacity:.85}.card-head .expand-btn{margin-left:4px}.status-pill{display:inline-flex;align-items:center;gap:4px;border-radius:999px;padding:2px 8px}.status-pill.ok{background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}.status-pill.warn{background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container)}.status-pill.err{background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container)}.map-chip{display:inline-flex;align-items:center;gap:6px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.map-chip .map-icon{font-size:16px}.pdx-conn-editor{min-height:540px;padding:8px;border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;background:var(--md-sys-color-surface)}.pdx-empty-list{padding:16px;display:grid;place-items:center;gap:6px;opacity:.8}.pdx-empty-editor{padding:16px;opacity:.75}.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;align-content:start}.full-span{grid-column:1/-1}.stepper{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;padding:8px 0}.step{display:grid;grid-template-columns:12px 1fr;gap:8px;align-items:start}.dot{width:12px;height:12px;border-radius:50%;margin-top:5px}.dot.from{background:var(--md-sys-color-primary-container)}.dot.to{background:var(--md-sys-color-secondary-container)}.dot.map{background:var(--md-sys-color-tertiary-container)}.group-block{padding:6px 6px 10px}.group-header{display:flex;gap:8px;align-items:center;padding:8px 0}.group-title{font-weight:600}.group-count{opacity:.75}.group-list{display:grid;gap:6px}.pdx-badge{width:10px;height:10px;border-radius:50%;display:inline-block}.pdx-badge.from{background:var(--md-sys-color-primary-container)}.pdx-badge.to{background:var(--md-sys-color-secondary-container)}.pdx-badge.map{background:var(--md-sys-color-tertiary-container)}.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:12px}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.help-icon-button mat-icon{font-size:18px;line-height:18px;width:18px;height:18px}@media(max-width:960px){.pdx-conn-grid{grid-template-columns:1fr}.pdx-conn-list,.pdx-conn-editor{min-height:360px}.pdx-conn-search,.pdx-conn-select,.pdx-conn-group{width:100%}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.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: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i3$2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$2.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3$2.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i4.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: MatSelectModule }, { kind: "directive", type: i8.CdkScrollable, selector: "[cdk-scrollable], [cdkScrollable]" }, { kind: "component", type: i9.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i9.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { 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: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", 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: MatListModule }, { kind: "component", type: i12.MatList, selector: "mat-list", exportAs: ["matList"] }, { kind: "component", type: i12.MatListItem, selector: "mat-list-item, a[mat-list-item], button[mat-list-item]", inputs: ["activated"], exportAs: ["matListItem"] }, { kind: "component", type: i12.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "directive", type: i12.MatListItemLine, selector: "[matListItemLine]" }, { kind: "directive", type: i12.MatListItemTitle, selector: "[matListItemTitle]" }, { kind: "ngmodule", type: MatDialogModule }, { kind: "ngmodule", type: MatAutocompleteModule }, { kind: "ngmodule", type: MatMenuModule }, { kind: "ngmodule", type: MatTabsModule }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatProgressBarModule }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "ngmodule", type: MatSnackBarModule }, { kind: "ngmodule", type: ScrollingModule }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
745
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", 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\u00C3\u00B5es\">\n <div class=\"pdx-conn-head\">\n <span class=\"pdx-conn-title\">Conex\u00C3\u00B5es</span>\n <span class=\"pdx-conn-count\" aria-label=\"Conex\u00C3\u00B5es 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> Alertas</button>\n <mat-form-field appearance=\"outline\" class=\"pdx-conn-select\">\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\" class=\"pdx-conn-group\">\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\u00C3\u00A1vel/t\u00C3\u00A9cnico\"><mat-icon [praxisIcon]=\"'translate'\"></mat-icon></button>\n <button mat-stroked-button (click)=\"createNew()\" aria-label=\"Nova conex\u00C3\u00A3o\"><mat-icon [praxisIcon]=\"'add'\"></mat-icon> Nova conex\u00C3\u00A3o</button>\n <button mat-flat-button color=\"accent\" class=\"pdx-diagram-cta\" (click)=\"openDiagramFullscreen()\" aria-label=\"Abrir diagrama em tela cheia\" matTooltip=\"Visualizar conex\u00C3\u00B5es em grafo\"><mat-icon [praxisIcon]=\"'schema'\"></mat-icon><span>Diagrama</span></button>\n </div>\n\n <div class=\"pdx-conn-grid\">\n <div class=\"pdx-conn-list\" role=\"list\" aria-label=\"Lista de conex\u00C3\u00B5es\" 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]=\"fromTooltip(c)\">\n <mat-icon class=\"comp-icon from\" [matTooltip]=\"fromIconTooltip(c)\">{{ fromIcon(c) }}</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\">\u00E2\u2020\u2019</span>\n <mat-icon class=\"comp-icon to\" [matTooltip]=\"toIconTooltip(c)\">{{ toIcon(c) }}</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)\">\u00E2\u20AC\u00A2</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\u00C3\u00A7\u00C3\u00A3o 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 [matTooltip]=\"fromTooltip(c)\">\n <mat-icon class=\"comp-icon from\" [matTooltip]=\"fromIconTooltip(c)\">{{ fromIcon(c) }}</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\">\u00E2\u2020\u2019</span>\n <mat-icon class=\"comp-icon to\" [matTooltip]=\"toIconTooltip(c)\">{{ toIcon(c) }}</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)\">\u00E2\u20AC\u00A2</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\u00C3\u00A7\u00C3\u00A3o 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 </ng-template>\n </mat-list>\n <ng-template #noConns>\n <div class=\"pdx-empty-list\" role=\"status\" aria-live=\"polite\">\n <mat-icon>link_off</mat-icon>\n <div>Nenhuma conex\u00C3\u00A3o definida. Use \"Nova conex\u00C3\u00A3o\".</div>\n </div>\n </ng-template>\n </div>\n\n <div class=\"pdx-conn-editor\" role=\"form\" aria-label=\"Editor de conex\u00C3\u00A3o\">\n <ng-container *ngIf=\"selectedIndex() >= 0; else emptyEditor\">\n <div class=\"form-grid\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Origem (widget.key)</mat-label>\n <input matInput [ngModel]=\"selectedSourcePrimaryValue()\" (ngModelChange)=\"setSelectedSourcePrimaryValue($event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" *ngIf=\"showSelectedSourceSecondary()\">\n <mat-label>Evento (output)</mat-label>\n <input matInput [ngModel]=\"selectedSourceSecondaryValue()\" (ngModelChange)=\"setSelectedSourceSecondaryValue($event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Destino (widget.key)</mat-label>\n <input matInput [ngModel]=\"selectedTargetPrimaryValue()\" (ngModelChange)=\"setSelectedTargetPrimaryValue($event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" *ngIf=\"showSelectedTargetSecondary()\">\n <mat-label>Input</mat-label>\n <input matInput [ngModel]=\"selectedTargetSecondaryValue()\" (ngModelChange)=\"setSelectedTargetSecondaryValue($event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"full-span\">\n <mat-label>Map (opcional)</mat-label>\n <input matInput [ngModel]=\"selectedMapValue()\" (ngModelChange)=\"setSelectedMapValue($event)\" [placeholder]=\"mapPlaceholder\" />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"Ex.: payload.id ou ${payload.id}.\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <div class=\"full-span\">\n <button mat-flat-button color=\"primary\" (click)=\"onSave()\"><mat-icon [praxisIcon]=\"'save'\"></mat-icon>Salvar</button>\n </div>\n </div>\n </ng-container>\n <ng-template #emptyEditor>\n <div class=\"pdx-empty-editor\">Selecione uma conex\u00C3\u00A3o para editar.</div>\n </ng-template>\n </div>\n </div>\n</div>\n", styles: [".pdx-conn-root{display:grid;gap:12px;color:var(--md-sys-color-on-surface)}.pdx-conn-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.pdx-conn-title{font-weight:600}.pdx-conn-count{color:var(--md-sys-color-on-surface-variant)}.pdx-conn-search{width:260px}.pdx-conn-select{width:160px}.pdx-conn-group{width:170px}.pdx-spacer{flex:1}.pdx-diagram-cta span{margin-left:6px}.pdx-conn-grid{display:grid;grid-template-columns:1.4fr 1fr;gap:12px}.pdx-conn-list{height:540px;overflow:auto;padding:8px 0;border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;background:var(--md-sys-color-surface)}.pdx-conn-list .mat-mdc-list-item.mdc-list-item{height:auto;padding:10px 8px 8px}.pdx-conn-list .mat-mdc-list-item.selected{background:var(--md-sys-color-primary-container)}.card-head{display:flex;align-items:center;gap:6px}.card-head .spacer{flex:1}.card-head .comp-icon{opacity:.85}.card-head .expand-btn{margin-left:4px}.status-pill{display:inline-flex;align-items:center;gap:4px;border-radius:999px;padding:2px 8px}.status-pill.ok{background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}.status-pill.warn{background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container)}.status-pill.err{background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container)}.map-chip{display:inline-flex;align-items:center;gap:6px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.map-chip .map-icon{font-size:16px}.pdx-conn-editor{min-height:540px;padding:8px;border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;background:var(--md-sys-color-surface)}.pdx-empty-list{padding:16px;display:grid;place-items:center;gap:6px;opacity:.8}.pdx-empty-editor{padding:16px;opacity:.75}.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;align-content:start}.full-span{grid-column:1/-1}.stepper{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;padding:8px 0}.step{display:grid;grid-template-columns:12px 1fr;gap:8px;align-items:start}.dot{width:12px;height:12px;border-radius:50%;margin-top:5px}.dot.from{background:var(--md-sys-color-primary-container)}.dot.to{background:var(--md-sys-color-secondary-container)}.dot.map{background:var(--md-sys-color-tertiary-container)}.group-block{padding:6px 6px 10px}.group-header{display:flex;gap:8px;align-items:center;padding:8px 0}.group-title{font-weight:600}.group-count{opacity:.75}.group-list{display:grid;gap:6px}.pdx-badge{width:10px;height:10px;border-radius:50%;display:inline-block}.pdx-badge.from{background:var(--md-sys-color-primary-container)}.pdx-badge.to{background:var(--md-sys-color-secondary-container)}.pdx-badge.map{background:var(--md-sys-color-tertiary-container)}.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:12px}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.help-icon-button mat-icon{font-size:18px;line-height:18px;width:18px;height:18px}@media(max-width:960px){.pdx-conn-grid{grid-template-columns:1fr}.pdx-conn-list,.pdx-conn-editor{min-height:360px}.pdx-conn-search,.pdx-conn-select,.pdx-conn-group{width:100%}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.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: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i3$2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$2.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3$2.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i4.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: MatSelectModule }, { kind: "directive", type: i8.CdkScrollable, selector: "[cdk-scrollable], [cdkScrollable]" }, { kind: "component", type: i6.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { 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: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", 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: MatListModule }, { kind: "component", type: i12.MatList, selector: "mat-list", exportAs: ["matList"] }, { kind: "component", type: i12.MatListItem, selector: "mat-list-item, a[mat-list-item], button[mat-list-item]", inputs: ["activated"], exportAs: ["matListItem"] }, { kind: "component", type: i12.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "directive", type: i12.MatListItemLine, selector: "[matListItemLine]" }, { kind: "directive", type: i12.MatListItemTitle, selector: "[matListItemTitle]" }, { kind: "ngmodule", type: MatDialogModule }, { kind: "ngmodule", type: MatAutocompleteModule }, { kind: "ngmodule", type: MatMenuModule }, { kind: "ngmodule", type: MatTabsModule }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatProgressBarModule }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "ngmodule", type: MatSnackBarModule }, { kind: "ngmodule", type: ScrollingModule }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
625
746
  }
626
747
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ConnectionBuilderComponent, decorators: [{
627
748
  type: Component,
628
- args: [{ selector: 'praxis-connection-builder', standalone: true, imports: [CommonModule, FormsModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatButtonModule, MatIconModule, MatListModule, MatDialogModule, MatAutocompleteModule, MatMenuModule, MatTabsModule, MatTooltipModule, MatProgressBarModule, MatCheckboxModule, MatSnackBarModule, ScrollingModule, PraxisIconDirective], changeDetection: ChangeDetectionStrategy.OnPush, 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> Alertas</button>\n <mat-form-field appearance=\"outline\" class=\"pdx-conn-select\">\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\" class=\"pdx-conn-group\">\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 [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 </ng-template>\n </mat-list>\n <ng-template #noConns>\n <div class=\"pdx-empty-list\" role=\"status\" aria-live=\"polite\">\n <mat-icon>link_off</mat-icon>\n <div>Nenhuma conex\u00E3o definida. Use \"Nova conex\u00E3o\".</div>\n </div>\n </ng-template>\n </div>\n\n <!-- Right: editor form -->\n <div class=\"pdx-conn-editor\" role=\"form\" aria-label=\"Editor de conex\u00E3o\">\n <ng-container *ngIf=\"selectedIndex() >= 0; else emptyEditor\">\n <div class=\"form-grid\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Origem (widget.key)</mat-label>\n <input matInput [(ngModel)]=\"connections()[selectedIndex()].from.widget\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Evento (output)</mat-label>\n <input matInput [(ngModel)]=\"connections()[selectedIndex()].from.output\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Destino (widget.key)</mat-label>\n <input matInput [(ngModel)]=\"connections()[selectedIndex()].to.widget\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Input</mat-label>\n <input matInput [(ngModel)]=\"connections()[selectedIndex()].to.input\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"full-span\">\n <mat-label>Map (opcional)</mat-label>\n <input matInput [(ngModel)]=\"connections()[selectedIndex()].map\" [placeholder]=\"mapPlaceholder\" />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"Ex.: payload.id ou ${payload.id}.\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <div class=\"full-span\">\n <button mat-flat-button color=\"primary\" (click)=\"onSave()\"><mat-icon [praxisIcon]=\"'save'\"></mat-icon>Salvar</button>\n </div>\n </div>\n </ng-container>\n <ng-template #emptyEditor>\n <div class=\"pdx-empty-editor\">Selecione uma conex\u00E3o para editar.</div>\n </ng-template>\n </div>\n </div>\n</div>\n", styles: [".pdx-conn-root{display:grid;gap:12px;color:var(--md-sys-color-on-surface)}.pdx-conn-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.pdx-conn-title{font-weight:600}.pdx-conn-count{color:var(--md-sys-color-on-surface-variant)}.pdx-conn-search{width:260px}.pdx-conn-select{width:160px}.pdx-conn-group{width:170px}.pdx-spacer{flex:1}.pdx-diagram-cta span{margin-left:6px}.pdx-conn-grid{display:grid;grid-template-columns:1.4fr 1fr;gap:12px}.pdx-conn-list{height:540px;overflow:auto;padding:8px 0;border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;background:var(--md-sys-color-surface)}.pdx-conn-list .mat-mdc-list-item.mdc-list-item{height:auto;padding:10px 8px 8px}.pdx-conn-list .mat-mdc-list-item.selected{background:var(--md-sys-color-primary-container)}.card-head{display:flex;align-items:center;gap:6px}.card-head .spacer{flex:1}.card-head .comp-icon{opacity:.85}.card-head .expand-btn{margin-left:4px}.status-pill{display:inline-flex;align-items:center;gap:4px;border-radius:999px;padding:2px 8px}.status-pill.ok{background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}.status-pill.warn{background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container)}.status-pill.err{background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container)}.map-chip{display:inline-flex;align-items:center;gap:6px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.map-chip .map-icon{font-size:16px}.pdx-conn-editor{min-height:540px;padding:8px;border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;background:var(--md-sys-color-surface)}.pdx-empty-list{padding:16px;display:grid;place-items:center;gap:6px;opacity:.8}.pdx-empty-editor{padding:16px;opacity:.75}.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;align-content:start}.full-span{grid-column:1/-1}.stepper{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;padding:8px 0}.step{display:grid;grid-template-columns:12px 1fr;gap:8px;align-items:start}.dot{width:12px;height:12px;border-radius:50%;margin-top:5px}.dot.from{background:var(--md-sys-color-primary-container)}.dot.to{background:var(--md-sys-color-secondary-container)}.dot.map{background:var(--md-sys-color-tertiary-container)}.group-block{padding:6px 6px 10px}.group-header{display:flex;gap:8px;align-items:center;padding:8px 0}.group-title{font-weight:600}.group-count{opacity:.75}.group-list{display:grid;gap:6px}.pdx-badge{width:10px;height:10px;border-radius:50%;display:inline-block}.pdx-badge.from{background:var(--md-sys-color-primary-container)}.pdx-badge.to{background:var(--md-sys-color-secondary-container)}.pdx-badge.map{background:var(--md-sys-color-tertiary-container)}.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:12px}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.help-icon-button mat-icon{font-size:18px;line-height:18px;width:18px;height:18px}@media(max-width:960px){.pdx-conn-grid{grid-template-columns:1fr}.pdx-conn-list,.pdx-conn-editor{min-height:360px}.pdx-conn-search,.pdx-conn-select,.pdx-conn-group{width:100%}}\n"] }]
749
+ args: [{ selector: 'praxis-connection-builder', standalone: true, imports: [CommonModule, FormsModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatButtonModule, MatIconModule, MatListModule, MatDialogModule, MatAutocompleteModule, MatMenuModule, MatTabsModule, MatTooltipModule, MatProgressBarModule, MatCheckboxModule, MatSnackBarModule, ScrollingModule, PraxisIconDirective], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"pdx-conn-root\" role=\"region\" aria-label=\"Construtor de Conex\u00C3\u00B5es\">\n <div class=\"pdx-conn-head\">\n <span class=\"pdx-conn-title\">Conex\u00C3\u00B5es</span>\n <span class=\"pdx-conn-count\" aria-label=\"Conex\u00C3\u00B5es 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> Alertas</button>\n <mat-form-field appearance=\"outline\" class=\"pdx-conn-select\">\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\" class=\"pdx-conn-group\">\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\u00C3\u00A1vel/t\u00C3\u00A9cnico\"><mat-icon [praxisIcon]=\"'translate'\"></mat-icon></button>\n <button mat-stroked-button (click)=\"createNew()\" aria-label=\"Nova conex\u00C3\u00A3o\"><mat-icon [praxisIcon]=\"'add'\"></mat-icon> Nova conex\u00C3\u00A3o</button>\n <button mat-flat-button color=\"accent\" class=\"pdx-diagram-cta\" (click)=\"openDiagramFullscreen()\" aria-label=\"Abrir diagrama em tela cheia\" matTooltip=\"Visualizar conex\u00C3\u00B5es em grafo\"><mat-icon [praxisIcon]=\"'schema'\"></mat-icon><span>Diagrama</span></button>\n </div>\n\n <div class=\"pdx-conn-grid\">\n <div class=\"pdx-conn-list\" role=\"list\" aria-label=\"Lista de conex\u00C3\u00B5es\" 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]=\"fromTooltip(c)\">\n <mat-icon class=\"comp-icon from\" [matTooltip]=\"fromIconTooltip(c)\">{{ fromIcon(c) }}</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\">\u00E2\u2020\u2019</span>\n <mat-icon class=\"comp-icon to\" [matTooltip]=\"toIconTooltip(c)\">{{ toIcon(c) }}</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)\">\u00E2\u20AC\u00A2</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\u00C3\u00A7\u00C3\u00A3o 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 [matTooltip]=\"fromTooltip(c)\">\n <mat-icon class=\"comp-icon from\" [matTooltip]=\"fromIconTooltip(c)\">{{ fromIcon(c) }}</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\">\u00E2\u2020\u2019</span>\n <mat-icon class=\"comp-icon to\" [matTooltip]=\"toIconTooltip(c)\">{{ toIcon(c) }}</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)\">\u00E2\u20AC\u00A2</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\u00C3\u00A7\u00C3\u00A3o 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 </ng-template>\n </mat-list>\n <ng-template #noConns>\n <div class=\"pdx-empty-list\" role=\"status\" aria-live=\"polite\">\n <mat-icon>link_off</mat-icon>\n <div>Nenhuma conex\u00C3\u00A3o definida. Use \"Nova conex\u00C3\u00A3o\".</div>\n </div>\n </ng-template>\n </div>\n\n <div class=\"pdx-conn-editor\" role=\"form\" aria-label=\"Editor de conex\u00C3\u00A3o\">\n <ng-container *ngIf=\"selectedIndex() >= 0; else emptyEditor\">\n <div class=\"form-grid\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Origem (widget.key)</mat-label>\n <input matInput [ngModel]=\"selectedSourcePrimaryValue()\" (ngModelChange)=\"setSelectedSourcePrimaryValue($event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" *ngIf=\"showSelectedSourceSecondary()\">\n <mat-label>Evento (output)</mat-label>\n <input matInput [ngModel]=\"selectedSourceSecondaryValue()\" (ngModelChange)=\"setSelectedSourceSecondaryValue($event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Destino (widget.key)</mat-label>\n <input matInput [ngModel]=\"selectedTargetPrimaryValue()\" (ngModelChange)=\"setSelectedTargetPrimaryValue($event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" *ngIf=\"showSelectedTargetSecondary()\">\n <mat-label>Input</mat-label>\n <input matInput [ngModel]=\"selectedTargetSecondaryValue()\" (ngModelChange)=\"setSelectedTargetSecondaryValue($event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"full-span\">\n <mat-label>Map (opcional)</mat-label>\n <input matInput [ngModel]=\"selectedMapValue()\" (ngModelChange)=\"setSelectedMapValue($event)\" [placeholder]=\"mapPlaceholder\" />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"Ex.: payload.id ou ${payload.id}.\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <div class=\"full-span\">\n <button mat-flat-button color=\"primary\" (click)=\"onSave()\"><mat-icon [praxisIcon]=\"'save'\"></mat-icon>Salvar</button>\n </div>\n </div>\n </ng-container>\n <ng-template #emptyEditor>\n <div class=\"pdx-empty-editor\">Selecione uma conex\u00C3\u00A3o para editar.</div>\n </ng-template>\n </div>\n </div>\n</div>\n", styles: [".pdx-conn-root{display:grid;gap:12px;color:var(--md-sys-color-on-surface)}.pdx-conn-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.pdx-conn-title{font-weight:600}.pdx-conn-count{color:var(--md-sys-color-on-surface-variant)}.pdx-conn-search{width:260px}.pdx-conn-select{width:160px}.pdx-conn-group{width:170px}.pdx-spacer{flex:1}.pdx-diagram-cta span{margin-left:6px}.pdx-conn-grid{display:grid;grid-template-columns:1.4fr 1fr;gap:12px}.pdx-conn-list{height:540px;overflow:auto;padding:8px 0;border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;background:var(--md-sys-color-surface)}.pdx-conn-list .mat-mdc-list-item.mdc-list-item{height:auto;padding:10px 8px 8px}.pdx-conn-list .mat-mdc-list-item.selected{background:var(--md-sys-color-primary-container)}.card-head{display:flex;align-items:center;gap:6px}.card-head .spacer{flex:1}.card-head .comp-icon{opacity:.85}.card-head .expand-btn{margin-left:4px}.status-pill{display:inline-flex;align-items:center;gap:4px;border-radius:999px;padding:2px 8px}.status-pill.ok{background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}.status-pill.warn{background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container)}.status-pill.err{background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container)}.map-chip{display:inline-flex;align-items:center;gap:6px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.map-chip .map-icon{font-size:16px}.pdx-conn-editor{min-height:540px;padding:8px;border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;background:var(--md-sys-color-surface)}.pdx-empty-list{padding:16px;display:grid;place-items:center;gap:6px;opacity:.8}.pdx-empty-editor{padding:16px;opacity:.75}.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;align-content:start}.full-span{grid-column:1/-1}.stepper{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;padding:8px 0}.step{display:grid;grid-template-columns:12px 1fr;gap:8px;align-items:start}.dot{width:12px;height:12px;border-radius:50%;margin-top:5px}.dot.from{background:var(--md-sys-color-primary-container)}.dot.to{background:var(--md-sys-color-secondary-container)}.dot.map{background:var(--md-sys-color-tertiary-container)}.group-block{padding:6px 6px 10px}.group-header{display:flex;gap:8px;align-items:center;padding:8px 0}.group-title{font-weight:600}.group-count{opacity:.75}.group-list{display:grid;gap:6px}.pdx-badge{width:10px;height:10px;border-radius:50%;display:inline-block}.pdx-badge.from{background:var(--md-sys-color-primary-container)}.pdx-badge.to{background:var(--md-sys-color-secondary-container)}.pdx-badge.map{background:var(--md-sys-color-tertiary-container)}.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:12px}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.help-icon-button mat-icon{font-size:18px;line-height:18px;width:18px;height:18px}@media(max-width:960px){.pdx-conn-grid{grid-template-columns:1fr}.pdx-conn-list,.pdx-conn-editor{min-height:360px}.pdx-conn-search,.pdx-conn-select,.pdx-conn-group{width:100%}}\n"] }]
629
750
  }], ctorParameters: () => [{ type: i1.MatDialog }, { type: i1$1.ComponentMetadataRegistry }, { type: i3$1.MatSnackBar }], propDecorators: { page: [{
630
751
  type: Input
631
752
  }], widgets: [{
@@ -1729,7 +1850,7 @@ class WidgetShellEditorComponent {
1729
1850
  <div class="shell-empty">Nenhuma ação configurada.</div>
1730
1851
  </ng-template>
1731
1852
  </div>
1732
- `, isInline: true, styles: [".shell-editor{display:grid;gap:12px;padding:16px;background:var(--md-sys-color-surface-container-lowest, var(--md-sys-color-surface));border:1px solid var(--md-sys-color-outline-variant);border-radius:12px}.shell-head{display:flex;align-items:center;gap:12px}.shell-title{font-weight:600}.shell-subtitle{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-section{font-weight:600;margin-top:8px}.shell-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}.shell-flags{display:flex;gap:16px;align-items:center}.shell-actions-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.shell-available{display:grid;gap:12px}.shell-available-toolbar{display:grid;gap:12px;grid-template-columns:minmax(200px,1fr)}.shell-available-filters{display:flex;gap:12px;align-items:center;flex-wrap:wrap}.shell-available-list{display:grid;gap:10px}.shell-available-item{display:grid;gap:8px;grid-template-columns:1fr auto;border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;padding:12px;background:var(--md-sys-color-surface-container-low)}.shell-available-title{display:flex;align-items:center;gap:10px}.shell-available-text{display:grid;gap:4px}.shell-available-label{font-weight:600}.shell-available-meta{display:flex;gap:8px;flex-wrap:wrap;font-size:11px;color:var(--md-sys-color-on-surface-variant)}.shell-available-desc{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-available-payload{font-size:11px;color:var(--md-sys-color-on-surface-variant)}.shell-available-warning{font-size:11px;color:var(--md-sys-color-error)}.shell-available-actions{display:flex;align-items:flex-start;justify-content:flex-end}.shell-badge{padding:2px 6px;border-radius:999px;border:1px solid var(--md-sys-color-outline-variant);font-size:10px}.shell-code{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.shell-actions{display:grid;gap:12px}.shell-action{border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;padding:12px;display:grid;gap:8px}.shell-action-row{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}.shell-action-actions{display:flex;justify-content:flex-end}.shell-action-hint,.shell-empty{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-hint{align-self:center;font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-quick-inputs{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));align-items:end}.shell-input-path{font-size:12px;color:var(--md-sys-color-on-surface-variant);align-self:center}.shell-editor .mat-mdc-form-field{width:100%}.shell-preview{border:1px dashed var(--md-sys-color-outline-variant);border-radius:12px;padding:12px}.shell-preview-card{background:var(--pdx-shell-card-bg, var(--md-sys-color-surface));border:1px solid var(--pdx-shell-card-border, var(--md-sys-color-outline-variant));border-radius:var(--pdx-shell-card-radius, 14px);box-shadow:var(--pdx-shell-card-shadow, var(--mat-elevation-level2));overflow:hidden}.shell-preview-card.no-shell{background:transparent;border:none;box-shadow:none}.shell-preview-card.expanded{box-shadow:var(--pdx-shell-card-shadow, 0 16px 34px rgba(0,0,0,.32));transform:scale(1.01)}.shell-preview-header{display:flex;align-items:center;gap:12px;padding:10px 12px 8px;border-bottom:1px solid var(--pdx-shell-header-border, var(--md-sys-color-outline-variant));background:var(--pdx-shell-header-bg, var(--md-sys-color-surface-container))}.shell-preview-title{display:flex;align-items:center;gap:10px;min-width:0;flex:1;color:var(--pdx-shell-title-color, inherit)}.shell-preview-title mat-icon{color:var(--pdx-shell-icon-color, currentColor)}.shell-preview-text{min-width:0}.shell-preview-title-text{font-weight:var(--pdx-shell-title-weight, 600);font-size:var(--pdx-shell-title-size, 14px);line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.shell-preview-subtitle{font-size:var(--pdx-shell-subtitle-size, 12px);opacity:.75;color:var(--pdx-shell-subtitle-color, currentColor);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.shell-preview-actions{display:flex;align-items:center;gap:6px}.pdx-preview-text{padding:0 8px}.pdx-preview-outlined{border:1px solid var(--md-sys-color-outline-variant);border-radius:999px;padding:0 10px}.pdx-preview-label{font-size:12px;font-weight:500}.shell-preview-body{padding:var(--pdx-shell-body-padding, 10px 12px 12px 12px);background:var(--pdx-shell-body-bg, transparent);color:var(--pdx-shell-body-color, inherit);font-size:12px}.shell-preview-body.hidden{display:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2$1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.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: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { 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: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i5.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i12.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i3$2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$2.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i4.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: MatSelectModule }, { kind: "component", type: i9.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i9.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1853
+ `, isInline: true, styles: [".shell-editor{display:grid;gap:12px;padding:16px;background:var(--md-sys-color-surface-container-lowest, var(--md-sys-color-surface));border:1px solid var(--md-sys-color-outline-variant);border-radius:12px}.shell-head{display:flex;align-items:center;gap:12px}.shell-title{font-weight:600}.shell-subtitle{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-section{font-weight:600;margin-top:8px}.shell-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}.shell-flags{display:flex;gap:16px;align-items:center}.shell-actions-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.shell-available{display:grid;gap:12px}.shell-available-toolbar{display:grid;gap:12px;grid-template-columns:minmax(200px,1fr)}.shell-available-filters{display:flex;gap:12px;align-items:center;flex-wrap:wrap}.shell-available-list{display:grid;gap:10px}.shell-available-item{display:grid;gap:8px;grid-template-columns:1fr auto;border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;padding:12px;background:var(--md-sys-color-surface-container-low)}.shell-available-title{display:flex;align-items:center;gap:10px}.shell-available-text{display:grid;gap:4px}.shell-available-label{font-weight:600}.shell-available-meta{display:flex;gap:8px;flex-wrap:wrap;font-size:11px;color:var(--md-sys-color-on-surface-variant)}.shell-available-desc{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-available-payload{font-size:11px;color:var(--md-sys-color-on-surface-variant)}.shell-available-warning{font-size:11px;color:var(--md-sys-color-error)}.shell-available-actions{display:flex;align-items:flex-start;justify-content:flex-end}.shell-badge{padding:2px 6px;border-radius:999px;border:1px solid var(--md-sys-color-outline-variant);font-size:10px}.shell-code{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.shell-actions{display:grid;gap:12px}.shell-action{border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;padding:12px;display:grid;gap:8px}.shell-action-row{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}.shell-action-actions{display:flex;justify-content:flex-end}.shell-action-hint,.shell-empty{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-hint{align-self:center;font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-quick-inputs{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));align-items:end}.shell-input-path{font-size:12px;color:var(--md-sys-color-on-surface-variant);align-self:center}.shell-editor .mat-mdc-form-field{width:100%}.shell-preview{border:1px dashed var(--md-sys-color-outline-variant);border-radius:12px;padding:12px}.shell-preview-card{background:var(--pdx-shell-card-bg, var(--md-sys-color-surface));border:1px solid var(--pdx-shell-card-border, var(--md-sys-color-outline-variant));border-radius:var(--pdx-shell-card-radius, 14px);box-shadow:var(--pdx-shell-card-shadow, var(--mat-elevation-level2));overflow:hidden}.shell-preview-card.no-shell{background:transparent;border:none;box-shadow:none}.shell-preview-card.expanded{box-shadow:var(--pdx-shell-card-shadow, 0 16px 34px rgba(0,0,0,.32));transform:scale(1.01)}.shell-preview-header{display:flex;align-items:center;gap:12px;padding:10px 12px 8px;border-bottom:1px solid var(--pdx-shell-header-border, var(--md-sys-color-outline-variant));background:var(--pdx-shell-header-bg, var(--md-sys-color-surface-container))}.shell-preview-title{display:flex;align-items:center;gap:10px;min-width:0;flex:1;color:var(--pdx-shell-title-color, inherit)}.shell-preview-title mat-icon{color:var(--pdx-shell-icon-color, currentColor)}.shell-preview-text{min-width:0}.shell-preview-title-text{font-weight:var(--pdx-shell-title-weight, 600);font-size:var(--pdx-shell-title-size, 14px);line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.shell-preview-subtitle{font-size:var(--pdx-shell-subtitle-size, 12px);opacity:.75;color:var(--pdx-shell-subtitle-color, currentColor);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.shell-preview-actions{display:flex;align-items:center;gap:6px}.pdx-preview-text{padding:0 8px}.pdx-preview-outlined{border:1px solid var(--md-sys-color-outline-variant);border-radius:999px;padding:0 10px}.pdx-preview-label{font-size:12px;font-weight:500}.shell-preview-body{padding:var(--pdx-shell-body-padding, 10px 12px 12px 12px);background:var(--pdx-shell-body-bg, transparent);color:var(--pdx-shell-body-color, inherit);font-size:12px}.shell-preview-body.hidden{display:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2$1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.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: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { 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: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i5.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i12.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i3$2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$2.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i4.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: MatSelectModule }, { kind: "component", type: i6.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1733
1854
  }
1734
1855
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: WidgetShellEditorComponent, decorators: [{
1735
1856
  type: Component,
@@ -2421,7 +2542,7 @@ class DynamicPageConfigEditorComponent {
2421
2542
  </mat-tab>
2422
2543
  </mat-tab-group>
2423
2544
  </div>
2424
- `, isInline: true, styles: [".page-config{display:flex;flex-direction:column;gap:12px;padding:16px;background:var(--md-sys-color-surface-container-lowest, var(--md-sys-color-surface));border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;height:100%;box-sizing:border-box}.page-head{display:flex;align-items:center;gap:12px;flex-shrink:0}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center}.help-icon-button mat-icon{font-size:18px;width:18px;height:18px}.page-title{font-weight:600}.spacer{flex:1}.page-section{font-weight:600;margin-top:8px}.page-form{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}.page-config .mat-mdc-form-field{width:100%}.config-tabs{flex:1;min-height:0;display:flex;flex-direction:column}::ng-deep .config-tabs .mat-mdc-tab-body-wrapper{flex:1;min-height:0;display:flex;flex-direction:column}::ng-deep .mat-mdc-tab-body,::ng-deep .mat-mdc-tab-body-content{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden}.tab-content{padding-top:16px;display:flex;flex-direction:column;gap:12px;height:100%;box-sizing:border-box;overflow-y:auto}.json-editor-container{display:flex;flex-direction:column;height:100%;flex:1;gap:8px;overflow:hidden}.json-toolbar{display:flex;align-items:center;gap:8px;flex-shrink:0}.json-editor-wrapper{flex:1;width:100%;height:100%;border:1px solid var(--md-sys-color-outline-variant);border-radius:4px;overflow:hidden;background-color:var(--md-sys-color-surface)}.json-editor-wrapper textarea{width:100%;height:100%;border:none;outline:none;resize:none;padding:12px;box-sizing:border-box;font-family:Monaco,Menlo,Ubuntu Mono,Consolas,monospace;font-size:13px;line-height:1.4;background:transparent;color:var(--md-sys-color-on-surface);white-space:pre}.json-editor-wrapper textarea:focus{background-color:var(--md-sys-color-surface-container-low)}.json-error{color:var(--md-sys-color-error);font-size:12px;padding:0 8px;flex-shrink:0}.validation-status{font-size:12px;font-weight:500;padding:4px 8px;border-radius:4px}.validation-status.valid{color:var(--md-sys-color-primary);background:var(--md-sys-color-primary-container)}.validation-status.invalid{color:var(--md-sys-color-error);background:var(--md-sys-color-error-container)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$2.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: i1$2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { 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: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i3$2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$2.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i4.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: MatSelectModule }, { kind: "component", type: i9.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i9.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i9$1.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i9$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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2545
+ `, isInline: true, styles: [".page-config{display:flex;flex-direction:column;gap:12px;padding:16px;background:var(--md-sys-color-surface-container-lowest, var(--md-sys-color-surface));border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;height:100%;box-sizing:border-box}.page-head{display:flex;align-items:center;gap:12px;flex-shrink:0}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center}.help-icon-button mat-icon{font-size:18px;width:18px;height:18px}.page-title{font-weight:600}.spacer{flex:1}.page-section{font-weight:600;margin-top:8px}.page-form{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}.page-config .mat-mdc-form-field{width:100%}.config-tabs{flex:1;min-height:0;display:flex;flex-direction:column}::ng-deep .config-tabs .mat-mdc-tab-body-wrapper{flex:1;min-height:0;display:flex;flex-direction:column}::ng-deep .mat-mdc-tab-body,::ng-deep .mat-mdc-tab-body-content{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden}.tab-content{padding-top:16px;display:flex;flex-direction:column;gap:12px;height:100%;box-sizing:border-box;overflow-y:auto}.json-editor-container{display:flex;flex-direction:column;height:100%;flex:1;gap:8px;overflow:hidden}.json-toolbar{display:flex;align-items:center;gap:8px;flex-shrink:0}.json-editor-wrapper{flex:1;width:100%;height:100%;border:1px solid var(--md-sys-color-outline-variant);border-radius:4px;overflow:hidden;background-color:var(--md-sys-color-surface)}.json-editor-wrapper textarea{width:100%;height:100%;border:none;outline:none;resize:none;padding:12px;box-sizing:border-box;font-family:Monaco,Menlo,Ubuntu Mono,Consolas,monospace;font-size:13px;line-height:1.4;background:transparent;color:var(--md-sys-color-on-surface);white-space:pre}.json-editor-wrapper textarea:focus{background-color:var(--md-sys-color-surface-container-low)}.json-error{color:var(--md-sys-color-error);font-size:12px;padding:0 8px;flex-shrink:0}.validation-status{font-size:12px;font-weight:500;padding:4px 8px;border-radius:4px}.validation-status.valid{color:var(--md-sys-color-primary);background:var(--md-sys-color-primary-container)}.validation-status.invalid{color:var(--md-sys-color-error);background:var(--md-sys-color-error-container)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$2.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: i1$2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { 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: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i3$2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$2.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i4.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: MatSelectModule }, { kind: "component", type: i6.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i9.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i9.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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2425
2546
  }
2426
2547
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: DynamicPageConfigEditorComponent, decorators: [{
2427
2548
  type: Component,
@@ -2555,7 +2676,7 @@ const PAGE_BUILDER_AI_CAPABILITIES = {
2555
2676
  'praxis-dynamic-gridster-page',
2556
2677
  ],
2557
2678
  notes: [
2558
- 'page segue o modelo GridPageDefinition (widgets, connections, options, context).',
2679
+ 'page segue o modelo GridPageDefinition (widgets, connections, options, context e state opcional).',
2559
2680
  'pageIdentity identifica o escopo de persistência e não pertence ao objeto page.',
2560
2681
  ],
2561
2682
  capabilities: [
@@ -2577,6 +2698,11 @@ const PAGE_BUILDER_AI_CAPABILITIES = {
2577
2698
  { path: 'page.options.cols', category: 'layout', valueKind: 'number', description: 'Numero de colunas do grid.' },
2578
2699
  { path: 'page.options.rowHeight', category: 'layout', valueKind: 'number', description: 'Altura das linhas (px).' },
2579
2700
  { path: 'page.options.gap', category: 'layout', valueKind: 'number', description: 'Espacamento entre widgets (px).' },
2701
+ // --- Page state ---
2702
+ { path: 'page.state', category: 'state', valueKind: 'object', description: 'Estado declarativo opcional da pagina.' },
2703
+ { path: 'page.state.values', category: 'state', valueKind: 'object', description: 'Valores primarios persistiveis do estado da pagina.' },
2704
+ { path: 'page.state.schema', category: 'state', valueKind: 'object', description: 'Descritores dos paths de estado primario.' },
2705
+ { path: 'page.state.derived', category: 'state', valueKind: 'object', description: 'Descritores de estado derivado recomputado pelo runtime.' },
2580
2706
  // --- Widgets ---
2581
2707
  { path: 'page.widgets', category: 'widgets', valueKind: 'array', description: 'Widgets da página.' },
2582
2708
  { path: 'page.widgets[].key', category: 'widgets', valueKind: 'string', description: 'ID unico do widget.', critical: true },
@@ -2595,10 +2721,12 @@ const PAGE_BUILDER_AI_CAPABILITIES = {
2595
2721
  { path: 'page.connections[].from', category: 'connections', valueKind: 'object', description: 'Origem da conexão.' },
2596
2722
  { path: 'page.connections[].from.widget', category: 'connections', valueKind: 'string', description: 'Widget origem (key).' },
2597
2723
  { path: 'page.connections[].from.output', category: 'connections', valueKind: 'string', description: 'Output do widget origem.' },
2724
+ { path: 'page.connections[].from.state', category: 'connections', valueKind: 'string', description: 'Path de state origem quando a conexao parte do estado da pagina.' },
2598
2725
  { path: 'page.connections[].to', category: 'connections', valueKind: 'object', description: 'Destino da conexão.' },
2599
2726
  { path: 'page.connections[].to.widget', category: 'connections', valueKind: 'string', description: 'Widget destino (key).' },
2600
2727
  { path: 'page.connections[].to.input', category: 'connections', valueKind: 'string', description: 'Input destino (path).' },
2601
2728
  { path: 'page.connections[].to.bindingOrder', category: 'connections', valueKind: 'array', description: 'Ordem de aplicação dos inputs.' },
2729
+ { path: 'page.connections[].to.state', category: 'connections', valueKind: 'string', description: 'Path de state destino quando a conexao escreve no estado da pagina.' },
2602
2730
  { path: 'page.connections[].map', category: 'connections', valueKind: 'expression', description: 'Mapeamento do payload.' },
2603
2731
  { path: 'page.connections[].set', category: 'connections', valueKind: 'object', description: 'Overrides fixos aplicados ao destino.' },
2604
2732
  { path: 'page.connections[].meta', category: 'connections', valueKind: 'object', description: 'Metadados opcionais para operadores de runtime.' },
@@ -2906,6 +3034,12 @@ class GraphMapperService {
2906
3034
  constructor(registry) {
2907
3035
  this.registry = registry;
2908
3036
  }
3037
+ isWidgetSource(from) {
3038
+ return 'widget' in from;
3039
+ }
3040
+ isWidgetTarget(to) {
3041
+ return 'widget' in to;
3042
+ }
2909
3043
  /** Build nodes and edges for a given page definition and widgets. */
2910
3044
  mapToGraph(page, widgets) {
2911
3045
  const nodes = [];
@@ -2918,6 +3052,7 @@ class GraphMapperService {
2918
3052
  const gap = 28;
2919
3053
  let leftY = 0;
2920
3054
  let rightY = 0;
3055
+ const stateNodeIds = new Map();
2921
3056
  // First pass: create a node per widget
2922
3057
  for (const w of widgets) {
2923
3058
  const meta = this.registry.get(w.definition.id);
@@ -3055,14 +3190,25 @@ class GraphMapperService {
3055
3190
  const conns = (page?.connections || []);
3056
3191
  for (let i = 0; i < conns.length; i++) {
3057
3192
  const c = conns[i];
3058
- const fromNodeId = c.from.widget;
3059
- const fromPortId = `out:${c.from.output}`;
3060
- let toNodeId = c.to.widget;
3061
- let toPortId = `in:${c.to.input}`;
3062
- const deep = this.parseTabsPath(c.to.input);
3193
+ const fromState = !this.isWidgetSource(c.from)
3194
+ ? this.ensureStateNode(stateNodeIds, nodeById, nodes, page, c.from.state, 'output', colWidth, gap, leftY)
3195
+ : null;
3196
+ const fromNodeId = this.isWidgetSource(c.from) ? c.from.widget : fromState.nodeId;
3197
+ const fromPortId = this.isWidgetSource(c.from) ? `out:${c.from.output}` : 'out:state';
3198
+ const toState = !this.isWidgetTarget(c.to)
3199
+ ? this.ensureStateNode(stateNodeIds, nodeById, nodes, page, c.to.state, 'input', colWidth, gap, rightY)
3200
+ : null;
3201
+ let toNodeId = this.isWidgetTarget(c.to) ? c.to.widget : toState.nodeId;
3202
+ let toPortId = this.isWidgetTarget(c.to) ? `in:${c.to.input}` : 'in:state';
3203
+ if (fromState?.created)
3204
+ leftY += rowHeight + gap;
3205
+ if (toState?.created)
3206
+ rightY += rowHeight + gap;
3207
+ const deep = this.isWidgetTarget(c.to) ? this.parseTabsPath(c.to.input) : null;
3063
3208
  let friendlyPath;
3064
- if (deep) {
3065
- const childId = `${c.to.widget}#${deep.kind}[${deep.index}]/${deep.widgetIndex}`;
3209
+ if (deep && this.isWidgetTarget(c.to)) {
3210
+ const target = c.to;
3211
+ const childId = `${target.widget}#${deep.kind}[${deep.index}]/${deep.widgetIndex}`;
3066
3212
  // If such child exists, route edge to it and its input name
3067
3213
  if (nodes.some((n) => n.id === childId)) {
3068
3214
  toNodeId = childId;
@@ -3072,7 +3218,7 @@ class GraphMapperService {
3072
3218
  // Keep to widget but still expose label/path
3073
3219
  toPortId = `in:${deep.input}`;
3074
3220
  }
3075
- const cfg = tabsCfgByKey.get(c.to.widget);
3221
+ const cfg = tabsCfgByKey.get(target.widget);
3076
3222
  const labelFromCfg = deep.kind === 'group' ? cfg?.tabs?.[deep.index]?.textLabel : cfg?.nav?.links?.[deep.index]?.label;
3077
3223
  const containerLabel = labelFromCfg ? (deep.kind === 'group' ? `Aba: ${labelFromCfg}` : `Link: ${labelFromCfg}`) : (deep.kind === 'group' ? `Aba ${deep.index + 1}` : `Link ${deep.index + 1}`);
3078
3224
  const child = nodes.find(n => n.id === childId);
@@ -3086,12 +3232,50 @@ class GraphMapperService {
3086
3232
  from: { nodeId: fromNodeId, portId: fromPortId },
3087
3233
  to: { nodeId: toNodeId, portId: toPortId },
3088
3234
  label: c.map || undefined,
3089
- meta: { map: c.map, bindingOrder: c.to.bindingOrder, ...(friendlyPath ? { friendlyPath } : {}) },
3235
+ meta: { map: c.map, bindingOrder: this.isWidgetTarget(c.to) ? c.to.bindingOrder : undefined, ...(friendlyPath ? { friendlyPath } : {}) },
3090
3236
  };
3091
3237
  edges.push(edge);
3092
3238
  }
3093
3239
  return { nodes, edges };
3094
3240
  }
3241
+ ensureStateNode(stateNodeIds, nodeById, nodes, page, statePath, kind, colWidth, gap, offsetY) {
3242
+ const key = `${kind}:${statePath}`;
3243
+ const existing = stateNodeIds.get(key);
3244
+ if (existing)
3245
+ return { nodeId: existing, created: false };
3246
+ const nodeId = `state:${kind}:${encodeURIComponent(statePath)}`;
3247
+ const portId = kind === 'output' ? 'out:state' : 'in:state';
3248
+ const x = kind === 'output' ? gap : colWidth + gap * 3;
3249
+ const structuredState = this.normalizeState(page?.state);
3250
+ const isDerived = !!structuredState?.derived?.[statePath];
3251
+ const node = {
3252
+ id: nodeId,
3253
+ label: `${isDerived ? 'derived' : 'state'}:${statePath}`,
3254
+ icon: isDerived ? 'function' : 'account_tree',
3255
+ type: isDerived ? 'page-derived-state' : 'page-state',
3256
+ parentId: null,
3257
+ collapsed: true,
3258
+ bounds: { x, y: gap + offsetY, width: colWidth, height: 72 },
3259
+ ports: [{
3260
+ id: portId,
3261
+ label: statePath,
3262
+ description: statePath,
3263
+ kind,
3264
+ anchor: { x: kind === 'output' ? x : x + colWidth, y: gap + offsetY + 24 },
3265
+ }],
3266
+ };
3267
+ nodes.push(node);
3268
+ nodeById.set(nodeId, node);
3269
+ stateNodeIds.set(key, nodeId);
3270
+ return { nodeId, created: true };
3271
+ }
3272
+ normalizeState(state) {
3273
+ if (!state || typeof state !== 'object' || Array.isArray(state))
3274
+ return null;
3275
+ if ('values' in state || 'schema' in state || 'derived' in state)
3276
+ return state;
3277
+ return null;
3278
+ }
3095
3279
  ensurePort(nodeById, nodeId, portId, kind, colWidth) {
3096
3280
  const n = nodeById.get(nodeId);
3097
3281
  if (!n)
@@ -3171,6 +3355,21 @@ class ConnectionGraphComponent {
3171
3355
  initialSnapshot = '[]';
3172
3356
  placeholderSample = 'payload | payload.id | ${payload.id}';
3173
3357
  connectedPorts = new Map();
3358
+ isWidgetSource(from) {
3359
+ return 'widget' in from;
3360
+ }
3361
+ isWidgetTarget(to) {
3362
+ return 'widget' in to;
3363
+ }
3364
+ fromLabel(c) {
3365
+ return this.isWidgetSource(c.from) ? `${c.from.widget}.${c.from.output}` : `state:${c.from.state}`;
3366
+ }
3367
+ toLabel(c) {
3368
+ return this.isWidgetTarget(c.to) ? `${c.to.widget}.${c.to.input}` : `state:${c.to.state}`;
3369
+ }
3370
+ bindingOrderLabel(c) {
3371
+ return this.isWidgetTarget(c.to) && c.to.bindingOrder?.length ? c.to.bindingOrder.join(', ') : '';
3372
+ }
3174
3373
  isPortConnected(nodeId, portId) {
3175
3374
  const set = this.connectedPorts.get(nodeId);
3176
3375
  return !!set && set.has(portId);
@@ -3244,12 +3443,12 @@ class ConnectionGraphComponent {
3244
3443
  }
3245
3444
  // External focus by connection (from Builder or other callers)
3246
3445
  focusConnection(conn) {
3247
- const fromNodeId = conn.from.widget;
3248
- const fromPortId = `out:${conn.from.output}`;
3249
- let toNodeId = conn.to.widget;
3250
- let toPortId = `in:${conn.to.input}`;
3251
- const deep = this.parseTabsPath(conn.to.input);
3252
- if (deep) {
3446
+ const fromNodeId = this.isWidgetSource(conn.from) ? conn.from.widget : `state:output:${encodeURIComponent(conn.from.state)}`;
3447
+ const fromPortId = this.isWidgetSource(conn.from) ? `out:${conn.from.output}` : 'out:state';
3448
+ let toNodeId = this.isWidgetTarget(conn.to) ? conn.to.widget : `state:input:${encodeURIComponent(conn.to.state)}`;
3449
+ let toPortId = this.isWidgetTarget(conn.to) ? `in:${conn.to.input}` : 'in:state';
3450
+ const deep = this.isWidgetTarget(conn.to) ? this.parseTabsPath(conn.to.input) : null;
3451
+ if (deep && this.isWidgetTarget(conn.to)) {
3253
3452
  const childId = `${conn.to.widget}#${deep.kind}[${deep.index}]/${deep.widgetIndex}`;
3254
3453
  if (this.nodes().some(n => n.id === childId)) {
3255
3454
  toNodeId = childId;
@@ -3619,10 +3818,10 @@ class ConnectionGraphComponent {
3619
3818
  <button mat-icon-button (click)="showDetails=false" aria-label="Fechar"><mat-icon>close</mat-icon></button>
3620
3819
  </div>
3621
3820
  <div class="details-body" *ngIf="currentConn() as c">
3622
- <div><b>De:</b> {{ c.from.widget }}.{{ c.from.output }}</div>
3623
- <div><b>Para:</b> {{ c.to.widget }}.{{ c.to.input }}</div>
3821
+ <div><b>De:</b> {{ fromLabel(c) }}</div>
3822
+ <div><b>Para:</b> {{ toLabel(c) }}</div>
3624
3823
  <div><b>map:</b> {{ c.map || 'payload' }}</div>
3625
- <div *ngIf="c.to.bindingOrder?.length"><b>bindingOrder:</b> {{ c.to.bindingOrder?.join(', ') }}</div>
3824
+ <div *ngIf="bindingOrderLabel(c)"><b>bindingOrder:</b> {{ bindingOrderLabel(c) }}</div>
3626
3825
  <div class="details-actions">
3627
3826
  <button mat-button color="primary" (click)="openBuilder()"><mat-icon>tune</mat-icon> Editar no Builder</button>
3628
3827
  <button mat-button color="warn" (click)="removeEdge(selectedEdgeIndex())"><mat-icon>delete</mat-icon> Remover</button>
@@ -3755,10 +3954,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3755
3954
  <button mat-icon-button (click)="showDetails=false" aria-label="Fechar"><mat-icon>close</mat-icon></button>
3756
3955
  </div>
3757
3956
  <div class="details-body" *ngIf="currentConn() as c">
3758
- <div><b>De:</b> {{ c.from.widget }}.{{ c.from.output }}</div>
3759
- <div><b>Para:</b> {{ c.to.widget }}.{{ c.to.input }}</div>
3957
+ <div><b>De:</b> {{ fromLabel(c) }}</div>
3958
+ <div><b>Para:</b> {{ toLabel(c) }}</div>
3760
3959
  <div><b>map:</b> {{ c.map || 'payload' }}</div>
3761
- <div *ngIf="c.to.bindingOrder?.length"><b>bindingOrder:</b> {{ c.to.bindingOrder?.join(', ') }}</div>
3960
+ <div *ngIf="bindingOrderLabel(c)"><b>bindingOrder:</b> {{ bindingOrderLabel(c) }}</div>
3762
3961
  <div class="details-actions">
3763
3962
  <button mat-button color="primary" (click)="openBuilder()"><mat-icon>tune</mat-icon> Editar no Builder</button>
3764
3963
  <button mat-button color="warn" (click)="removeEdge(selectedEdgeIndex())"><mat-icon>delete</mat-icon> Remover</button>
@@ -3945,7 +4144,7 @@ class ConnectionEditorComponent {
3945
4144
  return input;
3946
4145
  }
3947
4146
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ConnectionEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3948
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: ConnectionEditorComponent, isStandalone: true, selector: "praxis-connection-editor", inputs: { page: { classPropertyName: "page", publicName: "page", isSignal: true, isRequired: false, transformFunction: null }, widgets: { classPropertyName: "widgets", publicName: "widgets", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pageChange: "pageChange" }, ngImport: i0, template: "<div class=\"conn-editor\">\n <mat-tab-group class=\"conn-tabs\">\n <mat-tab label=\"Construtor\">\n <praxis-connection-builder\n [page]=\"pageState() || undefined\"\n [widgets]=\"widgets() || undefined\"\n (pageChange)=\"onPageChange($event)\"\n ></praxis-connection-builder>\n </mat-tab>\n <mat-tab label=\"Diagrama\">\n <praxis-connection-graph\n [page]=\"pageState() || undefined\"\n [widgets]=\"widgets() || undefined\"\n (pageChange)=\"onPageChange($event)\"\n ></praxis-connection-graph>\n </mat-tab>\n <mat-tab label=\"JSON\">\n <div class=\"conn-json\">\n <mat-form-field class=\"json-field\" appearance=\"outline\">\n <mat-label>Pagina (JSON)</mat-label>\n <textarea matInput rows=\"16\" [formControl]=\"jsonControl\"></textarea>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"Use este JSON para copiar/colar a p\u00E1gina completa.\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n @if (jsonError()) {\n <div class=\"json-error\" role=\"alert\">{{ jsonError() }}</div>\n }\n <div class=\"json-actions\">\n <button mat-stroked-button type=\"button\" (click)=\"resetJson()\">Recarregar</button>\n </div>\n </div>\n </mat-tab>\n </mat-tab-group>\n</div>\n", styles: [".conn-editor{display:flex;flex-direction:column;height:100%;min-height:480px;color:var(--md-sys-color-on-surface)}.conn-tabs{flex:1}.conn-json{display:grid;gap:12px;padding:16px;background:var(--md-sys-color-surface-container-lowest, var(--md-sys-color-surface));border:1px solid var(--md-sys-color-outline-variant);border-radius:12px}.json-field{width:100%}.json-error{color:var(--md-sys-color-error);font-size:13px}.json-actions{display:flex;justify-content:flex-end}.conn-editor .mat-mdc-form-field{width:100%;--mdc-outlined-text-field-container-height: 48px;--mdc-outlined-text-field-outline-color: var(--md-sys-color-outline-variant);--mdc-outlined-text-field-hover-outline-color: var(--md-sys-color-outline);--mdc-outlined-text-field-focus-outline-color: var(--md-sys-color-primary);--mdc-outlined-text-field-error-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-focus-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-hover-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-label-text-color: var(--md-sys-color-on-surface-variant);--mdc-outlined-text-field-input-text-color: var(--md-sys-color-on-surface);--mdc-outlined-text-field-supporting-text-color: var(--md-sys-color-on-surface-variant)}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.help-icon-button mat-icon{font-size:18px;line-height:18px;width:18px;height:18px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$2.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: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i9$1.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i9$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: i3$2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$2.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3$2.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i4.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: 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: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: ConnectionBuilderComponent, selector: "praxis-connection-builder", inputs: ["page", "widgets"], outputs: ["pageChange"] }, { kind: "component", type: ConnectionGraphComponent, selector: "praxis-connection-graph", inputs: ["page", "widgets"], outputs: ["pageChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4147
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: ConnectionEditorComponent, isStandalone: true, selector: "praxis-connection-editor", inputs: { page: { classPropertyName: "page", publicName: "page", isSignal: true, isRequired: false, transformFunction: null }, widgets: { classPropertyName: "widgets", publicName: "widgets", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pageChange: "pageChange" }, ngImport: i0, template: "<div class=\"conn-editor\">\n <mat-tab-group class=\"conn-tabs\">\n <mat-tab label=\"Construtor\">\n <praxis-connection-builder\n [page]=\"pageState() || undefined\"\n [widgets]=\"widgets() || undefined\"\n (pageChange)=\"onPageChange($event)\"\n ></praxis-connection-builder>\n </mat-tab>\n <mat-tab label=\"Diagrama\">\n <praxis-connection-graph\n [page]=\"pageState() || undefined\"\n [widgets]=\"widgets() || undefined\"\n (pageChange)=\"onPageChange($event)\"\n ></praxis-connection-graph>\n </mat-tab>\n <mat-tab label=\"JSON\">\n <div class=\"conn-json\">\n <mat-form-field class=\"json-field\" appearance=\"outline\">\n <mat-label>Pagina (JSON)</mat-label>\n <textarea matInput rows=\"16\" [formControl]=\"jsonControl\"></textarea>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"Use este JSON para copiar/colar a p\u00E1gina completa.\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n @if (jsonError()) {\n <div class=\"json-error\" role=\"alert\">{{ jsonError() }}</div>\n }\n <div class=\"json-actions\">\n <button mat-stroked-button type=\"button\" (click)=\"resetJson()\">Recarregar</button>\n </div>\n </div>\n </mat-tab>\n </mat-tab-group>\n</div>\n", styles: [".conn-editor{display:flex;flex-direction:column;height:100%;min-height:480px;color:var(--md-sys-color-on-surface)}.conn-tabs{flex:1}.conn-json{display:grid;gap:12px;padding:16px;background:var(--md-sys-color-surface-container-lowest, var(--md-sys-color-surface));border:1px solid var(--md-sys-color-outline-variant);border-radius:12px}.json-field{width:100%}.json-error{color:var(--md-sys-color-error);font-size:13px}.json-actions{display:flex;justify-content:flex-end}.conn-editor .mat-mdc-form-field{width:100%;--mdc-outlined-text-field-container-height: 48px;--mdc-outlined-text-field-outline-color: var(--md-sys-color-outline-variant);--mdc-outlined-text-field-hover-outline-color: var(--md-sys-color-outline);--mdc-outlined-text-field-focus-outline-color: var(--md-sys-color-primary);--mdc-outlined-text-field-error-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-focus-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-hover-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-label-text-color: var(--md-sys-color-on-surface-variant);--mdc-outlined-text-field-input-text-color: var(--md-sys-color-on-surface);--mdc-outlined-text-field-supporting-text-color: var(--md-sys-color-on-surface-variant)}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.help-icon-button mat-icon{font-size:18px;line-height:18px;width:18px;height:18px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$2.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: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i9.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i9.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: i3$2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$2.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3$2.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i4.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: 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: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: ConnectionBuilderComponent, selector: "praxis-connection-builder", inputs: ["page", "widgets"], outputs: ["pageChange"] }, { kind: "component", type: ConnectionGraphComponent, selector: "praxis-connection-graph", inputs: ["page", "widgets"], outputs: ["pageChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3949
4148
  }
3950
4149
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ConnectionEditorComponent, decorators: [{
3951
4150
  type: Component,
@@ -4052,8 +4251,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
4052
4251
  args: [MAT_DIALOG_DATA]
4053
4252
  }] }, { type: i1.MatDialogRef }] });
4054
4253
 
4254
+ const DERIVED_OPERATORS = ['merge-objects', 'pick', 'omit'];
4255
+ const MERGE_STRATEGIES = ['replace', 'merge', 'append', 'remove-keys'];
4055
4256
  class PageConfigEditorComponent {
4056
- contextExample = 'Ex.: {\"tenantId\":\"acme\",\"locale\":\"pt-BR\"}';
4257
+ contextExample = 'Ex.: {"tenantId":"acme","locale":"pt-BR"}';
4258
+ stateValuesExample = 'Ex.: {"query":{"filter":{"year":2026}},"selection":{"department":"Finance"}}';
4259
+ derivedOperators = DERIVED_OPERATORS;
4260
+ mergeStrategies = MERGE_STRATEGIES;
4057
4261
  page = input(null, ...(ngDevMode ? [{ debugName: "page" }] : []));
4058
4262
  identity = input(null, ...(ngDevMode ? [{ debugName: "identity" }] : []));
4059
4263
  gridsterOptions = input(null, ...(ngDevMode ? [{ debugName: "gridsterOptions" }] : []));
@@ -4064,8 +4268,13 @@ class PageConfigEditorComponent {
4064
4268
  gap: new FormControl(null),
4065
4269
  });
4066
4270
  contextControl = new FormControl('', { nonNullable: true });
4271
+ stateValuesControl = new FormControl('', { nonNullable: true });
4272
+ schemaNodes = new FormArray([]);
4273
+ derivedNodes = new FormArray([]);
4067
4274
  contextError = signal(null, ...(ngDevMode ? [{ debugName: "contextError" }] : []));
4275
+ stateError = signal(null, ...(ngDevMode ? [{ debugName: "stateError" }] : []));
4068
4276
  lastContext = '';
4277
+ lastStateValues = '';
4069
4278
  constructor() {
4070
4279
  effect(() => {
4071
4280
  const next = this.parsePage(this.page());
@@ -4081,23 +4290,51 @@ class PageConfigEditorComponent {
4081
4290
  this.contextControl.setValue(context, { emitEvent: false });
4082
4291
  this.contextError.set(null);
4083
4292
  }
4084
- }, { allowSignalWrites: true });
4293
+ this.hydrateStateEditor(next?.state);
4294
+ });
4085
4295
  this.contextControl.valueChanges
4086
4296
  .pipe(takeUntilDestroyed())
4087
4297
  .subscribe(() => this.contextError.set(null));
4298
+ this.stateValuesControl.valueChanges
4299
+ .pipe(takeUntilDestroyed())
4300
+ .subscribe(() => this.stateError.set(null));
4301
+ this.schemaNodes.valueChanges
4302
+ .pipe(takeUntilDestroyed())
4303
+ .subscribe(() => this.stateError.set(null));
4304
+ this.derivedNodes.valueChanges
4305
+ .pipe(takeUntilDestroyed())
4306
+ .subscribe(() => this.stateError.set(null));
4307
+ }
4308
+ addSchemaNode(seed) {
4309
+ this.schemaNodes.push(this.createSchemaNodeGroup(seed));
4310
+ }
4311
+ removeSchemaNode(index) {
4312
+ this.schemaNodes.removeAt(index);
4313
+ }
4314
+ addDerivedNode(seed) {
4315
+ this.derivedNodes.push(this.createDerivedNodeGroup(seed));
4316
+ }
4317
+ removeDerivedNode(index) {
4318
+ this.derivedNodes.removeAt(index);
4088
4319
  }
4089
4320
  apply() {
4090
4321
  const next = this.parsePage(this.page()) || { widgets: [] };
4091
4322
  const options = this.buildOptions(this.form.value);
4092
4323
  const context = this.parseContext(this.contextControl.value);
4324
+ const state = this.buildState();
4093
4325
  if (context === null) {
4094
4326
  this.contextError.set('JSON invalido');
4095
4327
  return;
4096
4328
  }
4329
+ if (state === null) {
4330
+ this.stateError.set('JSON invalido');
4331
+ return;
4332
+ }
4097
4333
  const updated = {
4098
4334
  ...next,
4099
4335
  options,
4100
4336
  context: context || undefined,
4337
+ state: state || undefined,
4101
4338
  };
4102
4339
  this.pageChange.emit(updated);
4103
4340
  }
@@ -4113,6 +4350,197 @@ class PageConfigEditorComponent {
4113
4350
  this.lastContext = context;
4114
4351
  this.contextControl.setValue(context, { emitEvent: false });
4115
4352
  this.contextError.set(null);
4353
+ this.hydrateStateEditor(next?.state);
4354
+ }
4355
+ hydrateStateEditor(state) {
4356
+ const normalized = this.normalizeState(state);
4357
+ const nextValues = normalized.values ? JSON.stringify(normalized.values, null, 2) : '';
4358
+ if (nextValues !== this.lastStateValues) {
4359
+ this.lastStateValues = nextValues;
4360
+ this.stateValuesControl.setValue(nextValues, { emitEvent: false });
4361
+ this.stateError.set(null);
4362
+ }
4363
+ this.replaceSchemaNodes(normalized.schema);
4364
+ this.replaceDerivedNodes(normalized.derived);
4365
+ }
4366
+ replaceSchemaNodes(schema) {
4367
+ this.schemaNodes.clear({ emitEvent: false });
4368
+ for (const [path, config] of Object.entries(schema || {})) {
4369
+ this.schemaNodes.push(this.createSchemaNodeGroup({ path, config }), { emitEvent: false });
4370
+ }
4371
+ }
4372
+ replaceDerivedNodes(derived) {
4373
+ this.derivedNodes.clear({ emitEvent: false });
4374
+ for (const [path, config] of Object.entries(derived || {})) {
4375
+ this.derivedNodes.push(this.createDerivedNodeGroup({ path, config }), { emitEvent: false });
4376
+ }
4377
+ }
4378
+ createSchemaNodeGroup(seed) {
4379
+ const config = seed?.config || {};
4380
+ return new FormGroup({
4381
+ path: new FormControl(seed?.path || '', { nonNullable: true, validators: [Validators.required] }),
4382
+ type: new FormControl(config.type || '', { nonNullable: true }),
4383
+ initial: new FormControl(config.initial === undefined ? '' : JSON.stringify(config.initial, null, 2), { nonNullable: true }),
4384
+ persist: new FormControl(typeof config.persist === 'boolean' ? String(config.persist) : '', { nonNullable: true }),
4385
+ mergeStrategy: new FormControl(config.mergeStrategy || '', { nonNullable: true }),
4386
+ description: new FormControl(config.description || '', { nonNullable: true }),
4387
+ });
4388
+ }
4389
+ createDerivedNodeGroup(seed) {
4390
+ const config = seed?.config;
4391
+ const compute = config?.compute;
4392
+ const normalizedComputeKind = compute?.kind === 'operator' && compute.operator === 'template'
4393
+ ? 'template'
4394
+ : compute?.kind || 'operator';
4395
+ const operatorOptions = compute?.kind === 'operator'
4396
+ ? compute.options || {}
4397
+ : {};
4398
+ const sourcePath = typeof operatorOptions['source'] === 'string'
4399
+ ? operatorOptions['source']
4400
+ : '';
4401
+ const keysCsv = Array.isArray(operatorOptions['keys'])
4402
+ ? operatorOptions['keys'].filter((item) => typeof item === 'string').join(', ')
4403
+ : '';
4404
+ return new FormGroup({
4405
+ path: new FormControl(seed?.path || '', { nonNullable: true, validators: [Validators.required] }),
4406
+ dependsOn: new FormControl((config?.dependsOn || []).join(', '), { nonNullable: true }),
4407
+ computeKind: new FormControl(normalizedComputeKind, { nonNullable: true }),
4408
+ operator: new FormControl(compute?.kind === 'operator' ? compute.operator || 'merge-objects' : 'merge-objects', {
4409
+ nonNullable: true,
4410
+ }),
4411
+ sourcePath: new FormControl(sourcePath, { nonNullable: true }),
4412
+ keysCsv: new FormControl(keysCsv, { nonNullable: true }),
4413
+ expression: new FormControl(compute?.kind === 'expr' ? compute.expression || '' : '', { nonNullable: true }),
4414
+ transformerId: new FormControl(compute?.kind === 'transformer' ? compute.transformerId || '' : '', { nonNullable: true }),
4415
+ options: new FormControl(compute?.kind === 'transformer'
4416
+ ? JSON.stringify(compute.options || {}, null, 2)
4417
+ : '', { nonNullable: true }),
4418
+ template: new FormControl(compute?.kind === 'template'
4419
+ ? JSON.stringify(compute.value, null, 2)
4420
+ : compute?.kind === 'operator' && compute.operator === 'template'
4421
+ ? JSON.stringify(compute.options?.['value'], null, 2)
4422
+ : '', { nonNullable: true }),
4423
+ description: new FormControl(config?.description || '', { nonNullable: true }),
4424
+ });
4425
+ }
4426
+ buildState() {
4427
+ const values = this.parseJson(this.stateValuesControl.value, {});
4428
+ if (values === null)
4429
+ return null;
4430
+ const schema = this.buildSchema();
4431
+ if (schema === null)
4432
+ return null;
4433
+ const derived = this.buildDerived();
4434
+ if (derived === null)
4435
+ return null;
4436
+ if (!Object.keys(values).length && !Object.keys(schema).length && !Object.keys(derived).length) {
4437
+ return undefined;
4438
+ }
4439
+ const state = {};
4440
+ if (Object.keys(values).length)
4441
+ state.values = values;
4442
+ if (Object.keys(schema).length)
4443
+ state.schema = schema;
4444
+ if (Object.keys(derived).length)
4445
+ state.derived = derived;
4446
+ return state;
4447
+ }
4448
+ buildSchema() {
4449
+ const schema = {};
4450
+ for (const group of this.schemaNodes.controls) {
4451
+ const path = group.controls.path.value.trim();
4452
+ if (!path)
4453
+ continue;
4454
+ const initial = this.parseJson(group.controls.initial.value, undefined);
4455
+ if (initial === null)
4456
+ return null;
4457
+ const node = {};
4458
+ const type = group.controls.type.value.trim();
4459
+ const persist = group.controls.persist.value.trim();
4460
+ const mergeStrategy = group.controls.mergeStrategy.value.trim();
4461
+ const description = group.controls.description.value.trim();
4462
+ if (type)
4463
+ node.type = type;
4464
+ if (initial !== undefined)
4465
+ node.initial = initial;
4466
+ if (persist === 'true')
4467
+ node.persist = true;
4468
+ if (persist === 'false')
4469
+ node.persist = false;
4470
+ if (mergeStrategy)
4471
+ node.mergeStrategy = mergeStrategy;
4472
+ if (description)
4473
+ node.description = description;
4474
+ schema[path] = node;
4475
+ }
4476
+ return schema;
4477
+ }
4478
+ buildDerived() {
4479
+ const derived = {};
4480
+ for (const group of this.derivedNodes.controls) {
4481
+ const path = group.controls.path.value.trim();
4482
+ if (!path)
4483
+ continue;
4484
+ const dependsOn = group.controls.dependsOn.value
4485
+ .split(',')
4486
+ .map((item) => item.trim())
4487
+ .filter(Boolean);
4488
+ const computeKind = group.controls.computeKind.value;
4489
+ const description = group.controls.description.value.trim();
4490
+ let compute;
4491
+ if (computeKind === 'template') {
4492
+ const value = this.parseJson(group.controls.template.value, undefined);
4493
+ if (value === null)
4494
+ return null;
4495
+ compute = { kind: 'template', value };
4496
+ }
4497
+ else if (computeKind === 'expr') {
4498
+ compute = { kind: 'expr', expression: group.controls.expression.value.trim() };
4499
+ }
4500
+ else if (computeKind === 'transformer') {
4501
+ const options = this.parseJson(group.controls.options.value, {});
4502
+ if (options === null)
4503
+ return null;
4504
+ compute = {
4505
+ kind: 'transformer',
4506
+ transformerId: group.controls.transformerId.value.trim(),
4507
+ options,
4508
+ };
4509
+ }
4510
+ else {
4511
+ const operator = group.controls.operator.value.trim() || 'merge-objects';
4512
+ const options = this.buildOperatorOptions(group);
4513
+ if (options === null)
4514
+ return null;
4515
+ compute = {
4516
+ kind: 'operator',
4517
+ operator,
4518
+ options,
4519
+ };
4520
+ }
4521
+ derived[path] = {
4522
+ dependsOn,
4523
+ compute,
4524
+ ...(description ? { description } : {}),
4525
+ };
4526
+ }
4527
+ return derived;
4528
+ }
4529
+ normalizeState(state) {
4530
+ if (!state)
4531
+ return { values: {} };
4532
+ const isStructured = typeof state === 'object'
4533
+ && !Array.isArray(state)
4534
+ && ('values' in state || 'schema' in state || 'derived' in state);
4535
+ if (isStructured) {
4536
+ const structured = state;
4537
+ return {
4538
+ values: this.clone(structured.values) || {},
4539
+ schema: this.clone(structured.schema),
4540
+ derived: this.clone(structured.derived),
4541
+ };
4542
+ }
4543
+ return { values: this.clone(state) || {} };
4116
4544
  }
4117
4545
  parsePage(input) {
4118
4546
  if (!input)
@@ -4137,6 +4565,33 @@ class PageConfigEditorComponent {
4137
4565
  return null;
4138
4566
  }
4139
4567
  }
4568
+ parseJson(raw, emptyValue) {
4569
+ if (!raw.trim())
4570
+ return emptyValue;
4571
+ try {
4572
+ return JSON.parse(raw);
4573
+ }
4574
+ catch {
4575
+ return null;
4576
+ }
4577
+ }
4578
+ buildOperatorOptions(group) {
4579
+ const operator = group.controls.operator.value.trim();
4580
+ if (operator === 'pick' || operator === 'omit') {
4581
+ const source = group.controls.sourcePath.value.trim();
4582
+ const keys = group.controls.keysCsv.value
4583
+ .split(',')
4584
+ .map((item) => item.trim())
4585
+ .filter(Boolean);
4586
+ const options = {};
4587
+ if (source)
4588
+ options['source'] = source;
4589
+ if (keys.length)
4590
+ options['keys'] = keys;
4591
+ return options;
4592
+ }
4593
+ return {};
4594
+ }
4140
4595
  buildOptions(values) {
4141
4596
  const options = {};
4142
4597
  if (values.cols != null)
@@ -4147,6 +4602,11 @@ class PageConfigEditorComponent {
4147
4602
  options.gap = values.gap;
4148
4603
  return options;
4149
4604
  }
4605
+ clone(value) {
4606
+ if (value == null || typeof value !== 'object')
4607
+ return value;
4608
+ return JSON.parse(JSON.stringify(value));
4609
+ }
4150
4610
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PageConfigEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4151
4611
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PageConfigEditorComponent, isStandalone: true, selector: "praxis-page-config-editor", inputs: { page: { classPropertyName: "page", publicName: "page", isSignal: true, isRequired: false, transformFunction: null }, identity: { classPropertyName: "identity", publicName: "identity", isSignal: true, isRequired: false, transformFunction: null }, gridsterOptions: { classPropertyName: "gridsterOptions", publicName: "gridsterOptions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pageChange: "pageChange" }, ngImport: i0, template: `
4152
4612
  <div class="page-config">
@@ -4204,15 +4664,202 @@ class PageConfigEditorComponent {
4204
4664
  <div class="page-error" role="alert">{{ contextError() }}</div>
4205
4665
  }
4206
4666
 
4667
+ <div class="page-section">Estado da Página</div>
4668
+ <div class="state-grid">
4669
+ <mat-form-field appearance="outline" class="state-values">
4670
+ <mat-label>Valores primários (JSON)</mat-label>
4671
+ <textarea matInput rows="10" [formControl]="stateValuesControl"></textarea>
4672
+ <button
4673
+ mat-icon-button
4674
+ matSuffix
4675
+ class="help-icon-button"
4676
+ type="button"
4677
+ [matTooltip]="stateValuesExample"
4678
+ matTooltipPosition="above"
4679
+ >
4680
+ <mat-icon>help_outline</mat-icon>
4681
+ </button>
4682
+ </mat-form-field>
4683
+
4684
+ <div class="state-group">
4685
+ <div class="state-group-head">
4686
+ <div class="state-group-title">Schema</div>
4687
+ <button mat-stroked-button type="button" (click)="addSchemaNode()">
4688
+ <mat-icon>add</mat-icon>
4689
+ Adicionar nó
4690
+ </button>
4691
+ </div>
4692
+ <div class="state-group-help">Define caminhos primários, valor inicial e estratégia de merge.</div>
4693
+ @if (!schemaNodes.length) {
4694
+ <div class="state-empty">Nenhum nó de schema configurado.</div>
4695
+ }
4696
+ <div class="state-list">
4697
+ @for (group of schemaNodes.controls; track $index) {
4698
+ <div class="state-card" [formGroup]="group">
4699
+ <div class="state-card-head">
4700
+ <div class="state-card-title">Nó {{ $index + 1 }}</div>
4701
+ <button mat-icon-button type="button" (click)="removeSchemaNode($index)" aria-label="Remover nó de schema">
4702
+ <mat-icon>delete</mat-icon>
4703
+ </button>
4704
+ </div>
4705
+ <div class="state-card-grid">
4706
+ <mat-form-field appearance="outline">
4707
+ <mat-label>Path</mat-label>
4708
+ <input matInput formControlName="path" />
4709
+ </mat-form-field>
4710
+ <mat-form-field appearance="outline">
4711
+ <mat-label>Tipo</mat-label>
4712
+ <input matInput formControlName="type" />
4713
+ </mat-form-field>
4714
+ <mat-form-field appearance="outline">
4715
+ <mat-label>Persistência</mat-label>
4716
+ <mat-select formControlName="persist">
4717
+ <mat-option value="">Padrão</mat-option>
4718
+ <mat-option value="true">Persistir</mat-option>
4719
+ <mat-option value="false">Não persistir</mat-option>
4720
+ </mat-select>
4721
+ </mat-form-field>
4722
+ <mat-form-field appearance="outline">
4723
+ <mat-label>Merge</mat-label>
4724
+ <mat-select formControlName="mergeStrategy">
4725
+ <mat-option value="">Padrão</mat-option>
4726
+ @for (strategy of mergeStrategies; track strategy) {
4727
+ <mat-option [value]="strategy">{{ strategy }}</mat-option>
4728
+ }
4729
+ </mat-select>
4730
+ </mat-form-field>
4731
+ <mat-form-field appearance="outline" class="span-2">
4732
+ <mat-label>Initial (JSON)</mat-label>
4733
+ <textarea matInput rows="4" formControlName="initial"></textarea>
4734
+ </mat-form-field>
4735
+ <mat-form-field appearance="outline" class="span-2">
4736
+ <mat-label>Descrição</mat-label>
4737
+ <input matInput formControlName="description" />
4738
+ </mat-form-field>
4739
+ </div>
4740
+ </div>
4741
+ }
4742
+ </div>
4743
+ </div>
4744
+
4745
+ <div class="state-group">
4746
+ <div class="state-group-head">
4747
+ <div class="state-group-title">Derived</div>
4748
+ <button mat-stroked-button type="button" (click)="addDerivedNode()">
4749
+ <mat-icon>add</mat-icon>
4750
+ Adicionar derivação
4751
+ </button>
4752
+ </div>
4753
+ <div class="state-group-help">Modela recomputações do runtime com dependsOn e operadores canônicos.</div>
4754
+ @if (!derivedNodes.length) {
4755
+ <div class="state-empty">Nenhum nó derivado configurado.</div>
4756
+ }
4757
+ <div class="state-list">
4758
+ @for (group of derivedNodes.controls; track $index) {
4759
+ <div class="state-card" [formGroup]="group">
4760
+ <div class="state-card-head">
4761
+ <div class="state-card-title">Derived {{ $index + 1 }}</div>
4762
+ <button mat-icon-button type="button" (click)="removeDerivedNode($index)" aria-label="Remover derivação">
4763
+ <mat-icon>delete</mat-icon>
4764
+ </button>
4765
+ </div>
4766
+ <div class="state-card-grid">
4767
+ <mat-form-field appearance="outline">
4768
+ <mat-label>Path</mat-label>
4769
+ <input matInput formControlName="path" />
4770
+ </mat-form-field>
4771
+ <mat-form-field appearance="outline">
4772
+ <mat-label>Compute</mat-label>
4773
+ <mat-select formControlName="computeKind">
4774
+ <mat-option value="operator">Operator</mat-option>
4775
+ <mat-option value="template">Template</mat-option>
4776
+ <mat-option value="expr">Expr</mat-option>
4777
+ <mat-option value="transformer">Transformer</mat-option>
4778
+ </mat-select>
4779
+ </mat-form-field>
4780
+ <mat-form-field appearance="outline" class="span-2">
4781
+ <mat-label>DependsOn (vírgula)</mat-label>
4782
+ <input matInput formControlName="dependsOn" />
4783
+ </mat-form-field>
4784
+ @if (group.controls.computeKind.value === 'operator') {
4785
+ <mat-form-field appearance="outline">
4786
+ <mat-label>Operator</mat-label>
4787
+ <mat-select formControlName="operator">
4788
+ @for (operator of derivedOperators; track operator) {
4789
+ <mat-option [value]="operator">{{ operator }}</mat-option>
4790
+ }
4791
+ </mat-select>
4792
+ </mat-form-field>
4793
+ @if (group.controls.operator.value === 'pick' || group.controls.operator.value === 'omit') {
4794
+ <mat-form-field appearance="outline">
4795
+ <mat-label>Source path</mat-label>
4796
+ <input matInput formControlName="sourcePath" />
4797
+ </mat-form-field>
4798
+ <mat-form-field appearance="outline" class="span-2">
4799
+ <mat-label>Keys (vírgula)</mat-label>
4800
+ <input matInput formControlName="keysCsv" />
4801
+ </mat-form-field>
4802
+ } @else {
4803
+ <div class="state-operator-hint span-3">
4804
+ <mat-icon>account_tree</mat-icon>
4805
+ <span>O operador {{ group.controls.operator.value }} usa os caminhos de dependsOn como fonte principal.</span>
4806
+ </div>
4807
+ }
4808
+ }
4809
+ @if (group.controls.computeKind.value === 'template') {
4810
+ <mat-form-field appearance="outline" class="span-3">
4811
+ <mat-label>Template (JSON)</mat-label>
4812
+ <textarea matInput rows="4" formControlName="template"></textarea>
4813
+ </mat-form-field>
4814
+ }
4815
+ @if (group.controls.computeKind.value === 'expr') {
4816
+ <mat-form-field appearance="outline" class="span-3">
4817
+ <mat-label>Expression</mat-label>
4818
+ <input matInput formControlName="expression" />
4819
+ </mat-form-field>
4820
+ }
4821
+ @if (group.controls.computeKind.value === 'transformer') {
4822
+ <mat-form-field appearance="outline">
4823
+ <mat-label>Transformer</mat-label>
4824
+ <input matInput formControlName="transformerId" />
4825
+ </mat-form-field>
4826
+ <mat-form-field appearance="outline" class="span-2">
4827
+ <mat-label>Options (JSON)</mat-label>
4828
+ <textarea matInput rows="4" formControlName="options"></textarea>
4829
+ </mat-form-field>
4830
+ }
4831
+ <mat-form-field appearance="outline" class="span-3">
4832
+ <mat-label>Descrição</mat-label>
4833
+ <input matInput formControlName="description" />
4834
+ </mat-form-field>
4835
+ </div>
4836
+ </div>
4837
+ }
4838
+ </div>
4839
+ </div>
4840
+ </div>
4841
+ @if (stateError()) {
4842
+ <div class="page-error" role="alert">{{ stateError() }}</div>
4843
+ }
4844
+
4207
4845
  <div class="page-actions">
4208
4846
  <button mat-flat-button color="primary" type="button" (click)="apply()">Aplicar</button>
4209
4847
  </div>
4210
4848
  </div>
4211
- `, isInline: true, styles: [".page-config{display:grid;gap:12px;padding:16px;background:var(--md-sys-color-surface-container-lowest, var(--md-sys-color-surface));border:1px solid var(--md-sys-color-outline-variant);border-radius:12px}.page-head{display:flex;align-items:center;gap:12px}.page-title{font-weight:600}.spacer{flex:1}.page-identity{display:grid;gap:4px;font-size:13px;color:var(--md-sys-color-on-surface-variant)}.page-section{font-weight:600;margin-top:8px}.grid-form{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}.page-actions{display:flex;justify-content:flex-end}.page-error{color:var(--md-sys-color-error);font-size:13px}.page-config .mat-mdc-form-field{width:100%;--mdc-outlined-text-field-container-height: 48px;--mdc-outlined-text-field-outline-color: var(--md-sys-color-outline-variant);--mdc-outlined-text-field-hover-outline-color: var(--md-sys-color-outline);--mdc-outlined-text-field-focus-outline-color: var(--md-sys-color-primary);--mdc-outlined-text-field-error-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-focus-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-hover-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-label-text-color: var(--md-sys-color-on-surface-variant);--mdc-outlined-text-field-input-text-color: var(--md-sys-color-on-surface);--mdc-outlined-text-field-supporting-text-color: var(--md-sys-color-on-surface-variant)}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;margin-right:-4px}.help-icon-button mat-icon{font-size:18px;width:18px;height:18px}.mat-mdc-form-field-icon-suffix{align-self:center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$2.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: i1$2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { 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: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i3$2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$2.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3$2.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i4.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: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4849
+ `, isInline: true, styles: [".page-config{display:grid;gap:12px;padding:16px;background:var(--md-sys-color-surface-container-lowest, var(--md-sys-color-surface));border:1px solid var(--md-sys-color-outline-variant);border-radius:12px}.page-head{display:flex;align-items:center;gap:12px}.page-title{font-weight:600}.spacer{flex:1}.page-identity{display:grid;gap:4px;font-size:13px;color:var(--md-sys-color-on-surface-variant)}.page-section{font-weight:600;margin-top:8px}.grid-form{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}.state-grid{display:grid;gap:16px}.state-values,.state-group{padding:14px;border:1px solid var(--md-sys-color-outline-variant);border-radius:14px;background:linear-gradient(180deg,color-mix(in srgb,var(--md-sys-color-surface-container-low) 92%,transparent),transparent),var(--md-sys-color-surface-container-lowest, var(--md-sys-color-surface))}.state-group{display:grid;gap:12px}.state-group-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.state-group-title{font-weight:600}.state-group-help{color:var(--md-sys-color-on-surface-variant);font-size:13px}.state-list{display:grid;gap:12px}.state-card{display:grid;gap:12px;padding:12px;border-radius:12px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 80%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface-container) 84%,transparent)}.state-card-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.state-card-title{font-weight:600}.state-card-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}.span-2{grid-column:span 2}.span-3{grid-column:1 / -1}.state-empty{color:var(--md-sys-color-on-surface-variant);font-size:13px}.state-operator-hint{display:flex;align-items:center;gap:8px;min-height:48px;padding:0 12px;border-radius:12px;color:var(--md-sys-color-on-surface-variant);background:color-mix(in srgb,var(--md-sys-color-secondary-container) 35%,transparent)}.state-operator-hint mat-icon{color:var(--md-sys-color-secondary)}.page-actions{display:flex;justify-content:flex-end}.page-error{color:var(--md-sys-color-error);font-size:13px}.page-config .mat-mdc-form-field{width:100%;--mdc-outlined-text-field-container-height: 48px;--mdc-outlined-text-field-outline-color: var(--md-sys-color-outline-variant);--mdc-outlined-text-field-hover-outline-color: var(--md-sys-color-outline);--mdc-outlined-text-field-focus-outline-color: var(--md-sys-color-primary);--mdc-outlined-text-field-error-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-focus-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-hover-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-label-text-color: var(--md-sys-color-on-surface-variant);--mdc-outlined-text-field-input-text-color: var(--md-sys-color-on-surface);--mdc-outlined-text-field-supporting-text-color: var(--md-sys-color-on-surface-variant)}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;margin-right:-4px}.help-icon-button mat-icon{font-size:18px;width:18px;height:18px}.mat-mdc-form-field-icon-suffix{align-self:center}@media(max-width:720px){.span-2,.span-3{grid-column:auto}.state-group-head{align-items:stretch;flex-direction:column}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$2.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: i1$2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { 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: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i3$2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$2.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3$2.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i4.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: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4212
4850
  }
4213
4851
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PageConfigEditorComponent, decorators: [{
4214
4852
  type: Component,
4215
- args: [{ selector: 'praxis-page-config-editor', standalone: true, imports: [CommonModule, ReactiveFormsModule, MatButtonModule, MatFormFieldModule, MatInputModule, MatIconModule, MatTooltipModule], template: `
4853
+ args: [{ selector: 'praxis-page-config-editor', standalone: true, imports: [
4854
+ CommonModule,
4855
+ ReactiveFormsModule,
4856
+ MatButtonModule,
4857
+ MatFormFieldModule,
4858
+ MatInputModule,
4859
+ MatIconModule,
4860
+ MatSelectModule,
4861
+ MatTooltipModule,
4862
+ ], template: `
4216
4863
  <div class="page-config">
4217
4864
  <div class="page-head">
4218
4865
  <div class="page-title">Configurações da Página</div>
@@ -4268,11 +4915,189 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
4268
4915
  <div class="page-error" role="alert">{{ contextError() }}</div>
4269
4916
  }
4270
4917
 
4918
+ <div class="page-section">Estado da Página</div>
4919
+ <div class="state-grid">
4920
+ <mat-form-field appearance="outline" class="state-values">
4921
+ <mat-label>Valores primários (JSON)</mat-label>
4922
+ <textarea matInput rows="10" [formControl]="stateValuesControl"></textarea>
4923
+ <button
4924
+ mat-icon-button
4925
+ matSuffix
4926
+ class="help-icon-button"
4927
+ type="button"
4928
+ [matTooltip]="stateValuesExample"
4929
+ matTooltipPosition="above"
4930
+ >
4931
+ <mat-icon>help_outline</mat-icon>
4932
+ </button>
4933
+ </mat-form-field>
4934
+
4935
+ <div class="state-group">
4936
+ <div class="state-group-head">
4937
+ <div class="state-group-title">Schema</div>
4938
+ <button mat-stroked-button type="button" (click)="addSchemaNode()">
4939
+ <mat-icon>add</mat-icon>
4940
+ Adicionar nó
4941
+ </button>
4942
+ </div>
4943
+ <div class="state-group-help">Define caminhos primários, valor inicial e estratégia de merge.</div>
4944
+ @if (!schemaNodes.length) {
4945
+ <div class="state-empty">Nenhum nó de schema configurado.</div>
4946
+ }
4947
+ <div class="state-list">
4948
+ @for (group of schemaNodes.controls; track $index) {
4949
+ <div class="state-card" [formGroup]="group">
4950
+ <div class="state-card-head">
4951
+ <div class="state-card-title">Nó {{ $index + 1 }}</div>
4952
+ <button mat-icon-button type="button" (click)="removeSchemaNode($index)" aria-label="Remover nó de schema">
4953
+ <mat-icon>delete</mat-icon>
4954
+ </button>
4955
+ </div>
4956
+ <div class="state-card-grid">
4957
+ <mat-form-field appearance="outline">
4958
+ <mat-label>Path</mat-label>
4959
+ <input matInput formControlName="path" />
4960
+ </mat-form-field>
4961
+ <mat-form-field appearance="outline">
4962
+ <mat-label>Tipo</mat-label>
4963
+ <input matInput formControlName="type" />
4964
+ </mat-form-field>
4965
+ <mat-form-field appearance="outline">
4966
+ <mat-label>Persistência</mat-label>
4967
+ <mat-select formControlName="persist">
4968
+ <mat-option value="">Padrão</mat-option>
4969
+ <mat-option value="true">Persistir</mat-option>
4970
+ <mat-option value="false">Não persistir</mat-option>
4971
+ </mat-select>
4972
+ </mat-form-field>
4973
+ <mat-form-field appearance="outline">
4974
+ <mat-label>Merge</mat-label>
4975
+ <mat-select formControlName="mergeStrategy">
4976
+ <mat-option value="">Padrão</mat-option>
4977
+ @for (strategy of mergeStrategies; track strategy) {
4978
+ <mat-option [value]="strategy">{{ strategy }}</mat-option>
4979
+ }
4980
+ </mat-select>
4981
+ </mat-form-field>
4982
+ <mat-form-field appearance="outline" class="span-2">
4983
+ <mat-label>Initial (JSON)</mat-label>
4984
+ <textarea matInput rows="4" formControlName="initial"></textarea>
4985
+ </mat-form-field>
4986
+ <mat-form-field appearance="outline" class="span-2">
4987
+ <mat-label>Descrição</mat-label>
4988
+ <input matInput formControlName="description" />
4989
+ </mat-form-field>
4990
+ </div>
4991
+ </div>
4992
+ }
4993
+ </div>
4994
+ </div>
4995
+
4996
+ <div class="state-group">
4997
+ <div class="state-group-head">
4998
+ <div class="state-group-title">Derived</div>
4999
+ <button mat-stroked-button type="button" (click)="addDerivedNode()">
5000
+ <mat-icon>add</mat-icon>
5001
+ Adicionar derivação
5002
+ </button>
5003
+ </div>
5004
+ <div class="state-group-help">Modela recomputações do runtime com dependsOn e operadores canônicos.</div>
5005
+ @if (!derivedNodes.length) {
5006
+ <div class="state-empty">Nenhum nó derivado configurado.</div>
5007
+ }
5008
+ <div class="state-list">
5009
+ @for (group of derivedNodes.controls; track $index) {
5010
+ <div class="state-card" [formGroup]="group">
5011
+ <div class="state-card-head">
5012
+ <div class="state-card-title">Derived {{ $index + 1 }}</div>
5013
+ <button mat-icon-button type="button" (click)="removeDerivedNode($index)" aria-label="Remover derivação">
5014
+ <mat-icon>delete</mat-icon>
5015
+ </button>
5016
+ </div>
5017
+ <div class="state-card-grid">
5018
+ <mat-form-field appearance="outline">
5019
+ <mat-label>Path</mat-label>
5020
+ <input matInput formControlName="path" />
5021
+ </mat-form-field>
5022
+ <mat-form-field appearance="outline">
5023
+ <mat-label>Compute</mat-label>
5024
+ <mat-select formControlName="computeKind">
5025
+ <mat-option value="operator">Operator</mat-option>
5026
+ <mat-option value="template">Template</mat-option>
5027
+ <mat-option value="expr">Expr</mat-option>
5028
+ <mat-option value="transformer">Transformer</mat-option>
5029
+ </mat-select>
5030
+ </mat-form-field>
5031
+ <mat-form-field appearance="outline" class="span-2">
5032
+ <mat-label>DependsOn (vírgula)</mat-label>
5033
+ <input matInput formControlName="dependsOn" />
5034
+ </mat-form-field>
5035
+ @if (group.controls.computeKind.value === 'operator') {
5036
+ <mat-form-field appearance="outline">
5037
+ <mat-label>Operator</mat-label>
5038
+ <mat-select formControlName="operator">
5039
+ @for (operator of derivedOperators; track operator) {
5040
+ <mat-option [value]="operator">{{ operator }}</mat-option>
5041
+ }
5042
+ </mat-select>
5043
+ </mat-form-field>
5044
+ @if (group.controls.operator.value === 'pick' || group.controls.operator.value === 'omit') {
5045
+ <mat-form-field appearance="outline">
5046
+ <mat-label>Source path</mat-label>
5047
+ <input matInput formControlName="sourcePath" />
5048
+ </mat-form-field>
5049
+ <mat-form-field appearance="outline" class="span-2">
5050
+ <mat-label>Keys (vírgula)</mat-label>
5051
+ <input matInput formControlName="keysCsv" />
5052
+ </mat-form-field>
5053
+ } @else {
5054
+ <div class="state-operator-hint span-3">
5055
+ <mat-icon>account_tree</mat-icon>
5056
+ <span>O operador {{ group.controls.operator.value }} usa os caminhos de dependsOn como fonte principal.</span>
5057
+ </div>
5058
+ }
5059
+ }
5060
+ @if (group.controls.computeKind.value === 'template') {
5061
+ <mat-form-field appearance="outline" class="span-3">
5062
+ <mat-label>Template (JSON)</mat-label>
5063
+ <textarea matInput rows="4" formControlName="template"></textarea>
5064
+ </mat-form-field>
5065
+ }
5066
+ @if (group.controls.computeKind.value === 'expr') {
5067
+ <mat-form-field appearance="outline" class="span-3">
5068
+ <mat-label>Expression</mat-label>
5069
+ <input matInput formControlName="expression" />
5070
+ </mat-form-field>
5071
+ }
5072
+ @if (group.controls.computeKind.value === 'transformer') {
5073
+ <mat-form-field appearance="outline">
5074
+ <mat-label>Transformer</mat-label>
5075
+ <input matInput formControlName="transformerId" />
5076
+ </mat-form-field>
5077
+ <mat-form-field appearance="outline" class="span-2">
5078
+ <mat-label>Options (JSON)</mat-label>
5079
+ <textarea matInput rows="4" formControlName="options"></textarea>
5080
+ </mat-form-field>
5081
+ }
5082
+ <mat-form-field appearance="outline" class="span-3">
5083
+ <mat-label>Descrição</mat-label>
5084
+ <input matInput formControlName="description" />
5085
+ </mat-form-field>
5086
+ </div>
5087
+ </div>
5088
+ }
5089
+ </div>
5090
+ </div>
5091
+ </div>
5092
+ @if (stateError()) {
5093
+ <div class="page-error" role="alert">{{ stateError() }}</div>
5094
+ }
5095
+
4271
5096
  <div class="page-actions">
4272
5097
  <button mat-flat-button color="primary" type="button" (click)="apply()">Aplicar</button>
4273
5098
  </div>
4274
5099
  </div>
4275
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".page-config{display:grid;gap:12px;padding:16px;background:var(--md-sys-color-surface-container-lowest, var(--md-sys-color-surface));border:1px solid var(--md-sys-color-outline-variant);border-radius:12px}.page-head{display:flex;align-items:center;gap:12px}.page-title{font-weight:600}.spacer{flex:1}.page-identity{display:grid;gap:4px;font-size:13px;color:var(--md-sys-color-on-surface-variant)}.page-section{font-weight:600;margin-top:8px}.grid-form{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}.page-actions{display:flex;justify-content:flex-end}.page-error{color:var(--md-sys-color-error);font-size:13px}.page-config .mat-mdc-form-field{width:100%;--mdc-outlined-text-field-container-height: 48px;--mdc-outlined-text-field-outline-color: var(--md-sys-color-outline-variant);--mdc-outlined-text-field-hover-outline-color: var(--md-sys-color-outline);--mdc-outlined-text-field-focus-outline-color: var(--md-sys-color-primary);--mdc-outlined-text-field-error-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-focus-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-hover-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-label-text-color: var(--md-sys-color-on-surface-variant);--mdc-outlined-text-field-input-text-color: var(--md-sys-color-on-surface);--mdc-outlined-text-field-supporting-text-color: var(--md-sys-color-on-surface-variant)}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;margin-right:-4px}.help-icon-button mat-icon{font-size:18px;width:18px;height:18px}.mat-mdc-form-field-icon-suffix{align-self:center}\n"] }]
5100
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".page-config{display:grid;gap:12px;padding:16px;background:var(--md-sys-color-surface-container-lowest, var(--md-sys-color-surface));border:1px solid var(--md-sys-color-outline-variant);border-radius:12px}.page-head{display:flex;align-items:center;gap:12px}.page-title{font-weight:600}.spacer{flex:1}.page-identity{display:grid;gap:4px;font-size:13px;color:var(--md-sys-color-on-surface-variant)}.page-section{font-weight:600;margin-top:8px}.grid-form{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}.state-grid{display:grid;gap:16px}.state-values,.state-group{padding:14px;border:1px solid var(--md-sys-color-outline-variant);border-radius:14px;background:linear-gradient(180deg,color-mix(in srgb,var(--md-sys-color-surface-container-low) 92%,transparent),transparent),var(--md-sys-color-surface-container-lowest, var(--md-sys-color-surface))}.state-group{display:grid;gap:12px}.state-group-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.state-group-title{font-weight:600}.state-group-help{color:var(--md-sys-color-on-surface-variant);font-size:13px}.state-list{display:grid;gap:12px}.state-card{display:grid;gap:12px;padding:12px;border-radius:12px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 80%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface-container) 84%,transparent)}.state-card-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.state-card-title{font-weight:600}.state-card-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}.span-2{grid-column:span 2}.span-3{grid-column:1 / -1}.state-empty{color:var(--md-sys-color-on-surface-variant);font-size:13px}.state-operator-hint{display:flex;align-items:center;gap:8px;min-height:48px;padding:0 12px;border-radius:12px;color:var(--md-sys-color-on-surface-variant);background:color-mix(in srgb,var(--md-sys-color-secondary-container) 35%,transparent)}.state-operator-hint mat-icon{color:var(--md-sys-color-secondary)}.page-actions{display:flex;justify-content:flex-end}.page-error{color:var(--md-sys-color-error);font-size:13px}.page-config .mat-mdc-form-field{width:100%;--mdc-outlined-text-field-container-height: 48px;--mdc-outlined-text-field-outline-color: var(--md-sys-color-outline-variant);--mdc-outlined-text-field-hover-outline-color: var(--md-sys-color-outline);--mdc-outlined-text-field-focus-outline-color: var(--md-sys-color-primary);--mdc-outlined-text-field-error-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-focus-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-hover-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-label-text-color: var(--md-sys-color-on-surface-variant);--mdc-outlined-text-field-input-text-color: var(--md-sys-color-on-surface);--mdc-outlined-text-field-supporting-text-color: var(--md-sys-color-on-surface-variant)}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;margin-right:-4px}.help-icon-button mat-icon{font-size:18px;width:18px;height:18px}.mat-mdc-form-field-icon-suffix{align-self:center}@media(max-width:720px){.span-2,.span-3{grid-column:auto}.state-group-head{align-items:stretch;flex-direction:column}}\n"] }]
4276
5101
  }], ctorParameters: () => [], propDecorators: { page: [{ type: i0.Input, args: [{ isSignal: true, alias: "page", required: false }] }], identity: [{ type: i0.Input, args: [{ isSignal: true, alias: "identity", required: false }] }], gridsterOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "gridsterOptions", required: false }] }], pageChange: [{ type: i0.Output, args: ["pageChange"] }] } });
4277
5102
 
4278
5103
  class PageBuilderAiAdapter {
@@ -4520,13 +5345,15 @@ class PageBuilderAiAdapter {
4520
5345
  connectionKey(connection) {
4521
5346
  if (!connection)
4522
5347
  return null;
4523
- const fromWidget = connection.from?.widget;
4524
- const fromOutput = connection.from?.output;
4525
- const toWidget = connection.to?.widget;
4526
- const toInput = connection.to?.input;
4527
- if (!fromWidget || !fromOutput || !toWidget || !toInput)
5348
+ const from = 'widget' in connection.from
5349
+ ? (connection.from.widget && connection.from.output ? `widget:${connection.from.widget}::${connection.from.output}` : null)
5350
+ : (connection.from.state ? `state:${connection.from.state}` : null);
5351
+ const to = 'widget' in connection.to
5352
+ ? (connection.to.widget && connection.to.input ? `widget:${connection.to.widget}::${connection.to.input}` : null)
5353
+ : (connection.to.state ? `state:${connection.to.state}` : null);
5354
+ if (!from || !to)
4528
5355
  return null;
4529
- return `${fromWidget}::${fromOutput}->${toWidget}::${toInput}`;
5356
+ return `${from}->${to}`;
4530
5357
  }
4531
5358
  gridsterFromPageOptions(options) {
4532
5359
  if (!options)
@@ -4864,7 +5691,11 @@ class DynamicGridsterPageComponent {
4864
5691
  this.widgets.set(widgets);
4865
5692
  const parsed = this.parsePage(this.page) || { widgets };
4866
5693
  // Remove connections referencing this widget
4867
- const conns = (parsed.connections || []).filter((c) => c.from.widget !== key && c.to.widget !== key);
5694
+ const conns = (parsed.connections || []).filter((c) => {
5695
+ const fromMatches = c?.from && 'widget' in c.from && c.from.widget === key;
5696
+ const toMatches = c?.to && 'widget' in c.to && c.to.widget === key;
5697
+ return !fromMatches && !toMatches;
5698
+ });
4868
5699
  const next = { ...parsed, widgets, connections: conns };
4869
5700
  this.page = next;
4870
5701
  this.pageChange.emit(next);
@@ -5016,6 +5847,8 @@ class DynamicGridsterPageComponent {
5016
5847
  const matches = this.conn.findMatches(p?.connections, fromKey, evt.output);
5017
5848
  let widgets = [...this.widgets()];
5018
5849
  for (const c of matches) {
5850
+ if (!('widget' in c.to))
5851
+ continue;
5019
5852
  widgets = this.conn.applyMatch(widgets, c, evt);
5020
5853
  }
5021
5854
  this.widgets.set(widgets);