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

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.
@@ -9,7 +9,7 @@ import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
9
9
  import * as i3 from '@angular/material/icon';
10
10
  import { MatIconModule } from '@angular/material/icon';
11
11
  import * as i1$1 from '@praxisui/core';
12
- import { PraxisIconDirective, BUILTIN_SHELL_PRESETS, deepMerge, generateId, GlobalActionService, buildPageKey, ASYNC_CONFIG_STORAGE, TABLE_CONFIG_EDITOR, STEPPER_CONFIG_EDITOR, SETTINGS_PANEL_BRIDGE, DynamicWidgetLoaderDirective, WidgetShellComponent } from '@praxisui/core';
12
+ import { PraxisIconDirective, BUILTIN_SHELL_PRESETS, BUILTIN_PAGE_LAYOUT_PRESETS, BUILTIN_PAGE_THEME_PRESETS, deepMerge, generateId, GlobalActionService, buildPageKey, ASYNC_CONFIG_STORAGE, TABLE_CONFIG_EDITOR, STEPPER_CONFIG_EDITOR, SETTINGS_PANEL_BRIDGE, DynamicWidgetLoaderDirective, WidgetShellComponent } from '@praxisui/core';
13
13
  export { WidgetShellComponent } from '@praxisui/core';
14
14
  import * as i7 from '@angular/material/tooltip';
15
15
  import { MatTooltipModule } from '@angular/material/tooltip';
@@ -480,6 +480,7 @@ class ConnectionBuilderComponent {
480
480
  // Signals
481
481
  connections = signal([], ...(ngDevMode ? [{ debugName: "connections" }] : []));
482
482
  selectedIndex = signal(-1, ...(ngDevMode ? [{ debugName: "selectedIndex" }] : []));
483
+ stateOptions = signal([], ...(ngDevMode ? [{ debugName: "stateOptions" }] : []));
483
484
  // Derived lists
484
485
  filteredConnections = computed(() => this.applyFilters(this.connections()), ...(ngDevMode ? [{ debugName: "filteredConnections" }] : []));
485
486
  groupedConnections = computed(() => this.applyGrouping(this.filteredConnections()), ...(ngDevMode ? [{ debugName: "groupedConnections" }] : []));
@@ -492,6 +493,7 @@ class ConnectionBuilderComponent {
492
493
  const p = this.parsePage(this.page);
493
494
  const ws = this.widgets || p?.widgets || [];
494
495
  this.widgets = ws;
496
+ this.refreshStateOptions(p);
495
497
  const conns = [...(p?.connections || [])];
496
498
  this.connections.set(conns);
497
499
  this.originalSnapshot = JSON.stringify(conns);
@@ -500,6 +502,7 @@ class ConnectionBuilderComponent {
500
502
  if (changes['page'] || changes['widgets']) {
501
503
  const p = this.parsePage(this.page);
502
504
  this.widgets = this.widgets || p?.widgets || [];
505
+ this.refreshStateOptions(p);
503
506
  const next = [...(p?.connections || [])];
504
507
  this.connections.set(next);
505
508
  this.originalSnapshot = JSON.stringify(next);
@@ -528,10 +531,14 @@ class ConnectionBuilderComponent {
528
531
  return this.isWidgetTarget(c.to) ? `${c.to.widget}.${c.to.input}` : `state:${c.to.state}`;
529
532
  }
530
533
  fromFriendly(c) {
531
- return this.isWidgetSource(c.from) ? `${this.widgetFriendlyNameForKey(c.from.widget)}.${c.from.output}` : `state:${c.from.state}`;
534
+ return this.isWidgetSource(c.from)
535
+ ? `${this.widgetFriendlyNameForKey(c.from.widget)}.${c.from.output}`
536
+ : `state:${this.stateOptionLabel(c.from.state)}`;
532
537
  }
533
538
  toFriendly(c) {
534
- return this.isWidgetTarget(c.to) ? `${this.widgetFriendlyNameForKey(c.to.widget)}.${c.to.input}` : `state:${c.to.state}`;
539
+ return this.isWidgetTarget(c.to)
540
+ ? `${this.widgetFriendlyNameForKey(c.to.widget)}.${c.to.input}`
541
+ : `state:${this.stateOptionLabel(c.to.state)}`;
535
542
  }
536
543
  outputDescription(c) {
537
544
  if (!this.isWidgetSource(c.from))
@@ -557,6 +564,44 @@ class ConnectionBuilderComponent {
557
564
  const index = this.selectedIndex();
558
565
  return index >= 0 ? this.connections()[index] : undefined;
559
566
  }
567
+ selectedSourceKind() {
568
+ const selected = this.selectedConnection();
569
+ return selected && this.isWidgetSource(selected.from) ? 'widget' : 'state';
570
+ }
571
+ setSelectedSourceKind(kind) {
572
+ const selected = this.selectedConnection();
573
+ if (!selected)
574
+ return;
575
+ if (kind === 'widget' && !this.isWidgetSource(selected.from)) {
576
+ selected.from = {
577
+ widget: this.widgets?.[0]?.key || '',
578
+ output: this.availableOutputsForWidget(this.widgets?.[0]?.key || '')[0] || '',
579
+ };
580
+ }
581
+ if (kind === 'state' && this.isWidgetSource(selected.from)) {
582
+ selected.from = { state: this.stateOptions()[0]?.value || '' };
583
+ }
584
+ this.connections.set([...this.connections()]);
585
+ }
586
+ selectedTargetKind() {
587
+ const selected = this.selectedConnection();
588
+ return selected && this.isWidgetTarget(selected.to) ? 'widget' : 'state';
589
+ }
590
+ setSelectedTargetKind(kind) {
591
+ const selected = this.selectedConnection();
592
+ if (!selected)
593
+ return;
594
+ if (kind === 'widget' && !this.isWidgetTarget(selected.to)) {
595
+ selected.to = {
596
+ widget: this.widgets?.[0]?.key || '',
597
+ input: this.availableInputsForWidget(this.widgets?.[0]?.key || '')[0] || '',
598
+ };
599
+ }
600
+ if (kind === 'state' && this.isWidgetTarget(selected.to)) {
601
+ selected.to = { state: this.stateOptions()[0]?.value || '' };
602
+ }
603
+ this.connections.set([...this.connections()]);
604
+ }
560
605
  selectedSourcePrimaryValue() {
561
606
  const selected = this.selectedConnection();
562
607
  if (!selected)
@@ -629,6 +674,29 @@ class ConnectionBuilderComponent {
629
674
  const selected = this.selectedConnection();
630
675
  return !!selected && this.isWidgetTarget(selected.to);
631
676
  }
677
+ availableOutputsForSelectedWidget() {
678
+ return this.availableOutputsForWidget(this.selectedSourcePrimaryValue());
679
+ }
680
+ availableInputsForSelectedWidget() {
681
+ return this.availableInputsForWidget(this.selectedTargetPrimaryValue());
682
+ }
683
+ availableStateOptions() {
684
+ return this.stateOptions();
685
+ }
686
+ stateOptionLabel(path) {
687
+ return this.stateOptions().find((option) => option.value === path)?.label || path;
688
+ }
689
+ stateOptionIcon(path) {
690
+ return this.stateOptions().find((option) => option.value === path)?.kind === 'derived'
691
+ ? 'function'
692
+ : 'account_tree';
693
+ }
694
+ widgetOptions() {
695
+ return (this.widgets || []).map((widget) => ({
696
+ key: widget.key,
697
+ label: `${this.widgetFriendlyNameForKey(widget.key)} (${widget.key})`,
698
+ }));
699
+ }
632
700
  applyFilters(list) {
633
701
  const q = (this.filterText || '').toLowerCase();
634
702
  const arr = list.filter((c) => {
@@ -670,7 +738,10 @@ class ConnectionBuilderComponent {
670
738
  createNew() {
671
739
  const fromKey = this.widgets?.[0]?.key || '';
672
740
  const toKey = this.widgets?.[1]?.key || '';
673
- const conn = { from: { widget: fromKey, output: 'submit' }, to: { widget: toKey, input: 'context' } };
741
+ const conn = {
742
+ from: { widget: fromKey, output: this.availableOutputsForWidget(fromKey)[0] || 'submit' },
743
+ to: { widget: toKey, input: this.availableInputsForWidget(toKey)[0] || 'context' },
744
+ };
674
745
  this.connections.set([conn, ...this.connections()]);
675
746
  }
676
747
  startEdit(index, _c) { this.selectedIndex.set(index); }
@@ -741,12 +812,81 @@ class ConnectionBuilderComponent {
741
812
  }
742
813
  return input;
743
814
  }
815
+ availableOutputsForWidget(key) {
816
+ const widgetType = this.widgetTypeByKey(key);
817
+ return (this.registry.get(widgetType)?.outputs || []).map((output) => output.name);
818
+ }
819
+ availableInputsForWidget(key) {
820
+ const widgetType = this.widgetTypeByKey(key);
821
+ return (this.registry.get(widgetType)?.inputs || []).map((input) => input.name);
822
+ }
823
+ refreshStateOptions(page) {
824
+ this.stateOptions.set(this.collectStateOptions(page?.state));
825
+ }
826
+ collectStateOptions(state) {
827
+ const normalized = this.normalizeState(state);
828
+ const options = new Map();
829
+ for (const [path, config] of Object.entries(normalized.schema || {})) {
830
+ options.set(path, {
831
+ value: path,
832
+ label: config.description ? `${path} - ${config.description}` : path,
833
+ kind: 'primary',
834
+ description: config.description,
835
+ });
836
+ }
837
+ for (const path of this.collectPaths(normalized.values || {})) {
838
+ if (!options.has(path)) {
839
+ options.set(path, { value: path, label: path, kind: 'primary' });
840
+ }
841
+ }
842
+ for (const [path, config] of Object.entries(normalized.derived || {})) {
843
+ options.set(path, {
844
+ value: path,
845
+ label: config.description ? `${path} - ${config.description}` : `${path} [derived]`,
846
+ kind: 'derived',
847
+ description: config.description,
848
+ });
849
+ }
850
+ return Array.from(options.values()).sort((left, right) => left.value.localeCompare(right.value));
851
+ }
852
+ normalizeState(state) {
853
+ if (!state)
854
+ return { values: {} };
855
+ const isStructured = typeof state === 'object'
856
+ && !Array.isArray(state)
857
+ && ('values' in state || 'schema' in state || 'derived' in state);
858
+ if (isStructured) {
859
+ const structured = state;
860
+ return {
861
+ values: this.clone(structured.values) || {},
862
+ schema: this.clone(structured.schema),
863
+ derived: this.clone(structured.derived),
864
+ };
865
+ }
866
+ return { values: this.clone(state) || {} };
867
+ }
868
+ collectPaths(source, prefix = '') {
869
+ const paths = [];
870
+ for (const [key, value] of Object.entries(source || {})) {
871
+ const path = prefix ? `${prefix}.${key}` : key;
872
+ paths.push(path);
873
+ if (value != null && typeof value === 'object' && !Array.isArray(value)) {
874
+ paths.push(...this.collectPaths(value, path));
875
+ }
876
+ }
877
+ return paths;
878
+ }
879
+ clone(value) {
880
+ if (value == null || typeof value !== 'object')
881
+ return value;
882
+ return JSON.parse(JSON.stringify(value));
883
+ }
744
884
  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 });
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 });
885
+ 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\" (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\" [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\" (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\" [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>Tipo da origem</mat-label>\n <mat-select [ngModel]=\"selectedSourceKind()\" (ngModelChange)=\"setSelectedSourceKind($event)\">\n <mat-option value=\"widget\">Widget</mat-option>\n <mat-option value=\"state\">State</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ selectedSourceKind() === 'widget' ? 'Origem (widget.key)' : 'Origem (state)' }}</mat-label>\n <mat-select [ngModel]=\"selectedSourcePrimaryValue()\" (ngModelChange)=\"setSelectedSourcePrimaryValue($event)\">\n <ng-container *ngIf=\"selectedSourceKind() === 'widget'; else sourceStateOptions\">\n <mat-option *ngFor=\"let option of widgetOptions()\" [value]=\"option.key\">\n {{ option.label }}\n </mat-option>\n </ng-container>\n <ng-template #sourceStateOptions>\n <mat-option *ngFor=\"let option of availableStateOptions()\" [value]=\"option.value\">\n {{ option.label }}\n </mat-option>\n </ng-template>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" *ngIf=\"showSelectedSourceSecondary()\">\n <mat-label>Evento (output)</mat-label>\n <mat-select [ngModel]=\"selectedSourceSecondaryValue()\" (ngModelChange)=\"setSelectedSourceSecondaryValue($event)\">\n <mat-option *ngFor=\"let output of availableOutputsForSelectedWidget()\" [value]=\"output\">{{ output }}</mat-option>\n </mat-select>\n </mat-form-field>\n <div class=\"endpoint-hint full-span\" *ngIf=\"selectedSourceKind() === 'state' && selectedSourcePrimaryValue()\">\n <mat-icon>{{ stateOptionIcon(selectedSourcePrimaryValue()) }}</mat-icon>\n <span>{{ stateOptionLabel(selectedSourcePrimaryValue()) }}</span>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo do destino</mat-label>\n <mat-select [ngModel]=\"selectedTargetKind()\" (ngModelChange)=\"setSelectedTargetKind($event)\">\n <mat-option value=\"widget\">Widget</mat-option>\n <mat-option value=\"state\">State</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ selectedTargetKind() === 'widget' ? 'Destino (widget.key)' : 'Destino (state)' }}</mat-label>\n <mat-select [ngModel]=\"selectedTargetPrimaryValue()\" (ngModelChange)=\"setSelectedTargetPrimaryValue($event)\">\n <ng-container *ngIf=\"selectedTargetKind() === 'widget'; else targetStateOptions\">\n <mat-option *ngFor=\"let option of widgetOptions()\" [value]=\"option.key\">\n {{ option.label }}\n </mat-option>\n </ng-container>\n <ng-template #targetStateOptions>\n <mat-option *ngFor=\"let option of availableStateOptions()\" [value]=\"option.value\">\n {{ option.label }}\n </mat-option>\n </ng-template>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" *ngIf=\"showSelectedTargetSecondary()\">\n <mat-label>Input</mat-label>\n <mat-select [ngModel]=\"selectedTargetSecondaryValue()\" (ngModelChange)=\"setSelectedTargetSecondaryValue($event)\">\n <mat-option *ngFor=\"let input of availableInputsForSelectedWidget()\" [value]=\"input\">{{ input }}</mat-option>\n </mat-select>\n </mat-form-field>\n <div class=\"endpoint-hint full-span\" *ngIf=\"selectedTargetKind() === 'state' && selectedTargetPrimaryValue()\">\n <mat-icon>{{ stateOptionIcon(selectedTargetPrimaryValue()) }}</mat-icon>\n <span>{{ stateOptionLabel(selectedTargetPrimaryValue()) }}</span>\n </div>\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}.endpoint-hint{display:flex;align-items:center;gap:8px;min-height:44px;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)}.endpoint-hint mat-icon{color:var(--md-sys-color-secondary)}.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 });
746
886
  }
747
887
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ConnectionBuilderComponent, decorators: [{
748
888
  type: Component,
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"] }]
889
+ 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\" (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\" [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\" (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\" [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>Tipo da origem</mat-label>\n <mat-select [ngModel]=\"selectedSourceKind()\" (ngModelChange)=\"setSelectedSourceKind($event)\">\n <mat-option value=\"widget\">Widget</mat-option>\n <mat-option value=\"state\">State</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ selectedSourceKind() === 'widget' ? 'Origem (widget.key)' : 'Origem (state)' }}</mat-label>\n <mat-select [ngModel]=\"selectedSourcePrimaryValue()\" (ngModelChange)=\"setSelectedSourcePrimaryValue($event)\">\n <ng-container *ngIf=\"selectedSourceKind() === 'widget'; else sourceStateOptions\">\n <mat-option *ngFor=\"let option of widgetOptions()\" [value]=\"option.key\">\n {{ option.label }}\n </mat-option>\n </ng-container>\n <ng-template #sourceStateOptions>\n <mat-option *ngFor=\"let option of availableStateOptions()\" [value]=\"option.value\">\n {{ option.label }}\n </mat-option>\n </ng-template>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" *ngIf=\"showSelectedSourceSecondary()\">\n <mat-label>Evento (output)</mat-label>\n <mat-select [ngModel]=\"selectedSourceSecondaryValue()\" (ngModelChange)=\"setSelectedSourceSecondaryValue($event)\">\n <mat-option *ngFor=\"let output of availableOutputsForSelectedWidget()\" [value]=\"output\">{{ output }}</mat-option>\n </mat-select>\n </mat-form-field>\n <div class=\"endpoint-hint full-span\" *ngIf=\"selectedSourceKind() === 'state' && selectedSourcePrimaryValue()\">\n <mat-icon>{{ stateOptionIcon(selectedSourcePrimaryValue()) }}</mat-icon>\n <span>{{ stateOptionLabel(selectedSourcePrimaryValue()) }}</span>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo do destino</mat-label>\n <mat-select [ngModel]=\"selectedTargetKind()\" (ngModelChange)=\"setSelectedTargetKind($event)\">\n <mat-option value=\"widget\">Widget</mat-option>\n <mat-option value=\"state\">State</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ selectedTargetKind() === 'widget' ? 'Destino (widget.key)' : 'Destino (state)' }}</mat-label>\n <mat-select [ngModel]=\"selectedTargetPrimaryValue()\" (ngModelChange)=\"setSelectedTargetPrimaryValue($event)\">\n <ng-container *ngIf=\"selectedTargetKind() === 'widget'; else targetStateOptions\">\n <mat-option *ngFor=\"let option of widgetOptions()\" [value]=\"option.key\">\n {{ option.label }}\n </mat-option>\n </ng-container>\n <ng-template #targetStateOptions>\n <mat-option *ngFor=\"let option of availableStateOptions()\" [value]=\"option.value\">\n {{ option.label }}\n </mat-option>\n </ng-template>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" *ngIf=\"showSelectedTargetSecondary()\">\n <mat-label>Input</mat-label>\n <mat-select [ngModel]=\"selectedTargetSecondaryValue()\" (ngModelChange)=\"setSelectedTargetSecondaryValue($event)\">\n <mat-option *ngFor=\"let input of availableInputsForSelectedWidget()\" [value]=\"input\">{{ input }}</mat-option>\n </mat-select>\n </mat-form-field>\n <div class=\"endpoint-hint full-span\" *ngIf=\"selectedTargetKind() === 'state' && selectedTargetPrimaryValue()\">\n <mat-icon>{{ stateOptionIcon(selectedTargetPrimaryValue()) }}</mat-icon>\n <span>{{ stateOptionLabel(selectedTargetPrimaryValue()) }}</span>\n </div>\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}.endpoint-hint{display:flex;align-items:center;gap:8px;min-height:44px;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)}.endpoint-hint mat-icon{color:var(--md-sys-color-secondary)}.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"] }]
750
890
  }], ctorParameters: () => [{ type: i1.MatDialog }, { type: i1$1.ComponentMetadataRegistry }, { type: i3$1.MatSnackBar }], propDecorators: { page: [{
751
891
  type: Input
752
892
  }], widgets: [{
@@ -3050,6 +3190,7 @@ class GraphMapperService {
3050
3190
  const colWidth = 240;
3051
3191
  const rowHeight = 96;
3052
3192
  const gap = 28;
3193
+ const structuredState = this.normalizeState(page?.state);
3053
3194
  let leftY = 0;
3054
3195
  let rightY = 0;
3055
3196
  const stateNodeIds = new Map();
@@ -3186,17 +3327,71 @@ class GraphMapperService {
3186
3327
  }
3187
3328
  }
3188
3329
  }
3330
+ let structureY = Math.max(leftY, rightY) + gap * 2;
3331
+ const structureX = gap;
3332
+ for (const group of page?.grouping || []) {
3333
+ const label = group.kind === 'tabs'
3334
+ ? `${group.kind}: ${group.id} (${group.tabs.length} tabs)`
3335
+ : `${group.kind}: ${group.id}`;
3336
+ const widgetCount = group.kind === 'tabs'
3337
+ ? group.tabs.reduce((total, tab) => total + tab.widgetKeys.length, 0)
3338
+ : group.widgetKeys.length;
3339
+ const node = {
3340
+ id: `group:${group.id}`,
3341
+ label,
3342
+ icon: group.kind === 'tabs' ? 'tabs' : group.kind === 'hero' ? 'web_asset' : group.kind === 'rail' ? 'view_sidebar' : 'dashboard_customize',
3343
+ type: `page-group:${group.kind}`,
3344
+ parentId: null,
3345
+ collapsed: true,
3346
+ bounds: { x: structureX, y: structureY, width: colWidth, height: 72 },
3347
+ ports: [],
3348
+ };
3349
+ nodes.push(node);
3350
+ nodeById.set(node.id, node);
3351
+ structureY += 72 + gap;
3352
+ if (widgetCount > 0) {
3353
+ node.label = `${node.label} • ${widgetCount} widgets`;
3354
+ }
3355
+ }
3356
+ for (const device of ['desktop', 'tablet', 'mobile']) {
3357
+ const variant = page?.deviceLayouts?.[device];
3358
+ if (!variant)
3359
+ continue;
3360
+ const detail = [];
3361
+ if (variant.layout?.orientation)
3362
+ detail.push(variant.layout.orientation);
3363
+ if (variant.layout?.columns)
3364
+ detail.push(`${variant.layout.columns} cols`);
3365
+ if (variant.groupingOverrides?.length)
3366
+ detail.push(`${variant.groupingOverrides.length} grouping overrides`);
3367
+ if (variant.widgetOverrides && Object.keys(variant.widgetOverrides).length) {
3368
+ detail.push(`${Object.keys(variant.widgetOverrides).length} widget overrides`);
3369
+ }
3370
+ const node = {
3371
+ id: `device:${device}`,
3372
+ label: `device: ${device}${detail.length ? ` • ${detail.join(' • ')}` : ''}`,
3373
+ icon: device === 'desktop' ? 'desktop_windows' : device === 'tablet' ? 'tablet_mac' : 'smartphone',
3374
+ type: 'page-device-layout',
3375
+ parentId: null,
3376
+ collapsed: true,
3377
+ bounds: { x: structureX + colWidth + gap, y: structureY, width: colWidth + 40, height: 72 },
3378
+ ports: [],
3379
+ };
3380
+ nodes.push(node);
3381
+ nodeById.set(node.id, node);
3382
+ structureY += 72 + gap;
3383
+ }
3189
3384
  // Second pass: map edges from page.connections
3190
3385
  const conns = (page?.connections || []);
3191
3386
  for (let i = 0; i < conns.length; i++) {
3192
3387
  const c = conns[i];
3193
3388
  const fromState = !this.isWidgetSource(c.from)
3194
- ? this.ensureStateNode(stateNodeIds, nodeById, nodes, page, c.from.state, 'output', colWidth, gap, leftY)
3389
+ ? this.ensureStateNode(stateNodeIds, nodeById, nodes, structuredState, c.from.state, 'output', colWidth, gap, leftY)
3195
3390
  : null;
3196
3391
  const fromNodeId = this.isWidgetSource(c.from) ? c.from.widget : fromState.nodeId;
3197
3392
  const fromPortId = this.isWidgetSource(c.from) ? `out:${c.from.output}` : 'out:state';
3198
3393
  const toState = !this.isWidgetTarget(c.to)
3199
- ? this.ensureStateNode(stateNodeIds, nodeById, nodes, page, c.to.state, 'input', colWidth, gap, rightY)
3394
+ ? this.ensureStateNode(stateNodeIds, nodeById, nodes, structuredState, c.to.state, 'input', colWidth, gap, rightY)
3200
3395
  : null;
3201
3396
  let toNodeId = this.isWidgetTarget(c.to) ? c.to.widget : toState.nodeId;
3202
3397
  let toPortId = this.isWidgetTarget(c.to) ? `in:${c.to.input}` : 'in:state';
@@ -3232,13 +3427,20 @@ class GraphMapperService {
3232
3427
  from: { nodeId: fromNodeId, portId: fromPortId },
3233
3428
  to: { nodeId: toNodeId, portId: toPortId },
3234
3429
  label: c.map || undefined,
3235
- meta: { map: c.map, bindingOrder: this.isWidgetTarget(c.to) ? c.to.bindingOrder : undefined, ...(friendlyPath ? { friendlyPath } : {}) },
3430
+ meta: {
3431
+ map: c.map,
3432
+ bindingOrder: this.isWidgetTarget(c.to) ? c.to.bindingOrder : undefined,
3433
+ ...(friendlyPath ? { friendlyPath } : {}),
3434
+ fromLabel: this.connectionEndpointLabel(c.from, widgets, structuredState),
3435
+ toLabel: this.connectionEndpointLabel(c.to, widgets, structuredState),
3436
+ connectionKind: this.connectionKind(c),
3437
+ },
3236
3438
  };
3237
3439
  edges.push(edge);
3238
3440
  }
3239
3441
  return { nodes, edges };
3240
3442
  }
3241
- ensureStateNode(stateNodeIds, nodeById, nodes, page, statePath, kind, colWidth, gap, offsetY) {
3443
+ ensureStateNode(stateNodeIds, nodeById, nodes, structuredState, statePath, kind, colWidth, gap, offsetY) {
3242
3444
  const key = `${kind}:${statePath}`;
3243
3445
  const existing = stateNodeIds.get(key);
3244
3446
  if (existing)
@@ -3246,11 +3448,12 @@ class GraphMapperService {
3246
3448
  const nodeId = `state:${kind}:${encodeURIComponent(statePath)}`;
3247
3449
  const portId = kind === 'output' ? 'out:state' : 'in:state';
3248
3450
  const x = kind === 'output' ? gap : colWidth + gap * 3;
3249
- const structuredState = this.normalizeState(page?.state);
3250
3451
  const isDerived = !!structuredState?.derived?.[statePath];
3452
+ const description = structuredState?.derived?.[statePath]?.description || structuredState?.schema?.[statePath]?.description;
3453
+ const titlePrefix = isDerived ? 'Derived State' : 'Page State';
3251
3454
  const node = {
3252
3455
  id: nodeId,
3253
- label: `${isDerived ? 'derived' : 'state'}:${statePath}`,
3456
+ label: description ? `${titlePrefix}: ${description}` : `${titlePrefix}: ${statePath}`,
3254
3457
  icon: isDerived ? 'function' : 'account_tree',
3255
3458
  type: isDerived ? 'page-derived-state' : 'page-state',
3256
3459
  parentId: null,
@@ -3259,7 +3462,7 @@ class GraphMapperService {
3259
3462
  ports: [{
3260
3463
  id: portId,
3261
3464
  label: statePath,
3262
- description: statePath,
3465
+ description: description ? `${description}\n${statePath}` : statePath,
3263
3466
  kind,
3264
3467
  anchor: { x: kind === 'output' ? x : x + colWidth, y: gap + offsetY + 24 },
3265
3468
  }],
@@ -3274,7 +3477,29 @@ class GraphMapperService {
3274
3477
  return null;
3275
3478
  if ('values' in state || 'schema' in state || 'derived' in state)
3276
3479
  return state;
3277
- return null;
3480
+ return { values: state };
3481
+ }
3482
+ connectionEndpointLabel(endpoint, widgets, structuredState) {
3483
+ if ('widget' in endpoint) {
3484
+ const widget = widgets.find((item) => item.key === endpoint.widget);
3485
+ const meta = widget ? this.registry.get(widget.definition.id) : null;
3486
+ if ('output' in endpoint)
3487
+ return `${meta?.friendlyName || endpoint.widget}.${endpoint.output}`;
3488
+ return `${meta?.friendlyName || endpoint.widget}.${endpoint.input}`;
3489
+ }
3490
+ const description = structuredState?.derived?.[endpoint.state]?.description || structuredState?.schema?.[endpoint.state]?.description;
3491
+ return description ? `${endpoint.state} - ${description}` : endpoint.state;
3492
+ }
3493
+ connectionKind(connection) {
3494
+ const fromWidget = this.isWidgetSource(connection.from);
3495
+ const toWidget = this.isWidgetTarget(connection.to);
3496
+ if (fromWidget && toWidget)
3497
+ return 'widget-to-widget';
3498
+ if (fromWidget && !toWidget)
3499
+ return 'widget-to-state';
3500
+ if (!fromWidget && toWidget)
3501
+ return 'state-to-widget';
3502
+ return 'state-to-state';
3278
3503
  }
3279
3504
  ensurePort(nodeById, nodeId, portId, kind, colWidth) {
3280
3505
  const n = nodeById.get(nodeId);
@@ -3362,14 +3587,37 @@ class ConnectionGraphComponent {
3362
3587
  return 'widget' in to;
3363
3588
  }
3364
3589
  fromLabel(c) {
3365
- return this.isWidgetSource(c.from) ? `${c.from.widget}.${c.from.output}` : `state:${c.from.state}`;
3590
+ return this.currentEdge()?.meta?.fromLabel || (this.isWidgetSource(c.from) ? `${c.from.widget}.${c.from.output}` : `state:${c.from.state}`);
3366
3591
  }
3367
3592
  toLabel(c) {
3368
- return this.isWidgetTarget(c.to) ? `${c.to.widget}.${c.to.input}` : `state:${c.to.state}`;
3593
+ return this.currentEdge()?.meta?.toLabel || (this.isWidgetTarget(c.to) ? `${c.to.widget}.${c.to.input}` : `state:${c.to.state}`);
3369
3594
  }
3370
3595
  bindingOrderLabel(c) {
3371
3596
  return this.isWidgetTarget(c.to) && c.to.bindingOrder?.length ? c.to.bindingOrder.join(', ') : '';
3372
3597
  }
3598
+ connectionKindSummary() {
3599
+ const counts = new Map();
3600
+ for (const edge of this.edges()) {
3601
+ const kind = edge.meta?.connectionKind;
3602
+ if (!kind)
3603
+ continue;
3604
+ counts.set(kind, (counts.get(kind) || 0) + 1);
3605
+ }
3606
+ return Array.from(counts.entries()).map(([kind, count]) => ({
3607
+ kind,
3608
+ count,
3609
+ label: this.connectionKindLabel(kind),
3610
+ }));
3611
+ }
3612
+ connectionKindLabel(kind) {
3613
+ switch (kind) {
3614
+ case 'widget-to-widget': return 'Widget -> Widget';
3615
+ case 'widget-to-state': return 'Widget -> State';
3616
+ case 'state-to-widget': return 'State -> Widget';
3617
+ case 'state-to-state': return 'State -> State';
3618
+ default: return 'Conexão';
3619
+ }
3620
+ }
3373
3621
  isPortConnected(nodeId, portId) {
3374
3622
  const set = this.connectedPorts.get(nodeId);
3375
3623
  return !!set && set.has(portId);
@@ -3508,11 +3756,10 @@ class ConnectionGraphComponent {
3508
3756
  return routeElbow(from, to);
3509
3757
  }
3510
3758
  edgeTooltip(e) {
3511
- const toNode = this.nodes().find(n => n.id === e.to.nodeId);
3512
- const fromNode = this.nodes().find(n => n.id === e.from.nodeId);
3513
3759
  const parts = [
3514
- `De: ${fromNode?.label || e.from.nodeId}.${e.from.portId.replace('out:', '')}`,
3515
- `Para: ${toNode?.label || e.to.nodeId}.${e.to.portId.replace('in:', '')}`,
3760
+ `Tipo: ${this.connectionKindLabel(e.meta?.connectionKind)}`,
3761
+ `De: ${e.meta?.fromLabel || e.from.nodeId}.${e.from.portId.replace('out:', '')}`,
3762
+ `Para: ${e.meta?.toLabel || e.to.nodeId}.${e.to.portId.replace('in:', '')}`,
3516
3763
  ];
3517
3764
  if (e.meta && e.meta.friendlyPath)
3518
3765
  parts.push(`Destino: ${e.meta.friendlyPath}`);
@@ -3708,6 +3955,7 @@ class ConnectionGraphComponent {
3708
3955
  this.selectedEdgeIndex.set(index);
3709
3956
  this.showDetails = true;
3710
3957
  }
3958
+ currentEdge() { const i = this.selectedEdgeIndex(); return (i >= 0 ? this.edges()[i] : undefined); }
3711
3959
  currentConn() { const i = this.selectedEdgeIndex(); return (i >= 0 ? this.connections()[i] : undefined); }
3712
3960
  emitPageChange() {
3713
3961
  const parsed = this.parsePage(this.page) || { widgets: this.widgets || [] };
@@ -3809,6 +4057,9 @@ class ConnectionGraphComponent {
3809
4057
  </button>
3810
4058
  <button mat-stroked-button color="primary" (click)="openBuilder()" aria-label="Editar no Builder"><mat-icon>tune</mat-icon><span>Editar no Builder</span></button>
3811
4059
  <div class="cg-count" [attr.aria-label]="'Total de conexões: ' + connections().length">{{ connections().length }}</div>
4060
+ <div class="cg-kind-summary" *ngIf="connections().length">
4061
+ <span class="cg-kind-pill" *ngFor="let item of connectionKindSummary()">{{ item.label }}: {{ item.count }}</span>
4062
+ </div>
3812
4063
  </div>
3813
4064
  <div class="cg-canvas">
3814
4065
  <!-- Edge details panel -->
@@ -3818,6 +4069,7 @@ class ConnectionGraphComponent {
3818
4069
  <button mat-icon-button (click)="showDetails=false" aria-label="Fechar"><mat-icon>close</mat-icon></button>
3819
4070
  </div>
3820
4071
  <div class="details-body" *ngIf="currentConn() as c">
4072
+ <div><b>Tipo:</b> {{ connectionKindLabel(currentEdge()?.meta?.connectionKind) }}</div>
3821
4073
  <div><b>De:</b> {{ fromLabel(c) }}</div>
3822
4074
  <div><b>Para:</b> {{ toLabel(c) }}</div>
3823
4075
  <div><b>map:</b> {{ c.map || 'payload' }}</div>
@@ -3922,7 +4174,7 @@ class ConnectionGraphComponent {
3922
4174
  </div>
3923
4175
  </div>
3924
4176
  </div>
3925
- `, isInline: true, styles: [".cg-root{display:flex;flex-direction:column;height:100%;width:100%}.cg-head{display:flex;align-items:center;gap:12px;padding:8px 12px;background:var(--md-sys-color-surface);border-bottom:1px solid var(--md-sys-color-outline)}.cg-title{font-weight:600}.spacer{flex:1}.cg-count{color:var(--md-sys-color-on-surface-variant)}.cg-canvas{position:relative;flex:1;min-height:300px}svg{background:var(--md-sys-color-surface-container-lowest);color:var(--md-sys-color-on-surface)}.node-box{fill:var(--md-sys-color-surface-container);stroke:var(--md-sys-color-outline)}.node-title{fill:var(--md-sys-color-on-surface);font-size:14px;font-weight:600}.port-dot{fill:currentColor;r:6}.port-label{fill:var(--md-sys-color-on-surface-variant);font-size:13px}.edge{stroke:var(--md-sys-color-primary);stroke-width:2;fill:none}.edge.hover{stroke:var(--md-sys-color-tertiary)}.edge.temp{stroke-dasharray:4 4;opacity:.7}.edge-label{fill:var(--md-sys-color-on-surface);font-size:12px;dominant-baseline:middle;text-anchor:middle}.edge-label-bg{fill:var(--md-sys-color-surface);opacity:.72}.edge-toolbar .edge-toolbar-bg{fill:var(--md-sys-color-surface);stroke:var(--md-sys-color-outline)}.edge-toolbar .icon{fill:var(--md-sys-color-on-surface);font-size:12px;cursor:pointer;-webkit-user-select:none;user-select:none}.popover{position:absolute;transform:translate(-50%,-50%);z-index:10}.popover-body{background:var(--md-sys-color-surface);color:var(--md-sys-color-on-surface);border:1px solid var(--md-sys-color-outline);border-radius:8px;padding:8px;min-width:220px;box-shadow:var(--mat-elevation-level4)}.popover-title{font-weight:600;margin-bottom:8px}.popover-grid{display:grid;gap:6px;min-width:260px}.popover-chips{display:flex;gap:6px;flex-wrap:wrap}.popover-rows{display:grid;gap:6px}.popover-inline{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.popover-inline-field{display:inline-flex;align-items:center;gap:6px}.popover-body label{display:grid;gap:4px;font-size:12px;color:var(--md-sys-color-on-surface-variant)}.popover-input{background:var(--md-sys-color-surface-container-lowest);color:var(--md-sys-color-on-surface);border:1px solid var(--md-sys-color-outline);border-radius:6px;padding:4px 6px;font:inherit}.popover-input:focus-visible{outline:2px solid var(--md-sys-color-primary);outline-offset:1px}.popover-input--short{width:90px}.popover-preview{font-size:12px;opacity:.8;color:var(--md-sys-color-on-surface-variant)}.actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}.port-hit{fill:transparent;cursor:crosshair;height:24px}.cg-details{position:absolute;right:8px;top:48px;width:320px;background:var(--md-sys-color-surface);color:var(--md-sys-color-on-surface);border:1px solid var(--md-sys-color-outline);border-radius:8px;box-shadow:var(--mat-elevation-level3);z-index:12}.details-head{display:flex;align-items:center;justify-content:space-between;padding:8px 10px;border-bottom:1px solid var(--md-sys-color-outline)}.details-title{font-weight:600}.details-body{padding:8px 10px;display:grid;gap:6px}.details-actions{display:flex;gap:8px;margin-top:8px}\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.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { 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.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { 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: MatTooltipModule }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: DragConnectDirective, selector: "[dragConnect]", inputs: ["dragConnect"], outputs: ["connectDragStart", "connectDragMove", "connectDragEnd"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4177
+ `, isInline: true, styles: [".cg-root{display:flex;flex-direction:column;height:100%;width:100%}.cg-head{display:flex;align-items:center;gap:12px;padding:8px 12px;background:var(--md-sys-color-surface);border-bottom:1px solid var(--md-sys-color-outline)}.cg-title{font-weight:600}.spacer{flex:1}.cg-count{color:var(--md-sys-color-on-surface-variant)}.cg-kind-summary{display:flex;gap:8px;flex-wrap:wrap}.cg-kind-pill{padding:4px 10px;border-radius:999px;background:var(--md-sys-color-secondary-container);color:var(--md-sys-color-on-secondary-container);font-size:12px;font-weight:600}.cg-canvas{position:relative;flex:1;min-height:300px}svg{background:var(--md-sys-color-surface-container-lowest);color:var(--md-sys-color-on-surface)}.node-box{fill:var(--md-sys-color-surface-container);stroke:var(--md-sys-color-outline)}.node-title{fill:var(--md-sys-color-on-surface);font-size:14px;font-weight:600}.port-dot{fill:currentColor;r:6}.port-label{fill:var(--md-sys-color-on-surface-variant);font-size:13px}.edge{stroke:var(--md-sys-color-primary);stroke-width:2;fill:none}.edge.hover{stroke:var(--md-sys-color-tertiary)}.edge.temp{stroke-dasharray:4 4;opacity:.7}.edge-label{fill:var(--md-sys-color-on-surface);font-size:12px;dominant-baseline:middle;text-anchor:middle}.edge-label-bg{fill:var(--md-sys-color-surface);opacity:.72}.edge-toolbar .edge-toolbar-bg{fill:var(--md-sys-color-surface);stroke:var(--md-sys-color-outline)}.edge-toolbar .icon{fill:var(--md-sys-color-on-surface);font-size:12px;cursor:pointer;-webkit-user-select:none;user-select:none}.popover{position:absolute;transform:translate(-50%,-50%);z-index:10}.popover-body{background:var(--md-sys-color-surface);color:var(--md-sys-color-on-surface);border:1px solid var(--md-sys-color-outline);border-radius:8px;padding:8px;min-width:220px;box-shadow:var(--mat-elevation-level4)}.popover-title{font-weight:600;margin-bottom:8px}.popover-grid{display:grid;gap:6px;min-width:260px}.popover-chips{display:flex;gap:6px;flex-wrap:wrap}.popover-rows{display:grid;gap:6px}.popover-inline{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.popover-inline-field{display:inline-flex;align-items:center;gap:6px}.popover-body label{display:grid;gap:4px;font-size:12px;color:var(--md-sys-color-on-surface-variant)}.popover-input{background:var(--md-sys-color-surface-container-lowest);color:var(--md-sys-color-on-surface);border:1px solid var(--md-sys-color-outline);border-radius:6px;padding:4px 6px;font:inherit}.popover-input:focus-visible{outline:2px solid var(--md-sys-color-primary);outline-offset:1px}.popover-input--short{width:90px}.popover-preview{font-size:12px;opacity:.8;color:var(--md-sys-color-on-surface-variant)}.actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}.port-hit{fill:transparent;cursor:crosshair;height:24px}.cg-details{position:absolute;right:8px;top:48px;width:320px;background:var(--md-sys-color-surface);color:var(--md-sys-color-on-surface);border:1px solid var(--md-sys-color-outline);border-radius:8px;box-shadow:var(--mat-elevation-level3);z-index:12}.details-head{display:flex;align-items:center;justify-content:space-between;padding:8px 10px;border-bottom:1px solid var(--md-sys-color-outline)}.details-title{font-weight:600}.details-body{padding:8px 10px;display:grid;gap:6px}.details-actions{display:flex;gap:8px;margin-top:8px}\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.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { 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.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { 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: MatTooltipModule }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: DragConnectDirective, selector: "[dragConnect]", inputs: ["dragConnect"], outputs: ["connectDragStart", "connectDragMove", "connectDragEnd"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3926
4178
  }
3927
4179
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ConnectionGraphComponent, decorators: [{
3928
4180
  type: Component,
@@ -3945,6 +4197,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3945
4197
  </button>
3946
4198
  <button mat-stroked-button color="primary" (click)="openBuilder()" aria-label="Editar no Builder"><mat-icon>tune</mat-icon><span>Editar no Builder</span></button>
3947
4199
  <div class="cg-count" [attr.aria-label]="'Total de conexões: ' + connections().length">{{ connections().length }}</div>
4200
+ <div class="cg-kind-summary" *ngIf="connections().length">
4201
+ <span class="cg-kind-pill" *ngFor="let item of connectionKindSummary()">{{ item.label }}: {{ item.count }}</span>
4202
+ </div>
3948
4203
  </div>
3949
4204
  <div class="cg-canvas">
3950
4205
  <!-- Edge details panel -->
@@ -3954,6 +4209,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3954
4209
  <button mat-icon-button (click)="showDetails=false" aria-label="Fechar"><mat-icon>close</mat-icon></button>
3955
4210
  </div>
3956
4211
  <div class="details-body" *ngIf="currentConn() as c">
4212
+ <div><b>Tipo:</b> {{ connectionKindLabel(currentEdge()?.meta?.connectionKind) }}</div>
3957
4213
  <div><b>De:</b> {{ fromLabel(c) }}</div>
3958
4214
  <div><b>Para:</b> {{ toLabel(c) }}</div>
3959
4215
  <div><b>map:</b> {{ c.map || 'payload' }}</div>
@@ -4058,7 +4314,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
4058
4314
  </div>
4059
4315
  </div>
4060
4316
  </div>
4061
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".cg-root{display:flex;flex-direction:column;height:100%;width:100%}.cg-head{display:flex;align-items:center;gap:12px;padding:8px 12px;background:var(--md-sys-color-surface);border-bottom:1px solid var(--md-sys-color-outline)}.cg-title{font-weight:600}.spacer{flex:1}.cg-count{color:var(--md-sys-color-on-surface-variant)}.cg-canvas{position:relative;flex:1;min-height:300px}svg{background:var(--md-sys-color-surface-container-lowest);color:var(--md-sys-color-on-surface)}.node-box{fill:var(--md-sys-color-surface-container);stroke:var(--md-sys-color-outline)}.node-title{fill:var(--md-sys-color-on-surface);font-size:14px;font-weight:600}.port-dot{fill:currentColor;r:6}.port-label{fill:var(--md-sys-color-on-surface-variant);font-size:13px}.edge{stroke:var(--md-sys-color-primary);stroke-width:2;fill:none}.edge.hover{stroke:var(--md-sys-color-tertiary)}.edge.temp{stroke-dasharray:4 4;opacity:.7}.edge-label{fill:var(--md-sys-color-on-surface);font-size:12px;dominant-baseline:middle;text-anchor:middle}.edge-label-bg{fill:var(--md-sys-color-surface);opacity:.72}.edge-toolbar .edge-toolbar-bg{fill:var(--md-sys-color-surface);stroke:var(--md-sys-color-outline)}.edge-toolbar .icon{fill:var(--md-sys-color-on-surface);font-size:12px;cursor:pointer;-webkit-user-select:none;user-select:none}.popover{position:absolute;transform:translate(-50%,-50%);z-index:10}.popover-body{background:var(--md-sys-color-surface);color:var(--md-sys-color-on-surface);border:1px solid var(--md-sys-color-outline);border-radius:8px;padding:8px;min-width:220px;box-shadow:var(--mat-elevation-level4)}.popover-title{font-weight:600;margin-bottom:8px}.popover-grid{display:grid;gap:6px;min-width:260px}.popover-chips{display:flex;gap:6px;flex-wrap:wrap}.popover-rows{display:grid;gap:6px}.popover-inline{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.popover-inline-field{display:inline-flex;align-items:center;gap:6px}.popover-body label{display:grid;gap:4px;font-size:12px;color:var(--md-sys-color-on-surface-variant)}.popover-input{background:var(--md-sys-color-surface-container-lowest);color:var(--md-sys-color-on-surface);border:1px solid var(--md-sys-color-outline);border-radius:6px;padding:4px 6px;font:inherit}.popover-input:focus-visible{outline:2px solid var(--md-sys-color-primary);outline-offset:1px}.popover-input--short{width:90px}.popover-preview{font-size:12px;opacity:.8;color:var(--md-sys-color-on-surface-variant)}.actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}.port-hit{fill:transparent;cursor:crosshair;height:24px}.cg-details{position:absolute;right:8px;top:48px;width:320px;background:var(--md-sys-color-surface);color:var(--md-sys-color-on-surface);border:1px solid var(--md-sys-color-outline);border-radius:8px;box-shadow:var(--mat-elevation-level3);z-index:12}.details-head{display:flex;align-items:center;justify-content:space-between;padding:8px 10px;border-bottom:1px solid var(--md-sys-color-outline)}.details-title{font-weight:600}.details-body{padding:8px 10px;display:grid;gap:6px}.details-actions{display:flex;gap:8px;margin-top:8px}\n"] }]
4317
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".cg-root{display:flex;flex-direction:column;height:100%;width:100%}.cg-head{display:flex;align-items:center;gap:12px;padding:8px 12px;background:var(--md-sys-color-surface);border-bottom:1px solid var(--md-sys-color-outline)}.cg-title{font-weight:600}.spacer{flex:1}.cg-count{color:var(--md-sys-color-on-surface-variant)}.cg-kind-summary{display:flex;gap:8px;flex-wrap:wrap}.cg-kind-pill{padding:4px 10px;border-radius:999px;background:var(--md-sys-color-secondary-container);color:var(--md-sys-color-on-secondary-container);font-size:12px;font-weight:600}.cg-canvas{position:relative;flex:1;min-height:300px}svg{background:var(--md-sys-color-surface-container-lowest);color:var(--md-sys-color-on-surface)}.node-box{fill:var(--md-sys-color-surface-container);stroke:var(--md-sys-color-outline)}.node-title{fill:var(--md-sys-color-on-surface);font-size:14px;font-weight:600}.port-dot{fill:currentColor;r:6}.port-label{fill:var(--md-sys-color-on-surface-variant);font-size:13px}.edge{stroke:var(--md-sys-color-primary);stroke-width:2;fill:none}.edge.hover{stroke:var(--md-sys-color-tertiary)}.edge.temp{stroke-dasharray:4 4;opacity:.7}.edge-label{fill:var(--md-sys-color-on-surface);font-size:12px;dominant-baseline:middle;text-anchor:middle}.edge-label-bg{fill:var(--md-sys-color-surface);opacity:.72}.edge-toolbar .edge-toolbar-bg{fill:var(--md-sys-color-surface);stroke:var(--md-sys-color-outline)}.edge-toolbar .icon{fill:var(--md-sys-color-on-surface);font-size:12px;cursor:pointer;-webkit-user-select:none;user-select:none}.popover{position:absolute;transform:translate(-50%,-50%);z-index:10}.popover-body{background:var(--md-sys-color-surface);color:var(--md-sys-color-on-surface);border:1px solid var(--md-sys-color-outline);border-radius:8px;padding:8px;min-width:220px;box-shadow:var(--mat-elevation-level4)}.popover-title{font-weight:600;margin-bottom:8px}.popover-grid{display:grid;gap:6px;min-width:260px}.popover-chips{display:flex;gap:6px;flex-wrap:wrap}.popover-rows{display:grid;gap:6px}.popover-inline{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.popover-inline-field{display:inline-flex;align-items:center;gap:6px}.popover-body label{display:grid;gap:4px;font-size:12px;color:var(--md-sys-color-on-surface-variant)}.popover-input{background:var(--md-sys-color-surface-container-lowest);color:var(--md-sys-color-on-surface);border:1px solid var(--md-sys-color-outline);border-radius:6px;padding:4px 6px;font:inherit}.popover-input:focus-visible{outline:2px solid var(--md-sys-color-primary);outline-offset:1px}.popover-input--short{width:90px}.popover-preview{font-size:12px;opacity:.8;color:var(--md-sys-color-on-surface-variant)}.actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}.port-hit{fill:transparent;cursor:crosshair;height:24px}.cg-details{position:absolute;right:8px;top:48px;width:320px;background:var(--md-sys-color-surface);color:var(--md-sys-color-on-surface);border:1px solid var(--md-sys-color-outline);border-radius:8px;box-shadow:var(--mat-elevation-level3);z-index:12}.details-head{display:flex;align-items:center;justify-content:space-between;padding:8px 10px;border-bottom:1px solid var(--md-sys-color-outline)}.details-title{font-weight:600}.details-body{padding:8px 10px;display:grid;gap:6px}.details-actions{display:flex;gap:8px;margin-top:8px}\n"] }]
4062
4318
  }], ctorParameters: () => [{ type: GraphMapperService }, { type: i2$2.SettingsPanelService }, { type: undefined, decorators: [{
4063
4319
  type: Optional
4064
4320
  }, {
@@ -4084,6 +4340,93 @@ class ConnectionEditorComponent {
4084
4340
  pageState = signal(undefined, ...(ngDevMode ? [{ debugName: "pageState" }] : []));
4085
4341
  jsonControl = new FormControl('', { nonNullable: true });
4086
4342
  jsonError = signal(null, ...(ngDevMode ? [{ debugName: "jsonError" }] : []));
4343
+ connectionSummary = computed(() => {
4344
+ const page = this.pageState();
4345
+ const connections = page?.connections || [];
4346
+ let widgetToWidget = 0;
4347
+ let widgetToState = 0;
4348
+ let stateToWidget = 0;
4349
+ let stateToState = 0;
4350
+ for (const connection of connections) {
4351
+ const fromWidget = 'widget' in connection.from;
4352
+ const toWidget = 'widget' in connection.to;
4353
+ if (fromWidget && toWidget)
4354
+ widgetToWidget += 1;
4355
+ if (fromWidget && !toWidget)
4356
+ widgetToState += 1;
4357
+ if (!fromWidget && toWidget)
4358
+ stateToWidget += 1;
4359
+ if (!fromWidget && !toWidget)
4360
+ stateToState += 1;
4361
+ }
4362
+ return {
4363
+ total: connections.length,
4364
+ widgetToWidget,
4365
+ widgetToState,
4366
+ stateToWidget,
4367
+ stateToState,
4368
+ };
4369
+ }, ...(ngDevMode ? [{ debugName: "connectionSummary" }] : []));
4370
+ stateSummary = computed(() => {
4371
+ const page = this.pageState();
4372
+ const normalized = this.normalizeState(page?.state);
4373
+ const items = new Map();
4374
+ for (const [path, config] of Object.entries(normalized.schema || {})) {
4375
+ items.set(path, {
4376
+ path,
4377
+ kind: 'primary',
4378
+ label: config.description ? `${path} - ${config.description}` : path,
4379
+ });
4380
+ }
4381
+ for (const path of this.collectPaths(normalized.values || {})) {
4382
+ if (!items.has(path)) {
4383
+ items.set(path, { path, kind: 'primary', label: path });
4384
+ }
4385
+ }
4386
+ for (const [path, config] of Object.entries(normalized.derived || {})) {
4387
+ items.set(path, {
4388
+ path,
4389
+ kind: 'derived',
4390
+ label: config.description ? `${path} - ${config.description}` : `${path} [derived]`,
4391
+ });
4392
+ }
4393
+ return Array.from(items.values()).sort((left, right) => left.path.localeCompare(right.path));
4394
+ }, ...(ngDevMode ? [{ debugName: "stateSummary" }] : []));
4395
+ primaryStateCount = computed(() => this.stateSummary().filter((item) => item.kind === 'primary').length, ...(ngDevMode ? [{ debugName: "primaryStateCount" }] : []));
4396
+ derivedStateCount = computed(() => this.stateSummary().filter((item) => item.kind === 'derived').length, ...(ngDevMode ? [{ debugName: "derivedStateCount" }] : []));
4397
+ groupingSummary = computed(() => {
4398
+ const page = this.pageState();
4399
+ return (page?.grouping || []).map((group) => ({
4400
+ id: group.id,
4401
+ kind: group.kind,
4402
+ label: group.kind === 'tabs'
4403
+ ? `${group.id} - ${group.tabs.length} tabs`
4404
+ : `${group.id} - ${group.widgetKeys.length} widgets`,
4405
+ }));
4406
+ }, ...(ngDevMode ? [{ debugName: "groupingSummary" }] : []));
4407
+ deviceLayoutSummary = computed(() => {
4408
+ const page = this.pageState();
4409
+ const items = [];
4410
+ for (const device of ['desktop', 'tablet', 'mobile']) {
4411
+ const variant = page?.deviceLayouts?.[device];
4412
+ if (!variant)
4413
+ continue;
4414
+ const layoutBits = [];
4415
+ if (variant.layout?.orientation)
4416
+ layoutBits.push(variant.layout.orientation);
4417
+ if (variant.layout?.columns)
4418
+ layoutBits.push(`${variant.layout.columns} cols`);
4419
+ if (variant.groupingOverrides?.length)
4420
+ layoutBits.push(`${variant.groupingOverrides.length} grouping overrides`);
4421
+ if (variant.widgetOverrides && Object.keys(variant.widgetOverrides).length)
4422
+ layoutBits.push(`${Object.keys(variant.widgetOverrides).length} widget overrides`);
4423
+ items.push({
4424
+ device,
4425
+ label: layoutBits.join(' • ') || 'variant configured',
4426
+ });
4427
+ }
4428
+ return items;
4429
+ }, ...(ngDevMode ? [{ debugName: "deviceLayoutSummary" }] : []));
4087
4430
  lastSerialized = '';
4088
4431
  constructor() {
4089
4432
  effect(() => {
@@ -4095,7 +4438,7 @@ class ConnectionEditorComponent {
4095
4438
  this.jsonControl.setValue(serialized, { emitEvent: false });
4096
4439
  this.jsonError.set(null);
4097
4440
  }
4098
- }, { allowSignalWrites: true });
4441
+ });
4099
4442
  this.jsonControl.valueChanges
4100
4443
  .pipe(takeUntilDestroyed())
4101
4444
  .subscribe((next) => this.onJsonInput(next));
@@ -4143,8 +4486,40 @@ class ConnectionEditorComponent {
4143
4486
  }
4144
4487
  return input;
4145
4488
  }
4489
+ normalizeState(state) {
4490
+ if (!state)
4491
+ return { values: {} };
4492
+ const isStructured = typeof state === 'object'
4493
+ && !Array.isArray(state)
4494
+ && ('values' in state || 'schema' in state || 'derived' in state);
4495
+ if (isStructured) {
4496
+ const structured = state;
4497
+ return {
4498
+ values: this.clone(structured.values) || {},
4499
+ schema: this.clone(structured.schema),
4500
+ derived: this.clone(structured.derived),
4501
+ };
4502
+ }
4503
+ return { values: this.clone(state) || {} };
4504
+ }
4505
+ collectPaths(source, prefix = '') {
4506
+ const paths = [];
4507
+ for (const [key, value] of Object.entries(source || {})) {
4508
+ const path = prefix ? `${prefix}.${key}` : key;
4509
+ paths.push(path);
4510
+ if (value != null && typeof value === 'object' && !Array.isArray(value)) {
4511
+ paths.push(...this.collectPaths(value, path));
4512
+ }
4513
+ }
4514
+ return paths;
4515
+ }
4516
+ clone(value) {
4517
+ if (value == null || typeof value !== 'object')
4518
+ return value;
4519
+ return JSON.parse(JSON.stringify(value));
4520
+ }
4146
4521
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ConnectionEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
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 });
4522
+ 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 <div class=\"json-summary\">\n <div class=\"summary-card\">\n <div class=\"summary-title\">Conex\u00F5es</div>\n <div class=\"summary-metrics\">\n <div class=\"metric\">\n <span class=\"metric-value\">{{ connectionSummary().total }}</span>\n <span class=\"metric-label\">total</span>\n </div>\n <div class=\"metric\">\n <span class=\"metric-value\">{{ connectionSummary().widgetToWidget }}</span>\n <span class=\"metric-label\">widget \u2192 widget</span>\n </div>\n <div class=\"metric\">\n <span class=\"metric-value\">{{ connectionSummary().widgetToState }}</span>\n <span class=\"metric-label\">widget \u2192 state</span>\n </div>\n <div class=\"metric\">\n <span class=\"metric-value\">{{ connectionSummary().stateToWidget }}</span>\n <span class=\"metric-label\">state \u2192 widget</span>\n </div>\n <div class=\"metric\">\n <span class=\"metric-value\">{{ connectionSummary().stateToState }}</span>\n <span class=\"metric-label\">state \u2192 state</span>\n </div>\n </div>\n </div>\n\n <div class=\"summary-card\">\n <div class=\"summary-title\">Page State</div>\n <div class=\"summary-metrics compact\">\n <div class=\"metric\">\n <span class=\"metric-value\">{{ primaryStateCount() }}</span>\n <span class=\"metric-label\">paths prim\u00E1rios</span>\n </div>\n <div class=\"metric\">\n <span class=\"metric-value\">{{ derivedStateCount() }}</span>\n <span class=\"metric-label\">paths derivados</span>\n </div>\n </div>\n @if (stateSummary().length) {\n <div class=\"state-chip-list\">\n @for (item of stateSummary(); track item.path) {\n <div class=\"state-chip\" [class.derived]=\"item.kind === 'derived'\" [matTooltip]=\"item.label\">\n <mat-icon>{{ item.kind === 'derived' ? 'function' : 'account_tree' }}</mat-icon>\n <span>{{ item.path }}</span>\n </div>\n }\n </div>\n } @else {\n <div class=\"summary-empty\">Nenhum path de state detectado nesta p\u00E1gina.</div>\n }\n </div>\n <div class=\"summary-card\">\n <div class=\"summary-title\">Grouping</div>\n @if (groupingSummary().length) {\n <div class=\"state-chip-list\">\n @for (item of groupingSummary(); track item.id) {\n <div class=\"state-chip\" [matTooltip]=\"item.label\">\n <mat-icon>dashboard_customize</mat-icon>\n <span>{{ item.kind }}: {{ item.id }}</span>\n </div>\n }\n </div>\n } @else {\n <div class=\"summary-empty\">Nenhum grouping definido nesta p\u00C3\u00A1gina.</div>\n }\n </div>\n\n <div class=\"summary-card\">\n <div class=\"summary-title\">Device Layouts</div>\n @if (deviceLayoutSummary().length) {\n <div class=\"state-chip-list\">\n @for (item of deviceLayoutSummary(); track item.device) {\n <div class=\"state-chip\" [matTooltip]=\"item.label\">\n <mat-icon>devices</mat-icon>\n <span>{{ item.device }}</span>\n </div>\n }\n </div>\n } @else {\n <div class=\"summary-empty\">Nenhum override por device configurado.</div>\n }\n </div>\n </div>\n\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-summary{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(260px,1fr))}.summary-card{display:grid;gap:12px;padding:14px;border-radius:14px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 85%,transparent);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))}.summary-title{font-weight:600}.summary-metrics{display:grid;gap:10px;grid-template-columns:repeat(auto-fit,minmax(92px,1fr))}.summary-metrics.compact{grid-template-columns:repeat(2,minmax(100px,1fr))}.metric{display:grid;gap:4px;padding:10px 12px;border-radius:12px;background:color-mix(in srgb,var(--md-sys-color-surface-container) 88%,transparent)}.metric-value{font-size:20px;font-weight:700;line-height:1}.metric-label{color:var(--md-sys-color-on-surface-variant);font-size:12px}.state-chip-list{display:flex;flex-wrap:wrap;gap:8px}.state-chip{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:999px;background:color-mix(in srgb,var(--md-sys-color-primary-container) 70%,transparent);color:var(--md-sys-color-on-primary-container);font-size:12px}.state-chip.derived{background:color-mix(in srgb,var(--md-sys-color-tertiary-container) 80%,transparent);color:var(--md-sys-color-on-tertiary-container)}.state-chip mat-icon{font-size:16px;width:16px;height:16px}.summary-empty{color:var(--md-sys-color-on-surface-variant);font-size:13px}.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: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { 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 });
4148
4523
  }
4149
4524
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ConnectionEditorComponent, decorators: [{
4150
4525
  type: Component,
@@ -4156,9 +4531,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
4156
4531
  MatInputModule,
4157
4532
  MatButtonModule,
4158
4533
  MatIconModule,
4534
+ MatTooltipModule,
4159
4535
  ConnectionBuilderComponent,
4160
4536
  ConnectionGraphComponent,
4161
- ], changeDetection: ChangeDetectionStrategy.OnPush, 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"] }]
4537
+ ], changeDetection: ChangeDetectionStrategy.OnPush, 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 <div class=\"json-summary\">\n <div class=\"summary-card\">\n <div class=\"summary-title\">Conex\u00F5es</div>\n <div class=\"summary-metrics\">\n <div class=\"metric\">\n <span class=\"metric-value\">{{ connectionSummary().total }}</span>\n <span class=\"metric-label\">total</span>\n </div>\n <div class=\"metric\">\n <span class=\"metric-value\">{{ connectionSummary().widgetToWidget }}</span>\n <span class=\"metric-label\">widget \u2192 widget</span>\n </div>\n <div class=\"metric\">\n <span class=\"metric-value\">{{ connectionSummary().widgetToState }}</span>\n <span class=\"metric-label\">widget \u2192 state</span>\n </div>\n <div class=\"metric\">\n <span class=\"metric-value\">{{ connectionSummary().stateToWidget }}</span>\n <span class=\"metric-label\">state \u2192 widget</span>\n </div>\n <div class=\"metric\">\n <span class=\"metric-value\">{{ connectionSummary().stateToState }}</span>\n <span class=\"metric-label\">state \u2192 state</span>\n </div>\n </div>\n </div>\n\n <div class=\"summary-card\">\n <div class=\"summary-title\">Page State</div>\n <div class=\"summary-metrics compact\">\n <div class=\"metric\">\n <span class=\"metric-value\">{{ primaryStateCount() }}</span>\n <span class=\"metric-label\">paths prim\u00E1rios</span>\n </div>\n <div class=\"metric\">\n <span class=\"metric-value\">{{ derivedStateCount() }}</span>\n <span class=\"metric-label\">paths derivados</span>\n </div>\n </div>\n @if (stateSummary().length) {\n <div class=\"state-chip-list\">\n @for (item of stateSummary(); track item.path) {\n <div class=\"state-chip\" [class.derived]=\"item.kind === 'derived'\" [matTooltip]=\"item.label\">\n <mat-icon>{{ item.kind === 'derived' ? 'function' : 'account_tree' }}</mat-icon>\n <span>{{ item.path }}</span>\n </div>\n }\n </div>\n } @else {\n <div class=\"summary-empty\">Nenhum path de state detectado nesta p\u00E1gina.</div>\n }\n </div>\n <div class=\"summary-card\">\n <div class=\"summary-title\">Grouping</div>\n @if (groupingSummary().length) {\n <div class=\"state-chip-list\">\n @for (item of groupingSummary(); track item.id) {\n <div class=\"state-chip\" [matTooltip]=\"item.label\">\n <mat-icon>dashboard_customize</mat-icon>\n <span>{{ item.kind }}: {{ item.id }}</span>\n </div>\n }\n </div>\n } @else {\n <div class=\"summary-empty\">Nenhum grouping definido nesta p\u00C3\u00A1gina.</div>\n }\n </div>\n\n <div class=\"summary-card\">\n <div class=\"summary-title\">Device Layouts</div>\n @if (deviceLayoutSummary().length) {\n <div class=\"state-chip-list\">\n @for (item of deviceLayoutSummary(); track item.device) {\n <div class=\"state-chip\" [matTooltip]=\"item.label\">\n <mat-icon>devices</mat-icon>\n <span>{{ item.device }}</span>\n </div>\n }\n </div>\n } @else {\n <div class=\"summary-empty\">Nenhum override por device configurado.</div>\n }\n </div>\n </div>\n\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-summary{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(260px,1fr))}.summary-card{display:grid;gap:12px;padding:14px;border-radius:14px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 85%,transparent);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))}.summary-title{font-weight:600}.summary-metrics{display:grid;gap:10px;grid-template-columns:repeat(auto-fit,minmax(92px,1fr))}.summary-metrics.compact{grid-template-columns:repeat(2,minmax(100px,1fr))}.metric{display:grid;gap:4px;padding:10px 12px;border-radius:12px;background:color-mix(in srgb,var(--md-sys-color-surface-container) 88%,transparent)}.metric-value{font-size:20px;font-weight:700;line-height:1}.metric-label{color:var(--md-sys-color-on-surface-variant);font-size:12px}.state-chip-list{display:flex;flex-wrap:wrap;gap:8px}.state-chip{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:999px;background:color-mix(in srgb,var(--md-sys-color-primary-container) 70%,transparent);color:var(--md-sys-color-on-primary-container);font-size:12px}.state-chip.derived{background:color-mix(in srgb,var(--md-sys-color-tertiary-container) 80%,transparent);color:var(--md-sys-color-on-tertiary-container)}.state-chip mat-icon{font-size:16px;width:16px;height:16px}.summary-empty{color:var(--md-sys-color-on-surface-variant);font-size:13px}.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"] }]
4162
4538
  }], ctorParameters: () => [], propDecorators: { page: [{ type: i0.Input, args: [{ isSignal: true, alias: "page", required: false }] }], widgets: [{ type: i0.Input, args: [{ isSignal: true, alias: "widgets", required: false }] }], pageChange: [{ type: i0.Output, args: ["pageChange"] }] } });
4163
4539
 
4164
4540
  class ConnectionEditorDialogComponent {
@@ -4253,11 +4629,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
4253
4629
 
4254
4630
  const DERIVED_OPERATORS = ['merge-objects', 'pick', 'omit'];
4255
4631
  const MERGE_STRATEGIES = ['replace', 'merge', 'append', 'remove-keys'];
4632
+ const VALUE_MODES = ['json', 'string', 'number', 'boolean', 'null'];
4633
+ const GROUPING_KINDS = ['section', 'tabs', 'hero', 'rail'];
4256
4634
  class PageConfigEditorComponent {
4257
4635
  contextExample = 'Ex.: {"tenantId":"acme","locale":"pt-BR"}';
4258
4636
  stateValuesExample = 'Ex.: {"query":{"filter":{"year":2026}},"selection":{"department":"Finance"}}';
4259
4637
  derivedOperators = DERIVED_OPERATORS;
4260
4638
  mergeStrategies = MERGE_STRATEGIES;
4639
+ valueModes = VALUE_MODES;
4640
+ groupingKinds = GROUPING_KINDS;
4641
+ layoutPresetEntries = Object.values(BUILTIN_PAGE_LAYOUT_PRESETS);
4642
+ themePresetEntries = Object.values(BUILTIN_PAGE_THEME_PRESETS);
4261
4643
  page = input(null, ...(ngDevMode ? [{ debugName: "page" }] : []));
4262
4644
  identity = input(null, ...(ngDevMode ? [{ debugName: "identity" }] : []));
4263
4645
  gridsterOptions = input(null, ...(ngDevMode ? [{ debugName: "gridsterOptions" }] : []));
@@ -4268,12 +4650,23 @@ class PageConfigEditorComponent {
4268
4650
  gap: new FormControl(null),
4269
4651
  });
4270
4652
  contextControl = new FormControl('', { nonNullable: true });
4653
+ layoutPresetControl = new FormControl('', { nonNullable: true });
4654
+ themePresetControl = new FormControl('', { nonNullable: true });
4655
+ groupingControl = new FormControl('', { nonNullable: true });
4656
+ deviceLayoutsControl = new FormControl('', { nonNullable: true });
4271
4657
  stateValuesControl = new FormControl('', { nonNullable: true });
4658
+ groupingNodes = new FormArray([]);
4272
4659
  schemaNodes = new FormArray([]);
4273
4660
  derivedNodes = new FormArray([]);
4661
+ deviceDesktopForm = this.createDeviceLayoutForm();
4662
+ deviceTabletForm = this.createDeviceLayoutForm();
4663
+ deviceMobileForm = this.createDeviceLayoutForm();
4274
4664
  contextError = signal(null, ...(ngDevMode ? [{ debugName: "contextError" }] : []));
4665
+ layoutError = signal(null, ...(ngDevMode ? [{ debugName: "layoutError" }] : []));
4275
4666
  stateError = signal(null, ...(ngDevMode ? [{ debugName: "stateError" }] : []));
4276
4667
  lastContext = '';
4668
+ lastGrouping = '';
4669
+ lastDeviceLayouts = '';
4277
4670
  lastStateValues = '';
4278
4671
  constructor() {
4279
4672
  effect(() => {
@@ -4290,11 +4683,45 @@ class PageConfigEditorComponent {
4290
4683
  this.contextControl.setValue(context, { emitEvent: false });
4291
4684
  this.contextError.set(null);
4292
4685
  }
4686
+ this.layoutPresetControl.setValue(next?.layoutPreset || '', { emitEvent: false });
4687
+ this.themePresetControl.setValue(next?.themePreset || '', { emitEvent: false });
4688
+ const grouping = next?.grouping ? JSON.stringify(next.grouping, null, 2) : '';
4689
+ if (grouping !== this.lastGrouping) {
4690
+ this.lastGrouping = grouping;
4691
+ this.groupingControl.setValue(grouping, { emitEvent: false });
4692
+ this.replaceGroupingNodes(next?.grouping);
4693
+ this.layoutError.set(null);
4694
+ }
4695
+ const deviceLayouts = next?.deviceLayouts ? JSON.stringify(next.deviceLayouts, null, 2) : '';
4696
+ if (deviceLayouts !== this.lastDeviceLayouts) {
4697
+ this.lastDeviceLayouts = deviceLayouts;
4698
+ this.deviceLayoutsControl.setValue(deviceLayouts, { emitEvent: false });
4699
+ this.hydrateDeviceLayoutControls(next?.deviceLayouts);
4700
+ this.layoutError.set(null);
4701
+ }
4293
4702
  this.hydrateStateEditor(next?.state);
4294
4703
  });
4295
4704
  this.contextControl.valueChanges
4296
4705
  .pipe(takeUntilDestroyed())
4297
4706
  .subscribe(() => this.contextError.set(null));
4707
+ this.groupingControl.valueChanges
4708
+ .pipe(takeUntilDestroyed())
4709
+ .subscribe(() => this.layoutError.set(null));
4710
+ this.deviceLayoutsControl.valueChanges
4711
+ .pipe(takeUntilDestroyed())
4712
+ .subscribe(() => this.layoutError.set(null));
4713
+ this.groupingNodes.valueChanges
4714
+ .pipe(takeUntilDestroyed())
4715
+ .subscribe(() => this.layoutError.set(null));
4716
+ this.deviceDesktopForm.valueChanges
4717
+ .pipe(takeUntilDestroyed())
4718
+ .subscribe(() => this.layoutError.set(null));
4719
+ this.deviceTabletForm.valueChanges
4720
+ .pipe(takeUntilDestroyed())
4721
+ .subscribe(() => this.layoutError.set(null));
4722
+ this.deviceMobileForm.valueChanges
4723
+ .pipe(takeUntilDestroyed())
4724
+ .subscribe(() => this.layoutError.set(null));
4298
4725
  this.stateValuesControl.valueChanges
4299
4726
  .pipe(takeUntilDestroyed())
4300
4727
  .subscribe(() => this.stateError.set(null));
@@ -4317,15 +4744,27 @@ class PageConfigEditorComponent {
4317
4744
  removeDerivedNode(index) {
4318
4745
  this.derivedNodes.removeAt(index);
4319
4746
  }
4747
+ addGroupingNode(seed) {
4748
+ this.groupingNodes.push(this.createGroupingNodeGroup(seed));
4749
+ }
4750
+ removeGroupingNode(index) {
4751
+ this.groupingNodes.removeAt(index);
4752
+ }
4320
4753
  apply() {
4321
4754
  const next = this.parsePage(this.page()) || { widgets: [] };
4322
4755
  const options = this.buildOptions(this.form.value);
4323
4756
  const context = this.parseContext(this.contextControl.value);
4757
+ const grouping = this.buildGrouping();
4758
+ const deviceLayouts = this.buildDeviceLayouts();
4324
4759
  const state = this.buildState();
4325
4760
  if (context === null) {
4326
4761
  this.contextError.set('JSON invalido');
4327
4762
  return;
4328
4763
  }
4764
+ if (grouping === null || deviceLayouts === null) {
4765
+ this.layoutError.set('JSON invalido');
4766
+ return;
4767
+ }
4329
4768
  if (state === null) {
4330
4769
  this.stateError.set('JSON invalido');
4331
4770
  return;
@@ -4333,6 +4772,10 @@ class PageConfigEditorComponent {
4333
4772
  const updated = {
4334
4773
  ...next,
4335
4774
  options,
4775
+ layoutPreset: this.layoutPresetControl.value.trim() || undefined,
4776
+ themePreset: this.themePresetControl.value.trim() || undefined,
4777
+ grouping: grouping || undefined,
4778
+ deviceLayouts: deviceLayouts || undefined,
4336
4779
  context: context || undefined,
4337
4780
  state: state || undefined,
4338
4781
  };
@@ -4350,8 +4793,167 @@ class PageConfigEditorComponent {
4350
4793
  this.lastContext = context;
4351
4794
  this.contextControl.setValue(context, { emitEvent: false });
4352
4795
  this.contextError.set(null);
4796
+ this.layoutPresetControl.setValue(next?.layoutPreset || '', { emitEvent: false });
4797
+ this.themePresetControl.setValue(next?.themePreset || '', { emitEvent: false });
4798
+ const grouping = next?.grouping ? JSON.stringify(next.grouping, null, 2) : '';
4799
+ this.lastGrouping = grouping;
4800
+ this.groupingControl.setValue(grouping, { emitEvent: false });
4801
+ this.replaceGroupingNodes(next?.grouping);
4802
+ const deviceLayouts = next?.deviceLayouts ? JSON.stringify(next.deviceLayouts, null, 2) : '';
4803
+ this.lastDeviceLayouts = deviceLayouts;
4804
+ this.deviceLayoutsControl.setValue(deviceLayouts, { emitEvent: false });
4805
+ this.hydrateDeviceLayoutControls(next?.deviceLayouts);
4806
+ this.layoutError.set(null);
4353
4807
  this.hydrateStateEditor(next?.state);
4354
4808
  }
4809
+ replaceGroupingNodes(grouping) {
4810
+ this.groupingNodes.clear({ emitEvent: false });
4811
+ for (const group of grouping || []) {
4812
+ this.groupingNodes.push(this.createGroupingNodeGroup(group), { emitEvent: false });
4813
+ }
4814
+ }
4815
+ createGroupingNodeGroup(seed) {
4816
+ const label = seed && 'label' in seed && typeof seed.label === 'string'
4817
+ ? seed.label
4818
+ : '';
4819
+ return new FormGroup({
4820
+ kind: new FormControl(seed?.kind || 'section', { nonNullable: true }),
4821
+ id: new FormControl(seed?.id || '', { nonNullable: true, validators: [Validators.required] }),
4822
+ label: new FormControl(label, { nonNullable: true }),
4823
+ widgetKeysCsv: new FormControl(this.stringifyGroupingWidgetKeys(seed), { nonNullable: true }),
4824
+ layout: new FormControl(seed?.kind === 'section' ? seed.layout || '' : '', { nonNullable: true }),
4825
+ emphasis: new FormControl(seed?.kind === 'hero' ? seed.emphasis || '' : '', { nonNullable: true }),
4826
+ side: new FormControl(seed?.kind === 'rail' ? seed.side || 'right' : 'right', { nonNullable: true }),
4827
+ tabsJson: new FormControl(seed?.kind === 'tabs' ? JSON.stringify(seed.tabs || [], null, 2) : '', { nonNullable: true }),
4828
+ });
4829
+ }
4830
+ stringifyGroupingWidgetKeys(seed) {
4831
+ if (!seed || seed.kind === 'tabs')
4832
+ return '';
4833
+ return seed.widgetKeys.join(', ');
4834
+ }
4835
+ buildGrouping() {
4836
+ const grouping = [];
4837
+ for (const group of this.groupingNodes.controls) {
4838
+ const kind = group.controls.kind.value;
4839
+ const id = group.controls.id.value.trim();
4840
+ if (!id)
4841
+ continue;
4842
+ const label = group.controls.label.value.trim();
4843
+ if (kind === 'tabs') {
4844
+ const tabs = this.parseJson(group.controls.tabsJson.value, []);
4845
+ if (tabs === null)
4846
+ return null;
4847
+ grouping.push({
4848
+ kind: 'tabs',
4849
+ id,
4850
+ ...(label ? { label } : {}),
4851
+ tabs,
4852
+ });
4853
+ continue;
4854
+ }
4855
+ const widgetKeys = group.controls.widgetKeysCsv.value
4856
+ .split(',')
4857
+ .map((item) => item.trim())
4858
+ .filter(Boolean);
4859
+ if (kind === 'section') {
4860
+ grouping.push({
4861
+ kind: 'section',
4862
+ id,
4863
+ ...(label ? { label } : {}),
4864
+ widgetKeys,
4865
+ ...(group.controls.layout.value ? { layout: group.controls.layout.value } : {}),
4866
+ });
4867
+ continue;
4868
+ }
4869
+ if (kind === 'hero') {
4870
+ grouping.push({
4871
+ kind: 'hero',
4872
+ id,
4873
+ widgetKeys,
4874
+ ...(group.controls.emphasis.value ? { emphasis: group.controls.emphasis.value } : {}),
4875
+ });
4876
+ continue;
4877
+ }
4878
+ grouping.push({
4879
+ kind: 'rail',
4880
+ id,
4881
+ side: (group.controls.side.value || 'right'),
4882
+ widgetKeys,
4883
+ });
4884
+ }
4885
+ const json = grouping.length ? JSON.stringify(grouping, null, 2) : '';
4886
+ this.lastGrouping = json;
4887
+ this.groupingControl.setValue(json, { emitEvent: false });
4888
+ return grouping;
4889
+ }
4890
+ hydrateDeviceLayoutControls(deviceLayouts) {
4891
+ this.patchDeviceLayoutForm(this.deviceDesktopForm, deviceLayouts?.desktop);
4892
+ this.patchDeviceLayoutForm(this.deviceTabletForm, deviceLayouts?.tablet);
4893
+ this.patchDeviceLayoutForm(this.deviceMobileForm, deviceLayouts?.mobile);
4894
+ }
4895
+ buildDeviceLayouts() {
4896
+ const desktop = this.buildDeviceLayoutVariant(this.deviceDesktopForm);
4897
+ const tablet = this.buildDeviceLayoutVariant(this.deviceTabletForm);
4898
+ const mobile = this.buildDeviceLayoutVariant(this.deviceMobileForm);
4899
+ if (desktop === null || tablet === null || mobile === null)
4900
+ return null;
4901
+ const deviceLayouts = {};
4902
+ if (Object.keys(desktop).length)
4903
+ deviceLayouts.desktop = desktop;
4904
+ if (Object.keys(tablet).length)
4905
+ deviceLayouts.tablet = tablet;
4906
+ if (Object.keys(mobile).length)
4907
+ deviceLayouts.mobile = mobile;
4908
+ const json = Object.keys(deviceLayouts).length ? JSON.stringify(deviceLayouts, null, 2) : '';
4909
+ this.lastDeviceLayouts = json;
4910
+ this.deviceLayoutsControl.setValue(json, { emitEvent: false });
4911
+ return deviceLayouts;
4912
+ }
4913
+ createDeviceLayoutForm() {
4914
+ return new FormGroup({
4915
+ orientation: new FormControl('', { nonNullable: true }),
4916
+ columns: new FormControl(null),
4917
+ gap: new FormControl('', { nonNullable: true }),
4918
+ breakpointsJson: new FormControl('', { nonNullable: true }),
4919
+ groupingOverridesJson: new FormControl('', { nonNullable: true }),
4920
+ widgetOverridesJson: new FormControl('', { nonNullable: true }),
4921
+ });
4922
+ }
4923
+ patchDeviceLayoutForm(form, variant) {
4924
+ form.setValue({
4925
+ orientation: variant?.layout?.orientation || '',
4926
+ columns: variant?.layout?.columns ?? null,
4927
+ gap: variant?.layout?.gap || '',
4928
+ breakpointsJson: variant?.layout?.breakpoints ? JSON.stringify(variant.layout.breakpoints, null, 2) : '',
4929
+ groupingOverridesJson: variant?.groupingOverrides ? JSON.stringify(variant.groupingOverrides, null, 2) : '',
4930
+ widgetOverridesJson: variant?.widgetOverrides ? JSON.stringify(variant.widgetOverrides, null, 2) : '',
4931
+ }, { emitEvent: false });
4932
+ }
4933
+ buildDeviceLayoutVariant(form) {
4934
+ const breakpoints = this.parseJson(form.controls.breakpointsJson.value, {});
4935
+ const groupingOverrides = this.parseJson(form.controls.groupingOverridesJson.value, []);
4936
+ const widgetOverrides = this.parseJson(form.controls.widgetOverridesJson.value, {});
4937
+ if (breakpoints === null || groupingOverrides === null || widgetOverrides === null)
4938
+ return null;
4939
+ const layout = {};
4940
+ if (form.controls.orientation.value)
4941
+ layout.orientation = form.controls.orientation.value;
4942
+ if (form.controls.columns.value != null)
4943
+ layout.columns = form.controls.columns.value;
4944
+ if (form.controls.gap.value.trim())
4945
+ layout.gap = form.controls.gap.value.trim();
4946
+ if (Object.keys(breakpoints).length)
4947
+ layout.breakpoints = breakpoints;
4948
+ const variant = {};
4949
+ if (Object.keys(layout).length)
4950
+ variant.layout = layout;
4951
+ if (groupingOverrides.length)
4952
+ variant.groupingOverrides = groupingOverrides;
4953
+ if (Object.keys(widgetOverrides).length)
4954
+ variant.widgetOverrides = widgetOverrides;
4955
+ return variant;
4956
+ }
4355
4957
  hydrateStateEditor(state) {
4356
4958
  const normalized = this.normalizeState(state);
4357
4959
  const nextValues = normalized.values ? JSON.stringify(normalized.values, null, 2) : '';
@@ -4377,10 +4979,13 @@ class PageConfigEditorComponent {
4377
4979
  }
4378
4980
  createSchemaNodeGroup(seed) {
4379
4981
  const config = seed?.config || {};
4982
+ const initialValueConfig = this.describeValue(config.initial);
4380
4983
  return new FormGroup({
4381
4984
  path: new FormControl(seed?.path || '', { nonNullable: true, validators: [Validators.required] }),
4382
4985
  type: new FormControl(config.type || '', { nonNullable: true }),
4383
- initial: new FormControl(config.initial === undefined ? '' : JSON.stringify(config.initial, null, 2), { nonNullable: true }),
4986
+ initialMode: new FormControl(initialValueConfig.mode, { nonNullable: true }),
4987
+ initialJson: new FormControl(initialValueConfig.json, { nonNullable: true }),
4988
+ initialScalar: new FormControl(initialValueConfig.scalar, { nonNullable: true }),
4384
4989
  persist: new FormControl(typeof config.persist === 'boolean' ? String(config.persist) : '', { nonNullable: true }),
4385
4990
  mergeStrategy: new FormControl(config.mergeStrategy || '', { nonNullable: true }),
4386
4991
  description: new FormControl(config.description || '', { nonNullable: true }),
@@ -4401,6 +5006,12 @@ class PageConfigEditorComponent {
4401
5006
  const keysCsv = Array.isArray(operatorOptions['keys'])
4402
5007
  ? operatorOptions['keys'].filter((item) => typeof item === 'string').join(', ')
4403
5008
  : '';
5009
+ const templateValue = compute?.kind === 'template'
5010
+ ? compute.value
5011
+ : compute?.kind === 'operator' && compute.operator === 'template'
5012
+ ? compute.options?.['value']
5013
+ : undefined;
5014
+ const templateValueConfig = this.describeValue(templateValue);
4404
5015
  return new FormGroup({
4405
5016
  path: new FormControl(seed?.path || '', { nonNullable: true, validators: [Validators.required] }),
4406
5017
  dependsOn: new FormControl((config?.dependsOn || []).join(', '), { nonNullable: true }),
@@ -4415,11 +5026,9 @@ class PageConfigEditorComponent {
4415
5026
  options: new FormControl(compute?.kind === 'transformer'
4416
5027
  ? JSON.stringify(compute.options || {}, null, 2)
4417
5028
  : '', { 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 }),
5029
+ templateMode: new FormControl(templateValueConfig.mode, { nonNullable: true }),
5030
+ templateJson: new FormControl(templateValueConfig.json, { nonNullable: true }),
5031
+ templateScalar: new FormControl(templateValueConfig.scalar, { nonNullable: true }),
4423
5032
  description: new FormControl(config?.description || '', { nonNullable: true }),
4424
5033
  });
4425
5034
  }
@@ -4451,8 +5060,8 @@ class PageConfigEditorComponent {
4451
5060
  const path = group.controls.path.value.trim();
4452
5061
  if (!path)
4453
5062
  continue;
4454
- const initial = this.parseJson(group.controls.initial.value, undefined);
4455
- if (initial === null)
5063
+ const initial = this.parseValueInput(group.controls.initialMode.value, group.controls.initialScalar.value, group.controls.initialJson.value, undefined);
5064
+ if (initial.invalid)
4456
5065
  return null;
4457
5066
  const node = {};
4458
5067
  const type = group.controls.type.value.trim();
@@ -4461,8 +5070,8 @@ class PageConfigEditorComponent {
4461
5070
  const description = group.controls.description.value.trim();
4462
5071
  if (type)
4463
5072
  node.type = type;
4464
- if (initial !== undefined)
4465
- node.initial = initial;
5073
+ if (initial.value !== undefined)
5074
+ node.initial = initial.value;
4466
5075
  if (persist === 'true')
4467
5076
  node.persist = true;
4468
5077
  if (persist === 'false')
@@ -4489,10 +5098,10 @@ class PageConfigEditorComponent {
4489
5098
  const description = group.controls.description.value.trim();
4490
5099
  let compute;
4491
5100
  if (computeKind === 'template') {
4492
- const value = this.parseJson(group.controls.template.value, undefined);
4493
- if (value === null)
5101
+ const templateValue = this.parseValueInput(group.controls.templateMode.value, group.controls.templateScalar.value, group.controls.templateJson.value, undefined);
5102
+ if (templateValue.invalid)
4494
5103
  return null;
4495
- compute = { kind: 'template', value };
5104
+ compute = { kind: 'template', value: templateValue.value };
4496
5105
  }
4497
5106
  else if (computeKind === 'expr') {
4498
5107
  compute = { kind: 'expr', expression: group.controls.expression.value.trim() };
@@ -4565,6 +5174,26 @@ class PageConfigEditorComponent {
4565
5174
  return null;
4566
5175
  }
4567
5176
  }
5177
+ parseGrouping(raw) {
5178
+ if (!raw.trim())
5179
+ return [];
5180
+ try {
5181
+ return JSON.parse(raw);
5182
+ }
5183
+ catch {
5184
+ return null;
5185
+ }
5186
+ }
5187
+ parseDeviceLayouts(raw) {
5188
+ if (!raw.trim())
5189
+ return {};
5190
+ try {
5191
+ return JSON.parse(raw);
5192
+ }
5193
+ catch {
5194
+ return null;
5195
+ }
5196
+ }
4568
5197
  parseJson(raw, emptyValue) {
4569
5198
  if (!raw.trim())
4570
5199
  return emptyValue;
@@ -4575,6 +5204,43 @@ class PageConfigEditorComponent {
4575
5204
  return null;
4576
5205
  }
4577
5206
  }
5207
+ parseValueInput(mode, scalarRaw, jsonRaw, emptyValue) {
5208
+ if (mode === 'json') {
5209
+ const parsed = this.parseJson(jsonRaw, emptyValue);
5210
+ return { value: parsed, invalid: parsed === null };
5211
+ }
5212
+ if (mode === 'string')
5213
+ return { value: scalarRaw, invalid: false };
5214
+ if (mode === 'number') {
5215
+ const trimmed = scalarRaw.trim();
5216
+ if (!trimmed)
5217
+ return { value: emptyValue, invalid: false };
5218
+ const parsed = Number(trimmed);
5219
+ return { value: parsed, invalid: Number.isNaN(parsed) };
5220
+ }
5221
+ if (mode === 'boolean')
5222
+ return { value: scalarRaw === 'true', invalid: false };
5223
+ if (mode === 'null')
5224
+ return { value: null, invalid: false };
5225
+ return { value: emptyValue, invalid: false };
5226
+ }
5227
+ describeValue(value) {
5228
+ if (value === undefined)
5229
+ return { mode: 'json', scalar: '', json: '' };
5230
+ if (value === null)
5231
+ return { mode: 'null', scalar: '', json: '' };
5232
+ if (typeof value === 'string')
5233
+ return { mode: 'string', scalar: value, json: '' };
5234
+ if (typeof value === 'number')
5235
+ return { mode: 'number', scalar: String(value), json: '' };
5236
+ if (typeof value === 'boolean')
5237
+ return { mode: 'boolean', scalar: String(value), json: '' };
5238
+ return {
5239
+ mode: 'json',
5240
+ scalar: '',
5241
+ json: JSON.stringify(value, null, 2),
5242
+ };
5243
+ }
4578
5244
  buildOperatorOptions(group) {
4579
5245
  const operator = group.controls.operator.value.trim();
4580
5246
  if (operator === 'pick' || operator === 'omit') {
@@ -4645,6 +5311,226 @@ class PageConfigEditorComponent {
4645
5311
  </mat-form-field>
4646
5312
  </form>
4647
5313
 
5314
+ <div class="page-section">Layout Preset</div>
5315
+ <div class="grid-form">
5316
+ <mat-form-field appearance="outline">
5317
+ <mat-label>Layout preset</mat-label>
5318
+ <mat-select [formControl]="layoutPresetControl">
5319
+ <mat-option value="">Nenhum</mat-option>
5320
+ @for (preset of layoutPresetEntries; track preset.id) {
5321
+ <mat-option [value]="preset.id">{{ preset.label }}</mat-option>
5322
+ }
5323
+ </mat-select>
5324
+ </mat-form-field>
5325
+ <mat-form-field appearance="outline">
5326
+ <mat-label>Theme preset</mat-label>
5327
+ <mat-select [formControl]="themePresetControl">
5328
+ <mat-option value="">Padrão do layout</mat-option>
5329
+ @for (preset of themePresetEntries; track preset.id) {
5330
+ <mat-option [value]="preset.id">{{ preset.label }}</mat-option>
5331
+ }
5332
+ </mat-select>
5333
+ </mat-form-field>
5334
+ </div>
5335
+
5336
+ <div class="page-section">Grouping</div>
5337
+ <div class="state-group">
5338
+ <div class="state-group-head">
5339
+ <div class="state-group-title">Grouping</div>
5340
+ <button mat-stroked-button type="button" (click)="addGroupingNode()">
5341
+ <mat-icon>add</mat-icon>
5342
+ Adicionar grupo
5343
+ </button>
5344
+ </div>
5345
+ <div class="state-group-help">Organiza a página em section, tabs, hero e rail com intenção estrutural.</div>
5346
+ @if (!groupingNodes.length) {
5347
+ <div class="state-empty">Nenhum grupo configurado.</div>
5348
+ }
5349
+ <div class="state-list">
5350
+ @for (group of groupingNodes.controls; track $index) {
5351
+ <div class="state-card" [formGroup]="group">
5352
+ <div class="state-card-head">
5353
+ <div class="state-card-title">Grupo {{ $index + 1 }}</div>
5354
+ <button mat-icon-button type="button" (click)="removeGroupingNode($index)" aria-label="Remover grupo">
5355
+ <mat-icon>delete</mat-icon>
5356
+ </button>
5357
+ </div>
5358
+ <div class="state-card-grid">
5359
+ <mat-form-field appearance="outline">
5360
+ <mat-label>Kind</mat-label>
5361
+ <mat-select formControlName="kind">
5362
+ @for (kind of groupingKinds; track kind) {
5363
+ <mat-option [value]="kind">{{ kind }}</mat-option>
5364
+ }
5365
+ </mat-select>
5366
+ </mat-form-field>
5367
+ <mat-form-field appearance="outline">
5368
+ <mat-label>Id</mat-label>
5369
+ <input matInput formControlName="id" />
5370
+ </mat-form-field>
5371
+ <mat-form-field appearance="outline">
5372
+ <mat-label>Label</mat-label>
5373
+ <input matInput formControlName="label" />
5374
+ </mat-form-field>
5375
+ @if (group.controls.kind.value === 'section') {
5376
+ <mat-form-field appearance="outline">
5377
+ <mat-label>Layout</mat-label>
5378
+ <mat-select formControlName="layout">
5379
+ <mat-option value="">Padrão</mat-option>
5380
+ <mat-option value="stack">stack</mat-option>
5381
+ <mat-option value="grid">grid</mat-option>
5382
+ <mat-option value="row">row</mat-option>
5383
+ </mat-select>
5384
+ </mat-form-field>
5385
+ }
5386
+ @if (group.controls.kind.value === 'hero') {
5387
+ <mat-form-field appearance="outline">
5388
+ <mat-label>Emphasis</mat-label>
5389
+ <mat-select formControlName="emphasis">
5390
+ <mat-option value="">Padrão</mat-option>
5391
+ <mat-option value="high">high</mat-option>
5392
+ <mat-option value="medium">medium</mat-option>
5393
+ </mat-select>
5394
+ </mat-form-field>
5395
+ }
5396
+ @if (group.controls.kind.value === 'rail') {
5397
+ <mat-form-field appearance="outline">
5398
+ <mat-label>Side</mat-label>
5399
+ <mat-select formControlName="side">
5400
+ <mat-option value="left">left</mat-option>
5401
+ <mat-option value="right">right</mat-option>
5402
+ </mat-select>
5403
+ </mat-form-field>
5404
+ }
5405
+ @if (group.controls.kind.value === 'tabs') {
5406
+ <mat-form-field appearance="outline" class="span-3">
5407
+ <mat-label>Tabs (JSON)</mat-label>
5408
+ <textarea matInput rows="5" formControlName="tabsJson"></textarea>
5409
+ </mat-form-field>
5410
+ } @else {
5411
+ <mat-form-field appearance="outline" class="span-3">
5412
+ <mat-label>Widget keys (vírgula)</mat-label>
5413
+ <input matInput formControlName="widgetKeysCsv" />
5414
+ </mat-form-field>
5415
+ }
5416
+ </div>
5417
+ </div>
5418
+ }
5419
+ </div>
5420
+ </div>
5421
+
5422
+ <div class="page-section">Device Layouts</div>
5423
+ <div class="state-group">
5424
+ <div class="state-group-head">
5425
+ <div class="state-group-title">Device Layouts</div>
5426
+ </div>
5427
+ <div class="state-group-help">Overrides por device para layout, grouping e widgets com campos separados por intenção.</div>
5428
+ <div class="state-list">
5429
+ <div class="state-card" [formGroup]="deviceDesktopForm">
5430
+ <div class="state-card-head"><div class="state-card-title">Desktop</div></div>
5431
+ <div class="state-card-grid">
5432
+ <mat-form-field appearance="outline">
5433
+ <mat-label>Orientation</mat-label>
5434
+ <mat-select formControlName="orientation">
5435
+ <mat-option value="">Padrão</mat-option>
5436
+ <mat-option value="vertical">vertical</mat-option>
5437
+ <mat-option value="columns">columns</mat-option>
5438
+ </mat-select>
5439
+ </mat-form-field>
5440
+ <mat-form-field appearance="outline">
5441
+ <mat-label>Columns</mat-label>
5442
+ <input matInput type="number" min="1" formControlName="columns" />
5443
+ </mat-form-field>
5444
+ <mat-form-field appearance="outline">
5445
+ <mat-label>Gap</mat-label>
5446
+ <input matInput formControlName="gap" />
5447
+ </mat-form-field>
5448
+ <mat-form-field appearance="outline" class="span-3">
5449
+ <mat-label>Breakpoints (JSON)</mat-label>
5450
+ <textarea matInput rows="3" formControlName="breakpointsJson"></textarea>
5451
+ </mat-form-field>
5452
+ <mat-form-field appearance="outline" class="span-3">
5453
+ <mat-label>Grouping overrides (JSON)</mat-label>
5454
+ <textarea matInput rows="4" formControlName="groupingOverridesJson"></textarea>
5455
+ </mat-form-field>
5456
+ <mat-form-field appearance="outline" class="span-3">
5457
+ <mat-label>Widget overrides (JSON)</mat-label>
5458
+ <textarea matInput rows="4" formControlName="widgetOverridesJson"></textarea>
5459
+ </mat-form-field>
5460
+ </div>
5461
+ </div>
5462
+ <div class="state-card" [formGroup]="deviceTabletForm">
5463
+ <div class="state-card-head"><div class="state-card-title">Tablet</div></div>
5464
+ <div class="state-card-grid">
5465
+ <mat-form-field appearance="outline">
5466
+ <mat-label>Orientation</mat-label>
5467
+ <mat-select formControlName="orientation">
5468
+ <mat-option value="">Padrão</mat-option>
5469
+ <mat-option value="vertical">vertical</mat-option>
5470
+ <mat-option value="columns">columns</mat-option>
5471
+ </mat-select>
5472
+ </mat-form-field>
5473
+ <mat-form-field appearance="outline">
5474
+ <mat-label>Columns</mat-label>
5475
+ <input matInput type="number" min="1" formControlName="columns" />
5476
+ </mat-form-field>
5477
+ <mat-form-field appearance="outline">
5478
+ <mat-label>Gap</mat-label>
5479
+ <input matInput formControlName="gap" />
5480
+ </mat-form-field>
5481
+ <mat-form-field appearance="outline" class="span-3">
5482
+ <mat-label>Breakpoints (JSON)</mat-label>
5483
+ <textarea matInput rows="3" formControlName="breakpointsJson"></textarea>
5484
+ </mat-form-field>
5485
+ <mat-form-field appearance="outline" class="span-3">
5486
+ <mat-label>Grouping overrides (JSON)</mat-label>
5487
+ <textarea matInput rows="4" formControlName="groupingOverridesJson"></textarea>
5488
+ </mat-form-field>
5489
+ <mat-form-field appearance="outline" class="span-3">
5490
+ <mat-label>Widget overrides (JSON)</mat-label>
5491
+ <textarea matInput rows="4" formControlName="widgetOverridesJson"></textarea>
5492
+ </mat-form-field>
5493
+ </div>
5494
+ </div>
5495
+ <div class="state-card" [formGroup]="deviceMobileForm">
5496
+ <div class="state-card-head"><div class="state-card-title">Mobile</div></div>
5497
+ <div class="state-card-grid">
5498
+ <mat-form-field appearance="outline">
5499
+ <mat-label>Orientation</mat-label>
5500
+ <mat-select formControlName="orientation">
5501
+ <mat-option value="">Padrão</mat-option>
5502
+ <mat-option value="vertical">vertical</mat-option>
5503
+ <mat-option value="columns">columns</mat-option>
5504
+ </mat-select>
5505
+ </mat-form-field>
5506
+ <mat-form-field appearance="outline">
5507
+ <mat-label>Columns</mat-label>
5508
+ <input matInput type="number" min="1" formControlName="columns" />
5509
+ </mat-form-field>
5510
+ <mat-form-field appearance="outline">
5511
+ <mat-label>Gap</mat-label>
5512
+ <input matInput formControlName="gap" />
5513
+ </mat-form-field>
5514
+ <mat-form-field appearance="outline" class="span-3">
5515
+ <mat-label>Breakpoints (JSON)</mat-label>
5516
+ <textarea matInput rows="3" formControlName="breakpointsJson"></textarea>
5517
+ </mat-form-field>
5518
+ <mat-form-field appearance="outline" class="span-3">
5519
+ <mat-label>Grouping overrides (JSON)</mat-label>
5520
+ <textarea matInput rows="4" formControlName="groupingOverridesJson"></textarea>
5521
+ </mat-form-field>
5522
+ <mat-form-field appearance="outline" class="span-3">
5523
+ <mat-label>Widget overrides (JSON)</mat-label>
5524
+ <textarea matInput rows="4" formControlName="widgetOverridesJson"></textarea>
5525
+ </mat-form-field>
5526
+ </div>
5527
+ </div>
5528
+ </div>
5529
+ </div>
5530
+ @if (layoutError()) {
5531
+ <div class="page-error" role="alert">{{ layoutError() }}</div>
5532
+ }
5533
+
4648
5534
  <div class="page-section">Contexto</div>
4649
5535
  <mat-form-field appearance="outline">
4650
5536
  <mat-label>Contexto (JSON)</mat-label>
@@ -4728,10 +5614,38 @@ class PageConfigEditorComponent {
4728
5614
  }
4729
5615
  </mat-select>
4730
5616
  </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>
5617
+ <mat-form-field appearance="outline">
5618
+ <mat-label>Initial</mat-label>
5619
+ <mat-select formControlName="initialMode">
5620
+ @for (mode of valueModes; track mode) {
5621
+ <mat-option [value]="mode">{{ mode }}</mat-option>
5622
+ }
5623
+ </mat-select>
4734
5624
  </mat-form-field>
5625
+ @if (group.controls.initialMode.value === 'json') {
5626
+ <mat-form-field appearance="outline" class="span-2">
5627
+ <mat-label>Initial (JSON)</mat-label>
5628
+ <textarea matInput rows="4" formControlName="initialJson"></textarea>
5629
+ </mat-form-field>
5630
+ } @else if (group.controls.initialMode.value === 'boolean') {
5631
+ <mat-form-field appearance="outline" class="span-2">
5632
+ <mat-label>Initial</mat-label>
5633
+ <mat-select formControlName="initialScalar">
5634
+ <mat-option value="true">true</mat-option>
5635
+ <mat-option value="false">false</mat-option>
5636
+ </mat-select>
5637
+ </mat-form-field>
5638
+ } @else if (group.controls.initialMode.value === 'null') {
5639
+ <div class="state-operator-hint span-2">
5640
+ <mat-icon>block</mat-icon>
5641
+ <span>O valor inicial será null.</span>
5642
+ </div>
5643
+ } @else {
5644
+ <mat-form-field appearance="outline" class="span-2">
5645
+ <mat-label>Initial</mat-label>
5646
+ <input matInput formControlName="initialScalar" />
5647
+ </mat-form-field>
5648
+ }
4735
5649
  <mat-form-field appearance="outline" class="span-2">
4736
5650
  <mat-label>Descrição</mat-label>
4737
5651
  <input matInput formControlName="description" />
@@ -4807,10 +5721,38 @@ class PageConfigEditorComponent {
4807
5721
  }
4808
5722
  }
4809
5723
  @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>
5724
+ <mat-form-field appearance="outline">
5725
+ <mat-label>Template</mat-label>
5726
+ <mat-select formControlName="templateMode">
5727
+ @for (mode of valueModes; track mode) {
5728
+ <mat-option [value]="mode">{{ mode }}</mat-option>
5729
+ }
5730
+ </mat-select>
4813
5731
  </mat-form-field>
5732
+ @if (group.controls.templateMode.value === 'json') {
5733
+ <mat-form-field appearance="outline" class="span-2">
5734
+ <mat-label>Template (JSON)</mat-label>
5735
+ <textarea matInput rows="4" formControlName="templateJson"></textarea>
5736
+ </mat-form-field>
5737
+ } @else if (group.controls.templateMode.value === 'boolean') {
5738
+ <mat-form-field appearance="outline" class="span-2">
5739
+ <mat-label>Template</mat-label>
5740
+ <mat-select formControlName="templateScalar">
5741
+ <mat-option value="true">true</mat-option>
5742
+ <mat-option value="false">false</mat-option>
5743
+ </mat-select>
5744
+ </mat-form-field>
5745
+ } @else if (group.controls.templateMode.value === 'null') {
5746
+ <div class="state-operator-hint span-2">
5747
+ <mat-icon>block</mat-icon>
5748
+ <span>O template retornará null.</span>
5749
+ </div>
5750
+ } @else {
5751
+ <mat-form-field appearance="outline" class="span-2">
5752
+ <mat-label>Template</mat-label>
5753
+ <input matInput formControlName="templateScalar" />
5754
+ </mat-form-field>
5755
+ }
4814
5756
  }
4815
5757
  @if (group.controls.computeKind.value === 'expr') {
4816
5758
  <mat-form-field appearance="outline" class="span-3">
@@ -4896,6 +5838,226 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
4896
5838
  </mat-form-field>
4897
5839
  </form>
4898
5840
 
5841
+ <div class="page-section">Layout Preset</div>
5842
+ <div class="grid-form">
5843
+ <mat-form-field appearance="outline">
5844
+ <mat-label>Layout preset</mat-label>
5845
+ <mat-select [formControl]="layoutPresetControl">
5846
+ <mat-option value="">Nenhum</mat-option>
5847
+ @for (preset of layoutPresetEntries; track preset.id) {
5848
+ <mat-option [value]="preset.id">{{ preset.label }}</mat-option>
5849
+ }
5850
+ </mat-select>
5851
+ </mat-form-field>
5852
+ <mat-form-field appearance="outline">
5853
+ <mat-label>Theme preset</mat-label>
5854
+ <mat-select [formControl]="themePresetControl">
5855
+ <mat-option value="">Padrão do layout</mat-option>
5856
+ @for (preset of themePresetEntries; track preset.id) {
5857
+ <mat-option [value]="preset.id">{{ preset.label }}</mat-option>
5858
+ }
5859
+ </mat-select>
5860
+ </mat-form-field>
5861
+ </div>
5862
+
5863
+ <div class="page-section">Grouping</div>
5864
+ <div class="state-group">
5865
+ <div class="state-group-head">
5866
+ <div class="state-group-title">Grouping</div>
5867
+ <button mat-stroked-button type="button" (click)="addGroupingNode()">
5868
+ <mat-icon>add</mat-icon>
5869
+ Adicionar grupo
5870
+ </button>
5871
+ </div>
5872
+ <div class="state-group-help">Organiza a página em section, tabs, hero e rail com intenção estrutural.</div>
5873
+ @if (!groupingNodes.length) {
5874
+ <div class="state-empty">Nenhum grupo configurado.</div>
5875
+ }
5876
+ <div class="state-list">
5877
+ @for (group of groupingNodes.controls; track $index) {
5878
+ <div class="state-card" [formGroup]="group">
5879
+ <div class="state-card-head">
5880
+ <div class="state-card-title">Grupo {{ $index + 1 }}</div>
5881
+ <button mat-icon-button type="button" (click)="removeGroupingNode($index)" aria-label="Remover grupo">
5882
+ <mat-icon>delete</mat-icon>
5883
+ </button>
5884
+ </div>
5885
+ <div class="state-card-grid">
5886
+ <mat-form-field appearance="outline">
5887
+ <mat-label>Kind</mat-label>
5888
+ <mat-select formControlName="kind">
5889
+ @for (kind of groupingKinds; track kind) {
5890
+ <mat-option [value]="kind">{{ kind }}</mat-option>
5891
+ }
5892
+ </mat-select>
5893
+ </mat-form-field>
5894
+ <mat-form-field appearance="outline">
5895
+ <mat-label>Id</mat-label>
5896
+ <input matInput formControlName="id" />
5897
+ </mat-form-field>
5898
+ <mat-form-field appearance="outline">
5899
+ <mat-label>Label</mat-label>
5900
+ <input matInput formControlName="label" />
5901
+ </mat-form-field>
5902
+ @if (group.controls.kind.value === 'section') {
5903
+ <mat-form-field appearance="outline">
5904
+ <mat-label>Layout</mat-label>
5905
+ <mat-select formControlName="layout">
5906
+ <mat-option value="">Padrão</mat-option>
5907
+ <mat-option value="stack">stack</mat-option>
5908
+ <mat-option value="grid">grid</mat-option>
5909
+ <mat-option value="row">row</mat-option>
5910
+ </mat-select>
5911
+ </mat-form-field>
5912
+ }
5913
+ @if (group.controls.kind.value === 'hero') {
5914
+ <mat-form-field appearance="outline">
5915
+ <mat-label>Emphasis</mat-label>
5916
+ <mat-select formControlName="emphasis">
5917
+ <mat-option value="">Padrão</mat-option>
5918
+ <mat-option value="high">high</mat-option>
5919
+ <mat-option value="medium">medium</mat-option>
5920
+ </mat-select>
5921
+ </mat-form-field>
5922
+ }
5923
+ @if (group.controls.kind.value === 'rail') {
5924
+ <mat-form-field appearance="outline">
5925
+ <mat-label>Side</mat-label>
5926
+ <mat-select formControlName="side">
5927
+ <mat-option value="left">left</mat-option>
5928
+ <mat-option value="right">right</mat-option>
5929
+ </mat-select>
5930
+ </mat-form-field>
5931
+ }
5932
+ @if (group.controls.kind.value === 'tabs') {
5933
+ <mat-form-field appearance="outline" class="span-3">
5934
+ <mat-label>Tabs (JSON)</mat-label>
5935
+ <textarea matInput rows="5" formControlName="tabsJson"></textarea>
5936
+ </mat-form-field>
5937
+ } @else {
5938
+ <mat-form-field appearance="outline" class="span-3">
5939
+ <mat-label>Widget keys (vírgula)</mat-label>
5940
+ <input matInput formControlName="widgetKeysCsv" />
5941
+ </mat-form-field>
5942
+ }
5943
+ </div>
5944
+ </div>
5945
+ }
5946
+ </div>
5947
+ </div>
5948
+
5949
+ <div class="page-section">Device Layouts</div>
5950
+ <div class="state-group">
5951
+ <div class="state-group-head">
5952
+ <div class="state-group-title">Device Layouts</div>
5953
+ </div>
5954
+ <div class="state-group-help">Overrides por device para layout, grouping e widgets com campos separados por intenção.</div>
5955
+ <div class="state-list">
5956
+ <div class="state-card" [formGroup]="deviceDesktopForm">
5957
+ <div class="state-card-head"><div class="state-card-title">Desktop</div></div>
5958
+ <div class="state-card-grid">
5959
+ <mat-form-field appearance="outline">
5960
+ <mat-label>Orientation</mat-label>
5961
+ <mat-select formControlName="orientation">
5962
+ <mat-option value="">Padrão</mat-option>
5963
+ <mat-option value="vertical">vertical</mat-option>
5964
+ <mat-option value="columns">columns</mat-option>
5965
+ </mat-select>
5966
+ </mat-form-field>
5967
+ <mat-form-field appearance="outline">
5968
+ <mat-label>Columns</mat-label>
5969
+ <input matInput type="number" min="1" formControlName="columns" />
5970
+ </mat-form-field>
5971
+ <mat-form-field appearance="outline">
5972
+ <mat-label>Gap</mat-label>
5973
+ <input matInput formControlName="gap" />
5974
+ </mat-form-field>
5975
+ <mat-form-field appearance="outline" class="span-3">
5976
+ <mat-label>Breakpoints (JSON)</mat-label>
5977
+ <textarea matInput rows="3" formControlName="breakpointsJson"></textarea>
5978
+ </mat-form-field>
5979
+ <mat-form-field appearance="outline" class="span-3">
5980
+ <mat-label>Grouping overrides (JSON)</mat-label>
5981
+ <textarea matInput rows="4" formControlName="groupingOverridesJson"></textarea>
5982
+ </mat-form-field>
5983
+ <mat-form-field appearance="outline" class="span-3">
5984
+ <mat-label>Widget overrides (JSON)</mat-label>
5985
+ <textarea matInput rows="4" formControlName="widgetOverridesJson"></textarea>
5986
+ </mat-form-field>
5987
+ </div>
5988
+ </div>
5989
+ <div class="state-card" [formGroup]="deviceTabletForm">
5990
+ <div class="state-card-head"><div class="state-card-title">Tablet</div></div>
5991
+ <div class="state-card-grid">
5992
+ <mat-form-field appearance="outline">
5993
+ <mat-label>Orientation</mat-label>
5994
+ <mat-select formControlName="orientation">
5995
+ <mat-option value="">Padrão</mat-option>
5996
+ <mat-option value="vertical">vertical</mat-option>
5997
+ <mat-option value="columns">columns</mat-option>
5998
+ </mat-select>
5999
+ </mat-form-field>
6000
+ <mat-form-field appearance="outline">
6001
+ <mat-label>Columns</mat-label>
6002
+ <input matInput type="number" min="1" formControlName="columns" />
6003
+ </mat-form-field>
6004
+ <mat-form-field appearance="outline">
6005
+ <mat-label>Gap</mat-label>
6006
+ <input matInput formControlName="gap" />
6007
+ </mat-form-field>
6008
+ <mat-form-field appearance="outline" class="span-3">
6009
+ <mat-label>Breakpoints (JSON)</mat-label>
6010
+ <textarea matInput rows="3" formControlName="breakpointsJson"></textarea>
6011
+ </mat-form-field>
6012
+ <mat-form-field appearance="outline" class="span-3">
6013
+ <mat-label>Grouping overrides (JSON)</mat-label>
6014
+ <textarea matInput rows="4" formControlName="groupingOverridesJson"></textarea>
6015
+ </mat-form-field>
6016
+ <mat-form-field appearance="outline" class="span-3">
6017
+ <mat-label>Widget overrides (JSON)</mat-label>
6018
+ <textarea matInput rows="4" formControlName="widgetOverridesJson"></textarea>
6019
+ </mat-form-field>
6020
+ </div>
6021
+ </div>
6022
+ <div class="state-card" [formGroup]="deviceMobileForm">
6023
+ <div class="state-card-head"><div class="state-card-title">Mobile</div></div>
6024
+ <div class="state-card-grid">
6025
+ <mat-form-field appearance="outline">
6026
+ <mat-label>Orientation</mat-label>
6027
+ <mat-select formControlName="orientation">
6028
+ <mat-option value="">Padrão</mat-option>
6029
+ <mat-option value="vertical">vertical</mat-option>
6030
+ <mat-option value="columns">columns</mat-option>
6031
+ </mat-select>
6032
+ </mat-form-field>
6033
+ <mat-form-field appearance="outline">
6034
+ <mat-label>Columns</mat-label>
6035
+ <input matInput type="number" min="1" formControlName="columns" />
6036
+ </mat-form-field>
6037
+ <mat-form-field appearance="outline">
6038
+ <mat-label>Gap</mat-label>
6039
+ <input matInput formControlName="gap" />
6040
+ </mat-form-field>
6041
+ <mat-form-field appearance="outline" class="span-3">
6042
+ <mat-label>Breakpoints (JSON)</mat-label>
6043
+ <textarea matInput rows="3" formControlName="breakpointsJson"></textarea>
6044
+ </mat-form-field>
6045
+ <mat-form-field appearance="outline" class="span-3">
6046
+ <mat-label>Grouping overrides (JSON)</mat-label>
6047
+ <textarea matInput rows="4" formControlName="groupingOverridesJson"></textarea>
6048
+ </mat-form-field>
6049
+ <mat-form-field appearance="outline" class="span-3">
6050
+ <mat-label>Widget overrides (JSON)</mat-label>
6051
+ <textarea matInput rows="4" formControlName="widgetOverridesJson"></textarea>
6052
+ </mat-form-field>
6053
+ </div>
6054
+ </div>
6055
+ </div>
6056
+ </div>
6057
+ @if (layoutError()) {
6058
+ <div class="page-error" role="alert">{{ layoutError() }}</div>
6059
+ }
6060
+
4899
6061
  <div class="page-section">Contexto</div>
4900
6062
  <mat-form-field appearance="outline">
4901
6063
  <mat-label>Contexto (JSON)</mat-label>
@@ -4979,10 +6141,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
4979
6141
  }
4980
6142
  </mat-select>
4981
6143
  </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>
6144
+ <mat-form-field appearance="outline">
6145
+ <mat-label>Initial</mat-label>
6146
+ <mat-select formControlName="initialMode">
6147
+ @for (mode of valueModes; track mode) {
6148
+ <mat-option [value]="mode">{{ mode }}</mat-option>
6149
+ }
6150
+ </mat-select>
4985
6151
  </mat-form-field>
6152
+ @if (group.controls.initialMode.value === 'json') {
6153
+ <mat-form-field appearance="outline" class="span-2">
6154
+ <mat-label>Initial (JSON)</mat-label>
6155
+ <textarea matInput rows="4" formControlName="initialJson"></textarea>
6156
+ </mat-form-field>
6157
+ } @else if (group.controls.initialMode.value === 'boolean') {
6158
+ <mat-form-field appearance="outline" class="span-2">
6159
+ <mat-label>Initial</mat-label>
6160
+ <mat-select formControlName="initialScalar">
6161
+ <mat-option value="true">true</mat-option>
6162
+ <mat-option value="false">false</mat-option>
6163
+ </mat-select>
6164
+ </mat-form-field>
6165
+ } @else if (group.controls.initialMode.value === 'null') {
6166
+ <div class="state-operator-hint span-2">
6167
+ <mat-icon>block</mat-icon>
6168
+ <span>O valor inicial será null.</span>
6169
+ </div>
6170
+ } @else {
6171
+ <mat-form-field appearance="outline" class="span-2">
6172
+ <mat-label>Initial</mat-label>
6173
+ <input matInput formControlName="initialScalar" />
6174
+ </mat-form-field>
6175
+ }
4986
6176
  <mat-form-field appearance="outline" class="span-2">
4987
6177
  <mat-label>Descrição</mat-label>
4988
6178
  <input matInput formControlName="description" />
@@ -5058,10 +6248,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
5058
6248
  }
5059
6249
  }
5060
6250
  @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>
6251
+ <mat-form-field appearance="outline">
6252
+ <mat-label>Template</mat-label>
6253
+ <mat-select formControlName="templateMode">
6254
+ @for (mode of valueModes; track mode) {
6255
+ <mat-option [value]="mode">{{ mode }}</mat-option>
6256
+ }
6257
+ </mat-select>
5064
6258
  </mat-form-field>
6259
+ @if (group.controls.templateMode.value === 'json') {
6260
+ <mat-form-field appearance="outline" class="span-2">
6261
+ <mat-label>Template (JSON)</mat-label>
6262
+ <textarea matInput rows="4" formControlName="templateJson"></textarea>
6263
+ </mat-form-field>
6264
+ } @else if (group.controls.templateMode.value === 'boolean') {
6265
+ <mat-form-field appearance="outline" class="span-2">
6266
+ <mat-label>Template</mat-label>
6267
+ <mat-select formControlName="templateScalar">
6268
+ <mat-option value="true">true</mat-option>
6269
+ <mat-option value="false">false</mat-option>
6270
+ </mat-select>
6271
+ </mat-form-field>
6272
+ } @else if (group.controls.templateMode.value === 'null') {
6273
+ <div class="state-operator-hint span-2">
6274
+ <mat-icon>block</mat-icon>
6275
+ <span>O template retornará null.</span>
6276
+ </div>
6277
+ } @else {
6278
+ <mat-form-field appearance="outline" class="span-2">
6279
+ <mat-label>Template</mat-label>
6280
+ <input matInput formControlName="templateScalar" />
6281
+ </mat-form-field>
6282
+ }
5065
6283
  }
5066
6284
  @if (group.controls.computeKind.value === 'expr') {
5067
6285
  <mat-form-field appearance="outline" class="span-3">
@@ -5650,6 +6868,96 @@ class DynamicGridsterPageComponent {
5650
6868
  getWidgetType(key) { return (this.widgets().find((w) => w.key === key)?.definition?.id) || undefined; }
5651
6869
  getWidgetDef(key) { return this.widgets().find((w) => w.key === key)?.definition; }
5652
6870
  getWidgetShell(key) { return this.widgets().find((w) => w.key === key)?.shell; }
6871
+ hasCanvasSummary() { return !!(this.layoutPresetLabel() || this.themePresetLabel() || this.groupingCount() || this.configuredDeviceKinds().length); }
6872
+ groupingCount() { return this.currentPage()?.grouping?.length || 0; }
6873
+ selectedWidgetKey() { return this.selectedKey(); }
6874
+ layoutPresetLabel() {
6875
+ const presetId = this.currentPage()?.layoutPreset;
6876
+ if (!presetId)
6877
+ return null;
6878
+ return BUILTIN_PAGE_LAYOUT_PRESETS[presetId]?.label || presetId;
6879
+ }
6880
+ themePresetLabel() {
6881
+ const presetId = this.currentPage()?.themePreset;
6882
+ if (!presetId)
6883
+ return null;
6884
+ return BUILTIN_PAGE_THEME_PRESETS[presetId]?.label || presetId;
6885
+ }
6886
+ configuredDeviceKinds() {
6887
+ const deviceLayouts = this.currentPage()?.deviceLayouts;
6888
+ if (!deviceLayouts)
6889
+ return [];
6890
+ return ['desktop', 'tablet', 'mobile'].filter((device) => !!deviceLayouts[device]);
6891
+ }
6892
+ hasTileMetadata(key) {
6893
+ return this.widgetSlotLabels(key).length > 0 || this.widgetGroupingLabels(key).length > 0 || this.widgetDeviceLabels(key).length > 0;
6894
+ }
6895
+ presetSlots() {
6896
+ return this.activeLayoutPreset()?.slotModel || [];
6897
+ }
6898
+ canAssignSelectedWidgetToSlot() {
6899
+ return !!(this.enableCustomization && this.selectedKey() && this.presetSlots().length);
6900
+ }
6901
+ slotOccupancy(slotId) {
6902
+ return this.widgets().filter((widget) => this.widgetSlotIds(widget.key).includes(slotId)).length;
6903
+ }
6904
+ slotCapacityLabel(slot) {
6905
+ return slot.maxItems != null ? String(slot.maxItems) : 'n';
6906
+ }
6907
+ widgetSlotLabels(key) {
6908
+ return this.widgetSlotIds(key)
6909
+ .map((slotId) => this.presetSlots().find((slot) => slot.id === slotId)?.label || slotId)
6910
+ .map((label) => `slot ${label}`);
6911
+ }
6912
+ isRecommendedSlotForSelectedWidget(slot) {
6913
+ const selected = this.selectedKey();
6914
+ if (!selected)
6915
+ return false;
6916
+ const widgetType = this.getWidgetType(selected);
6917
+ return !!this.activeLayoutPreset()?.widgetSuggestions?.some((suggestion) => suggestion.slot === slot.id && suggestion.widgetType === widgetType);
6918
+ }
6919
+ assignSelectedWidgetToSlot(slotId, event) {
6920
+ event?.preventDefault();
6921
+ event?.stopPropagation();
6922
+ const widgetKey = this.selectedKey();
6923
+ const preset = this.activeLayoutPreset();
6924
+ if (!widgetKey || !preset)
6925
+ return;
6926
+ const page = this.currentPage() || { widgets: this.widgets() };
6927
+ const grouping = this.reassignWidgetToSlot(page.grouping, preset, widgetKey, slotId);
6928
+ const next = { ...page, grouping };
6929
+ this.page = next;
6930
+ this.pageChange.emit(next);
6931
+ }
6932
+ widgetGroupingLabels(key) {
6933
+ const labels = [];
6934
+ for (const group of this.currentPage()?.grouping || []) {
6935
+ if (this.groupContainsWidget(group, key)) {
6936
+ const base = this.groupDisplayLabel(group);
6937
+ labels.push(group.kind === 'tabs' ? `tab ${base}` : `${group.kind} ${base}`);
6938
+ }
6939
+ }
6940
+ return labels;
6941
+ }
6942
+ widgetDeviceLabels(key) {
6943
+ const labels = [];
6944
+ const deviceLayouts = this.currentPage()?.deviceLayouts;
6945
+ for (const device of this.configuredDeviceKinds()) {
6946
+ const variant = deviceLayouts?.[device];
6947
+ if (!variant)
6948
+ continue;
6949
+ const widgetOverride = variant.widgetOverrides?.[key];
6950
+ const groupingOverride = variant.groupingOverrides?.some((override) => this.groupingOverrideContainsWidget(override, key));
6951
+ if (widgetOverride) {
6952
+ const detail = widgetOverride.hidden ? 'hidden' : widgetOverride.span ? `span ${widgetOverride.span}` : 'override';
6953
+ labels.push(`${device} ${detail}`);
6954
+ }
6955
+ else if (groupingOverride) {
6956
+ labels.push(`${device} grouping`);
6957
+ }
6958
+ }
6959
+ return labels;
6960
+ }
5653
6961
  onTileClick(key, ev) {
5654
6962
  if (!this.enableCustomization)
5655
6963
  return;
@@ -6029,14 +7337,175 @@ class DynamicGridsterPageComponent {
6029
7337
  parts.push(identity.locale);
6030
7338
  return parts.join(':');
6031
7339
  }
7340
+ currentPage() {
7341
+ return this.parsePage(this.page);
7342
+ }
7343
+ activeLayoutPreset() {
7344
+ const presetId = this.currentPage()?.layoutPreset;
7345
+ if (!presetId)
7346
+ return null;
7347
+ return BUILTIN_PAGE_LAYOUT_PRESETS[presetId] || null;
7348
+ }
7349
+ groupContainsWidget(group, widgetKey) {
7350
+ if (group.kind === 'tabs') {
7351
+ return group.tabs.some((tab) => tab.widgetKeys.includes(widgetKey));
7352
+ }
7353
+ return group.widgetKeys.includes(widgetKey);
7354
+ }
7355
+ groupDisplayLabel(group) {
7356
+ if ('label' in group && typeof group.label === 'string' && group.label.trim()) {
7357
+ return group.label.trim();
7358
+ }
7359
+ return group.id;
7360
+ }
7361
+ widgetSlotIds(widgetKey) {
7362
+ const preset = this.activeLayoutPreset();
7363
+ if (!preset)
7364
+ return [];
7365
+ const grouping = this.currentPage()?.grouping || preset.defaultGrouping || [];
7366
+ return preset.slotModel
7367
+ .filter((slot) => this.slotContainsWidget(grouping, slot.id, widgetKey, preset))
7368
+ .map((slot) => slot.id);
7369
+ }
7370
+ slotContainsWidget(grouping, slotId, widgetKey, preset) {
7371
+ const directTarget = this.findSlotTarget(grouping, slotId, preset);
7372
+ if (directTarget?.kind === 'group')
7373
+ return directTarget.group.widgetKeys.includes(widgetKey);
7374
+ if (directTarget?.kind === 'tab')
7375
+ return directTarget.tab.widgetKeys.includes(widgetKey);
7376
+ return false;
7377
+ }
7378
+ reassignWidgetToSlot(grouping, preset, widgetKey, slotId) {
7379
+ const nextGrouping = this.cloneGrouping(grouping?.length ? grouping : preset.defaultGrouping || []);
7380
+ this.removeWidgetFromGrouping(nextGrouping, widgetKey);
7381
+ const target = this.findSlotTarget(nextGrouping, slotId, preset);
7382
+ if (target?.kind === 'group') {
7383
+ if (!target.group.widgetKeys.includes(widgetKey))
7384
+ target.group.widgetKeys.push(widgetKey);
7385
+ return nextGrouping;
7386
+ }
7387
+ if (target?.kind === 'tab') {
7388
+ if (!target.tab.widgetKeys.includes(widgetKey))
7389
+ target.tab.widgetKeys.push(widgetKey);
7390
+ return nextGrouping;
7391
+ }
7392
+ const slot = preset.slotModel.find((entry) => entry.id === slotId);
7393
+ nextGrouping.push({
7394
+ kind: 'section',
7395
+ id: slotId,
7396
+ label: slot?.label || slotId,
7397
+ widgetKeys: [widgetKey],
7398
+ layout: 'stack',
7399
+ });
7400
+ return nextGrouping;
7401
+ }
7402
+ findSlotTarget(grouping, slotId, preset) {
7403
+ for (const group of grouping) {
7404
+ if (group.kind === 'tabs') {
7405
+ const directTab = group.tabs.find((tab) => tab.id === slotId);
7406
+ if (directTab)
7407
+ return { kind: 'tab', group, tab: directTab };
7408
+ continue;
7409
+ }
7410
+ if (group.id === slotId)
7411
+ return { kind: 'group', group };
7412
+ }
7413
+ const templateGrouping = preset.defaultGrouping || [];
7414
+ for (const templateGroup of templateGrouping) {
7415
+ if (templateGroup.kind === 'tabs') {
7416
+ const templateTab = templateGroup.tabs.find((tab) => tab.id === slotId || tab.widgetKeys.includes(slotId));
7417
+ if (!templateTab)
7418
+ continue;
7419
+ const currentGroup = grouping.find((candidate) => candidate.kind === 'tabs' && candidate.id === templateGroup.id);
7420
+ const currentTab = currentGroup?.tabs.find((tab) => tab.id === templateTab.id);
7421
+ if (currentGroup && currentTab)
7422
+ return { kind: 'tab', group: currentGroup, tab: currentTab };
7423
+ continue;
7424
+ }
7425
+ if (templateGroup.id === slotId || templateGroup.widgetKeys.includes(slotId)) {
7426
+ const currentGroup = grouping.find((candidate) => candidate.kind !== 'tabs' && candidate.id === templateGroup.id);
7427
+ if (currentGroup)
7428
+ return { kind: 'group', group: currentGroup };
7429
+ }
7430
+ }
7431
+ return null;
7432
+ }
7433
+ removeWidgetFromGrouping(grouping, widgetKey) {
7434
+ for (const group of grouping) {
7435
+ if (group.kind === 'tabs') {
7436
+ group.tabs = group.tabs.map((tab) => ({
7437
+ ...tab,
7438
+ widgetKeys: tab.widgetKeys.filter((candidate) => candidate !== widgetKey),
7439
+ }));
7440
+ continue;
7441
+ }
7442
+ group.widgetKeys = group.widgetKeys.filter((candidate) => candidate !== widgetKey);
7443
+ }
7444
+ }
7445
+ cloneGrouping(grouping) {
7446
+ return grouping.map((group) => {
7447
+ if (group.kind === 'tabs') {
7448
+ return {
7449
+ ...group,
7450
+ tabs: group.tabs.map((tab) => ({
7451
+ ...tab,
7452
+ widgetKeys: [...tab.widgetKeys],
7453
+ })),
7454
+ };
7455
+ }
7456
+ return {
7457
+ ...group,
7458
+ widgetKeys: [...group.widgetKeys],
7459
+ };
7460
+ });
7461
+ }
7462
+ groupingOverrideContainsWidget(override, widgetKey) {
7463
+ if (override.widgetKeys?.includes(widgetKey))
7464
+ return true;
7465
+ return override.tabs?.some((tab) => tab.widgetKeys.includes(widgetKey)) || false;
7466
+ }
6032
7467
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: DynamicGridsterPageComponent, deps: [{ token: i1$1.ConnectionManagerService }, { token: i2$3.Router }, { token: i0.ElementRef }, { token: i1.MatDialog }, { token: GraphMapperService }, { token: i1$1.ComponentMetadataRegistry }, { token: ASYNC_CONFIG_STORAGE }, { token: i1$1.ComponentKeyService }, { token: TABLE_CONFIG_EDITOR, optional: true }, { token: STEPPER_CONFIG_EDITOR, optional: true }, { token: SETTINGS_PANEL_BRIDGE, optional: true }, { token: i2$3.ActivatedRoute, optional: true }], target: i0.ɵɵFactoryTarget.Component });
6033
7468
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", type: DynamicGridsterPageComponent, isStandalone: true, selector: "praxis-dynamic-gridster-page", inputs: { page: "page", context: "context", strictValidation: "strictValidation", gridsterOptions: "gridsterOptions", enableCustomization: "enableCustomization", showSettingsButton: "showSettingsButton", pageIdentity: "pageIdentity", componentInstanceId: "componentInstanceId" }, outputs: { layoutChange: "layoutChange", pageChange: "pageChange" }, usesOnChanges: true, ngImport: i0, template: `
7469
+ <div class="pdx-canvas-summary" *ngIf="showSettings() && hasCanvasSummary()">
7470
+ <div class="pdx-canvas-summary__headline">Estrutura da Pagina</div>
7471
+ <div class="pdx-canvas-summary__chips">
7472
+ <span class="pdx-chip pdx-chip--preset" *ngIf="layoutPresetLabel() as label">{{ label }}</span>
7473
+ <span class="pdx-chip pdx-chip--theme" *ngIf="themePresetLabel() as label">{{ label }}</span>
7474
+ <span class="pdx-chip" *ngIf="groupingCount() > 0">{{ groupingCount() }} grupos</span>
7475
+ <span class="pdx-chip" *ngFor="let device of configuredDeviceKinds()">device {{ device }}</span>
7476
+ </div>
7477
+ <div class="pdx-slot-rail" *ngIf="presetSlots().length > 0">
7478
+ <div class="pdx-slot-rail__headline">Slots do preset</div>
7479
+ <div class="pdx-slot-rail__chips">
7480
+ <button
7481
+ type="button"
7482
+ class="pdx-slot-chip"
7483
+ *ngFor="let slot of presetSlots()"
7484
+ [class.pdx-slot-chip--assignable]="canAssignSelectedWidgetToSlot()"
7485
+ [class.pdx-slot-chip--recommended]="isRecommendedSlotForSelectedWidget(slot)"
7486
+ [attr.data-slot-id]="slot.id"
7487
+ [attr.data-slot-occupancy]="slotOccupancy(slot.id)"
7488
+ [disabled]="!canAssignSelectedWidgetToSlot()"
7489
+ (click)="assignSelectedWidgetToSlot(slot.id, $event)"
7490
+ >
7491
+ <span class="pdx-slot-chip__label">{{ slot.label }}</span>
7492
+ <span class="pdx-slot-chip__meta">{{ slotOccupancy(slot.id) }}/{{ slotCapacityLabel(slot) }}</span>
7493
+ </button>
7494
+ </div>
7495
+ <div class="pdx-slot-rail__help" *ngIf="selectedWidgetKey() as key">
7496
+ Widget selecionado: <strong>{{ getWidgetType(key) || key }}</strong>. Clique em um slot para reatribuir o widget dentro do preset.
7497
+ </div>
7498
+ </div>
7499
+ </div>
6034
7500
  <gridster [options]="options" [class.editing]="overlayEnabled && showSettings()" [class.interacting]="overlayEnabled && interacting">
6035
7501
  <gridster-item *ngFor="let it of items(); trackBy: trackItem" [item]="it">
6036
7502
  <div class="pdx-gridster-item mat-elevation-z4"
6037
7503
  [attr.id]="computeTileDomId(it.__key__)"
6038
7504
  [attr.data-widget-key]="it.__key__"
6039
7505
  [attr.data-widget-type]="getWidgetType(it.__key__)"
7506
+ [attr.data-group-count]="widgetGroupingLabels(it.__key__).length"
7507
+ [attr.data-device-count]="widgetDeviceLabels(it.__key__).length"
7508
+ [attr.data-slot-count]="widgetSlotLabels(it.__key__).length"
6040
7509
  [class.selected]="isSelected(it.__key__)"
6041
7510
  (click)="onTileClick(it.__key__, $event)"
6042
7511
  tabindex="0">
@@ -6048,6 +7517,11 @@ class DynamicGridsterPageComponent {
6048
7517
  (settings)="openWidgetSettings(it.__key__)"
6049
7518
  (shell)="openWidgetShellSettings(it.__key__)"
6050
7519
  ></praxis-tile-toolbar>
7520
+ <div class="pdx-tile-meta" *ngIf="showSettings() && hasTileMetadata(it.__key__)">
7521
+ <span class="pdx-tile-chip pdx-tile-chip--slot" *ngFor="let label of widgetSlotLabels(it.__key__)">{{ label }}</span>
7522
+ <span class="pdx-tile-chip pdx-tile-chip--group" *ngFor="let label of widgetGroupingLabels(it.__key__)">{{ label }}</span>
7523
+ <span class="pdx-tile-chip pdx-tile-chip--device" *ngFor="let label of widgetDeviceLabels(it.__key__)">{{ label }}</span>
7524
+ </div>
6051
7525
  <praxis-widget-shell
6052
7526
  [shell]="getWidgetShell(it.__key__)"
6053
7527
  [context]="mergedContext"
@@ -6092,17 +7566,51 @@ class DynamicGridsterPageComponent {
6092
7566
  [adapter]="aiAdapter">
6093
7567
  </praxis-ai-assistant>
6094
7568
  </praxis-floating-toolbar>
6095
- `, isInline: true, styles: [":host{display:block;position:relative}:host ::ng-deep gridster{position:relative}:host ::ng-deep gridster.editing:before,:host ::ng-deep gridster.interacting:before{content:\"\";position:absolute;inset:0;pointer-events:none;opacity:.6;z-index:0;background-image:radial-gradient(circle at 1px 1px,var(--md-sys-color-outline-variant) 1px,transparent 1px),radial-gradient(circle,var(--md-sys-color-outline-variant) 1px,transparent 1px);background-size:var(--pdx-col-step, 16px) var(--pdx-row-step, 16px),calc(var(--pdx-col-step, 16px) * 4) calc(var(--pdx-row-step, 16px) * 4)}.pdx-gridster-item{position:relative;height: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: "component", type: GridsterComponent, selector: "gridster", inputs: ["options"] }, { kind: "component", type: GridsterItemComponent, selector: "gridster-item", inputs: ["item"], outputs: ["itemInit", "itemChange", "itemResize"] }, { kind: "directive", type: DynamicWidgetLoaderDirective, selector: "[dynamicWidgetLoader]", inputs: ["dynamicWidgetLoader", "context", "strictValidation", "autoWireOutputs"], outputs: ["widgetEvent"], exportAs: ["dynamicWidgetLoader"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatMiniFabButton, selector: "button[mat-mini-fab], a[mat-mini-fab], button[matMiniFab], a[matMiniFab]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatDialogModule }, { kind: "component", type: FloatingToolbarComponent, selector: "praxis-floating-toolbar", inputs: ["visible", "canUndo", "canRedo"], outputs: ["add", "undo", "redo", "settings", "preview", "connections", "connectionsEdit", "connectionsVisual", "save"] }, { kind: "component", type: TileToolbarComponent, selector: "praxis-tile-toolbar", inputs: ["selected", "widgetType"], outputs: ["remove", "settings", "shell"] }, { kind: "component", type: PraxisAiAssistantComponent, selector: "praxis-ai-assistant", inputs: ["adapter", "riskPolicy", "allowManualPatchEdit"] }, { kind: "component", type: WidgetShellComponent, selector: "praxis-widget-shell", inputs: ["shell", "context"], outputs: ["action"] }] });
7569
+ `, isInline: true, styles: [":host{display:block;position:relative}:host ::ng-deep gridster{position:relative}.pdx-canvas-summary{display:grid;gap:8px;margin-bottom:12px;padding:12px 14px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 70%,transparent);border-radius:16px;background:linear-gradient(135deg,color-mix(in srgb,var(--md-sys-color-surface-container-low) 92%,white 8%),color-mix(in srgb,var(--md-sys-color-surface-container) 96%,transparent)),radial-gradient(circle at top right,color-mix(in srgb,var(--md-sys-color-primary) 18%,transparent),transparent 58%)}.pdx-canvas-summary__headline{font-size:12px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--md-sys-color-on-surface-variant)}.pdx-canvas-summary__chips,.pdx-tile-meta{display:flex;flex-wrap:wrap;gap:6px}.pdx-slot-rail{display:grid;gap:8px;padding-top:4px}.pdx-slot-rail__headline{font-size:12px;font-weight:600;color:var(--md-sys-color-on-surface)}.pdx-slot-rail__chips{display:flex;flex-wrap:wrap;gap:8px}.pdx-slot-rail__help{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.pdx-chip,.pdx-tile-chip{display:inline-flex;align-items:center;min-height:24px;padding:0 10px;border-radius:999px;font-size:11px;font-weight:600;letter-spacing:.01em;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 72%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface-container-high) 88%,white 12%);color:var(--md-sys-color-on-surface)}.pdx-chip--preset,.pdx-tile-chip--group{background:color-mix(in srgb,var(--md-sys-color-primary-container) 86%,white 14%);color:var(--md-sys-color-on-primary-container)}.pdx-chip--theme,.pdx-tile-chip--device{background:color-mix(in srgb,var(--md-sys-color-secondary-container) 86%,white 14%);color:var(--md-sys-color-on-secondary-container)}.pdx-tile-chip--slot{background:color-mix(in srgb,var(--md-sys-color-tertiary-container) 86%,white 14%);color:var(--md-sys-color-on-tertiary-container)}.pdx-slot-chip{display:inline-flex;align-items:center;gap:8px;min-height:32px;padding:0 12px;border-radius:999px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 72%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface-container-highest) 82%,white 18%);color:var(--md-sys-color-on-surface);font:inherit;cursor:default;transition:transform .14s ease,box-shadow .14s ease,border-color .14s ease}.pdx-slot-chip--assignable{cursor:pointer;background:color-mix(in srgb,var(--md-sys-color-primary-container) 82%,white 18%);color:var(--md-sys-color-on-primary-container)}.pdx-slot-chip--assignable:hover{transform:translateY(-1px);box-shadow:0 8px 18px color-mix(in srgb,var(--md-sys-color-primary) 16%,transparent)}.pdx-slot-chip--recommended{border-color:color-mix(in srgb,var(--md-sys-color-primary) 56%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--md-sys-color-primary) 28%,transparent)}.pdx-slot-chip__label{font-weight:700}.pdx-slot-chip__meta{font-size:11px;color:inherit;opacity:.82}:host ::ng-deep gridster.editing:before,:host ::ng-deep gridster.interacting:before{content:\"\";position:absolute;inset:0;pointer-events:none;opacity:.6;z-index:0;background-image:radial-gradient(circle at 1px 1px,var(--md-sys-color-outline-variant) 1px,transparent 1px),radial-gradient(circle,var(--md-sys-color-outline-variant) 1px,transparent 1px);background-size:var(--pdx-col-step, 16px) var(--pdx-row-step, 16px),calc(var(--pdx-col-step, 16px) * 4) calc(var(--pdx-row-step, 16px) * 4)}.pdx-gridster-item{position:relative;height:100%}.pdx-tile-meta{position:absolute;top:44px;left:10px;right:10px;z-index:2;pointer-events:none}\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: "component", type: GridsterComponent, selector: "gridster", inputs: ["options"] }, { kind: "component", type: GridsterItemComponent, selector: "gridster-item", inputs: ["item"], outputs: ["itemInit", "itemChange", "itemResize"] }, { kind: "directive", type: DynamicWidgetLoaderDirective, selector: "[dynamicWidgetLoader]", inputs: ["dynamicWidgetLoader", "context", "strictValidation", "autoWireOutputs"], outputs: ["widgetEvent"], exportAs: ["dynamicWidgetLoader"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatMiniFabButton, selector: "button[mat-mini-fab], a[mat-mini-fab], button[matMiniFab], a[matMiniFab]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatDialogModule }, { kind: "component", type: FloatingToolbarComponent, selector: "praxis-floating-toolbar", inputs: ["visible", "canUndo", "canRedo"], outputs: ["add", "undo", "redo", "settings", "preview", "connections", "connectionsEdit", "connectionsVisual", "save"] }, { kind: "component", type: TileToolbarComponent, selector: "praxis-tile-toolbar", inputs: ["selected", "widgetType"], outputs: ["remove", "settings", "shell"] }, { kind: "component", type: PraxisAiAssistantComponent, selector: "praxis-ai-assistant", inputs: ["adapter", "riskPolicy", "allowManualPatchEdit"] }, { kind: "component", type: WidgetShellComponent, selector: "praxis-widget-shell", inputs: ["shell", "context"], outputs: ["action"] }] });
6096
7570
  }
6097
7571
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: DynamicGridsterPageComponent, decorators: [{
6098
7572
  type: Component,
6099
7573
  args: [{ selector: 'praxis-dynamic-gridster-page', standalone: true, imports: [CommonModule, GridsterComponent, GridsterItemComponent, DynamicWidgetLoaderDirective, MatButtonModule, MatIconModule, MatDialogModule, FloatingToolbarComponent, TileToolbarComponent, PraxisAiAssistantComponent, WidgetShellComponent], template: `
7574
+ <div class="pdx-canvas-summary" *ngIf="showSettings() && hasCanvasSummary()">
7575
+ <div class="pdx-canvas-summary__headline">Estrutura da Pagina</div>
7576
+ <div class="pdx-canvas-summary__chips">
7577
+ <span class="pdx-chip pdx-chip--preset" *ngIf="layoutPresetLabel() as label">{{ label }}</span>
7578
+ <span class="pdx-chip pdx-chip--theme" *ngIf="themePresetLabel() as label">{{ label }}</span>
7579
+ <span class="pdx-chip" *ngIf="groupingCount() > 0">{{ groupingCount() }} grupos</span>
7580
+ <span class="pdx-chip" *ngFor="let device of configuredDeviceKinds()">device {{ device }}</span>
7581
+ </div>
7582
+ <div class="pdx-slot-rail" *ngIf="presetSlots().length > 0">
7583
+ <div class="pdx-slot-rail__headline">Slots do preset</div>
7584
+ <div class="pdx-slot-rail__chips">
7585
+ <button
7586
+ type="button"
7587
+ class="pdx-slot-chip"
7588
+ *ngFor="let slot of presetSlots()"
7589
+ [class.pdx-slot-chip--assignable]="canAssignSelectedWidgetToSlot()"
7590
+ [class.pdx-slot-chip--recommended]="isRecommendedSlotForSelectedWidget(slot)"
7591
+ [attr.data-slot-id]="slot.id"
7592
+ [attr.data-slot-occupancy]="slotOccupancy(slot.id)"
7593
+ [disabled]="!canAssignSelectedWidgetToSlot()"
7594
+ (click)="assignSelectedWidgetToSlot(slot.id, $event)"
7595
+ >
7596
+ <span class="pdx-slot-chip__label">{{ slot.label }}</span>
7597
+ <span class="pdx-slot-chip__meta">{{ slotOccupancy(slot.id) }}/{{ slotCapacityLabel(slot) }}</span>
7598
+ </button>
7599
+ </div>
7600
+ <div class="pdx-slot-rail__help" *ngIf="selectedWidgetKey() as key">
7601
+ Widget selecionado: <strong>{{ getWidgetType(key) || key }}</strong>. Clique em um slot para reatribuir o widget dentro do preset.
7602
+ </div>
7603
+ </div>
7604
+ </div>
6100
7605
  <gridster [options]="options" [class.editing]="overlayEnabled && showSettings()" [class.interacting]="overlayEnabled && interacting">
6101
7606
  <gridster-item *ngFor="let it of items(); trackBy: trackItem" [item]="it">
6102
7607
  <div class="pdx-gridster-item mat-elevation-z4"
6103
7608
  [attr.id]="computeTileDomId(it.__key__)"
6104
7609
  [attr.data-widget-key]="it.__key__"
6105
7610
  [attr.data-widget-type]="getWidgetType(it.__key__)"
7611
+ [attr.data-group-count]="widgetGroupingLabels(it.__key__).length"
7612
+ [attr.data-device-count]="widgetDeviceLabels(it.__key__).length"
7613
+ [attr.data-slot-count]="widgetSlotLabels(it.__key__).length"
6106
7614
  [class.selected]="isSelected(it.__key__)"
6107
7615
  (click)="onTileClick(it.__key__, $event)"
6108
7616
  tabindex="0">
@@ -6114,6 +7622,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
6114
7622
  (settings)="openWidgetSettings(it.__key__)"
6115
7623
  (shell)="openWidgetShellSettings(it.__key__)"
6116
7624
  ></praxis-tile-toolbar>
7625
+ <div class="pdx-tile-meta" *ngIf="showSettings() && hasTileMetadata(it.__key__)">
7626
+ <span class="pdx-tile-chip pdx-tile-chip--slot" *ngFor="let label of widgetSlotLabels(it.__key__)">{{ label }}</span>
7627
+ <span class="pdx-tile-chip pdx-tile-chip--group" *ngFor="let label of widgetGroupingLabels(it.__key__)">{{ label }}</span>
7628
+ <span class="pdx-tile-chip pdx-tile-chip--device" *ngFor="let label of widgetDeviceLabels(it.__key__)">{{ label }}</span>
7629
+ </div>
6117
7630
  <praxis-widget-shell
6118
7631
  [shell]="getWidgetShell(it.__key__)"
6119
7632
  [context]="mergedContext"
@@ -6158,7 +7671,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
6158
7671
  [adapter]="aiAdapter">
6159
7672
  </praxis-ai-assistant>
6160
7673
  </praxis-floating-toolbar>
6161
- `, styles: [":host{display:block;position:relative}:host ::ng-deep gridster{position:relative}:host ::ng-deep gridster.editing:before,:host ::ng-deep gridster.interacting:before{content:\"\";position:absolute;inset:0;pointer-events:none;opacity:.6;z-index:0;background-image:radial-gradient(circle at 1px 1px,var(--md-sys-color-outline-variant) 1px,transparent 1px),radial-gradient(circle,var(--md-sys-color-outline-variant) 1px,transparent 1px);background-size:var(--pdx-col-step, 16px) var(--pdx-row-step, 16px),calc(var(--pdx-col-step, 16px) * 4) calc(var(--pdx-row-step, 16px) * 4)}.pdx-gridster-item{position:relative;height:100%}\n"] }]
7674
+ `, styles: [":host{display:block;position:relative}:host ::ng-deep gridster{position:relative}.pdx-canvas-summary{display:grid;gap:8px;margin-bottom:12px;padding:12px 14px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 70%,transparent);border-radius:16px;background:linear-gradient(135deg,color-mix(in srgb,var(--md-sys-color-surface-container-low) 92%,white 8%),color-mix(in srgb,var(--md-sys-color-surface-container) 96%,transparent)),radial-gradient(circle at top right,color-mix(in srgb,var(--md-sys-color-primary) 18%,transparent),transparent 58%)}.pdx-canvas-summary__headline{font-size:12px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--md-sys-color-on-surface-variant)}.pdx-canvas-summary__chips,.pdx-tile-meta{display:flex;flex-wrap:wrap;gap:6px}.pdx-slot-rail{display:grid;gap:8px;padding-top:4px}.pdx-slot-rail__headline{font-size:12px;font-weight:600;color:var(--md-sys-color-on-surface)}.pdx-slot-rail__chips{display:flex;flex-wrap:wrap;gap:8px}.pdx-slot-rail__help{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.pdx-chip,.pdx-tile-chip{display:inline-flex;align-items:center;min-height:24px;padding:0 10px;border-radius:999px;font-size:11px;font-weight:600;letter-spacing:.01em;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 72%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface-container-high) 88%,white 12%);color:var(--md-sys-color-on-surface)}.pdx-chip--preset,.pdx-tile-chip--group{background:color-mix(in srgb,var(--md-sys-color-primary-container) 86%,white 14%);color:var(--md-sys-color-on-primary-container)}.pdx-chip--theme,.pdx-tile-chip--device{background:color-mix(in srgb,var(--md-sys-color-secondary-container) 86%,white 14%);color:var(--md-sys-color-on-secondary-container)}.pdx-tile-chip--slot{background:color-mix(in srgb,var(--md-sys-color-tertiary-container) 86%,white 14%);color:var(--md-sys-color-on-tertiary-container)}.pdx-slot-chip{display:inline-flex;align-items:center;gap:8px;min-height:32px;padding:0 12px;border-radius:999px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 72%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface-container-highest) 82%,white 18%);color:var(--md-sys-color-on-surface);font:inherit;cursor:default;transition:transform .14s ease,box-shadow .14s ease,border-color .14s ease}.pdx-slot-chip--assignable{cursor:pointer;background:color-mix(in srgb,var(--md-sys-color-primary-container) 82%,white 18%);color:var(--md-sys-color-on-primary-container)}.pdx-slot-chip--assignable:hover{transform:translateY(-1px);box-shadow:0 8px 18px color-mix(in srgb,var(--md-sys-color-primary) 16%,transparent)}.pdx-slot-chip--recommended{border-color:color-mix(in srgb,var(--md-sys-color-primary) 56%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--md-sys-color-primary) 28%,transparent)}.pdx-slot-chip__label{font-weight:700}.pdx-slot-chip__meta{font-size:11px;color:inherit;opacity:.82}:host ::ng-deep gridster.editing:before,:host ::ng-deep gridster.interacting:before{content:\"\";position:absolute;inset:0;pointer-events:none;opacity:.6;z-index:0;background-image:radial-gradient(circle at 1px 1px,var(--md-sys-color-outline-variant) 1px,transparent 1px),radial-gradient(circle,var(--md-sys-color-outline-variant) 1px,transparent 1px);background-size:var(--pdx-col-step, 16px) var(--pdx-row-step, 16px),calc(var(--pdx-col-step, 16px) * 4) calc(var(--pdx-row-step, 16px) * 4)}.pdx-gridster-item{position:relative;height:100%}.pdx-tile-meta{position:absolute;top:44px;left:10px;right:10px;z-index:2;pointer-events:none}\n"] }]
6162
7675
  }], ctorParameters: () => [{ type: i1$1.ConnectionManagerService }, { type: i2$3.Router }, { type: i0.ElementRef }, { type: i1.MatDialog }, { type: GraphMapperService }, { type: i1$1.ComponentMetadataRegistry }, { type: undefined, decorators: [{
6163
7676
  type: Inject,
6164
7677
  args: [ASYNC_CONFIG_STORAGE]