@praxisui/page-builder 4.0.0-beta.0 → 6.0.0-beta.0

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.
package/README.md CHANGED
@@ -36,9 +36,11 @@ Peer dependencies (Angular v20):
36
36
  Este pacote expoe o shell ativo do builder canonico de pagina dinamica:
37
37
  - `ComponentPaletteDialogComponent` para adicionar widgets a pagina.
38
38
  - `FloatingToolbarComponent` e `TileToolbarComponent` para acoes rapidas de add/save/preview/configuracao.
39
+ - visualizador inicial de conexoes somente leitura para inspecao de `composition.links`.
39
40
  - `WidgetShellEditorComponent` e `DynamicPageConfigEditorComponent` para authoring de shell/canvas sem redefinir a semantica canonica de composicao.
40
41
 
41
42
  Conexoes continuam pertencendo ao contrato `WidgetPageDefinition`, mas a criacao/manutencao delas nesta fase deve acontecer por JSON/configuracao canonica e nao por builder/graph/editor visual legado.
43
+ O viewer ativo nesta fase existe apenas para inspecao contextual do contrato canonico e nao para editar links inline.
42
44
 
43
45
  ## AI Capabilities Registration
44
46
 
@@ -122,14 +124,14 @@ O builder canonico expoe authoring de shell/palette sobre `praxis-dynamic-page`.
122
124
  </praxis-dynamic-page-builder>
123
125
  ```
124
126
 
125
- `page` e um `WidgetPageDefinition` (do `@praxisui/core`) contendo `widgets` e opcionalmente `connections`. O campo `connections` deve ser preenchido e versionado via JSON canonico.
127
+ `page` e um `WidgetPageDefinition` (do `@praxisui/core`) contendo `widgets` e opcionalmente `composition.links`. O envelope `composition` e o caminho `composition.links` formam a superficie canonica persistida para wiring da pagina, com `condition` em Json Logic e `policy` para comportamento operacional.
126
128
 
127
129
  ## Widget Shell (Dashboard Cards)
128
130
 
129
131
  Cada widget pode declarar um `shell` para renderizar um card padronizado com cabecalho rico, acoes de contexto e controles de janela (expandir/recolher).
130
132
 
131
133
  Quando uma acao e clicada, o shell:
132
- - dispara um evento para o page-builder (`emit`, ou `shell:<id>` por padrao) para uso em `connections`;
134
+ - dispara um evento para o page-builder (`emit`, ou `shell:<id>` por padrao) para uso em `composition.links`;
133
135
  - tenta despachar a acao para o componente interno via metodo `handleShellAction(action)`.
134
136
 
135
137
  ### Presets visuais (global)
@@ -157,7 +159,8 @@ this.settingsPanel.open({
157
159
 
158
160
  ## Dicas
159
161
 
160
- - Trate `connections` como parte do contrato canonico salvo no JSON da pagina.
162
+ - Trate `composition.links` como parte do contrato canonico salvo no JSON da pagina.
163
+ - Use `composition.links[].condition` para guardas semanticas em Json Logic e `composition.links[].policy` para debounce/distinct/missing-value.
161
164
  - Quando precisar revisar ligacoes, prefira inspecao textual/versionada do contrato e do runtime, nao um editor visual legado.
162
165
 
163
166
  ## API (resumo)
@@ -11,9 +11,9 @@ import { MatIconModule } from '@angular/material/icon';
11
11
  import * as i2$2 from '@praxisui/core';
12
12
  import { PraxisIconDirective, BUILTIN_SHELL_PRESETS, SETTINGS_PANEL_DATA as SETTINGS_PANEL_DATA$1, providePraxisI18n, BUILTIN_PAGE_LAYOUT_PRESETS, BUILTIN_PAGE_THEME_PRESETS, PraxisI18nService, generateId, SETTINGS_PANEL_BRIDGE, DynamicWidgetPageComponent, DYNAMIC_PAGE_SHELL_EDITOR } from '@praxisui/core';
13
13
  export { WidgetShellComponent } from '@praxisui/core';
14
- import * as i3$1 from '@angular/material/tooltip';
14
+ import * as i4 from '@angular/material/tooltip';
15
15
  import { MatTooltipModule } from '@angular/material/tooltip';
16
- import * as i3$2 from '@angular/material/form-field';
16
+ import * as i3$1 from '@angular/material/form-field';
17
17
  import { MatFormFieldModule } from '@angular/material/form-field';
18
18
  import * as i9 from '@angular/material/input';
19
19
  import { MatInputModule } from '@angular/material/input';
@@ -119,7 +119,7 @@ class TileToolbarComponent {
119
119
  <mat-icon [praxisIcon]="'delete'"></mat-icon>
120
120
  </button>
121
121
  </div>
122
- `, isInline: true, styles: [":host{position:absolute;inset:0;pointer-events:none}.pdx-tile-toolbar{position:absolute;right:6px;top:-18px;display:flex;gap:6px;z-index:5;opacity:0;transition:opacity .12s ease-in-out;pointer-events:auto;background:var(--md-sys-color-surface-container-low);-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);border-radius:999px;padding:4px;box-shadow:var(--mat-elevation-level3)}.pdx-tile-toolbar.always-visible{opacity:1}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatMiniFabButton, selector: "button[mat-mini-fab], a[mat-mini-fab], button[matMiniFab], a[matMiniFab]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
122
+ `, isInline: true, styles: [":host{position:absolute;inset:0;pointer-events:none}.pdx-tile-toolbar{position:absolute;right:6px;top:-18px;display:flex;gap:6px;z-index:5;opacity:0;transition:opacity .12s ease-in-out;pointer-events:auto;background:var(--md-sys-color-surface-container-low);-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);border-radius:999px;padding:4px;box-shadow:var(--mat-elevation-level3)}.pdx-tile-toolbar.always-visible{opacity:1}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatMiniFabButton, selector: "button[mat-mini-fab], a[mat-mini-fab], button[matMiniFab], a[matMiniFab]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
123
123
  }
124
124
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TileToolbarComponent, decorators: [{
125
125
  type: Component,
@@ -211,7 +211,7 @@ class FloatingToolbarComponent {
211
211
  <ng-content select="[pdx-toolbar-extra]"></ng-content>
212
212
  </div>
213
213
  </div>
214
- `, isInline: true, styles: [":host{position:absolute;inset:0;pointer-events:none}.pdx-floating-toolbar{position:absolute;right:16px;bottom:16px;display:flex;gap:8px;align-items:center;pointer-events:auto}.pdx-actions{display:flex;gap:8px}.pdx-fab{box-shadow:var(--mat-elevation-level4)}@media(max-width:720px){.pdx-actions{gap:6px}.pdx-floating-toolbar{right:12px;bottom:12px}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatMiniFabButton, selector: "button[mat-mini-fab], a[mat-mini-fab], button[matMiniFab], a[matMiniFab]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatFabButton, selector: "button[mat-fab], a[mat-fab], button[matFab], a[matFab]", inputs: ["extended"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
214
+ `, isInline: true, styles: [":host{position:absolute;inset:0;pointer-events:none}.pdx-floating-toolbar{position:absolute;right:16px;bottom:16px;display:flex;gap:8px;align-items:center;pointer-events:auto}.pdx-actions{display:flex;gap:8px}.pdx-fab{box-shadow:var(--mat-elevation-level4)}@media(max-width:720px){.pdx-actions{gap:6px}.pdx-floating-toolbar{right:12px;bottom:12px}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatMiniFabButton, selector: "button[mat-mini-fab], a[mat-mini-fab], button[matMiniFab], a[matMiniFab]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatFabButton, selector: "button[mat-fab], a[mat-fab], button[matFab], a[matFab]", inputs: ["extended"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
215
215
  }
216
216
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: FloatingToolbarComponent, decorators: [{
217
217
  type: Component,
@@ -1516,7 +1516,7 @@ class WidgetShellEditorComponent {
1516
1516
  <div class="shell-empty">Nenhuma ação configurada.</div>
1517
1517
  </ng-template>
1518
1518
  </div>
1519
- `, isInline: true, styles: [".shell-editor{display:grid;gap:12px;padding:16px;background:var(--md-sys-color-surface-container-lowest, var(--md-sys-color-surface));border:1px solid var(--md-sys-color-outline-variant);border-radius:12px}.shell-head{display:flex;align-items:center;gap:12px}.shell-title{font-weight:600}.shell-subtitle{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-section{font-weight:600;margin-top:8px}.shell-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}.shell-flags{display:flex;gap:16px;align-items:center}.shell-actions-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.shell-available{display:grid;gap:12px}.shell-available-toolbar{display:grid;gap:12px;grid-template-columns:minmax(200px,1fr)}.shell-available-filters{display:flex;gap:12px;align-items:center;flex-wrap:wrap}.shell-available-list{display:grid;gap:10px}.shell-available-item{display:grid;gap:8px;grid-template-columns:1fr auto;border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;padding:12px;background:var(--md-sys-color-surface-container-low)}.shell-available-title{display:flex;align-items:center;gap:10px}.shell-available-text{display:grid;gap:4px}.shell-available-label{font-weight:600}.shell-available-meta{display:flex;gap:8px;flex-wrap:wrap;font-size:11px;color:var(--md-sys-color-on-surface-variant)}.shell-available-desc{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-available-payload{font-size:11px;color:var(--md-sys-color-on-surface-variant)}.shell-available-warning{font-size:11px;color:var(--md-sys-color-error)}.shell-available-actions{display:flex;align-items:flex-start;justify-content:flex-end}.shell-badge{padding:2px 6px;border-radius:999px;border:1px solid var(--md-sys-color-outline-variant);font-size:10px}.shell-code{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.shell-actions{display:grid;gap:12px}.shell-action{border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;padding:12px;display:grid;gap:8px}.shell-action-row{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}.shell-action-actions{display:flex;justify-content:flex-end}.shell-action-hint,.shell-empty{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-hint{align-self:center;font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-quick-inputs{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));align-items:end}.shell-input-path{font-size:12px;color:var(--md-sys-color-on-surface-variant);align-self:center}.shell-editor .mat-mdc-form-field{width:100%}.shell-preview{border:1px dashed var(--md-sys-color-outline-variant);border-radius:12px;padding:12px}.shell-preview-card{background:var(--pdx-shell-card-bg, var(--md-sys-color-surface));border:1px solid var(--pdx-shell-card-border, var(--md-sys-color-outline-variant));border-radius:var(--pdx-shell-card-radius, 14px);box-shadow:var(--pdx-shell-card-shadow, var(--mat-elevation-level2));overflow:hidden}.shell-preview-card.no-shell{background:transparent;border:none;box-shadow:none}.shell-preview-card.expanded{box-shadow:var(--pdx-shell-card-shadow, 0 16px 34px rgba(0,0,0,.32));transform:scale(1.01)}.shell-preview-header{display:flex;align-items:center;gap:12px;padding:10px 12px 8px;border-bottom:1px solid var(--pdx-shell-header-border, var(--md-sys-color-outline-variant));background:var(--pdx-shell-header-bg, var(--md-sys-color-surface-container))}.shell-preview-title{display:flex;align-items:center;gap:10px;min-width:0;flex:1;color:var(--pdx-shell-title-color, inherit)}.shell-preview-title mat-icon{color:var(--pdx-shell-icon-color, currentColor)}.shell-preview-text{min-width:0}.shell-preview-title-text{font-weight:var(--pdx-shell-title-weight, 600);font-size:var(--pdx-shell-title-size, 14px);line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.shell-preview-subtitle{font-size:var(--pdx-shell-subtitle-size, 12px);opacity:.75;color:var(--pdx-shell-subtitle-color, currentColor);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.shell-preview-actions{display:flex;align-items:center;gap:6px}.pdx-preview-text{padding:0 8px}.pdx-preview-outlined{border:1px solid var(--md-sys-color-outline-variant);border-radius:999px;padding:0 10px}.pdx-preview-label{font-size:12px;font-weight:500}.shell-preview-body{padding:var(--pdx-shell-body-padding, 10px 12px 12px 12px);background:var(--pdx-shell-body-bg, transparent);color:var(--pdx-shell-body-color, inherit);font-size:12px}.shell-preview-body.hidden{display:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2$1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.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$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i5.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i6.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i3$2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$2.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i9.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6$1.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$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1519
+ `, isInline: true, styles: [".shell-editor{display:grid;gap:12px;padding:16px;background:var(--md-sys-color-surface-container-lowest, var(--md-sys-color-surface));border:1px solid var(--md-sys-color-outline-variant);border-radius:12px}.shell-head{display:flex;align-items:center;gap:12px}.shell-title{font-weight:600}.shell-subtitle{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-section{font-weight:600;margin-top:8px}.shell-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}.shell-flags{display:flex;gap:16px;align-items:center}.shell-actions-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.shell-available{display:grid;gap:12px}.shell-available-toolbar{display:grid;gap:12px;grid-template-columns:minmax(200px,1fr)}.shell-available-filters{display:flex;gap:12px;align-items:center;flex-wrap:wrap}.shell-available-list{display:grid;gap:10px}.shell-available-item{display:grid;gap:8px;grid-template-columns:1fr auto;border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;padding:12px;background:var(--md-sys-color-surface-container-low)}.shell-available-title{display:flex;align-items:center;gap:10px}.shell-available-text{display:grid;gap:4px}.shell-available-label{font-weight:600}.shell-available-meta{display:flex;gap:8px;flex-wrap:wrap;font-size:11px;color:var(--md-sys-color-on-surface-variant)}.shell-available-desc{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-available-payload{font-size:11px;color:var(--md-sys-color-on-surface-variant)}.shell-available-warning{font-size:11px;color:var(--md-sys-color-error)}.shell-available-actions{display:flex;align-items:flex-start;justify-content:flex-end}.shell-badge{padding:2px 6px;border-radius:999px;border:1px solid var(--md-sys-color-outline-variant);font-size:10px}.shell-code{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.shell-actions{display:grid;gap:12px}.shell-action{border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;padding:12px;display:grid;gap:8px}.shell-action-row{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}.shell-action-actions{display:flex;justify-content:flex-end}.shell-action-hint,.shell-empty{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-hint{align-self:center;font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-quick-inputs{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));align-items:end}.shell-input-path{font-size:12px;color:var(--md-sys-color-on-surface-variant);align-self:center}.shell-editor .mat-mdc-form-field{width:100%}.shell-preview{border:1px dashed var(--md-sys-color-outline-variant);border-radius:12px;padding:12px}.shell-preview-card{background:var(--pdx-shell-card-bg, var(--md-sys-color-surface));border:1px solid var(--pdx-shell-card-border, var(--md-sys-color-outline-variant));border-radius:var(--pdx-shell-card-radius, 14px);box-shadow:var(--pdx-shell-card-shadow, var(--mat-elevation-level2));overflow:hidden}.shell-preview-card.no-shell{background:transparent;border:none;box-shadow:none}.shell-preview-card.expanded{box-shadow:var(--pdx-shell-card-shadow, 0 16px 34px rgba(0,0,0,.32));transform:scale(1.01)}.shell-preview-header{display:flex;align-items:center;gap:12px;padding:10px 12px 8px;border-bottom:1px solid var(--pdx-shell-header-border, var(--md-sys-color-outline-variant));background:var(--pdx-shell-header-bg, var(--md-sys-color-surface-container))}.shell-preview-title{display:flex;align-items:center;gap:10px;min-width:0;flex:1;color:var(--pdx-shell-title-color, inherit)}.shell-preview-title mat-icon{color:var(--pdx-shell-icon-color, currentColor)}.shell-preview-text{min-width:0}.shell-preview-title-text{font-weight:var(--pdx-shell-title-weight, 600);font-size:var(--pdx-shell-title-size, 14px);line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.shell-preview-subtitle{font-size:var(--pdx-shell-subtitle-size, 12px);opacity:.75;color:var(--pdx-shell-subtitle-color, currentColor);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.shell-preview-actions{display:flex;align-items:center;gap:6px}.pdx-preview-text{padding:0 8px}.pdx-preview-outlined{border:1px solid var(--md-sys-color-outline-variant);border-radius:999px;padding:0 10px}.pdx-preview-label{font-size:12px;font-weight:500}.shell-preview-body{padding:var(--pdx-shell-body-padding, 10px 12px 12px 12px);background:var(--pdx-shell-body-bg, transparent);color:var(--pdx-shell-body-color, inherit);font-size:12px}.shell-preview-body.hidden{display:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2$1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.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$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i5.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i6.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i9.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6$1.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$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1520
1520
  }
1521
1521
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: WidgetShellEditorComponent, decorators: [{
1522
1522
  type: Component,
@@ -2101,7 +2101,7 @@ class DynamicPageConfigEditorComponent {
2101
2101
  <mat-tab label="Estado da Pagina"><mat-form-field appearance="outline" class="wide"><mat-label>State (JSON)</mat-label><textarea matInput rows="14" [formControl]="stateControl"></textarea></mat-form-field></mat-tab>
2102
2102
  </mat-tab-group>
2103
2103
  </div>
2104
- `, isInline: true, styles: [".editor,.panel{display:grid;gap:16px}.head,.summary{display:flex;gap:12px;align-items:center;flex-wrap:wrap}.grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.card{display:grid;gap:12px;padding:16px;border:1px solid #d8e0ea;border-radius:16px;background:#fbfcfe}.wide{grid-column:1/-1}.hint{color:#5b6472;font-size:12px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.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$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i3$2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$2.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i9.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6$1.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$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i7.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i7.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2104
+ `, isInline: true, styles: [".editor,.panel{display:grid;gap:16px}.head,.summary{display:flex;gap:12px;align-items:center;flex-wrap:wrap}.grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.card{display:grid;gap:12px;padding:16px;border:1px solid #d8e0ea;border-radius:16px;background:#fbfcfe}.wide{grid-column:1/-1}.hint{color:#5b6472;font-size:12px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.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$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i9.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6$1.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$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i7.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i7.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2105
2105
  }
2106
2106
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: DynamicPageConfigEditorComponent, decorators: [{
2107
2107
  type: Component,
@@ -2236,7 +2236,7 @@ const PRAXIS_PAGE_BUILDER_EN_US = {
2236
2236
  'praxis.pageBuilder.editor.state.compute': 'Compute',
2237
2237
  'praxis.pageBuilder.editor.state.compute.operator': 'Operator',
2238
2238
  'praxis.pageBuilder.editor.state.compute.template': 'Template',
2239
- 'praxis.pageBuilder.editor.state.compute.expr': 'Expr',
2239
+ 'praxis.pageBuilder.editor.state.compute.expr': 'Json Logic',
2240
2240
  'praxis.pageBuilder.editor.state.compute.transformer': 'Transformer',
2241
2241
  'praxis.pageBuilder.editor.state.dependsOn': 'DependsOn (comma-separated)',
2242
2242
  'praxis.pageBuilder.editor.state.sourcePath': 'Source path',
@@ -2245,7 +2245,7 @@ const PRAXIS_PAGE_BUILDER_EN_US = {
2245
2245
  'praxis.pageBuilder.editor.state.template': 'Template',
2246
2246
  'praxis.pageBuilder.editor.state.templateJson': 'Template (JSON)',
2247
2247
  'praxis.pageBuilder.editor.state.templateNull': 'The template will return null.',
2248
- 'praxis.pageBuilder.editor.state.expression': 'Expression',
2248
+ 'praxis.pageBuilder.editor.state.expression': 'Json Logic',
2249
2249
  'praxis.pageBuilder.editor.state.transformer': 'Transformer',
2250
2250
  'praxis.pageBuilder.editor.state.optionsJson': 'Options (JSON)',
2251
2251
  'praxis.pageBuilder.editor.apply': 'Apply',
@@ -2262,6 +2262,44 @@ const PRAXIS_PAGE_BUILDER_EN_US = {
2262
2262
  'praxis.pageBuilder.editor.hints.schemaInitial': 'Define the initial value of this state path. This helps the page start in a consistent state even before remote connections and data.',
2263
2263
  'praxis.pageBuilder.editor.hints.templateJson': 'JSON templates are useful to generate complete derived objects, such as texts, summary cards or shell settings.',
2264
2264
  'praxis.pageBuilder.editor.hints.transformerOptions': 'Pass the registered transformer options here. Prefer lean JSON with only the parameters actually used by the runtime.',
2265
+ 'praxis.pageBuilder.connections.toggle': 'Inspect connections',
2266
+ 'praxis.pageBuilder.connections.toggleAria': 'Inspect connections',
2267
+ 'praxis.pageBuilder.connections.title': 'Connections',
2268
+ 'praxis.pageBuilder.connections.subtitle': 'Inspect the canonical composition links without editing the page document.',
2269
+ 'praxis.pageBuilder.connections.panelAria': 'Connections viewer',
2270
+ 'praxis.pageBuilder.connections.summary.widgets': 'Widgets',
2271
+ 'praxis.pageBuilder.connections.summary.links': 'Links',
2272
+ 'praxis.pageBuilder.connections.summary.conditions': 'With condition',
2273
+ 'praxis.pageBuilder.connections.summary.diagnostics': 'Diagnostics',
2274
+ 'praxis.pageBuilder.connections.filters.title': 'Filters',
2275
+ 'praxis.pageBuilder.connections.filters.all': 'All',
2276
+ 'praxis.pageBuilder.connections.filters.condition': 'Condition',
2277
+ 'praxis.pageBuilder.connections.filters.transform': 'Transform',
2278
+ 'praxis.pageBuilder.connections.filters.policy': 'Policy',
2279
+ 'praxis.pageBuilder.connections.filters.diagnostic': 'Diagnostic',
2280
+ 'praxis.pageBuilder.connections.widgets.title': 'Widgets',
2281
+ 'praxis.pageBuilder.connections.widgets.selected': 'Selected widget',
2282
+ 'praxis.pageBuilder.connections.widgets.incoming': 'In',
2283
+ 'praxis.pageBuilder.connections.widgets.outgoing': 'Out',
2284
+ 'praxis.pageBuilder.connections.links.title': 'Links',
2285
+ 'praxis.pageBuilder.connections.badges.condition': 'Condition',
2286
+ 'praxis.pageBuilder.connections.badges.transform': 'Transform',
2287
+ 'praxis.pageBuilder.connections.badges.policy': 'Policy',
2288
+ 'praxis.pageBuilder.connections.badges.diagnostic': 'Diagnostic',
2289
+ 'praxis.pageBuilder.connections.actions.focusSource': 'Focus source',
2290
+ 'praxis.pageBuilder.connections.actions.focusTarget': 'Focus target',
2291
+ 'praxis.pageBuilder.connections.actions.openPageSettings': 'Open page settings',
2292
+ 'praxis.pageBuilder.connections.details.title': 'Selected link',
2293
+ 'praxis.pageBuilder.connections.details.widgetId': 'Widget',
2294
+ 'praxis.pageBuilder.connections.details.componentType': 'Component',
2295
+ 'praxis.pageBuilder.connections.details.intent': 'Intent',
2296
+ 'praxis.pageBuilder.connections.details.from': 'From',
2297
+ 'praxis.pageBuilder.connections.details.to': 'To',
2298
+ 'praxis.pageBuilder.connections.details.condition': 'Condition',
2299
+ 'praxis.pageBuilder.connections.details.transform': 'Transform',
2300
+ 'praxis.pageBuilder.connections.details.policy': 'Policy',
2301
+ 'praxis.pageBuilder.connections.details.diagnostics': 'Diagnostics',
2302
+ 'praxis.pageBuilder.connections.palette.insertComponent': 'Insert component',
2265
2303
  };
2266
2304
 
2267
2305
  const PRAXIS_PAGE_BUILDER_PT_BR = {
@@ -2361,7 +2399,7 @@ const PRAXIS_PAGE_BUILDER_PT_BR = {
2361
2399
  'praxis.pageBuilder.editor.state.compute': 'Computar',
2362
2400
  'praxis.pageBuilder.editor.state.compute.operator': 'Operador',
2363
2401
  'praxis.pageBuilder.editor.state.compute.template': 'Modelo (Template)',
2364
- 'praxis.pageBuilder.editor.state.compute.expr': 'Expressão (Expr)',
2402
+ 'praxis.pageBuilder.editor.state.compute.expr': 'Json Logic',
2365
2403
  'praxis.pageBuilder.editor.state.compute.transformer': 'Transformador',
2366
2404
  'praxis.pageBuilder.editor.state.dependsOn': 'Depende de (vírgula)',
2367
2405
  'praxis.pageBuilder.editor.state.sourcePath': 'Caminho de origem',
@@ -2370,7 +2408,7 @@ const PRAXIS_PAGE_BUILDER_PT_BR = {
2370
2408
  'praxis.pageBuilder.editor.state.template': 'Modelo (Template)',
2371
2409
  'praxis.pageBuilder.editor.state.templateJson': 'Modelo (JSON)',
2372
2410
  'praxis.pageBuilder.editor.state.templateNull': 'O modelo retornará nulo (null).',
2373
- 'praxis.pageBuilder.editor.state.expression': 'Expressão',
2411
+ 'praxis.pageBuilder.editor.state.expression': 'Json Logic',
2374
2412
  'praxis.pageBuilder.editor.state.transformer': 'Transformador',
2375
2413
  'praxis.pageBuilder.editor.state.optionsJson': 'Opções (JSON)',
2376
2414
  'praxis.pageBuilder.editor.apply': 'Aplicar',
@@ -2387,6 +2425,44 @@ const PRAXIS_PAGE_BUILDER_PT_BR = {
2387
2425
  'praxis.pageBuilder.editor.hints.schemaInitial': 'Defina o valor inicial desse caminho de estado. Isso ajuda a página a nascer em um estado consistente mesmo antes de conexões e dados remotos.',
2388
2426
  'praxis.pageBuilder.editor.hints.templateJson': 'Modelos em JSON são úteis para gerar objetos derivados completos, como textos, cartões de resumo ou configurações de shell.',
2389
2427
  'praxis.pageBuilder.editor.hints.transformerOptions': 'Passe aqui as opções do transformador registrado. Prefira JSON enxuto com os parâmetros realmente usados pelo runtime.',
2428
+ 'praxis.pageBuilder.connections.toggle': 'Inspecionar conexões',
2429
+ 'praxis.pageBuilder.connections.toggleAria': 'Inspecionar conexões',
2430
+ 'praxis.pageBuilder.connections.title': 'Conexões',
2431
+ 'praxis.pageBuilder.connections.subtitle': 'Inspecione os links canônicos da composição sem editar o documento da página.',
2432
+ 'praxis.pageBuilder.connections.panelAria': 'Visualizador de conexões',
2433
+ 'praxis.pageBuilder.connections.summary.widgets': 'Widgets',
2434
+ 'praxis.pageBuilder.connections.summary.links': 'Links',
2435
+ 'praxis.pageBuilder.connections.summary.conditions': 'Com condição',
2436
+ 'praxis.pageBuilder.connections.summary.diagnostics': 'Diagnósticos',
2437
+ 'praxis.pageBuilder.connections.filters.title': 'Filtros',
2438
+ 'praxis.pageBuilder.connections.filters.all': 'Todos',
2439
+ 'praxis.pageBuilder.connections.filters.condition': 'Condition',
2440
+ 'praxis.pageBuilder.connections.filters.transform': 'Transform',
2441
+ 'praxis.pageBuilder.connections.filters.policy': 'Policy',
2442
+ 'praxis.pageBuilder.connections.filters.diagnostic': 'Diagnostic',
2443
+ 'praxis.pageBuilder.connections.widgets.title': 'Widgets',
2444
+ 'praxis.pageBuilder.connections.widgets.selected': 'Widget selecionado',
2445
+ 'praxis.pageBuilder.connections.widgets.incoming': 'Entrada',
2446
+ 'praxis.pageBuilder.connections.widgets.outgoing': 'Saída',
2447
+ 'praxis.pageBuilder.connections.links.title': 'Links',
2448
+ 'praxis.pageBuilder.connections.badges.condition': 'Condition',
2449
+ 'praxis.pageBuilder.connections.badges.transform': 'Transform',
2450
+ 'praxis.pageBuilder.connections.badges.policy': 'Policy',
2451
+ 'praxis.pageBuilder.connections.badges.diagnostic': 'Diagnostic',
2452
+ 'praxis.pageBuilder.connections.actions.focusSource': 'Focar origem',
2453
+ 'praxis.pageBuilder.connections.actions.focusTarget': 'Focar destino',
2454
+ 'praxis.pageBuilder.connections.actions.openPageSettings': 'Abrir configurações da página',
2455
+ 'praxis.pageBuilder.connections.details.title': 'Link selecionado',
2456
+ 'praxis.pageBuilder.connections.details.widgetId': 'Widget',
2457
+ 'praxis.pageBuilder.connections.details.componentType': 'Componente',
2458
+ 'praxis.pageBuilder.connections.details.intent': 'Intent',
2459
+ 'praxis.pageBuilder.connections.details.from': 'Origem',
2460
+ 'praxis.pageBuilder.connections.details.to': 'Destino',
2461
+ 'praxis.pageBuilder.connections.details.condition': 'Condition',
2462
+ 'praxis.pageBuilder.connections.details.transform': 'Transform',
2463
+ 'praxis.pageBuilder.connections.details.policy': 'Policy',
2464
+ 'praxis.pageBuilder.connections.details.diagnostics': 'Diagnostics',
2465
+ 'praxis.pageBuilder.connections.palette.insertComponent': 'Inserir componente',
2390
2466
  };
2391
2467
 
2392
2468
  function createPraxisPageBuilderI18nConfig(options = {}) {
@@ -2925,7 +3001,9 @@ class PageConfigEditorComponent {
2925
3001
  }),
2926
3002
  sourcePath: new FormControl(sourcePath, { nonNullable: true }),
2927
3003
  keysCsv: new FormControl(keysCsv, { nonNullable: true }),
2928
- expression: new FormControl(compute?.kind === 'expr' ? compute.expression || '' : '', { nonNullable: true }),
3004
+ expression: new FormControl(compute?.kind === 'json-logic'
3005
+ ? JSON.stringify(compute.expression ?? {}, null, 2)
3006
+ : '', { nonNullable: true }),
2929
3007
  transformerId: new FormControl(compute?.kind === 'transformer' ? compute.transformerId || '' : '', { nonNullable: true }),
2930
3008
  options: new FormControl(compute?.kind === 'transformer'
2931
3009
  ? JSON.stringify(compute.options || {}, null, 2)
@@ -3007,8 +3085,13 @@ class PageConfigEditorComponent {
3007
3085
  return null;
3008
3086
  compute = { kind: 'template', value: templateValue.value };
3009
3087
  }
3010
- else if (computeKind === 'expr') {
3011
- compute = { kind: 'expr', expression: group.controls.expression.value.trim() };
3088
+ else if (computeKind === 'json-logic') {
3089
+ const expression = this.parseJson(group.controls.expression.value, {});
3090
+ if (expression === null
3091
+ || (typeof expression === 'object' && !Array.isArray(expression) && Object.keys(expression).length === 0)) {
3092
+ return null;
3093
+ }
3094
+ compute = { kind: 'json-logic', expression };
3012
3095
  }
3013
3096
  else if (computeKind === 'transformer') {
3014
3097
  const options = this.parseJson(group.controls.options.value, {});
@@ -3769,7 +3852,7 @@ class PageConfigEditorComponent {
3769
3852
  <mat-select formControlName="computeKind">
3770
3853
  <mat-option value="operator">{{ tx('editor.state.compute.operator', 'Operator') }}</mat-option>
3771
3854
  <mat-option value="template">{{ tx('editor.state.compute.template', 'Template') }}</mat-option>
3772
- <mat-option value="expr">{{ tx('editor.state.compute.expr', 'Expr') }}</mat-option>
3855
+ <mat-option value="json-logic">{{ tx('editor.state.compute.expr', 'Json Logic') }}</mat-option>
3773
3856
  <mat-option value="transformer">{{ tx('editor.state.compute.transformer', 'Transformer') }}</mat-option>
3774
3857
  </mat-select>
3775
3858
  </mat-form-field>
@@ -3839,10 +3922,10 @@ class PageConfigEditorComponent {
3839
3922
  </mat-form-field>
3840
3923
  }
3841
3924
  }
3842
- @if (group.controls.computeKind.value === 'expr') {
3925
+ @if (group.controls.computeKind.value === 'json-logic') {
3843
3926
  <mat-form-field appearance="outline" class="span-3">
3844
- <mat-label>{{ tx('editor.state.expression', 'Expression') }}</mat-label>
3845
- <input matInput formControlName="expression" />
3927
+ <mat-label>{{ tx('editor.state.expression', 'Json Logic') }}</mat-label>
3928
+ <textarea matInput rows="4" formControlName="expression"></textarea>
3846
3929
  </mat-form-field>
3847
3930
  }
3848
3931
  @if (group.controls.computeKind.value === 'transformer') {
@@ -3881,7 +3964,7 @@ class PageConfigEditorComponent {
3881
3964
  </div>
3882
3965
  }
3883
3966
  </div>
3884
- `, isInline: true, styles: [".page-config{display:grid;gap:12px;padding:16px;background:var(--md-sys-color-surface-container-lowest, var(--md-sys-color-surface));border:1px solid var(--md-sys-color-outline-variant);border-radius:12px}.page-head{display:flex;align-items:flex-start;gap:12px}.page-head__copy{display:grid;gap:4px}.page-title{font-weight:600;font-size:18px;line-height:1.2}.page-subtitle{max-width:72ch;color:var(--md-sys-color-on-surface-variant);font-size:13px;line-height:1.45}.spacer{flex:1}.page-summary{display:flex;flex-wrap:wrap;gap:8px}.page-tabs{border-radius:16px;overflow:hidden;background:color-mix(in srgb,var(--md-sys-color-surface-container-low) 78%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 82%,transparent)}.page-tabs ::ng-deep .mdc-tab{min-width:120px}.page-tabs ::ng-deep .mdc-tab__text-label{font-size:13px;font-weight:600}.page-tab-panel{display:grid;gap:12px;padding:16px 4px 4px}.page-chip{display:inline-flex;align-items:center;gap:8px;min-height:36px;padding:0 12px;border-radius:999px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 88%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface-container-low) 82%,transparent);color:var(--md-sys-color-on-surface-variant);font-size:12px;font-weight:500}.page-chip mat-icon{width:18px;height:18px;font-size:18px}.page-chip--accent{border-color:color-mix(in srgb,var(--md-sys-color-primary) 35%,var(--md-sys-color-outline-variant));color:var(--md-sys-color-primary);background:color-mix(in srgb,var(--md-sys-color-primary-container) 30%,transparent)}.page-chip--warning{border-color:color-mix(in srgb,var(--md-sys-color-error) 35%,var(--md-sys-color-outline-variant));color:var(--md-sys-color-error);background:color-mix(in srgb,var(--md-sys-color-error-container) 35%,transparent)}.page-identity{display:grid;gap:4px;padding:12px 14px;border-radius:12px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 82%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface-container-low) 70%,transparent);font-size:13px;color:var(--md-sys-color-on-surface-variant)}.page-section{font-weight:600;margin-top:8px}.page-section-help{margin-top:-4px;color:var(--md-sys-color-on-surface-variant);font-size:13px;line-height:1.45}.grid-form{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}.state-grid{display:grid;gap:16px}.state-values,.state-group{padding:14px;border:1px solid var(--md-sys-color-outline-variant);border-radius:14px;background:linear-gradient(180deg,color-mix(in srgb,var(--md-sys-color-surface-container-low) 92%,transparent),transparent),var(--md-sys-color-surface-container-lowest, var(--md-sys-color-surface))}.state-group{display:grid;gap:12px}.state-group-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.state-group-title{font-weight:600}.state-group-help{color:var(--md-sys-color-on-surface-variant);font-size:13px}.state-list{display:grid;gap:12px}.state-card{display:grid;gap:12px;padding:12px;border-radius:12px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 80%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface-container) 84%,transparent)}.state-card-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.state-card-title{font-weight:600}.state-card-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}.span-2{grid-column:span 2}.span-3{grid-column:1 / -1}.state-empty{color:var(--md-sys-color-on-surface-variant);font-size:13px}.state-operator-hint{display:flex;align-items:center;gap:8px;min-height:48px;padding:0 12px;border-radius:12px;color:var(--md-sys-color-on-surface-variant);background:color-mix(in srgb,var(--md-sys-color-secondary-container) 35%,transparent)}.state-operator-hint mat-icon{color:var(--md-sys-color-secondary)}.page-actions{display:flex;justify-content:flex-end}.page-error{color:var(--md-sys-color-error);font-size:13px}.page-config .mat-mdc-form-field{width:100%;--mdc-outlined-text-field-container-height: 48px;--mdc-outlined-text-field-outline-color: var(--md-sys-color-outline-variant);--mdc-outlined-text-field-hover-outline-color: var(--md-sys-color-outline);--mdc-outlined-text-field-focus-outline-color: var(--md-sys-color-primary);--mdc-outlined-text-field-error-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-focus-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-hover-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-label-text-color: var(--md-sys-color-on-surface-variant);--mdc-outlined-text-field-input-text-color: var(--md-sys-color-on-surface);--mdc-outlined-text-field-supporting-text-color: var(--md-sys-color-on-surface-variant)}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;margin-right:-4px}.help-icon-button mat-icon{font-size:18px;width:18px;height:18px}.mat-mdc-form-field-icon-suffix{align-self:center}@media(max-width:720px){.span-2,.span-3{grid-column:auto}.page-head{flex-direction:column}.state-group-head{align-items:stretch;flex-direction:column}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.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$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i3$2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$2.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3$2.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i9.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6$1.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$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i7.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i7.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: MatTooltipModule }, { kind: "directive", type: i3$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3967
+ `, isInline: true, styles: [".page-config{display:grid;gap:12px;padding:16px;background:var(--md-sys-color-surface-container-lowest, var(--md-sys-color-surface));border:1px solid var(--md-sys-color-outline-variant);border-radius:12px}.page-head{display:flex;align-items:flex-start;gap:12px}.page-head__copy{display:grid;gap:4px}.page-title{font-weight:600;font-size:18px;line-height:1.2}.page-subtitle{max-width:72ch;color:var(--md-sys-color-on-surface-variant);font-size:13px;line-height:1.45}.spacer{flex:1}.page-summary{display:flex;flex-wrap:wrap;gap:8px}.page-tabs{border-radius:16px;overflow:hidden;background:color-mix(in srgb,var(--md-sys-color-surface-container-low) 78%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 82%,transparent)}.page-tabs ::ng-deep .mdc-tab{min-width:120px}.page-tabs ::ng-deep .mdc-tab__text-label{font-size:13px;font-weight:600}.page-tab-panel{display:grid;gap:12px;padding:16px 4px 4px}.page-chip{display:inline-flex;align-items:center;gap:8px;min-height:36px;padding:0 12px;border-radius:999px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 88%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface-container-low) 82%,transparent);color:var(--md-sys-color-on-surface-variant);font-size:12px;font-weight:500}.page-chip mat-icon{width:18px;height:18px;font-size:18px}.page-chip--accent{border-color:color-mix(in srgb,var(--md-sys-color-primary) 35%,var(--md-sys-color-outline-variant));color:var(--md-sys-color-primary);background:color-mix(in srgb,var(--md-sys-color-primary-container) 30%,transparent)}.page-chip--warning{border-color:color-mix(in srgb,var(--md-sys-color-error) 35%,var(--md-sys-color-outline-variant));color:var(--md-sys-color-error);background:color-mix(in srgb,var(--md-sys-color-error-container) 35%,transparent)}.page-identity{display:grid;gap:4px;padding:12px 14px;border-radius:12px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 82%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface-container-low) 70%,transparent);font-size:13px;color:var(--md-sys-color-on-surface-variant)}.page-section{font-weight:600;margin-top:8px}.page-section-help{margin-top:-4px;color:var(--md-sys-color-on-surface-variant);font-size:13px;line-height:1.45}.grid-form{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}.state-grid{display:grid;gap:16px}.state-values,.state-group{padding:14px;border:1px solid var(--md-sys-color-outline-variant);border-radius:14px;background:linear-gradient(180deg,color-mix(in srgb,var(--md-sys-color-surface-container-low) 92%,transparent),transparent),var(--md-sys-color-surface-container-lowest, var(--md-sys-color-surface))}.state-group{display:grid;gap:12px}.state-group-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.state-group-title{font-weight:600}.state-group-help{color:var(--md-sys-color-on-surface-variant);font-size:13px}.state-list{display:grid;gap:12px}.state-card{display:grid;gap:12px;padding:12px;border-radius:12px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 80%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface-container) 84%,transparent)}.state-card-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.state-card-title{font-weight:600}.state-card-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}.span-2{grid-column:span 2}.span-3{grid-column:1 / -1}.state-empty{color:var(--md-sys-color-on-surface-variant);font-size:13px}.state-operator-hint{display:flex;align-items:center;gap:8px;min-height:48px;padding:0 12px;border-radius:12px;color:var(--md-sys-color-on-surface-variant);background:color-mix(in srgb,var(--md-sys-color-secondary-container) 35%,transparent)}.state-operator-hint mat-icon{color:var(--md-sys-color-secondary)}.page-actions{display:flex;justify-content:flex-end}.page-error{color:var(--md-sys-color-error);font-size:13px}.page-config .mat-mdc-form-field{width:100%;--mdc-outlined-text-field-container-height: 48px;--mdc-outlined-text-field-outline-color: var(--md-sys-color-outline-variant);--mdc-outlined-text-field-hover-outline-color: var(--md-sys-color-outline);--mdc-outlined-text-field-focus-outline-color: var(--md-sys-color-primary);--mdc-outlined-text-field-error-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-focus-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-hover-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-label-text-color: var(--md-sys-color-on-surface-variant);--mdc-outlined-text-field-input-text-color: var(--md-sys-color-on-surface);--mdc-outlined-text-field-supporting-text-color: var(--md-sys-color-on-surface-variant)}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;margin-right:-4px}.help-icon-button mat-icon{font-size:18px;width:18px;height:18px}.mat-mdc-form-field-icon-suffix{align-self:center}@media(max-width:720px){.span-2,.span-3{grid-column:auto}.page-head{flex-direction:column}.state-group-head{align-items:stretch;flex-direction:column}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.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$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3$1.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i9.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6$1.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$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i7.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i7.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: MatTooltipModule }, { kind: "directive", type: i4.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3885
3968
  }
3886
3969
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PageConfigEditorComponent, decorators: [{
3887
3970
  type: Component,
@@ -4401,7 +4484,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
4401
4484
  <mat-select formControlName="computeKind">
4402
4485
  <mat-option value="operator">{{ tx('editor.state.compute.operator', 'Operator') }}</mat-option>
4403
4486
  <mat-option value="template">{{ tx('editor.state.compute.template', 'Template') }}</mat-option>
4404
- <mat-option value="expr">{{ tx('editor.state.compute.expr', 'Expr') }}</mat-option>
4487
+ <mat-option value="json-logic">{{ tx('editor.state.compute.expr', 'Json Logic') }}</mat-option>
4405
4488
  <mat-option value="transformer">{{ tx('editor.state.compute.transformer', 'Transformer') }}</mat-option>
4406
4489
  </mat-select>
4407
4490
  </mat-form-field>
@@ -4471,10 +4554,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
4471
4554
  </mat-form-field>
4472
4555
  }
4473
4556
  }
4474
- @if (group.controls.computeKind.value === 'expr') {
4557
+ @if (group.controls.computeKind.value === 'json-logic') {
4475
4558
  <mat-form-field appearance="outline" class="span-3">
4476
- <mat-label>{{ tx('editor.state.expression', 'Expression') }}</mat-label>
4477
- <input matInput formControlName="expression" />
4559
+ <mat-label>{{ tx('editor.state.expression', 'Json Logic') }}</mat-label>
4560
+ <textarea matInput rows="4" formControlName="expression"></textarea>
4478
4561
  </mat-form-field>
4479
4562
  }
4480
4563
  @if (group.controls.computeKind.value === 'transformer') {
@@ -4530,8 +4613,9 @@ const PAGE_BUILDER_AI_CAPABILITIES = {
4530
4613
  'praxis-dynamic-page-builder',
4531
4614
  ],
4532
4615
  notes: [
4533
- 'page segue o modelo canonico WidgetPageDefinition (widgets, connections, context e state opcional).',
4616
+ 'page segue o modelo canonico WidgetPageDefinition (widgets, composition.links, context e state opcional).',
4534
4617
  'Canvas e o modelo espacial canonico da pagina quando houver posicionamento explicito.',
4618
+ 'Taxonomia editorial: condition usa Json Logic canonico; transform usa pipeline declarativo; policy cobre apenas comportamento operacional.',
4535
4619
  'Payloads legados de layout ainda podem ser ingeridos, mas sao normalizados para canvas e nao devem ser reemitidos.',
4536
4620
  'pageIdentity identifica o escopo de persistencia e nao pertence ao objeto page.',
4537
4621
  ],
@@ -4573,23 +4657,17 @@ const PAGE_BUILDER_AI_CAPABILITIES = {
4573
4657
  { path: 'page.widgets[].definition.id', category: 'widgets', valueKind: 'string', description: 'ID do componente registrado.' },
4574
4658
  { path: 'page.widgets[].definition.inputs', category: 'widgets', valueKind: 'object', description: 'Inputs do componente.' },
4575
4659
  { path: 'page.widgets[].definition.bindingOrder', category: 'widgets', valueKind: 'array', description: 'Ordem de aplicacao dos inputs.' },
4576
- { path: 'page.connections', category: 'connections', valueKind: 'array', description: 'Conexoes entre widgets.' },
4577
- { path: 'page.connections[].from', category: 'connections', valueKind: 'object', description: 'Origem da conexao.' },
4578
- { path: 'page.connections[].from.widget', category: 'connections', valueKind: 'string', description: 'Widget origem (key).' },
4579
- { path: 'page.connections[].from.output', category: 'connections', valueKind: 'string', description: 'Output do widget origem.' },
4580
- { path: 'page.connections[].from.state', category: 'connections', valueKind: 'string', description: 'Path de state origem quando a conexao parte do estado da pagina.' },
4581
- { path: 'page.connections[].to', category: 'connections', valueKind: 'object', description: 'Destino da conexao.' },
4582
- { path: 'page.connections[].to.widget', category: 'connections', valueKind: 'string', description: 'Widget destino (key).' },
4583
- { path: 'page.connections[].to.input', category: 'connections', valueKind: 'string', description: 'Input destino (path).' },
4584
- { path: 'page.connections[].to.bindingOrder', category: 'connections', valueKind: 'array', description: 'Ordem de aplicacao dos inputs.' },
4585
- { path: 'page.connections[].to.state', category: 'connections', valueKind: 'string', description: 'Path de state destino quando a conexao escreve no estado da pagina.' },
4586
- { path: 'page.connections[].map', category: 'connections', valueKind: 'expression', description: 'Mapeamento do payload.' },
4587
- { path: 'page.connections[].set', category: 'connections', valueKind: 'object', description: 'Overrides fixos aplicados ao destino.' },
4588
- { path: 'page.connections[].meta', category: 'connections', valueKind: 'object', description: 'Metadados opcionais para operadores de runtime.' },
4589
- { path: 'page.connections[].meta.filterExpr', category: 'connections', valueKind: 'expression', description: 'Expressao de filtro para eventos.' },
4590
- { path: 'page.connections[].meta.debounceMs', category: 'connections', valueKind: 'number', description: 'Debounce em milissegundos.' },
4591
- { path: 'page.connections[].meta.distinct', category: 'connections', valueKind: 'boolean', description: 'Emite apenas quando valor mudar.' },
4592
- { path: 'page.connections[].meta.distinctBy', category: 'connections', valueKind: 'expression', description: 'Chave para comparacao distinct.' },
4660
+ { path: 'page.composition', category: 'connections', valueKind: 'object', description: 'Envelope canonico da composicao persistida.' },
4661
+ { path: 'page.composition.version', category: 'connections', valueKind: 'string', description: 'Versao do envelope de composicao.' },
4662
+ { path: 'page.composition.links', category: 'connections', valueKind: 'array', description: 'Links canonicos entre widgets e estado.' },
4663
+ { path: 'page.composition.links[].id', category: 'connections', valueKind: 'string', description: 'Identificador estavel do link.' },
4664
+ { path: 'page.composition.links[].from', category: 'connections', valueKind: 'object', description: 'Endpoint de origem do link.' },
4665
+ { path: 'page.composition.links[].to', category: 'connections', valueKind: 'object', description: 'Endpoint de destino do link.' },
4666
+ { path: 'page.composition.links[].intent', category: 'connections', valueKind: 'string', description: 'Intencao semantica do link.' },
4667
+ { path: 'page.composition.links[].transform', category: 'connections', valueKind: 'object', description: 'Pipeline de transformacao do link.' },
4668
+ { path: 'page.composition.links[].condition', category: 'connections', valueKind: 'expression', description: 'Guarda semantica opcional do link, expressa como um unico AST Json Logic canonico.' },
4669
+ { path: 'page.composition.links[].policy', category: 'connections', valueKind: 'object', description: 'Politicas operacionais opcionais do link, separadas da condition semantica.' },
4670
+ { path: 'page.composition.links[].metadata', category: 'connections', valueKind: 'object', description: 'Metadados opcionais do link.' },
4593
4671
  { path: 'page.context', category: 'context', valueKind: 'object', description: 'Contexto da pagina (compartilhado por widgets).' },
4594
4672
  ],
4595
4673
  };
@@ -4747,9 +4825,501 @@ function providePageBuilderWidgetAiCatalogs() {
4747
4825
  };
4748
4826
  }
4749
4827
 
4828
+ function buildConnectionsViewerModel(page) {
4829
+ const widgets = page?.widgets ?? [];
4830
+ const links = page?.composition?.links ?? [];
4831
+ const counts = new Map();
4832
+ for (const widget of widgets) {
4833
+ counts.set(widget.key, { incoming: 0, outgoing: 0 });
4834
+ }
4835
+ const edges = links.map((link) => {
4836
+ const fromWidgetId = link.from.kind === 'component-port' ? link.from.ref.widget : null;
4837
+ const toWidgetId = link.to.kind === 'component-port' ? link.to.ref.widget : null;
4838
+ const toStatePath = link.to.kind === 'state' ? link.to.ref.path : null;
4839
+ const diagnosticReasons = [];
4840
+ if (fromWidgetId && !counts.has(fromWidgetId)) {
4841
+ diagnosticReasons.push('missing-source-widget');
4842
+ }
4843
+ if (toWidgetId && !counts.has(toWidgetId)) {
4844
+ diagnosticReasons.push('missing-target-widget');
4845
+ }
4846
+ if (fromWidgetId) {
4847
+ const current = counts.get(fromWidgetId) ?? { incoming: 0, outgoing: 0 };
4848
+ current.outgoing += 1;
4849
+ counts.set(fromWidgetId, current);
4850
+ }
4851
+ if (toWidgetId) {
4852
+ const current = counts.get(toWidgetId) ?? { incoming: 0, outgoing: 0 };
4853
+ current.incoming += 1;
4854
+ counts.set(toWidgetId, current);
4855
+ }
4856
+ return {
4857
+ id: link.id,
4858
+ fromKind: link.from.kind,
4859
+ fromWidgetId,
4860
+ fromPort: link.from.kind === 'component-port' ? link.from.ref.port : null,
4861
+ toKind: link.to.kind,
4862
+ toWidgetId,
4863
+ toPort: link.to.kind === 'component-port' ? link.to.ref.port : null,
4864
+ toStatePath,
4865
+ intent: link.intent,
4866
+ hasCondition: !!link.condition,
4867
+ hasTransform: !!link.transform?.steps?.length,
4868
+ hasPolicy: !!link.policy && Object.keys(link.policy).length > 0,
4869
+ hasDiagnostics: diagnosticReasons.length > 0,
4870
+ diagnosticReasons,
4871
+ link,
4872
+ };
4873
+ });
4874
+ const nodes = widgets.map((widget) => {
4875
+ const counter = counts.get(widget.key) ?? { incoming: 0, outgoing: 0 };
4876
+ return {
4877
+ widgetId: widget.key,
4878
+ componentType: widget.definition?.id ?? 'unknown',
4879
+ incomingCount: counter.incoming,
4880
+ outgoingCount: counter.outgoing,
4881
+ };
4882
+ });
4883
+ return {
4884
+ nodes,
4885
+ edges,
4886
+ totalLinks: edges.length,
4887
+ conditionLinks: edges.filter((edge) => edge.hasCondition).length,
4888
+ transformLinks: edges.filter((edge) => edge.hasTransform).length,
4889
+ policyLinks: edges.filter((edge) => edge.hasPolicy).length,
4890
+ diagnosticLinks: edges.filter((edge) => edge.hasDiagnostics).length,
4891
+ };
4892
+ }
4893
+
4894
+ class ConnectionsViewerPanelComponent {
4895
+ i18n = inject(PraxisI18nService);
4896
+ open = input(false, ...(ngDevMode ? [{ debugName: "open" }] : []));
4897
+ page = input(undefined, ...(ngDevMode ? [{ debugName: "page" }] : []));
4898
+ focusWidget = output();
4899
+ openPageSettings = output();
4900
+ filters = [
4901
+ { id: 'all', label: this.tx('connections.filters.all', 'All') },
4902
+ { id: 'condition', label: this.tx('connections.filters.condition', 'Condition') },
4903
+ { id: 'transform', label: this.tx('connections.filters.transform', 'Transform') },
4904
+ { id: 'policy', label: this.tx('connections.filters.policy', 'Policy') },
4905
+ { id: 'diagnostic', label: this.tx('connections.filters.diagnostic', 'Diagnostic') },
4906
+ ];
4907
+ activeFilter = signal('all', ...(ngDevMode ? [{ debugName: "activeFilter" }] : []));
4908
+ selectedWidgetId = signal(null, ...(ngDevMode ? [{ debugName: "selectedWidgetId" }] : []));
4909
+ selectedLinkId = signal(null, ...(ngDevMode ? [{ debugName: "selectedLinkId" }] : []));
4910
+ model = computed(() => buildConnectionsViewerModel(this.page()), ...(ngDevMode ? [{ debugName: "model" }] : []));
4911
+ selectedNode = computed(() => this.model().nodes.find((node) => node.widgetId === this.selectedWidgetId()) ?? null, ...(ngDevMode ? [{ debugName: "selectedNode" }] : []));
4912
+ visibleEdges = computed(() => {
4913
+ const widgetId = this.selectedWidgetId();
4914
+ const filter = this.activeFilter();
4915
+ let edges = this.model().edges;
4916
+ if (widgetId) {
4917
+ edges = edges.filter((edge) => edge.fromWidgetId === widgetId || edge.toWidgetId === widgetId);
4918
+ }
4919
+ if (filter === 'condition') {
4920
+ edges = edges.filter((edge) => edge.hasCondition);
4921
+ }
4922
+ else if (filter === 'transform') {
4923
+ edges = edges.filter((edge) => edge.hasTransform);
4924
+ }
4925
+ else if (filter === 'policy') {
4926
+ edges = edges.filter((edge) => edge.hasPolicy);
4927
+ }
4928
+ else if (filter === 'diagnostic') {
4929
+ edges = edges.filter((edge) => edge.hasDiagnostics);
4930
+ }
4931
+ return edges;
4932
+ }, ...(ngDevMode ? [{ debugName: "visibleEdges" }] : []));
4933
+ selectedEdge = computed(() => this.model().edges.find((edge) => edge.id === this.selectedLinkId()) ?? null, ...(ngDevMode ? [{ debugName: "selectedEdge" }] : []));
4934
+ selectWidget(node) {
4935
+ this.selectedWidgetId.update((current) => (current === node.widgetId ? null : node.widgetId));
4936
+ this.selectedLinkId.set(null);
4937
+ }
4938
+ selectLink(edge) {
4939
+ this.selectedLinkId.update((current) => (current === edge.id ? null : edge.id));
4940
+ }
4941
+ setFilter(filter) {
4942
+ this.activeFilter.set(filter);
4943
+ this.selectedLinkId.set(null);
4944
+ }
4945
+ describeEdge(edge) {
4946
+ return `${this.describeSource(edge)} -> ${this.describeTarget(edge)}`;
4947
+ }
4948
+ describeSource(edge) {
4949
+ if (edge.fromKind === 'component-port') {
4950
+ return `${edge.fromWidgetId}.${edge.fromPort}`;
4951
+ }
4952
+ return edge.fromKind;
4953
+ }
4954
+ describeTarget(edge) {
4955
+ if (edge.toKind === 'component-port') {
4956
+ return `${edge.toWidgetId}.${edge.toPort}`;
4957
+ }
4958
+ return `state.${edge.toStatePath}`;
4959
+ }
4960
+ tx(key, fallback) {
4961
+ return resolvePraxisPageBuilderText(this.i18n, key, fallback);
4962
+ }
4963
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ConnectionsViewerPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4964
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.3.17", type: ConnectionsViewerPanelComponent, isStandalone: true, selector: "praxis-connections-viewer-panel", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, page: { classPropertyName: "page", publicName: "page", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { focusWidget: "focusWidget", openPageSettings: "openPageSettings" }, providers: [providePraxisPageBuilderI18n()], ngImport: i0, template: `
4965
+ <aside
4966
+ *ngIf="open()"
4967
+ class="connections-panel"
4968
+ data-testid="page-builder-connections-panel"
4969
+ [attr.aria-label]="tx('connections.panelAria', 'Connections viewer')"
4970
+ >
4971
+ <div class="connections-panel__header">
4972
+ <div class="connections-panel__title">{{ tx('connections.title', 'Connections') }}</div>
4973
+ <div class="connections-panel__subtitle">
4974
+ {{ tx('connections.subtitle', 'Inspect the canonical composition links without editing the page document.') }}
4975
+ </div>
4976
+ </div>
4977
+
4978
+ <div class="connections-panel__summary">
4979
+ <div class="connections-panel__metric">
4980
+ <span>{{ tx('connections.summary.widgets', 'Widgets') }}</span>
4981
+ <strong>{{ model().nodes.length }}</strong>
4982
+ </div>
4983
+ <div class="connections-panel__metric">
4984
+ <span>{{ tx('connections.summary.links', 'Links') }}</span>
4985
+ <strong>{{ model().totalLinks }}</strong>
4986
+ </div>
4987
+ <div class="connections-panel__metric">
4988
+ <span>{{ tx('connections.summary.conditions', 'With condition') }}</span>
4989
+ <strong>{{ model().conditionLinks }}</strong>
4990
+ </div>
4991
+ <div class="connections-panel__metric">
4992
+ <span>{{ tx('connections.summary.diagnostics', 'Diagnostics') }}</span>
4993
+ <strong>{{ model().diagnosticLinks }}</strong>
4994
+ </div>
4995
+ </div>
4996
+
4997
+ <section class="connections-panel__section">
4998
+ <div class="connections-panel__section-title">{{ tx('connections.filters.title', 'Filters') }}</div>
4999
+ <div class="connections-panel__filters">
5000
+ <button
5001
+ *ngFor="let filter of filters"
5002
+ type="button"
5003
+ class="connections-panel__filter"
5004
+ [class.connections-panel__filter--active]="activeFilter() === filter.id"
5005
+ [attr.data-testid]="'page-builder-connections-filter-' + filter.id"
5006
+ (click)="setFilter(filter.id)"
5007
+ >
5008
+ {{ filter.label }}
5009
+ </button>
5010
+ </div>
5011
+ </section>
5012
+
5013
+ <section class="connections-panel__section">
5014
+ <div class="connections-panel__section-title">{{ tx('connections.widgets.title', 'Widgets') }}</div>
5015
+ <button
5016
+ *ngFor="let node of model().nodes"
5017
+ type="button"
5018
+ [attr.data-testid]="'page-builder-connections-widget-' + node.widgetId"
5019
+ class="connections-panel__item"
5020
+ [class.connections-panel__item--active]="selectedWidgetId() === node.widgetId"
5021
+ (click)="selectWidget(node)"
5022
+ >
5023
+ <div class="connections-panel__item-title">{{ node.widgetId }}</div>
5024
+ <div class="connections-panel__item-copy">{{ node.componentType }}</div>
5025
+ <div class="connections-panel__item-meta">
5026
+ {{ tx('connections.widgets.incoming', 'In') }} {{ node.incomingCount }}
5027
+ |
5028
+ {{ tx('connections.widgets.outgoing', 'Out') }} {{ node.outgoingCount }}
5029
+ </div>
5030
+ </button>
5031
+ </section>
5032
+
5033
+ <section class="connections-panel__section" *ngIf="selectedNode() as node">
5034
+ <div class="connections-panel__section-title">{{ tx('connections.widgets.selected', 'Selected widget') }}</div>
5035
+ <div class="connections-panel__details-row" data-testid="page-builder-connections-selected-widget">
5036
+ <span>{{ tx('connections.details.widgetId', 'Widget') }}</span>
5037
+ <strong>{{ node.widgetId }}</strong>
5038
+ </div>
5039
+ <div class="connections-panel__details-row">
5040
+ <span>{{ tx('connections.details.componentType', 'Component') }}</span>
5041
+ <strong>{{ node.componentType }}</strong>
5042
+ </div>
5043
+ </section>
5044
+
5045
+ <section class="connections-panel__section">
5046
+ <div class="connections-panel__section-title">{{ tx('connections.links.title', 'Links') }}</div>
5047
+ <button
5048
+ *ngFor="let edge of visibleEdges()"
5049
+ type="button"
5050
+ [attr.data-testid]="'page-builder-connections-link-' + edge.id"
5051
+ class="connections-panel__item"
5052
+ [class.connections-panel__item--active]="selectedLinkId() === edge.id"
5053
+ (click)="selectLink(edge)"
5054
+ >
5055
+ <div class="connections-panel__item-title">{{ edge.id }}</div>
5056
+ <div class="connections-panel__item-copy">{{ describeEdge(edge) }}</div>
5057
+ <div class="connections-panel__badges">
5058
+ <span class="connections-panel__badge" *ngIf="edge.hasCondition">
5059
+ {{ tx('connections.badges.condition', 'Condition') }}
5060
+ </span>
5061
+ <span class="connections-panel__badge" *ngIf="edge.hasTransform">
5062
+ {{ tx('connections.badges.transform', 'Transform') }}
5063
+ </span>
5064
+ <span class="connections-panel__badge" *ngIf="edge.hasPolicy">
5065
+ {{ tx('connections.badges.policy', 'Policy') }}
5066
+ </span>
5067
+ <span class="connections-panel__badge connections-panel__badge--warning" *ngIf="edge.hasDiagnostics">
5068
+ {{ tx('connections.badges.diagnostic', 'Diagnostic') }}
5069
+ </span>
5070
+ </div>
5071
+ </button>
5072
+ </section>
5073
+
5074
+ <section
5075
+ class="connections-panel__section"
5076
+ *ngIf="selectedEdge() as edge"
5077
+ data-testid="page-builder-connections-link-details"
5078
+ >
5079
+ <div class="connections-panel__section-title">{{ tx('connections.details.title', 'Selected link') }}</div>
5080
+ <div class="connections-panel__details-row">
5081
+ <span>{{ tx('connections.details.intent', 'Intent') }}</span>
5082
+ <strong>{{ edge.intent }}</strong>
5083
+ </div>
5084
+ <div class="connections-panel__details-row">
5085
+ <span>{{ tx('connections.details.from', 'From') }}</span>
5086
+ <strong>{{ describeSource(edge) }}</strong>
5087
+ </div>
5088
+ <div class="connections-panel__details-row">
5089
+ <span>{{ tx('connections.details.to', 'To') }}</span>
5090
+ <strong>{{ describeTarget(edge) }}</strong>
5091
+ </div>
5092
+ <div class="connections-panel__actions">
5093
+ <button
5094
+ *ngIf="edge.fromWidgetId"
5095
+ type="button"
5096
+ class="connections-panel__action"
5097
+ data-testid="page-builder-connections-focus-source"
5098
+ (click)="focusWidget.emit(edge.fromWidgetId)"
5099
+ >
5100
+ {{ tx('connections.actions.focusSource', 'Focus source') }}
5101
+ </button>
5102
+ <button
5103
+ *ngIf="edge.toWidgetId"
5104
+ type="button"
5105
+ class="connections-panel__action"
5106
+ data-testid="page-builder-connections-focus-target"
5107
+ (click)="focusWidget.emit(edge.toWidgetId)"
5108
+ >
5109
+ {{ tx('connections.actions.focusTarget', 'Focus target') }}
5110
+ </button>
5111
+ <button
5112
+ type="button"
5113
+ class="connections-panel__action"
5114
+ data-testid="page-builder-connections-open-page-settings"
5115
+ (click)="openPageSettings.emit()"
5116
+ >
5117
+ {{ tx('connections.actions.openPageSettings', 'Open page settings') }}
5118
+ </button>
5119
+ </div>
5120
+ <div class="connections-panel__details-block" *ngIf="edge.link.condition">
5121
+ <div class="connections-panel__details-label">{{ tx('connections.details.condition', 'Condition') }}</div>
5122
+ <pre>{{ edge.link.condition | json }}</pre>
5123
+ </div>
5124
+ <div class="connections-panel__details-block" *ngIf="edge.link.transform?.steps?.length">
5125
+ <div class="connections-panel__details-label">{{ tx('connections.details.transform', 'Transform') }}</div>
5126
+ <pre>{{ edge.link.transform | json }}</pre>
5127
+ </div>
5128
+ <div class="connections-panel__details-block" *ngIf="edge.link.policy">
5129
+ <div class="connections-panel__details-label">{{ tx('connections.details.policy', 'Policy') }}</div>
5130
+ <pre>{{ edge.link.policy | json }}</pre>
5131
+ </div>
5132
+ <div class="connections-panel__details-block" *ngIf="edge.hasDiagnostics">
5133
+ <div class="connections-panel__details-label">{{ tx('connections.details.diagnostics', 'Diagnostics') }}</div>
5134
+ <pre>{{ edge.diagnosticReasons | json }}</pre>
5135
+ </div>
5136
+ </section>
5137
+ </aside>
5138
+ `, isInline: true, styles: [":host{position:absolute;inset-block:16px;inset-inline-end:16px;z-index:4;pointer-events:none}.connections-panel{width:min(420px,calc(100vw - 32px));max-height:calc(100vh - 32px);overflow:auto;padding:16px;border-radius:18px;background:#fffffffa;border:1px solid rgba(15,23,42,.08);box-shadow:0 20px 44px #0f172a2e;pointer-events:auto;display:grid;gap:16px}.connections-panel__header,.connections-panel__summary,.connections-panel__section{display:grid;gap:10px}.connections-panel__title{font-size:1rem;font-weight:700;color:#0f172a}.connections-panel__subtitle,.connections-panel__item-copy,.connections-panel__item-meta,.connections-panel__details-row span,.connections-panel__details-label{color:#475569;font-size:.86rem}.connections-panel__summary{grid-template-columns:repeat(4,minmax(0,1fr))}.connections-panel__metric{padding:12px;border-radius:12px;background:#f8fafc;border:1px solid #e2e8f0;display:grid;gap:4px}.connections-panel__metric strong,.connections-panel__details-row strong{color:#0f172a;font-size:.92rem}.connections-panel__section-title{font-size:.78rem;text-transform:uppercase;letter-spacing:.08em;color:#64748b;font-weight:700}.connections-panel__item{text-align:start;padding:12px;border-radius:12px;border:1px solid #e2e8f0;background:#fff;display:grid;gap:6px;cursor:pointer}.connections-panel__item--active{border-color:#2563eb;box-shadow:0 0 0 1px #2563eb1f;background:#eff6ff}.connections-panel__item-title{font-weight:700;color:#0f172a;word-break:break-word}.connections-panel__badges{display:flex;flex-wrap:wrap;gap:6px}.connections-panel__filters,.connections-panel__actions{display:flex;gap:8px;flex-wrap:wrap}.connections-panel__filter{border:1px solid #cbd5e1;background:#fff;color:#334155;border-radius:999px;padding:6px 10px;font-size:.75rem;font-weight:700;cursor:pointer}.connections-panel__filter--active{border-color:#2563eb;background:#dbeafe;color:#1d4ed8}.connections-panel__action{border:1px solid #cbd5e1;background:#fff;color:#334155;border-radius:999px;padding:6px 10px;font-size:.75rem;font-weight:700;cursor:pointer}.connections-panel__action:hover{border-color:#2563eb;color:#1d4ed8}.connections-panel__badge{padding:3px 8px;border-radius:999px;background:#dbeafe;color:#1d4ed8;font-size:.75rem;font-weight:700}.connections-panel__badge--warning{background:#fef3c7;color:#b45309}.connections-panel__details-row,.connections-panel__details-block{display:grid;gap:6px}pre{margin:0;padding:12px;border-radius:12px;background:#0f172a;color:#e2e8f0;overflow:auto;font-size:.75rem}@media(max-width:900px){:host{inset:auto 12px 88px}.connections-panel{width:auto;max-height:min(60vh,520px)}.connections-panel__summary{grid-template-columns:1fr}}\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: "pipe", type: i2$1.JsonPipe, name: "json" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5139
+ }
5140
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ConnectionsViewerPanelComponent, decorators: [{
5141
+ type: Component,
5142
+ args: [{ selector: 'praxis-connections-viewer-panel', standalone: true, imports: [CommonModule], providers: [providePraxisPageBuilderI18n()], template: `
5143
+ <aside
5144
+ *ngIf="open()"
5145
+ class="connections-panel"
5146
+ data-testid="page-builder-connections-panel"
5147
+ [attr.aria-label]="tx('connections.panelAria', 'Connections viewer')"
5148
+ >
5149
+ <div class="connections-panel__header">
5150
+ <div class="connections-panel__title">{{ tx('connections.title', 'Connections') }}</div>
5151
+ <div class="connections-panel__subtitle">
5152
+ {{ tx('connections.subtitle', 'Inspect the canonical composition links without editing the page document.') }}
5153
+ </div>
5154
+ </div>
5155
+
5156
+ <div class="connections-panel__summary">
5157
+ <div class="connections-panel__metric">
5158
+ <span>{{ tx('connections.summary.widgets', 'Widgets') }}</span>
5159
+ <strong>{{ model().nodes.length }}</strong>
5160
+ </div>
5161
+ <div class="connections-panel__metric">
5162
+ <span>{{ tx('connections.summary.links', 'Links') }}</span>
5163
+ <strong>{{ model().totalLinks }}</strong>
5164
+ </div>
5165
+ <div class="connections-panel__metric">
5166
+ <span>{{ tx('connections.summary.conditions', 'With condition') }}</span>
5167
+ <strong>{{ model().conditionLinks }}</strong>
5168
+ </div>
5169
+ <div class="connections-panel__metric">
5170
+ <span>{{ tx('connections.summary.diagnostics', 'Diagnostics') }}</span>
5171
+ <strong>{{ model().diagnosticLinks }}</strong>
5172
+ </div>
5173
+ </div>
5174
+
5175
+ <section class="connections-panel__section">
5176
+ <div class="connections-panel__section-title">{{ tx('connections.filters.title', 'Filters') }}</div>
5177
+ <div class="connections-panel__filters">
5178
+ <button
5179
+ *ngFor="let filter of filters"
5180
+ type="button"
5181
+ class="connections-panel__filter"
5182
+ [class.connections-panel__filter--active]="activeFilter() === filter.id"
5183
+ [attr.data-testid]="'page-builder-connections-filter-' + filter.id"
5184
+ (click)="setFilter(filter.id)"
5185
+ >
5186
+ {{ filter.label }}
5187
+ </button>
5188
+ </div>
5189
+ </section>
5190
+
5191
+ <section class="connections-panel__section">
5192
+ <div class="connections-panel__section-title">{{ tx('connections.widgets.title', 'Widgets') }}</div>
5193
+ <button
5194
+ *ngFor="let node of model().nodes"
5195
+ type="button"
5196
+ [attr.data-testid]="'page-builder-connections-widget-' + node.widgetId"
5197
+ class="connections-panel__item"
5198
+ [class.connections-panel__item--active]="selectedWidgetId() === node.widgetId"
5199
+ (click)="selectWidget(node)"
5200
+ >
5201
+ <div class="connections-panel__item-title">{{ node.widgetId }}</div>
5202
+ <div class="connections-panel__item-copy">{{ node.componentType }}</div>
5203
+ <div class="connections-panel__item-meta">
5204
+ {{ tx('connections.widgets.incoming', 'In') }} {{ node.incomingCount }}
5205
+ |
5206
+ {{ tx('connections.widgets.outgoing', 'Out') }} {{ node.outgoingCount }}
5207
+ </div>
5208
+ </button>
5209
+ </section>
5210
+
5211
+ <section class="connections-panel__section" *ngIf="selectedNode() as node">
5212
+ <div class="connections-panel__section-title">{{ tx('connections.widgets.selected', 'Selected widget') }}</div>
5213
+ <div class="connections-panel__details-row" data-testid="page-builder-connections-selected-widget">
5214
+ <span>{{ tx('connections.details.widgetId', 'Widget') }}</span>
5215
+ <strong>{{ node.widgetId }}</strong>
5216
+ </div>
5217
+ <div class="connections-panel__details-row">
5218
+ <span>{{ tx('connections.details.componentType', 'Component') }}</span>
5219
+ <strong>{{ node.componentType }}</strong>
5220
+ </div>
5221
+ </section>
5222
+
5223
+ <section class="connections-panel__section">
5224
+ <div class="connections-panel__section-title">{{ tx('connections.links.title', 'Links') }}</div>
5225
+ <button
5226
+ *ngFor="let edge of visibleEdges()"
5227
+ type="button"
5228
+ [attr.data-testid]="'page-builder-connections-link-' + edge.id"
5229
+ class="connections-panel__item"
5230
+ [class.connections-panel__item--active]="selectedLinkId() === edge.id"
5231
+ (click)="selectLink(edge)"
5232
+ >
5233
+ <div class="connections-panel__item-title">{{ edge.id }}</div>
5234
+ <div class="connections-panel__item-copy">{{ describeEdge(edge) }}</div>
5235
+ <div class="connections-panel__badges">
5236
+ <span class="connections-panel__badge" *ngIf="edge.hasCondition">
5237
+ {{ tx('connections.badges.condition', 'Condition') }}
5238
+ </span>
5239
+ <span class="connections-panel__badge" *ngIf="edge.hasTransform">
5240
+ {{ tx('connections.badges.transform', 'Transform') }}
5241
+ </span>
5242
+ <span class="connections-panel__badge" *ngIf="edge.hasPolicy">
5243
+ {{ tx('connections.badges.policy', 'Policy') }}
5244
+ </span>
5245
+ <span class="connections-panel__badge connections-panel__badge--warning" *ngIf="edge.hasDiagnostics">
5246
+ {{ tx('connections.badges.diagnostic', 'Diagnostic') }}
5247
+ </span>
5248
+ </div>
5249
+ </button>
5250
+ </section>
5251
+
5252
+ <section
5253
+ class="connections-panel__section"
5254
+ *ngIf="selectedEdge() as edge"
5255
+ data-testid="page-builder-connections-link-details"
5256
+ >
5257
+ <div class="connections-panel__section-title">{{ tx('connections.details.title', 'Selected link') }}</div>
5258
+ <div class="connections-panel__details-row">
5259
+ <span>{{ tx('connections.details.intent', 'Intent') }}</span>
5260
+ <strong>{{ edge.intent }}</strong>
5261
+ </div>
5262
+ <div class="connections-panel__details-row">
5263
+ <span>{{ tx('connections.details.from', 'From') }}</span>
5264
+ <strong>{{ describeSource(edge) }}</strong>
5265
+ </div>
5266
+ <div class="connections-panel__details-row">
5267
+ <span>{{ tx('connections.details.to', 'To') }}</span>
5268
+ <strong>{{ describeTarget(edge) }}</strong>
5269
+ </div>
5270
+ <div class="connections-panel__actions">
5271
+ <button
5272
+ *ngIf="edge.fromWidgetId"
5273
+ type="button"
5274
+ class="connections-panel__action"
5275
+ data-testid="page-builder-connections-focus-source"
5276
+ (click)="focusWidget.emit(edge.fromWidgetId)"
5277
+ >
5278
+ {{ tx('connections.actions.focusSource', 'Focus source') }}
5279
+ </button>
5280
+ <button
5281
+ *ngIf="edge.toWidgetId"
5282
+ type="button"
5283
+ class="connections-panel__action"
5284
+ data-testid="page-builder-connections-focus-target"
5285
+ (click)="focusWidget.emit(edge.toWidgetId)"
5286
+ >
5287
+ {{ tx('connections.actions.focusTarget', 'Focus target') }}
5288
+ </button>
5289
+ <button
5290
+ type="button"
5291
+ class="connections-panel__action"
5292
+ data-testid="page-builder-connections-open-page-settings"
5293
+ (click)="openPageSettings.emit()"
5294
+ >
5295
+ {{ tx('connections.actions.openPageSettings', 'Open page settings') }}
5296
+ </button>
5297
+ </div>
5298
+ <div class="connections-panel__details-block" *ngIf="edge.link.condition">
5299
+ <div class="connections-panel__details-label">{{ tx('connections.details.condition', 'Condition') }}</div>
5300
+ <pre>{{ edge.link.condition | json }}</pre>
5301
+ </div>
5302
+ <div class="connections-panel__details-block" *ngIf="edge.link.transform?.steps?.length">
5303
+ <div class="connections-panel__details-label">{{ tx('connections.details.transform', 'Transform') }}</div>
5304
+ <pre>{{ edge.link.transform | json }}</pre>
5305
+ </div>
5306
+ <div class="connections-panel__details-block" *ngIf="edge.link.policy">
5307
+ <div class="connections-panel__details-label">{{ tx('connections.details.policy', 'Policy') }}</div>
5308
+ <pre>{{ edge.link.policy | json }}</pre>
5309
+ </div>
5310
+ <div class="connections-panel__details-block" *ngIf="edge.hasDiagnostics">
5311
+ <div class="connections-panel__details-label">{{ tx('connections.details.diagnostics', 'Diagnostics') }}</div>
5312
+ <pre>{{ edge.diagnosticReasons | json }}</pre>
5313
+ </div>
5314
+ </section>
5315
+ </aside>
5316
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{position:absolute;inset-block:16px;inset-inline-end:16px;z-index:4;pointer-events:none}.connections-panel{width:min(420px,calc(100vw - 32px));max-height:calc(100vh - 32px);overflow:auto;padding:16px;border-radius:18px;background:#fffffffa;border:1px solid rgba(15,23,42,.08);box-shadow:0 20px 44px #0f172a2e;pointer-events:auto;display:grid;gap:16px}.connections-panel__header,.connections-panel__summary,.connections-panel__section{display:grid;gap:10px}.connections-panel__title{font-size:1rem;font-weight:700;color:#0f172a}.connections-panel__subtitle,.connections-panel__item-copy,.connections-panel__item-meta,.connections-panel__details-row span,.connections-panel__details-label{color:#475569;font-size:.86rem}.connections-panel__summary{grid-template-columns:repeat(4,minmax(0,1fr))}.connections-panel__metric{padding:12px;border-radius:12px;background:#f8fafc;border:1px solid #e2e8f0;display:grid;gap:4px}.connections-panel__metric strong,.connections-panel__details-row strong{color:#0f172a;font-size:.92rem}.connections-panel__section-title{font-size:.78rem;text-transform:uppercase;letter-spacing:.08em;color:#64748b;font-weight:700}.connections-panel__item{text-align:start;padding:12px;border-radius:12px;border:1px solid #e2e8f0;background:#fff;display:grid;gap:6px;cursor:pointer}.connections-panel__item--active{border-color:#2563eb;box-shadow:0 0 0 1px #2563eb1f;background:#eff6ff}.connections-panel__item-title{font-weight:700;color:#0f172a;word-break:break-word}.connections-panel__badges{display:flex;flex-wrap:wrap;gap:6px}.connections-panel__filters,.connections-panel__actions{display:flex;gap:8px;flex-wrap:wrap}.connections-panel__filter{border:1px solid #cbd5e1;background:#fff;color:#334155;border-radius:999px;padding:6px 10px;font-size:.75rem;font-weight:700;cursor:pointer}.connections-panel__filter--active{border-color:#2563eb;background:#dbeafe;color:#1d4ed8}.connections-panel__action{border:1px solid #cbd5e1;background:#fff;color:#334155;border-radius:999px;padding:6px 10px;font-size:.75rem;font-weight:700;cursor:pointer}.connections-panel__action:hover{border-color:#2563eb;color:#1d4ed8}.connections-panel__badge{padding:3px 8px;border-radius:999px;background:#dbeafe;color:#1d4ed8;font-size:.75rem;font-weight:700}.connections-panel__badge--warning{background:#fef3c7;color:#b45309}.connections-panel__details-row,.connections-panel__details-block{display:grid;gap:6px}pre{margin:0;padding:12px;border-radius:12px;background:#0f172a;color:#e2e8f0;overflow:auto;font-size:.75rem}@media(max-width:900px){:host{inset:auto 12px 88px}.connections-panel{width:auto;max-height:min(60vh,520px)}.connections-panel__summary{grid-template-columns:1fr}}\n"] }]
5317
+ }], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], page: [{ type: i0.Input, args: [{ isSignal: true, alias: "page", required: false }] }], focusWidget: [{ type: i0.Output, args: ["focusWidget"] }], openPageSettings: [{ type: i0.Output, args: ["openPageSettings"] }] } });
5318
+
4750
5319
  class DynamicPageBuilderComponent {
4751
5320
  dialog;
4752
5321
  settingsPanel;
5322
+ i18n = inject(PraxisI18nService);
4753
5323
  runtime;
4754
5324
  page;
4755
5325
  context = null;
@@ -4761,6 +5331,7 @@ class DynamicPageBuilderComponent {
4761
5331
  pageEditorComponent = DynamicPageConfigEditorComponent;
4762
5332
  pageChange = new EventEmitter();
4763
5333
  currentPage = signal({ widgets: [] }, ...(ngDevMode ? [{ debugName: "currentPage" }] : []));
5334
+ connectionsViewerOpen = signal(false, ...(ngDevMode ? [{ debugName: "connectionsViewerOpen" }] : []));
4764
5335
  previewMode = false;
4765
5336
  constructor(dialog, settingsPanel) {
4766
5337
  this.dialog = dialog;
@@ -4781,11 +5352,17 @@ class DynamicPageBuilderComponent {
4781
5352
  }
4782
5353
  togglePreview() {
4783
5354
  this.previewMode = !this.previewMode;
5355
+ if (this.previewMode) {
5356
+ this.connectionsViewerOpen.set(false);
5357
+ }
5358
+ }
5359
+ toggleConnectionsViewer() {
5360
+ this.connectionsViewerOpen.update((current) => !current);
4784
5361
  }
4785
5362
  onAddComponent() {
4786
5363
  const ref = this.dialog.open(ComponentPaletteDialogComponent, {
4787
5364
  width: '720px',
4788
- data: { title: 'Inserir componente' },
5365
+ data: { title: this.tx('connections.palette.insertComponent', 'Insert component') },
4789
5366
  });
4790
5367
  ref.afterClosed().subscribe((type) => {
4791
5368
  if (type)
@@ -4825,6 +5402,11 @@ class DynamicPageBuilderComponent {
4825
5402
  this.currentPage.set(cloned);
4826
5403
  this.pageChange.emit(cloned);
4827
5404
  }
5405
+ focusCanvasWidget(widgetKey) {
5406
+ if (!widgetKey)
5407
+ return;
5408
+ this.runtime?.selectCanvasWidget(widgetKey);
5409
+ }
4828
5410
  parsePage(input) {
4829
5411
  if (!input)
4830
5412
  return undefined;
@@ -4841,73 +5423,118 @@ class DynamicPageBuilderComponent {
4841
5423
  clonePage(page) {
4842
5424
  return JSON.parse(JSON.stringify(page));
4843
5425
  }
5426
+ tx(key, fallback) {
5427
+ return resolvePraxisPageBuilderText(this.i18n, key, fallback);
5428
+ }
4844
5429
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: DynamicPageBuilderComponent, deps: [{ token: i1.MatDialog }, { token: SETTINGS_PANEL_BRIDGE, optional: true }], target: i0.ɵɵFactoryTarget.Component });
4845
5430
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", type: DynamicPageBuilderComponent, isStandalone: true, selector: "praxis-dynamic-page-builder", inputs: { page: "page", context: "context", strictValidation: "strictValidation", enableCustomization: "enableCustomization", showSettingsButton: "showSettingsButton", pageIdentity: "pageIdentity", componentInstanceId: "componentInstanceId", pageEditorComponent: "pageEditorComponent" }, outputs: { pageChange: "pageChange" }, providers: [
5431
+ providePraxisPageBuilderI18n(),
4846
5432
  { provide: DYNAMIC_PAGE_SHELL_EDITOR, useValue: WidgetShellEditorComponent },
4847
5433
  ], viewQueries: [{ propertyName: "runtime", first: true, predicate: ["runtime"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
4848
- <praxis-dynamic-page
4849
- #runtime
4850
- [page]="currentPage()"
4851
- [context]="context"
4852
- [strictValidation]="strictValidation"
4853
- [enableCustomization]="showSettings()"
4854
- [showPageSettingsButton]="false"
4855
- [pageIdentity]="pageIdentity"
4856
- [componentInstanceId]="componentInstanceId"
4857
- [pageEditorComponent]="pageEditorComponent"
4858
- (pageChange)="onRuntimePageChange($event)"
4859
- />
5434
+ <div class="builder-shell">
5435
+ <praxis-dynamic-page
5436
+ #runtime
5437
+ [page]="currentPage()"
5438
+ [context]="context"
5439
+ [strictValidation]="strictValidation"
5440
+ [enableCustomization]="showSettings()"
5441
+ [showPageSettingsButton]="false"
5442
+ [pageIdentity]="pageIdentity"
5443
+ [componentInstanceId]="componentInstanceId"
5444
+ [pageEditorComponent]="pageEditorComponent"
5445
+ (pageChange)="onRuntimePageChange($event)"
5446
+ />
5447
+
5448
+ <praxis-connections-viewer-panel
5449
+ [open]="showSettings() && connectionsViewerOpen()"
5450
+ [page]="currentPage()"
5451
+ (focusWidget)="focusCanvasWidget($event)"
5452
+ (openPageSettings)="openPageSettings()"
5453
+ />
4860
5454
 
4861
- <praxis-floating-toolbar
4862
- [visible]="showSettings()"
4863
- [canUndo]="false"
4864
- [canRedo]="false"
4865
- (add)="onAddComponent()"
4866
- (settings)="openPageSettings()"
4867
- (save)="saveCurrentPage()"
4868
- (preview)="togglePreview()"
4869
- />
4870
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: DynamicWidgetPageComponent, selector: "praxis-dynamic-page", inputs: ["page", "context", "strictValidation", "enableCustomization", "showPageSettingsButton", "shellEditorComponent", "pageEditorComponent", "autoPersist", "pageIdentity", "componentInstanceId"], outputs: ["pageChange", "widgetDiagnosticsChange"] }, { kind: "component", type: FloatingToolbarComponent, selector: "praxis-floating-toolbar", inputs: ["visible", "canUndo", "canRedo"], outputs: ["add", "undo", "redo", "settings", "preview", "save"] }] });
5455
+ <praxis-floating-toolbar
5456
+ [visible]="showSettings()"
5457
+ [canUndo]="false"
5458
+ [canRedo]="false"
5459
+ (add)="onAddComponent()"
5460
+ (settings)="openPageSettings()"
5461
+ (save)="saveCurrentPage()"
5462
+ (preview)="togglePreview()"
5463
+ >
5464
+ <button
5465
+ pdx-toolbar-extra
5466
+ mat-mini-fab
5467
+ type="button"
5468
+ [attr.data-testid]="'page-builder-connections-toggle'"
5469
+ [matTooltip]="tx('connections.toggle', 'Inspect connections')"
5470
+ [attr.aria-label]="tx('connections.toggleAria', 'Inspect connections')"
5471
+ (click)="toggleConnectionsViewer()"
5472
+ >
5473
+ <mat-icon [praxisIcon]="'hub'"></mat-icon>
5474
+ </button>
5475
+ </praxis-floating-toolbar>
5476
+ </div>
5477
+ `, isInline: true, styles: [":host,.builder-shell{display:block;position:relative;min-height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatMiniFabButton, selector: "button[mat-mini-fab], a[mat-mini-fab], button[matMiniFab], a[matMiniFab]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "component", type: DynamicWidgetPageComponent, selector: "praxis-dynamic-page", inputs: ["page", "context", "strictValidation", "enableCustomization", "showPageSettingsButton", "shellEditorComponent", "pageEditorComponent", "autoPersist", "pageIdentity", "componentInstanceId"], outputs: ["pageChange", "widgetDiagnosticsChange"] }, { kind: "component", type: FloatingToolbarComponent, selector: "praxis-floating-toolbar", inputs: ["visible", "canUndo", "canRedo"], outputs: ["add", "undo", "redo", "settings", "preview", "save"] }, { kind: "component", type: ConnectionsViewerPanelComponent, selector: "praxis-connections-viewer-panel", inputs: ["open", "page"], outputs: ["focusWidget", "openPageSettings"] }] });
4871
5478
  }
4872
5479
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: DynamicPageBuilderComponent, decorators: [{
4873
5480
  type: Component,
4874
- args: [{
4875
- selector: 'praxis-dynamic-page-builder',
4876
- standalone: true,
4877
- imports: [
5481
+ args: [{ selector: 'praxis-dynamic-page-builder', standalone: true, imports: [
4878
5482
  CommonModule,
5483
+ MatButtonModule,
5484
+ MatIconModule,
5485
+ MatTooltipModule,
5486
+ PraxisIconDirective,
4879
5487
  DynamicWidgetPageComponent,
4880
5488
  FloatingToolbarComponent,
4881
- ComponentPaletteDialogComponent,
4882
- ],
4883
- providers: [
5489
+ ConnectionsViewerPanelComponent,
5490
+ ], providers: [
5491
+ providePraxisPageBuilderI18n(),
4884
5492
  { provide: DYNAMIC_PAGE_SHELL_EDITOR, useValue: WidgetShellEditorComponent },
4885
- ],
4886
- template: `
4887
- <praxis-dynamic-page
4888
- #runtime
4889
- [page]="currentPage()"
4890
- [context]="context"
4891
- [strictValidation]="strictValidation"
4892
- [enableCustomization]="showSettings()"
4893
- [showPageSettingsButton]="false"
4894
- [pageIdentity]="pageIdentity"
4895
- [componentInstanceId]="componentInstanceId"
4896
- [pageEditorComponent]="pageEditorComponent"
4897
- (pageChange)="onRuntimePageChange($event)"
4898
- />
5493
+ ], template: `
5494
+ <div class="builder-shell">
5495
+ <praxis-dynamic-page
5496
+ #runtime
5497
+ [page]="currentPage()"
5498
+ [context]="context"
5499
+ [strictValidation]="strictValidation"
5500
+ [enableCustomization]="showSettings()"
5501
+ [showPageSettingsButton]="false"
5502
+ [pageIdentity]="pageIdentity"
5503
+ [componentInstanceId]="componentInstanceId"
5504
+ [pageEditorComponent]="pageEditorComponent"
5505
+ (pageChange)="onRuntimePageChange($event)"
5506
+ />
4899
5507
 
4900
- <praxis-floating-toolbar
4901
- [visible]="showSettings()"
4902
- [canUndo]="false"
4903
- [canRedo]="false"
4904
- (add)="onAddComponent()"
4905
- (settings)="openPageSettings()"
4906
- (save)="saveCurrentPage()"
4907
- (preview)="togglePreview()"
4908
- />
4909
- `,
4910
- }]
5508
+ <praxis-connections-viewer-panel
5509
+ [open]="showSettings() && connectionsViewerOpen()"
5510
+ [page]="currentPage()"
5511
+ (focusWidget)="focusCanvasWidget($event)"
5512
+ (openPageSettings)="openPageSettings()"
5513
+ />
5514
+
5515
+ <praxis-floating-toolbar
5516
+ [visible]="showSettings()"
5517
+ [canUndo]="false"
5518
+ [canRedo]="false"
5519
+ (add)="onAddComponent()"
5520
+ (settings)="openPageSettings()"
5521
+ (save)="saveCurrentPage()"
5522
+ (preview)="togglePreview()"
5523
+ >
5524
+ <button
5525
+ pdx-toolbar-extra
5526
+ mat-mini-fab
5527
+ type="button"
5528
+ [attr.data-testid]="'page-builder-connections-toggle'"
5529
+ [matTooltip]="tx('connections.toggle', 'Inspect connections')"
5530
+ [attr.aria-label]="tx('connections.toggleAria', 'Inspect connections')"
5531
+ (click)="toggleConnectionsViewer()"
5532
+ >
5533
+ <mat-icon [praxisIcon]="'hub'"></mat-icon>
5534
+ </button>
5535
+ </praxis-floating-toolbar>
5536
+ </div>
5537
+ `, styles: [":host,.builder-shell{display:block;position:relative;min-height:100%}\n"] }]
4911
5538
  }], ctorParameters: () => [{ type: i1.MatDialog }, { type: undefined, decorators: [{
4912
5539
  type: Optional
4913
5540
  }, {
package/index.d.ts CHANGED
@@ -599,6 +599,7 @@ declare function providePageBuilderWidgetAiCatalogs(): Provider;
599
599
  declare class DynamicPageBuilderComponent implements OnChanges {
600
600
  private dialog;
601
601
  private settingsPanel;
602
+ private readonly i18n;
602
603
  runtime?: DynamicWidgetPageComponent;
603
604
  page?: WidgetPageDefinition | string;
604
605
  context?: Record<string, unknown> | null;
@@ -610,18 +611,22 @@ declare class DynamicPageBuilderComponent implements OnChanges {
610
611
  pageEditorComponent: Type<any> | undefined;
611
612
  pageChange: EventEmitter<WidgetPageDefinition>;
612
613
  readonly currentPage: i0.WritableSignal<WidgetPageDefinition>;
614
+ readonly connectionsViewerOpen: i0.WritableSignal<boolean>;
613
615
  private previewMode;
614
616
  constructor(dialog: MatDialog, settingsPanel: SettingsPanelBridge | null);
615
617
  ngOnChanges(changes: SimpleChanges): void;
616
618
  showSettings(): boolean;
617
619
  onRuntimePageChange(next: WidgetPageDefinition): void;
618
620
  togglePreview(): void;
621
+ toggleConnectionsViewer(): void;
619
622
  onAddComponent(): void;
620
623
  addWidget(type: string): void;
621
624
  openPageSettings(): void;
622
625
  saveCurrentPage(): void;
626
+ focusCanvasWidget(widgetKey: string): void;
623
627
  private parsePage;
624
628
  private clonePage;
629
+ tx(key: string, fallback: string): string;
625
630
  static ɵfac: i0.ɵɵFactoryDeclaration<DynamicPageBuilderComponent, [null, { optional: true; }]>;
626
631
  static ɵcmp: i0.ɵɵComponentDeclaration<DynamicPageBuilderComponent, "praxis-dynamic-page-builder", never, { "page": { "alias": "page"; "required": false; }; "context": { "alias": "context"; "required": false; }; "strictValidation": { "alias": "strictValidation"; "required": false; }; "enableCustomization": { "alias": "enableCustomization"; "required": false; }; "showSettingsButton": { "alias": "showSettingsButton"; "required": false; }; "pageIdentity": { "alias": "pageIdentity"; "required": false; }; "componentInstanceId": { "alias": "componentInstanceId"; "required": false; }; "pageEditorComponent": { "alias": "pageEditorComponent"; "required": false; }; }, { "pageChange": "pageChange"; }, never, never, true, never>;
627
632
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxisui/page-builder",
3
- "version": "4.0.0-beta.0",
3
+ "version": "6.0.0-beta.0",
4
4
  "description": "Page and widget builder utilities for Praxis UI (grid, dynamic widgets, editors).",
5
5
  "peerDependencies": {
6
6
  "@angular/common": "^20.0.0",
@@ -8,8 +8,8 @@
8
8
  "@angular/forms": "^20.0.0",
9
9
  "@angular/cdk": "^20.0.0",
10
10
  "@angular/material": "^20.0.0",
11
- "@praxisui/core": "^4.0.0-beta.0",
12
- "@praxisui/settings-panel": "^4.0.0-beta.0"
11
+ "@praxisui/core": "^6.0.0-beta.0",
12
+ "@praxisui/settings-panel": "^6.0.0-beta.0"
13
13
  },
14
14
  "dependencies": {
15
15
  "tslib": "^2.3.0"