@praxisui/page-builder 10.0.0-rc.37 → 10.0.0-rc.39

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.
@@ -7,7 +7,7 @@ import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
7
7
  import * as i2$1 from '@angular/material/icon';
8
8
  import { MatIconModule } from '@angular/material/icon';
9
9
  import * as i2$2 from '@praxisui/core';
10
- import { PraxisIconDirective, providePraxisI18n, BUILTIN_WIDGET_SHELL_PRESETS, SETTINGS_EDITOR_SECTIONS_HOSTED, PraxisI18nService, getWidgetShellActionIdError, WidgetShellComponent, SettingsSectionTabsComponent, PraxisIconButtonComponent, NestedWidgetConfigAccessor, GLOBAL_ACTION_CATALOG, ComponentMetadataRegistry, CompositionValidatorService, getGlobalActionCatalog, PRAXIS_GLOBAL_ACTION_CATALOG, SurfaceOpenActionEditorComponent, BUILTIN_PAGE_LAYOUT_PRESETS, resolveWidgetPageLayoutPreset, BUILTIN_PAGE_THEME_PRESETS, prepareCanvasArrangement, SETTINGS_PANEL_DATA as SETTINGS_PANEL_DATA$1, ResourceDiscoveryService, WidgetPageDataSourceRuntimeService, WidgetPageStateRuntimeService, applyDeclarativeValuePipe, validateCanvasArrangement, ResourceQuickConnectComponent, PraxisJsonLogicService, canonicalJsonSha256, ComponentRuntimeProfileService, buildApiUrl, deepMerge, generateId, ObservabilityDashboardService, PraxisRuntimeComponentObservationRegistryService, PraxisCustomizationModeService, domainKnowledgeTimelineToRichContentDocument, DomainRuleService, DomainKnowledgeService, SETTINGS_PANEL_BRIDGE, DynamicWidgetPageComponent, DYNAMIC_PAGE_SHELL_EDITOR } from '@praxisui/core';
10
+ import { PraxisIconDirective, providePraxisI18n, BUILTIN_WIDGET_SHELL_PRESETS, SETTINGS_EDITOR_SECTIONS_HOSTED, PraxisI18nService, getWidgetShellActionIdError, WidgetShellComponent, SettingsSectionTabsComponent, PraxisIconButtonComponent, NestedWidgetConfigAccessor, GLOBAL_ACTION_CATALOG, ComponentMetadataRegistry, CompositionValidatorService, getGlobalActionCatalog, PRAXIS_GLOBAL_ACTION_CATALOG, SurfaceOpenActionEditorComponent, BUILTIN_PAGE_LAYOUT_PRESETS, resolveWidgetPageLayoutPreset, BUILTIN_PAGE_THEME_PRESETS, prepareCanvasArrangement, SETTINGS_PANEL_DATA as SETTINGS_PANEL_DATA$1, ResourceDiscoveryService, WidgetPageDataSourceRuntimeService, WidgetPageStateRuntimeService, applyDeclarativeValuePipe, mapCatalogFieldsToMetadata, humanizeCatalogFieldName, validateCanvasArrangement, ResourceQuickConnectComponent, PraxisJsonLogicService, canonicalJsonSha256, ComponentRuntimeProfileService, buildApiUrl, deepMerge, generateId, ObservabilityDashboardService, PraxisRuntimeComponentObservationRegistryService, PraxisCustomizationModeService, domainKnowledgeTimelineToRichContentDocument, DomainRuleService, DomainKnowledgeService, SETTINGS_PANEL_BRIDGE, DynamicWidgetPageComponent, DYNAMIC_PAGE_SHELL_EDITOR, IconPickerService, createEmptyNavigationGroupsAuthoringDocument, GOVERNED_PAGE_DEFAULT_ALLOWED_ICONS, upsertNavigationGroupEntry, UiLayoutLifecycleContractError, UiLayoutLifecycleHttpError, decodePageAuthoringDocument, decodeNavigationGroupsAuthoringDocument, buildGovernedPageTarget, buildPublishedPageIdentity, validateRuntimeNavigationMenuNodes, validatePageAuthoringDocument, uiLayoutJsonIdentity, upsertPageNavigationBinding, materializeGovernedPagePreview } from '@praxisui/core';
11
11
  import * as i3 from '@angular/material/tooltip';
12
12
  import { MatTooltipModule } from '@angular/material/tooltip';
13
13
  import { A11yModule, CdkTrapFocus } from '@angular/cdk/a11y';
@@ -19,21 +19,24 @@ import * as i4 from '@angular/material/button-toggle';
19
19
  import { MatButtonToggleModule } from '@angular/material/button-toggle';
20
20
  import * as i5 from '@angular/material/checkbox';
21
21
  import { MatCheckboxModule } from '@angular/material/checkbox';
22
- import * as i7 from '@angular/material/form-field';
22
+ import * as i3$1 from '@angular/material/form-field';
23
23
  import { MatFormFieldModule } from '@angular/material/form-field';
24
- import * as i9 from '@angular/material/input';
24
+ import * as i4$1 from '@angular/material/input';
25
25
  import { MatInputModule } from '@angular/material/input';
26
- import * as i3$1 from '@angular/material/select';
26
+ import * as i3$2 from '@angular/material/select';
27
27
  import { MatSelectModule } from '@angular/material/select';
28
- import { BehaviorSubject, merge, timeout, map, catchError, throwError, firstValueFrom, from, share, concatMap, of, timer, takeUntil, Subject, tap, filter, take, isObservable } from 'rxjs';
28
+ import { BehaviorSubject, merge, timeout, map, catchError, throwError, firstValueFrom, from, share, concatMap, of, timer, takeUntil, Subject, tap, filter, take, isObservable, Observable } from 'rxjs';
29
29
  import { SETTINGS_PANEL_DATA } from '@praxisui/settings-panel';
30
30
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
31
31
  import * as i2$3 from '@angular/material/autocomplete';
32
32
  import { MatAutocompleteModule } from '@angular/material/autocomplete';
33
- import * as i9$1 from '@angular/material/tabs';
33
+ import * as i9 from '@angular/material/tabs';
34
34
  import { MatTabsModule } from '@angular/material/tabs';
35
35
  import { HttpClient, HttpHeaders } from '@angular/common/http';
36
36
  import { AgenticAuthoringTurnClientService, withAuthoringScopePolicyContextHints, toPraxisAssistantConversationMessages, PraxisAssistantTurnOrchestratorService, PraxisAssistantSessionRegistryService, createPraxisAssistantViewportLayout, PraxisAiAssistantShellComponent } from '@praxisui/ai';
37
+ import { RouterLink } from '@angular/router';
38
+ import { validateDynamicFormPageWidget, DYNAMIC_FORM_GOVERNED_PAGE_PREVIEW_ADAPTER } from '@praxisui/dynamic-form';
39
+ import { validateTablePageWidget, TABLE_GOVERNED_PAGE_PREVIEW_ADAPTER } from '@praxisui/table';
37
40
 
38
41
  const PLACEHOLDER = 1;
39
42
 
@@ -1550,6 +1553,19 @@ const PRAXIS_PAGE_BUILDER_EN_US = {
1550
1553
  'praxis.pageBuilder.editor.state.syncDataSourcesToState': "Sync target paths to state.values",
1551
1554
  'praxis.pageBuilder.studio.composer.groupFieldHint': "Choose the boolean or categorical field used in summary indicators.",
1552
1555
  'praxis.pageBuilder.studio.composer.statsUnsupportedHint': "This resource does not publish stats/group-by aggregation; summary indicators were disabled automatically.",
1556
+ 'praxis.pageBuilder.saveAs.title': 'Page name',
1557
+ 'praxis.pageBuilder.saveAs.menu': 'Menu name',
1558
+ 'praxis.pageBuilder.saveAs.menuHint': 'Use an existing menu name or create a new one.',
1559
+ 'praxis.pageBuilder.saveAs.icon': 'Choose menu icon',
1560
+ 'praxis.pageBuilder.saveAs.save': 'Save as new page',
1561
+ 'praxis.pageBuilder.saveAs.saving': 'Saving…',
1562
+ 'praxis.pageBuilder.saveAs.saved': 'Page and menu saved.',
1563
+ 'praxis.pageBuilder.saveAs.open': 'Open saved page',
1564
+ 'praxis.pageBuilder.saveAs.failed': 'Could not confirm the saved page. Review the destination and try again.',
1565
+ 'praxis.pageBuilder.saveAs.invalidIcon': 'Choose a Material Icons name without a family prefix.',
1566
+ 'praxis.pageBuilder.saveAs.invalidName': 'Enter a valid name within the allowed length.',
1567
+ 'praxis.pageBuilder.saveAs.copy': 'Create page copy',
1568
+ 'praxis.pageBuilder.saveAs.copyFailed': 'Could not prepare a copy of this page.',
1553
1569
  };
1554
1570
 
1555
1571
  const PRAXIS_PAGE_BUILDER_PT_BR = {
@@ -2740,6 +2756,19 @@ const PRAXIS_PAGE_BUILDER_PT_BR = {
2740
2756
  'praxis.pageBuilder.editor.state.syncDataSourcesToState': "Sincronizar targetPaths em state.values",
2741
2757
  'praxis.pageBuilder.studio.composer.groupFieldHint': "Escolha o campo booleano ou categórico usado nos indicadores de resumo.",
2742
2758
  'praxis.pageBuilder.studio.composer.statsUnsupportedHint': "Este recurso não publica agregação stats/group-by; os indicadores de resumo foram desativados automaticamente.",
2759
+ 'praxis.pageBuilder.saveAs.title': 'Nome da página',
2760
+ 'praxis.pageBuilder.saveAs.menu': 'Nome do menu',
2761
+ 'praxis.pageBuilder.saveAs.menuHint': 'Use o nome de um menu existente ou crie outro.',
2762
+ 'praxis.pageBuilder.saveAs.icon': 'Escolher ícone do menu',
2763
+ 'praxis.pageBuilder.saveAs.save': 'Salvar como nova página',
2764
+ 'praxis.pageBuilder.saveAs.saving': 'Salvando…',
2765
+ 'praxis.pageBuilder.saveAs.saved': 'Página e menu salvos.',
2766
+ 'praxis.pageBuilder.saveAs.open': 'Abrir página salva',
2767
+ 'praxis.pageBuilder.saveAs.failed': 'Não foi possível confirmar a página salva. Confira o destino e tente novamente.',
2768
+ 'praxis.pageBuilder.saveAs.invalidIcon': 'Escolha um nome Material Icons sem prefixo de família.',
2769
+ 'praxis.pageBuilder.saveAs.invalidName': 'Informe um nome válido dentro do limite permitido.',
2770
+ 'praxis.pageBuilder.saveAs.copy': 'Criar cópia da página',
2771
+ 'praxis.pageBuilder.saveAs.copyFailed': 'Não foi possível preparar a cópia desta página.',
2743
2772
  };
2744
2773
 
2745
2774
  function createPraxisPageBuilderI18nConfig(options = {}) {
@@ -5590,7 +5619,7 @@ class WidgetShellEditorComponent {
5590
5619
  </div>
5591
5620
  </details>
5592
5621
  </div>
5593
- `, isInline: true, styles: [":host{display:block;container:shell-editor / inline-size}.shell-effective-style{display:grid;gap:4px;margin:0;font-size:12px}.shell-effective-style span{color:var(--md-sys-color-on-surface-variant)}.shell-topic-panel,.shell-topic{display:grid;gap:var(--pfx-spacing-4, 16px)}.shell-topic-panel{padding-block:var(--pfx-spacing-4, 16px);min-width:0}.shell-topic-panel[hidden]{display:none}.shell-topic-panel>.shell-grid{margin-block-end:var(--pfx-spacing-4, 16px)}.shell-link-summary{display:grid;gap:var(--pfx-spacing-1, 4px);padding-block:var(--pfx-spacing-3, 12px);overflow-wrap:anywhere}.shell-button-task{display:grid;gap:var(--pfx-spacing-4, 16px);min-width:0}.shell-button-count{margin:0;font-size:14px}.shell-action-technical,.shell-action-scopes{min-width:0;font-size:12px}.shell-action-technical summary,.shell-action-scopes summary{cursor:pointer;padding-block:8px}.shell-action-technical summary:focus-visible,.shell-action-scopes summary:focus-visible{outline:2px solid var(--md-sys-color-primary);outline-offset:2px}.shell-action-contract{display:grid;grid-template-columns:auto minmax(0,1fr);gap:8px}.shell-action-contract dd{margin:0;overflow-wrap:anywhere}.shell-action-technical .shell-action-row{margin-top:12px}.shell-editor{display:grid;grid-template-columns:minmax(0,1fr);min-width:0;gap:20px;padding:12px 4px 20px}.shell-controls{display:grid;gap:var(--pfx-spacing-4, 16px);min-width:0;container:shell-controls / inline-size}.shell-preview-region{min-width:0}.shell-highlight-control{display:grid;gap:8px;min-width:0;grid-column:1 / -1}.shell-highlight-label{font-size:13px;font-weight:600}.shell-highlight-options{display:flex;flex-wrap:wrap;width:fit-content;max-width:100%}.shell-highlight-options mat-button-toggle{min-width:0}.shell-highlight-control p{margin:0;overflow-wrap:anywhere}@container shell-controls (max-width: 480px){.shell-controls .shell-grid,.shell-controls .shell-action-row,.shell-controls .shell-quick-inputs,.shell-controls .shell-available-item{grid-template-columns:minmax(0,1fr)}.shell-controls .shell-available-item>button{justify-self:start}}.shell-intro{margin:0;max-width:68ch;color:var(--md-sys-color-on-surface-variant);font-size:14px;line-height:1.5}.shell-details{border-top:1px solid var(--md-sys-color-outline-variant);padding-top:var(--pfx-spacing-4, 16px)}.shell-details summary{cursor:pointer;color:var(--md-sys-color-on-surface);font-weight:600;line-height:1.5}.shell-details summary:focus-visible{outline:2px solid var(--md-sys-color-primary);outline-offset:4px}.shell-option-content{display:grid;gap:var(--pfx-spacing-4, 16px);margin-top:var(--pfx-spacing-4, 16px);min-width:0}.shell-details>.shell-grid{margin-top:var(--pfx-spacing-4, 16px)}.shell-details>.shell-section{margin:var(--pfx-spacing-4, 16px) 0 var(--pfx-spacing-2, 8px)}.shell-validation{color:var(--md-sys-color-error);margin:0;line-height:1.5}.shell-section{margin:4px 0 -8px;color:var(--md-sys-color-on-surface);font-size:14px;font-weight:650;line-height:1.4}.shell-grid{display:grid;gap:12px 16px;grid-template-columns:repeat(2,minmax(0,1fr));align-items:start}.shell-field--wide{grid-column:1 / -1}.shell-preset-preview__body{display:grid;align-content:start;height:100%;color:var(--md-sys-color-on-surface-variant)}.shell-preset-preview__body .shell-mini-value,.shell-preset-preview__body .shell-mini-line{background:currentColor}.shell-preset-copy strong{display:flex;align-items:baseline;justify-content:space-between;gap:var(--pfx-spacing-2, 8px)}.shell-preset-check{color:var(--md-sys-color-primary);flex-shrink:0}.shell-preset-field,.shell-preset-copy{display:grid}.shell-preset-field{gap:8px}.shell-preset-label{color:var(--md-sys-color-on-surface);font-size:13px;font-weight:600}.shell-preset-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px}.shell-preset-option{min-width:0;display:grid;grid-template-columns:72px minmax(0,1fr);align-items:center;gap:10px;padding:10px;border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;background:var(--md-sys-color-surface);color:var(--md-sys-color-on-surface);text-align:left;cursor:pointer}.shell-preset-option:hover{background:var(--md-sys-color-surface-container-low)}.shell-preset-option:focus-visible{outline:2px solid var(--md-sys-color-primary);outline-offset:2px}.shell-preset-option--selected{border-color:var(--md-sys-color-primary);box-shadow:0 0 0 1px var(--md-sys-color-primary)}.shell-preset-preview{box-sizing:border-box;display:block;height:64px;overflow:hidden;border:1px solid var(--md-sys-color-outline-variant);background:var(--shell-mini-bg, var(--md-sys-color-surface));-webkit-backdrop-filter:var(--shell-mini-filter, none);backdrop-filter:var(--shell-mini-filter, none)}@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))){.shell-preset-preview--backdrop{background:var(--shell-mini-fallback, var(--md-sys-color-surface))}}@media(prefers-reduced-transparency:reduce){.shell-preset-preview--backdrop{background:var(--shell-mini-fallback, var(--md-sys-color-surface));-webkit-backdrop-filter:none;backdrop-filter:none}}.shell-preset-preview__header{display:flex;align-items:center;gap:4px;padding-inline:8px;height:22px;border-bottom:1px solid transparent}.shell-preset-preview--none{border-style:dashed;border-radius:8px;background:transparent;box-shadow:none}.shell-preset-copy{gap:3px}.shell-preset-copy small{color:var(--md-sys-color-on-surface-variant);font-size:11px;line-height:1.35}.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:minmax(0,1fr) auto;border-bottom:1px solid var(--md-sys-color-outline-variant);padding-block:12px;align-items:start;min-width:0}.shell-available-main{min-width:0;overflow-wrap:anywhere}.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-action-availability{font-size:12px;line-height:1.5;padding:8px 10px;border-inline-start:2px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface-container);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-top:1px solid var(--md-sys-color-outline-variant);padding-block:16px;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{margin:0;line-height:1.5;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%;min-width:0}:host{display:block;min-width:0}.shell-editor>*,.shell-action>*{min-width:0}.shell-available-text,.shell-input-path{min-width:0;overflow-wrap:anywhere}.shell-preview-region{min-width:0;background:var(--md-sys-color-surface)}.shell-preview-content{display:grid;gap:12px;padding-top:12px}.shell-preview-heading{cursor:pointer;padding-block:8px}.shell-preview-heading:before{content:\"\\25b8\"}.shell-preview-region[open]>.shell-preview-heading:before{content:\"\\25be\"}.shell-preview-heading:focus-visible{outline:2px solid var(--md-sys-color-primary);outline-offset:2px}.shell-preview-heading,.shell-override-summary{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px}.shell-preview-heading strong{margin-right:auto;font-size:14px}.shell-preview-heading>span,.shell-override-summary{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-preview{padding:16px;background:var(--md-sys-color-surface-container-low);min-width:0}.shell-sample-content{display:grid;gap:8px}.shell-sample-content>span{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-sample-content strong{font-size:32px;line-height:1.15;font-variant-numeric:tabular-nums}.shell-mini-title,.shell-mini-action,.shell-mini-value,.shell-mini-line{display:block;background:var(--md-sys-color-on-surface-variant);opacity:.45;border-radius:2px}.shell-mini-title{width:24px;height:3px}.shell-mini-action{width:4px;height:4px;margin-left:auto}.shell-mini-value{width:20px;height:8px;margin:8px 8px 4px;opacity:.7}.shell-mini-line{width:32px;height:3px;margin-inline:8px}@media(max-width:640px){.shell-editor{padding-inline:0}.shell-grid,.shell-action-row,.shell-quick-inputs,.shell-available-toolbar{grid-template-columns:minmax(0,1fr)}.shell-actions-head,.shell-flags{flex-wrap:wrap}.shell-field--wide{grid-column:auto}}\n"], dependencies: [{ kind: "component", type: WidgetShellComponent, selector: "praxis-widget-shell", inputs: ["shell", "context", "dragSurfaceEnabled", "dragSurfaceLabel"], outputs: ["action", "dragSurfacePointerDown", "dragSurfaceKeydown"] }, { kind: "ngmodule", type: CommonModule }, { 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],[formArray],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: "ngmodule", type: MatButtonToggleModule }, { kind: "directive", type: i4.MatButtonToggleGroup, selector: "mat-button-toggle-group", inputs: ["appearance", "name", "vertical", "value", "multiple", "disabled", "disabledInteractive", "hideSingleSelectionIndicator", "hideMultipleSelectionIndicator"], outputs: ["valueChange", "change"], exportAs: ["matButtonToggleGroup"] }, { kind: "component", type: i4.MatButtonToggle, selector: "mat-button-toggle", inputs: ["aria-label", "aria-labelledby", "id", "name", "value", "tabIndex", "disableRipple", "appearance", "checked", "disabled", "disabledInteractive"], outputs: ["change"], exportAs: ["matButtonToggle"] }, { 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: MatTooltipModule }, { kind: "directive", type: i3.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i7.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i7.MatLabel, selector: "mat-label" }, { kind: "directive", type: i7.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i7.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i7.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$1.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: i3$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: i3$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: SettingsSectionTabsComponent, selector: "praxis-settings-section-tabs", inputs: ["sections", "selectedId", "label", "invalidLabel", "panelId"], outputs: ["selectedIdChange"] }, { kind: "component", type: PraxisIconButtonComponent, selector: "button[praxisIconButton]", inputs: ["praxisIconButton", "size", "appearance", "presentation", "pressed", "busy"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5622
+ `, isInline: true, styles: [":host{display:block;container:shell-editor / inline-size}.shell-effective-style{display:grid;gap:4px;margin:0;font-size:12px}.shell-effective-style span{color:var(--md-sys-color-on-surface-variant)}.shell-topic-panel,.shell-topic{display:grid;gap:var(--pfx-spacing-4, 16px)}.shell-topic-panel{padding-block:var(--pfx-spacing-4, 16px);min-width:0}.shell-topic-panel[hidden]{display:none}.shell-topic-panel>.shell-grid{margin-block-end:var(--pfx-spacing-4, 16px)}.shell-link-summary{display:grid;gap:var(--pfx-spacing-1, 4px);padding-block:var(--pfx-spacing-3, 12px);overflow-wrap:anywhere}.shell-button-task{display:grid;gap:var(--pfx-spacing-4, 16px);min-width:0}.shell-button-count{margin:0;font-size:14px}.shell-action-technical,.shell-action-scopes{min-width:0;font-size:12px}.shell-action-technical summary,.shell-action-scopes summary{cursor:pointer;padding-block:8px}.shell-action-technical summary:focus-visible,.shell-action-scopes summary:focus-visible{outline:2px solid var(--md-sys-color-primary);outline-offset:2px}.shell-action-contract{display:grid;grid-template-columns:auto minmax(0,1fr);gap:8px}.shell-action-contract dd{margin:0;overflow-wrap:anywhere}.shell-action-technical .shell-action-row{margin-top:12px}.shell-editor{display:grid;grid-template-columns:minmax(0,1fr);min-width:0;gap:20px;padding:12px 4px 20px}.shell-controls{display:grid;gap:var(--pfx-spacing-4, 16px);min-width:0;container:shell-controls / inline-size}.shell-preview-region{min-width:0}.shell-highlight-control{display:grid;gap:8px;min-width:0;grid-column:1 / -1}.shell-highlight-label{font-size:13px;font-weight:600}.shell-highlight-options{display:flex;flex-wrap:wrap;width:fit-content;max-width:100%}.shell-highlight-options mat-button-toggle{min-width:0}.shell-highlight-control p{margin:0;overflow-wrap:anywhere}@container shell-controls (max-width: 480px){.shell-controls .shell-grid,.shell-controls .shell-action-row,.shell-controls .shell-quick-inputs,.shell-controls .shell-available-item{grid-template-columns:minmax(0,1fr)}.shell-controls .shell-available-item>button{justify-self:start}}.shell-intro{margin:0;max-width:68ch;color:var(--md-sys-color-on-surface-variant);font-size:14px;line-height:1.5}.shell-details{border-top:1px solid var(--md-sys-color-outline-variant);padding-top:var(--pfx-spacing-4, 16px)}.shell-details summary{cursor:pointer;color:var(--md-sys-color-on-surface);font-weight:600;line-height:1.5}.shell-details summary:focus-visible{outline:2px solid var(--md-sys-color-primary);outline-offset:4px}.shell-option-content{display:grid;gap:var(--pfx-spacing-4, 16px);margin-top:var(--pfx-spacing-4, 16px);min-width:0}.shell-details>.shell-grid{margin-top:var(--pfx-spacing-4, 16px)}.shell-details>.shell-section{margin:var(--pfx-spacing-4, 16px) 0 var(--pfx-spacing-2, 8px)}.shell-validation{color:var(--md-sys-color-error);margin:0;line-height:1.5}.shell-section{margin:4px 0 -8px;color:var(--md-sys-color-on-surface);font-size:14px;font-weight:650;line-height:1.4}.shell-grid{display:grid;gap:12px 16px;grid-template-columns:repeat(2,minmax(0,1fr));align-items:start}.shell-field--wide{grid-column:1 / -1}.shell-preset-preview__body{display:grid;align-content:start;height:100%;color:var(--md-sys-color-on-surface-variant)}.shell-preset-preview__body .shell-mini-value,.shell-preset-preview__body .shell-mini-line{background:currentColor}.shell-preset-copy strong{display:flex;align-items:baseline;justify-content:space-between;gap:var(--pfx-spacing-2, 8px)}.shell-preset-check{color:var(--md-sys-color-primary);flex-shrink:0}.shell-preset-field,.shell-preset-copy{display:grid}.shell-preset-field{gap:8px}.shell-preset-label{color:var(--md-sys-color-on-surface);font-size:13px;font-weight:600}.shell-preset-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px}.shell-preset-option{min-width:0;display:grid;grid-template-columns:72px minmax(0,1fr);align-items:center;gap:10px;padding:10px;border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;background:var(--md-sys-color-surface);color:var(--md-sys-color-on-surface);text-align:left;cursor:pointer}.shell-preset-option:hover{background:var(--md-sys-color-surface-container-low)}.shell-preset-option:focus-visible{outline:2px solid var(--md-sys-color-primary);outline-offset:2px}.shell-preset-option--selected{border-color:var(--md-sys-color-primary);box-shadow:0 0 0 1px var(--md-sys-color-primary)}.shell-preset-preview{box-sizing:border-box;display:block;height:64px;overflow:hidden;border:1px solid var(--md-sys-color-outline-variant);background:var(--shell-mini-bg, var(--md-sys-color-surface));-webkit-backdrop-filter:var(--shell-mini-filter, none);backdrop-filter:var(--shell-mini-filter, none)}@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))){.shell-preset-preview--backdrop{background:var(--shell-mini-fallback, var(--md-sys-color-surface))}}@media(prefers-reduced-transparency:reduce){.shell-preset-preview--backdrop{background:var(--shell-mini-fallback, var(--md-sys-color-surface));-webkit-backdrop-filter:none;backdrop-filter:none}}.shell-preset-preview__header{display:flex;align-items:center;gap:4px;padding-inline:8px;height:22px;border-bottom:1px solid transparent}.shell-preset-preview--none{border-style:dashed;border-radius:8px;background:transparent;box-shadow:none}.shell-preset-copy{gap:3px}.shell-preset-copy small{color:var(--md-sys-color-on-surface-variant);font-size:11px;line-height:1.35}.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:minmax(0,1fr) auto;border-bottom:1px solid var(--md-sys-color-outline-variant);padding-block:12px;align-items:start;min-width:0}.shell-available-main{min-width:0;overflow-wrap:anywhere}.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-action-availability{font-size:12px;line-height:1.5;padding:8px 10px;border-inline-start:2px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface-container);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-top:1px solid var(--md-sys-color-outline-variant);padding-block:16px;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{margin:0;line-height:1.5;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%;min-width:0}:host{display:block;min-width:0}.shell-editor>*,.shell-action>*{min-width:0}.shell-available-text,.shell-input-path{min-width:0;overflow-wrap:anywhere}.shell-preview-region{min-width:0;background:var(--md-sys-color-surface)}.shell-preview-content{display:grid;gap:12px;padding-top:12px}.shell-preview-heading{cursor:pointer;padding-block:8px}.shell-preview-heading:before{content:\"\\25b8\"}.shell-preview-region[open]>.shell-preview-heading:before{content:\"\\25be\"}.shell-preview-heading:focus-visible{outline:2px solid var(--md-sys-color-primary);outline-offset:2px}.shell-preview-heading,.shell-override-summary{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px}.shell-preview-heading strong{margin-right:auto;font-size:14px}.shell-preview-heading>span,.shell-override-summary{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-preview{padding:16px;background:var(--md-sys-color-surface-container-low);min-width:0}.shell-sample-content{display:grid;gap:8px}.shell-sample-content>span{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-sample-content strong{font-size:32px;line-height:1.15;font-variant-numeric:tabular-nums}.shell-mini-title,.shell-mini-action,.shell-mini-value,.shell-mini-line{display:block;background:var(--md-sys-color-on-surface-variant);opacity:.45;border-radius:2px}.shell-mini-title{width:24px;height:3px}.shell-mini-action{width:4px;height:4px;margin-left:auto}.shell-mini-value{width:20px;height:8px;margin:8px 8px 4px;opacity:.7}.shell-mini-line{width:32px;height:3px;margin-inline:8px}@media(max-width:640px){.shell-editor{padding-inline:0}.shell-grid,.shell-action-row,.shell-quick-inputs,.shell-available-toolbar{grid-template-columns:minmax(0,1fr)}.shell-actions-head,.shell-flags{flex-wrap:wrap}.shell-field--wide{grid-column:auto}}\n"], dependencies: [{ kind: "component", type: WidgetShellComponent, selector: "praxis-widget-shell", inputs: ["shell", "context", "dragSurfaceEnabled", "dragSurfaceLabel"], outputs: ["action", "dragSurfacePointerDown", "dragSurfaceKeydown"] }, { kind: "ngmodule", type: CommonModule }, { 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],[formArray],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: "ngmodule", type: MatButtonToggleModule }, { kind: "directive", type: i4.MatButtonToggleGroup, selector: "mat-button-toggle-group", inputs: ["appearance", "name", "vertical", "value", "multiple", "disabled", "disabledInteractive", "hideSingleSelectionIndicator", "hideMultipleSelectionIndicator"], outputs: ["valueChange", "change"], exportAs: ["matButtonToggleGroup"] }, { kind: "component", type: i4.MatButtonToggle, selector: "mat-button-toggle", inputs: ["aria-label", "aria-labelledby", "id", "name", "value", "tabIndex", "disableRipple", "appearance", "checked", "disabled", "disabledInteractive"], outputs: ["change"], exportAs: ["matButtonToggle"] }, { 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: MatTooltipModule }, { kind: "directive", type: i3.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { 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.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i3$1.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3$1.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i4$1.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: i3$2.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: i3$2.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: SettingsSectionTabsComponent, selector: "praxis-settings-section-tabs", inputs: ["sections", "selectedId", "label", "invalidLabel", "panelId"], outputs: ["selectedIdChange"] }, { kind: "component", type: PraxisIconButtonComponent, selector: "button[praxisIconButton]", inputs: ["praxisIconButton", "size", "appearance", "presentation", "pressed", "busy"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5594
5623
  }
5595
5624
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: WidgetShellEditorComponent, decorators: [{
5596
5625
  type: Component,
@@ -16952,11 +16981,49 @@ class DynamicPageConfigEditorComponent {
16952
16981
  }
16953
16982
  }
16954
16983
  const normalizedPath = resourcePath.replace(/^\/+|\/+$/g, '');
16955
- const fallbackFields = (entry?.responseFields || []).map((f) => ({
16956
- name: f.name,
16957
- label: f.label || f.name,
16958
- type: f.type || 'string',
16959
- }));
16984
+ const mappedMetas = mapCatalogFieldsToMetadata(entry?.responseFields);
16985
+ const metaByName = new Map(mappedMetas.map((meta) => [meta.name, meta]));
16986
+ const fallbackFields = (entry?.responseFields || [])
16987
+ .filter((f) => Boolean(f?.name && String(f.name).trim()))
16988
+ .map((f) => {
16989
+ const name = String(f.name).trim();
16990
+ const rawLabel = typeof f.label === 'string' ? f.label.trim() : '';
16991
+ const label = rawLabel && rawLabel !== name
16992
+ ? rawLabel
16993
+ : humanizeCatalogFieldName(name);
16994
+ const mapped = metaByName.get(name);
16995
+ const description = typeof f.description === 'string' && f.description.trim()
16996
+ ? f.description.trim()
16997
+ : undefined;
16998
+ return {
16999
+ name,
17000
+ label,
17001
+ type: f.type || 'string',
17002
+ ...(description ? { description } : {}),
17003
+ ...(typeof f.format === 'string' && f.format.trim()
17004
+ ? { format: f.format.trim() }
17005
+ : {}),
17006
+ ...(typeof f.controlType === 'string' && f.controlType.trim()
17007
+ ? { controlType: f.controlType.trim() }
17008
+ : mapped?.controlType
17009
+ ? { controlType: mapped.controlType }
17010
+ : {}),
17011
+ ...(Array.isArray(f.enumValues) ? { enumValues: f.enumValues } : {}),
17012
+ ...(mapped?.options?.length
17013
+ ? {
17014
+ options: mapped.options.map((opt) => {
17015
+ const optLabel = String(opt.label ?? opt.text ?? opt.value);
17016
+ return {
17017
+ key: String(opt.value),
17018
+ value: String(opt.value),
17019
+ label: optLabel,
17020
+ text: opt.text ?? optLabel,
17021
+ };
17022
+ }),
17023
+ }
17024
+ : {}),
17025
+ };
17026
+ });
16960
17027
  if (normalizedPath !== this.composerSchemaPath || fallbackFields.length > 0 || !entry) {
16961
17028
  this.composerSchemaPath = '';
16962
17029
  this.composerSchemaFields.set(fallbackFields);
@@ -16973,10 +17040,17 @@ class DynamicPageConfigEditorComponent {
16973
17040
  if (current && fields.some((f) => f.name === current))
16974
17041
  return;
16975
17042
  const isBoolean = (f) => this.getComposerFieldTypeBadge(f).toLowerCase() === 'boolean';
16976
- const isStatusSemantic = (f) => /^(ativo|status|situacao|enabled|active)/i.test(String(f.name || ''));
17043
+ const hasEnumOptions = (f) => {
17044
+ const raw = f;
17045
+ return ((Array.isArray(raw['enumValues']) && raw['enumValues'].length > 0)
17046
+ || (Array.isArray(f.options) && f.options.length > 0));
17047
+ };
17048
+ const isStatusSemantic = (f) => /^(ativo|status|situacao|situação|situacaofuncional|enabled|active)/i.test(String(f.name || ''));
16977
17049
  const candidate = fields.find((f) => isBoolean(f) && isStatusSemantic(f))
16978
17050
  ?? fields.find((f) => isBoolean(f))
16979
- ?? fields.find((f) => /^(ativo|status|situacao|enabled|active|tipo|categoria|estado)/i.test(String(f.name || '')))
17051
+ ?? fields.find((f) => hasEnumOptions(f) && isStatusSemantic(f))
17052
+ ?? fields.find((f) => /^(ativo|status|situacao|situação|situacaofuncional|enabled|active|tipo|categoria|estado)/i.test(String(f.name || '')))
17053
+ ?? fields.find((f) => hasEnumOptions(f))
16980
17054
  ?? fields[0];
16981
17055
  if (candidate?.name) {
16982
17056
  this.composerGroupFieldControl.setValue(candidate.name, { emitEvent: false });
@@ -17063,6 +17137,55 @@ class DynamicPageConfigEditorComponent {
17063
17137
  },
17064
17138
  });
17065
17139
  }
17140
+ resolveGroupByMappings(groupField) {
17141
+ const baseMappings = {
17142
+ true: 'active',
17143
+ false: 'inactive',
17144
+ 1: 'active',
17145
+ 0: 'inactive',
17146
+ ATIVO: 'active',
17147
+ INATIVO: 'inactive',
17148
+ ACTIVE: 'active',
17149
+ INACTIVE: 'inactive',
17150
+ A: 'active',
17151
+ I: 'inactive',
17152
+ };
17153
+ const fieldDef = this.composerSchemaFields().find((f) => String(f?.name || '').trim() === groupField);
17154
+ if (!fieldDef)
17155
+ return baseMappings;
17156
+ const raw = fieldDef;
17157
+ const enumValues = [];
17158
+ if (Array.isArray(raw['enumValues'])) {
17159
+ for (const v of raw['enumValues']) {
17160
+ const s = String(v ?? '').trim();
17161
+ if (s)
17162
+ enumValues.push(s);
17163
+ }
17164
+ }
17165
+ else if (Array.isArray(fieldDef.options)) {
17166
+ for (const opt of fieldDef.options) {
17167
+ const s = String(opt?.value ?? opt?.key ?? '').trim();
17168
+ if (s)
17169
+ enumValues.push(s);
17170
+ }
17171
+ }
17172
+ if (!enumValues.length)
17173
+ return baseMappings;
17174
+ const positiveRegex = /^(true|1|ativo|active|a|sim|yes|abert[ao]|em_andamento|andamento|aprovad[ao]|concluid[ao]|regular|valid[ao]|normal|enabled)$/i;
17175
+ const hasPositiveMatch = enumValues.some((v) => positiveRegex.test(v));
17176
+ for (let i = 0; i < enumValues.length; i += 1) {
17177
+ const val = enumValues[i];
17178
+ if (baseMappings[val] !== undefined)
17179
+ continue;
17180
+ if (hasPositiveMatch) {
17181
+ baseMappings[val] = positiveRegex.test(val) ? 'active' : 'inactive';
17182
+ }
17183
+ else {
17184
+ baseMappings[val] = i === 0 ? 'active' : 'inactive';
17185
+ }
17186
+ }
17187
+ return baseMappings;
17188
+ }
17066
17189
  composeCollectionScreen(overrides) {
17067
17190
  const resourcePath = (overrides?.resourcePath ?? this.composerResourcePathControl.value).trim();
17068
17191
  if (!resourcePath)
@@ -17274,18 +17397,7 @@ class DynamicPageConfigEditorComponent {
17274
17397
  transform: {
17275
17398
  kind: 'group-by-map',
17276
17399
  totalField: 'total',
17277
- mappings: {
17278
- true: 'active',
17279
- false: 'inactive',
17280
- 1: 'active',
17281
- 0: 'inactive',
17282
- ATIVO: 'active',
17283
- INATIVO: 'inactive',
17284
- ACTIVE: 'active',
17285
- INACTIVE: 'inactive',
17286
- A: 'active',
17287
- I: 'inactive',
17288
- },
17400
+ mappings: this.resolveGroupByMappings(groupField),
17289
17401
  ratios: {
17290
17402
  activeRatePct: {
17291
17403
  numerator: 'active',
@@ -18460,7 +18572,7 @@ class DynamicPageConfigEditorComponent {
18460
18572
  clone(value) { if (value == null || typeof value !== 'object')
18461
18573
  return value; return JSON.parse(JSON.stringify(value)); }
18462
18574
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicPageConfigEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
18463
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: DynamicPageConfigEditorComponent, isStandalone: true, selector: "praxis-dynamic-page-config-editor", inputs: { page: "page", layout: "layout", presets: "presets", pagePreset: "pagePreset" }, outputs: { pageChange: "pageChange" }, host: { listeners: { "window:resize": "refreshRenderedValidation()" } }, providers: [providePraxisPageBuilderI18n()], viewQueries: [{ propertyName: "arrangementFeedback", first: true, predicate: ["arrangementFeedback"], descendants: true }], ngImport: i0, template: "<div class=\"editor\">\n @if (!isHostedInSettingsPanel) { <div class=\"head\">\n <div>{{ tx('editor.title', 'Page settings') }}</div>\n <button mat-stroked-button type=\"button\" (click)=\"reset()\">{{ tx('editor.reload', 'Reload') }}</button>\n </div> }\n\n @if (nonLimitError) { <p class=\"limits-error\" role=\"alert\">{{ nonLimitError }}</p> }\n @if (limitsError() || previousAdjustableHeights().length) {\n <section class=\"card limits-blocking-banner\" role=\"region\" [attr.aria-label]=\"tx('editor.limits.blockedBannerTitle', 'Height geometry requires review in Widget limits')\" data-testid=\"preset-geometry-blocking-banner\">\n @if (limitsError()) {\n <div class=\"head\">\n <strong>{{ tx('editor.limits.blockedBannerTitle', 'Height geometry requires review in Widget limits') }}</strong>\n </div>\n <p class=\"hint\">{{ tx('editor.limits.blockedBannerHelp', 'One or more widgets use an adjustable pixel height that exceeds the current usable area or requires review before applying the page or template:') }}</p>\n <ul class=\"blocked-widgets-list\">\n @for (item of blockedGeometryWidgets; track item.scope + ':' + item.key) {\n <li>\n <strong>{{ item.title }}</strong>\n @if (item.title !== item.key && !item.title.endsWith('(' + item.key + ')')) { <code>({{ item.key }})</code> }\n <span> \u00B7 {{ item.scopeLabel }}</span>\n @if (item.heightPx !== null) {\n <span> \u00B7 {{ tx('editor.height.fixed', 'Adjustable') }}: <strong>{{ item.heightPx }}px</strong></span>\n }\n </li>\n }\n </ul>\n <p class=\"limits-error\" role=\"alert\">{{ limitsError() }}</p>\n <div class=\"head\">\n <button mat-stroked-button type=\"button\" data-testid=\"goto-blocked-widget-limits\" (click)=\"navigateToBlockedLimit()\">\n <mat-icon aria-hidden=\"true\">tune</mat-icon>{{ tx('editor.limits.gotoLimitsAction', 'Go to Adjustments \u2192 Widget limits') }}\n </button>\n <button mat-flat-button type=\"button\" data-testid=\"switch-blocked-widgets-auto-height\" (click)=\"switchBlockedWidgetsToAutoHeight()\">\n <mat-icon aria-hidden=\"true\">height</mat-icon>{{ tx('editor.limits.switchToAutoHeightAction', 'Switch affected widgets to Automatic height') }}\n </button>\n </div>\n }\n @if (previousAdjustableHeights().length) {\n <div class=\"previous-heights-summary\" data-testid=\"previous-adjustable-heights-summary\">\n <span>{{ tx('editor.limits.previousHeightsSummary', 'Previous adjustable heights preserved for reference:') }}</span>\n @for (prev of previousAdjustableHeights(); track prev.scope + ':' + prev.key) {\n <span class=\"previous-height-chip\">{{ prev.title }} ({{ prev.scopeLabel }}): <strong>{{ prev.heightPx }}px</strong></span>\n }\n </div>\n }\n </section>\n }\n <mat-tab-group class=\"editor-tabs studio-tabs\" [mat-stretch-tabs]=\"false\" [selectedIndex]=\"workspaceTab()\" (selectedIndexChange)=\"workspaceTab.set($event)\">\n <mat-tab [label]=\"tx('studio.visualTab', 'Visual templates')\"><ng-template matTabContent>\n <section class=\"studio-panel\" data-testid=\"studio-visual\">\n <header class=\"studio-heading\"><span class=\"eyebrow\">{{ tx('studio.visualEyebrow', 'Appearance') }}</span><h2>{{ tx('studio.visualTitle', 'Give the page a visual language') }}</h2><p>{{ tx('studio.visualHelp', 'Choose surfaces, density and chart styling together. Individual widget overrides still take precedence.') }}</p></header>\n <button mat-stroked-button type=\"button\" [attr.aria-pressed]=\"!themePresetControl.value\" (click)=\"themePresetControl.setValue('')\">{{ tx('editor.layout.layoutDefault', 'Layout default') }}</button>\n <p class=\"selection-line\">{{ tx('presets.effectiveTheme', 'Effective page theme') }}: <strong>{{ selectedThemePresetEntry?.label || themePresetControl.value || recommendedThemePresetEntry?.label || tx('presets.hostTheme', 'Host theme') }}</strong></p>\n <div class=\"studio-gallery\">\n @for (theme of themePresetEntries; track theme.id) {\n <button type=\"button\" class=\"studio-choice\" [class.selected]=\"themePresetControl.value === theme.id\" [attr.aria-pressed]=\"themePresetControl.value === theme.id\" [attr.data-theme-preset]=\"theme.id\" (click)=\"themePresetControl.setValue(theme.id)\">\n <span class=\"theme-sample\" aria-hidden=\"true\" [style.background]=\"themeTokens(theme.id)['--pdx-page-bg']\" [style.--sample-accent]=\"themeTokens(theme.id)['--pdx-page-accent']\" [style.--sample-surface]=\"themeTokens(theme.id)['--pdx-page-surface']\" [style.--sample-radius]=\"themeTokens(theme.id)['--pdx-page-card-radius']\" [style.--sample-shadow]=\"themeTokens(theme.id)['--pdx-page-card-shadow']\" [style.gap]=\"themeTokens(theme.id)['--pdx-page-gap']\">\n <span class=\"sample-metric\"><b>128</b><i></i></span><span class=\"sample-metric\"><b>86%</b><i></i></span><span class=\"sample-chart\"><i></i><i></i><i></i><i></i><i></i></span>\n </span>\n <span class=\"choice-title\">{{ theme.label }}@if (themePresetControl.value === theme.id) {<mat-icon aria-hidden=\"true\">check_circle</mat-icon>}</span>\n <span class=\"choice-description\">{{ tx('studio.theme.' + theme.id, theme.label) }}</span>\n <span class=\"choice-meta\">{{ tx('studio.density.' + theme.density, theme.density) }} \u00B7 {{ shellName(theme.shellPreset) }}</span>\n </button>\n }\n </div>\n <p class=\"hint\">{{ tx('studio.samplesHelp', 'Illustrative samples using the preset tokens. Apply to see the result with your content.') }}</p>\n <details class=\"shell-options\"><summary>{{ tx('studio.shellTitle', 'Customize widget surfaces') }} \u00B7 {{ shellPresetEntries.length }}</summary>\n <p>{{ tx('studio.shellHelp', 'Override the page theme\u2019s default widget surface. Per-widget appearance settings keep their priority.') }}</p>\n <button mat-stroked-button type=\"button\" [attr.aria-pressed]=\"!shellPresetControl.value\" (click)=\"shellPresetControl.setValue('')\">{{ tx('editor.layout.inheritShell', 'Inherit appearance') }}</button>\n <div class=\"studio-gallery shell-gallery\">@for (shell of shellPresetEntries; track shell.id) {\n <button type=\"button\" class=\"studio-choice\" [class.selected]=\"shellPresetControl.value === shell.id\" [attr.aria-pressed]=\"shellPresetControl.value === shell.id\" (click)=\"shellPresetControl.setValue(shell.id)\">\n <span class=\"shell-sample\" aria-hidden=\"true\" [style.background]=\"shell.appearance.card?.background\" [style.border-color]=\"shell.appearance.card?.borderColor\" [style.border-radius]=\"shell.appearance.card?.borderRadius\" [style.box-shadow]=\"shell.appearance.card?.shadow\"><span [style.background]=\"shell.appearance.header?.background\" [style.border-color]=\"shell.appearance.header?.borderColor\"><mat-icon>analytics</mat-icon><i></i></span><span [style.background]=\"shell.appearance.body?.background\"><i></i><i></i></span></span>\n <span class=\"choice-title\">{{ shellPresetLabel(shell) }}@if (shellPresetControl.value === shell.id) {<mat-icon aria-hidden=\"true\">check_circle</mat-icon>}</span><span class=\"choice-description\">{{ shellDescription(shell) }}</span>\n </button>\n }</div>\n </details>\n </section>\n </ng-template></mat-tab>\n <mat-tab [label]=\"tx('studio.layoutTab', 'Layout templates')\"><ng-template matTabContent>\n <section class=\"studio-panel\" data-testid=\"studio-layout\">\n <header class=\"studio-heading\"><span class=\"eyebrow\">{{ tx('studio.layoutEyebrow', 'Composition model') }}</span><h2>{{ tx('studio.layoutTitle', 'Choose the structure for your use case') }}</h2><p>{{ tx('studio.layoutHelp', 'Compare the roles and default theme of each model. To redistribute the widgets already on this canvas, open Arrange.') }}</p></header>\n @if (duplicateFilterOwners().length) {\n <section class=\"card filter-owner-banner\" role=\"region\" [attr.aria-label]=\"tx('studio.filterOwnerBannerTitle', 'Duplicate filter region detected')\" data-testid=\"duplicate-filter-owner-banner\">\n <div class=\"head\">\n <strong>{{ tx('studio.filterOwnerBannerTitle', 'Single filter region recommendation') }}</strong>\n </div>\n <p class=\"hint\">{{ tx('studio.filterOwnerBannerHelp', 'A table has both its integrated filter bar and an external filter link active. Choose a single query owner to avoid competing filter regions:') }}</p>\n @for (item of duplicateFilterOwners(); track item.tableKey) {\n <div class=\"filter-owner-item\" [attr.data-testid]=\"'duplicate-filter-owner-' + item.tableKey\">\n <span><strong>{{ item.tableTitle }}</strong> <code>({{ item.tableKey }})</code> \u2190 {{ item.externalSourceLabels.join(', ') }}</span>\n <div class=\"studio-actions\">\n <button mat-stroked-button type=\"button\" [attr.data-testid]=\"'consolidate-filter-table-toolbar-' + item.tableKey\" (click)=\"consolidateFilterOwnership(item.tableKey, 'keep-table-toolbar')\">\n {{ tx('studio.keepTableToolbarFilter', 'Use integrated table filter') }}\n </button>\n <button mat-stroked-button type=\"button\" [attr.data-testid]=\"'consolidate-filter-external-link-' + item.tableKey\" (click)=\"consolidateFilterOwnership(item.tableKey, 'keep-external-link')\">\n {{ tx('studio.keepExternalFilterLink', 'Use external filter bar') }}\n </button>\n </div>\n </div>\n }\n </section>\n }\n <details class=\"card composer-card\" data-testid=\"collection-screen-composer\">\n <summary>{{ tx('studio.composer.title', 'Guided collection screen composer (5-minute flow)') }}</summary>\n <p class=\"hint\">{{ tx('studio.composer.help', 'Assemble a complete collection screen with header, KPI indicators, single filter region, primary collection, and optional detail surface in the draft.') }}</p>\n <div class=\"grid\">\n <praxis-resource-quick-connect\n [formControl]=\"composerResourcePathControl\"\n intent=\"collection\"\n [label]=\"tx('studio.composer.resourcePath', 'Resource path')\"\n (resourceSelected)=\"onComposerResourceSelected($event)\"\n data-testid=\"composer-resource-path\"\n />\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('studio.composer.screenTitle', 'Screen title') }}</mat-label>\n <input matInput [formControl]=\"composerTitleControl\" data-testid=\"composer-title\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('studio.composer.groupField', 'KPI group-by field') }}</mat-label>\n <input\n matInput\n [formControl]=\"composerGroupFieldControl\"\n [matAutocomplete]=\"composerGroupFieldPicker\"\n data-testid=\"composer-group-field\"\n />\n <mat-autocomplete\n #composerGroupFieldPicker=\"matAutocomplete\"\n (optionSelected)=\"composerGroupFieldControl.markAsPristine()\"\n >\n @for (field of filteredComposerGroupFieldOptions(); track field.name) {\n <mat-option [value]=\"field.name\" [attr.data-testid]=\"'composer-group-field-option-' + field.name\">\n <div class=\"composer-rich-field-option\">\n <div class=\"composer-rich-field-option__header\">\n <span class=\"composer-rich-field-option__label\">{{ field.label || field.name }}</span>\n @if (field.label && field.label !== field.name) {\n <code class=\"composer-rich-field-option__code\">{{ field.name }}</code>\n }\n @if (getComposerFieldTypeBadge(field); as badge) {\n <span class=\"composer-rich-field-option__badge\">{{ badge }}</span>\n }\n </div>\n @if (getComposerFieldDescription(field); as desc) {\n <span class=\"composer-rich-field-option__desc\">{{ desc }}</span>\n }\n </div>\n </mat-option>\n }\n </mat-autocomplete>\n @if (composerSelectedResource()?.capabilities?.stats === false) {\n <mat-hint data-testid=\"composer-stats-unsupported-hint\">{{ tx('studio.composer.statsUnsupportedHint', 'This resource does not publish stats/group-by aggregation; summary indicators were disabled automatically.') }}</mat-hint>\n } @else {\n <mat-hint>{{ tx('studio.composer.groupFieldHint', 'Choose the boolean or categorical field used in summary indicators.') }}</mat-hint>\n }\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('studio.composer.filterMode', 'Query filter owner') }}</mat-label>\n <mat-select [formControl]=\"composerFilterModeControl\" data-testid=\"composer-filter-mode\">\n <mat-option value=\"table-integrated\">{{ tx('studio.composer.filterIntegrated', 'Integrated table toolbar') }}</mat-option>\n <mat-option value=\"external-filter-bar\">{{ tx('studio.composer.filterExternal', 'Dedicated filter bar widget') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('studio.composer.collectionKind', 'Primary collection') }}</mat-label>\n <mat-select [formControl]=\"composerCollectionKindControl\" data-testid=\"composer-collection-kind\">\n <mat-option value=\"table\">praxis-Table</mat-option>\n <mat-option value=\"list\">praxis-List</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('studio.composer.detailSurface', 'Secondary detail surface') }}</mat-label>\n <mat-select [formControl]=\"composerDetailSurfaceControl\" data-testid=\"composer-detail-surface\">\n <mat-option value=\"drawer\">Drawer</mat-option>\n <mat-option value=\"modal\">Modal</mat-option>\n <mat-option value=\"none\">{{ tx('studio.composer.noSurface', 'None') }}</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div class=\"studio-actions\">\n <button mat-flat-button type=\"button\" data-testid=\"composer-generate-btn\" (click)=\"composeCollectionScreen()\">\n <mat-icon aria-hidden=\"true\">auto_awesome</mat-icon>{{ tx('studio.composer.generate', 'Generate collection screen in draft') }}\n </button>\n </div>\n @if (composerMessage()) {\n <p class=\"studio-status\" role=\"status\" data-testid=\"composer-status-message\">{{ composerMessage() }}</p>\n }\n </details>\n <div class=\"studio-actions\"><button mat-stroked-button type=\"button\" (click)=\"layoutPresetControl.setValue('')\">{{ tx('studio.inheritLayout', 'Use inherited model') }}</button><button mat-button type=\"button\" (click)=\"workspaceTab.set(2)\">{{ tx('studio.goArrange', 'Arrange existing widgets') }}<mat-icon iconPositionEnd>arrow_forward</mat-icon></button></div>\n <praxis-page-preset-comparison [expanded]=\"true\" [layoutId]=\"layoutPresetControl.value\" [layoutPresetOptions]=\"layoutPresetOptions\" [themeId]=\"themePresetControl.value\" [canvas]=\"true\" (choose)=\"layoutPresetControl.setValue($event)\" />\n </section>\n </ng-template></mat-tab>\n <mat-tab [label]=\"tx('studio.arrangeTab', 'Arrange')\"><ng-template matTabContent>\n <section class=\"studio-panel\" data-testid=\"canvas-arrangement\">\n <header class=\"studio-heading\"><span class=\"eyebrow\">{{ tx('studio.arrangeEyebrow', 'Existing widgets') }} \u00B7 {{ canvasColumnsControl.value }} {{ tx('editor.layout.cols', 'Columns') }}</span><h2>{{ tx('studio.arrangeTitle', 'Find the right balance of space') }}</h2><p>{{ tx('studio.arrangeHelp', 'Choose a distribution, compare the real positions, then use it in the draft. Widgets keep their reading order and content.') }}</p></header>\n <div #arrangementFeedback tabindex=\"-1\" class=\"arrangement-feedback\">\n @if (arrangementPreview(); as preview) {\n <section class=\"arrangement-review\" aria-live=\"polite\" data-testid=\"arrangement-preview\">\n <div class=\"review-heading\"><h3>{{ tx('studio.reviewTitle', 'Compare before using') }}</h3><span>{{ preview.keys.length }} {{ tx('studio.changedWidgets', 'widgets change') }}</span></div>\n <div class=\"canvas-comparison\">\n @for (side of ['before', 'after']; track side) {\n <figure><figcaption>{{ side === 'before' ? tx('studio.before', 'Before') : tx('studio.after', 'After') }}</figcaption>\n <div class=\"canvas-map\" [style.grid-template-columns]=\"'repeat(' + preview.canvas.columns + ', minmax(0, 1fr))'\" [style.grid-template-rows]=\"'repeat(' + previewRowCount(preview.before, preview.canvas) + ', minmax(0, 1fr))'\" role=\"img\" [attr.aria-label]=\"side === 'before' ? tx('studio.before', 'Before') : tx('studio.after', 'After')\">\n @for (item of previewItems(side === 'before' ? preview.before : preview.canvas); track item.key) {\n <span [style.grid-column]=\"item.col + ' / span ' + item.colSpan\" [style.grid-row]=\"item.row + ' / span ' + item.rowSpan\" [class.changed]=\"preview.keys.includes(item.key)\" [matTooltip]=\"item.label\">{{ item.number }}</span>\n }\n </div>\n </figure>\n }\n </div>\n <p class=\"hint\">{{ tx('studio.mapHelp', 'Numbered map of your widgets. Positions and spans are to scale; content height is evaluated on the page.') }}</p>\n <details><summary>{{ tx('studio.widgetDetails', 'Widget positions and sizes') }}</summary><ol class=\"widget-changes\">@for (item of previewItems(preview.canvas); track item.key) {<li><strong>{{ item.label }}</strong><span>{{ tx('studio.before', 'Before') }}: {{ tx('editor.arrange.position', 'Column / row') }} {{ preview.before.items[item.key].col }} / {{ preview.before.items[item.key].row }} \u00B7 {{ tx('studio.gridSize', 'Size in columns \u00D7 rows') }} {{ preview.before.items[item.key].colSpan }} \u00D7 {{ preview.before.items[item.key].rowSpan }}</span><span>{{ tx('studio.after', 'After') }}: {{ tx('editor.arrange.position', 'Column / row') }} {{ item.col }} / {{ item.row }} \u00B7 {{ tx('studio.gridSize', 'Size in columns \u00D7 rows') }} {{ item.colSpan }} \u00D7 {{ item.rowSpan }}</span></li>}</ol></details>\n <button mat-flat-button type=\"button\" data-testid=\"arrangement-use\" (click)=\"useArrangement()\">{{ tx('editor.arrange.use', 'Use arrangement in draft') }}<mat-icon iconPositionEnd>arrow_forward</mat-icon></button>\n </section>\n }\n @if (arrangementMessage()) {<p class=\"studio-status\" role=\"status\">{{ arrangementMessage() }}</p>}\n @if (arrangementUndoAvailable()) {<button mat-stroked-button type=\"button\" data-testid=\"arrangement-undo\" (click)=\"undoArrangement()\"><mat-icon>undo</mat-icon>{{ tx('editor.arrange.undo', 'Undo arrangement') }}</button>}\n </div>\n <div class=\"studio-gallery arrangement-gallery\">\n @for (choice of arrangementChoices; track choice.id) {\n <button type=\"button\" class=\"studio-choice\" [class.selected]=\"arrangementSelection() === choice.id\" [attr.aria-pressed]=\"arrangementSelection() === choice.id\" [disabled]=\"!hasCanvasWidgets || !arrangementFits(choice.id)\" [attr.data-testid]=\"'arrange-' + choice.id\" (click)=\"prepareArrangement(choice.id)\">\n <span class=\"distribution-sample\" aria-hidden=\"true\" [style.grid-template-columns]=\"(arrangementFits(choice.id) ? arrangementWidths(choice.id) : choice.weights).join('fr ') + 'fr'\">@for (weight of choice.weights; track $index) {<i>{{ arrangementWidths(choice.id)[$index] }}</i>}@for (weight of choice.weights; track $index) {<i></i>}</span>\n <span class=\"choice-title\">{{ tx('editor.arrange.' + choice.id, choice.id) }}@if (arrangementSelection() === choice.id) {<mat-icon aria-hidden=\"true\">check_circle</mat-icon>}</span>\n <span class=\"choice-description\">{{ tx('studio.arrange.' + choice.id, choice.id) }}</span>\n <span class=\"choice-meta\">{{ arrangementFits(choice.id) ? arrangementWidths(choice.id).join(' + ') : tx('studio.moreColumns', 'Needs more grid columns') }}</span>\n </button>\n }\n </div>\n @if (hasCanvasWidgets) {\n <details class=\"card quick-geometry-card\" data-testid=\"quick-widget-geometry-list\">\n <summary>{{ tx('studio.quickGeometryTitle', 'Quick widget spans (columns \u00D7 rows)') }}</summary>\n <div class=\"quick-geometry-rows\">\n @for (w of quickCanvasWidgets(); track w.key) {\n <div class=\"quick-geometry-row\" [attr.data-testid]=\"'quick-geometry-row-' + w.key\">\n <span class=\"quick-geometry-label\"><mat-icon aria-hidden=\"true\">{{ w.icon }}</mat-icon><strong>{{ w.label }}</strong> <small>({{ w.colSpan }}\u00D7{{ w.rowSpan }})</small></span>\n <div class=\"studio-actions\">\n @for (span of [3, 4, 6, 12]; track span) {\n <button mat-stroked-button type=\"button\" [attr.data-testid]=\"'quick-colspan-' + w.key + '-' + span\" (click)=\"setWidgetCanvasSpan(w.key, span)\">{{ span }}c</button>\n }\n @for (rSpan of [1, 2, 4]; track rSpan) {\n <button mat-button type=\"button\" [attr.data-testid]=\"'quick-rowspan-' + w.key + '-' + rSpan\" (click)=\"setWidgetCanvasSpan(w.key, w.colSpan, rSpan)\">{{ rSpan }}r</button>\n }\n </div>\n </div>\n }\n </div>\n </details>\n }\n @if (!hasCanvasWidgets) {<p role=\"status\">{{ tx('editor.limits.empty', 'Add widgets to the canvas to configure their limits.') }}</p>}\n <details class=\"arrangement-rules\"><summary>{{ tx('studio.rules', 'Scope, devices and locked widgets') }}</summary><p>{{ tx('editor.arrange.help', 'Base canvas, in current reading order. Inheriting devices follow the base; explicit device positions are preserved. Locked positions stay in place.') }}</p><button mat-button type=\"button\" (click)=\"onSelectedTabIndexChange(5)\">{{ tx('studio.goLimits', 'Review widget limits') }}</button></details>\n </section>\n </ng-template></mat-tab>\n <mat-tab [label]=\"tx('studio.settingsTab', 'Adjustments')\">\n <div class=\"studio-panel\"><mat-tab-group class=\"editor-tabs\" [mat-stretch-tabs]=\"false\" [selectedIndex]=\"selectedTabIndex\" (selectedIndexChange)=\"selectDetailTab($event)\">\n <mat-tab [label]=\"tx('editor.tabs.layout', 'Layout')\"><div class=\"grid\"><mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.layout.cols', 'Columns') }}</mat-label><input matInput type=\"number\" min=\"1\" [formControl]=\"canvasColumnsControl\" /></mat-form-field><mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.layout.rowUnit', 'Row unit') }}</mat-label><input matInput [formControl]=\"canvasRowUnitControl\" /></mat-form-field><mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.layout.gap', 'Gap') }}</mat-label><input matInput [formControl]=\"canvasGapControl\" /></mat-form-field><mat-form-field class=\"page-inset-field\" appearance=\"outline\"><mat-label>{{ tx('editor.layout.paddingInline', 'Page side inset') }}</mat-label><input matInput [formControl]=\"paddingInlineControl\" placeholder=\"24px\" /><button praxisIconButton=\"help_outline\" size=\"dense\" matSuffix type=\"button\" [matTooltip]=\"tx('editor.hints.paddingInline', 'Shared space beside all page widgets. Use a CSS length or theme token; 0px removes it. Leave empty to inherit the host theme.')\" [attr.aria-label]=\"tx('editor.layout.paddingInline', 'Page side inset')\"></button></mat-form-field><mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.layout.autoRows', 'Auto rows') }}</mat-label><mat-select [formControl]=\"canvasAutoRowsControl\"><mat-option value=\"fixed\">{{ tx('editor.layout.autoRowsFixed', 'Fixed height') }}</mat-option><mat-option value=\"content\">{{ tx('editor.layout.autoRowsContent', 'Fit content') }}</mat-option></mat-select></mat-form-field></div></mat-tab>\n <mat-tab [label]=\"tx('editor.tabs.grouping', 'Grouping')\"><div class=\"panel\">\n <p class=\"hint\" data-testid=\"canvas-grouping-notice\">{{ tx('editor.grouping.canvasNotice', 'Canvas positions control this page. Groups below are preserved for flow layouts and do not arrange widgets in the canvas. Use the widget move and resize controls to place them side by side.') }}</p><div class=\"head\"><span>{{ tx('editor.grouping.title', 'Grouping') }}</span><button mat-stroked-button type=\"button\" (click)=\"addGroupingNode()\"><mat-icon>add</mat-icon>{{ tx('editor.grouping.add', 'Add group') }}</button></div>@for (group of groupingNodes.controls; track $index) {<div class=\"card\" [formGroup]=\"group\"><div class=\"head\"><span>{{ groupLabel($index) }}</span><button praxisIconButton=\"delete\" size=\"compact\" type=\"button\" [attr.aria-label]=\"tx('editor.grouping.removeAria', 'Remove group')\" (click)=\"removeGroupingNode($index)\"></button></div><div class=\"grid\"><mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.grouping.kind', 'Kind') }}</mat-label><mat-select formControlName=\"kind\"><mat-option value=\"section\">section</mat-option><mat-option value=\"tabs\">tabs</mat-option><mat-option value=\"hero\">hero</mat-option><mat-option value=\"rail\">rail</mat-option></mat-select></mat-form-field><mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.grouping.id', 'Id') }}</mat-label><input matInput formControlName=\"id\" /></mat-form-field><mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.grouping.label', 'Label') }}</mat-label><input matInput formControlName=\"label\" /></mat-form-field>@if (group.controls.kind.value === 'section') {<mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.grouping.layout', 'Layout') }}</mat-label><mat-select formControlName=\"layout\"><mat-option value=\"\">{{ tx('editor.grouping.layoutDefault', 'Default') }}</mat-option><mat-option value=\"stack\">stack</mat-option><mat-option value=\"grid\">grid</mat-option><mat-option value=\"row\">row</mat-option></mat-select></mat-form-field>}@if (group.controls.kind.value === 'hero') {<mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.grouping.emphasis', 'Emphasis') }}</mat-label><mat-select formControlName=\"emphasis\"><mat-option value=\"\">{{ tx('editor.grouping.layoutDefault', 'Default') }}</mat-option><mat-option value=\"high\">high</mat-option><mat-option value=\"medium\">medium</mat-option></mat-select></mat-form-field>}@if (group.controls.kind.value === 'rail') {<mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.grouping.side', 'Side') }}</mat-label><mat-select formControlName=\"side\"><mat-option value=\"left\">left</mat-option><mat-option value=\"right\">right</mat-option></mat-select></mat-form-field>}@if (group.controls.kind.value === 'tabs') {<mat-form-field appearance=\"outline\" class=\"wide\"><mat-label>{{ tx('editor.grouping.tabsJson', 'Tabs (JSON)') }}</mat-label><textarea matInput rows=\"4\" formControlName=\"tabsJson\"></textarea></mat-form-field>} @else {@if (bindablePageWidgets().length > 0) {<mat-form-field appearance=\"outline\" class=\"wide\"><mat-label>{{ tx('editor.grouping.widgets', 'Widgets in group') }}</mat-label><mat-select multiple [value]=\"groupingSelectedWidgetKeys($index)\" (selectionChange)=\"onGroupingWidgetSelectionChange($index, $event.value)\" [attr.data-testid]=\"'grouping-widget-select-' + $index\" panelClass=\"praxis-binder-widget-select-panel\"><mat-select-trigger><span class=\"binder-widget-trigger\"><mat-icon aria-hidden=\"true\" class=\"binder-widget-trigger-icon\">{{ groupingSelectedWidgetIcon($index) }}</mat-icon><span class=\"binder-widget-trigger-text\">{{ groupingSelectedWidgetsLabel($index) }}</span></span></mat-select-trigger>@for (w of bindablePageWidgets(); track w.key) {<mat-option [value]=\"w.key\" [attr.data-widget-key]=\"w.key\" class=\"binder-widget-option\"><span class=\"binder-widget-option-row\"><mat-icon class=\"binder-widget-option-icon\" aria-hidden=\"true\">{{ w.icon }}</mat-icon><span class=\"binder-widget-option-copy\"><strong class=\"binder-widget-option-title\">{{ w.title }}</strong>@if (w.description) {<small class=\"binder-widget-option-desc\">{{ w.description }}</small>}</span><span class=\"binder-widget-option-sr-key\" aria-hidden=\"true\">{{ w.key }}</span></span></mat-option>}</mat-select></mat-form-field>}<mat-form-field appearance=\"outline\" class=\"wide\"><mat-label>{{ tx('editor.grouping.widgetKeysCsv', 'Widget keys (comma-separated)') }}</mat-label><input matInput formControlName=\"widgetKeysCsv\" /></mat-form-field>}</div></div>}</div></mat-tab>\n <mat-tab [label]=\"tx('editor.tabs.devices', 'Device Layouts')\">\n <div class=\"panel\">\n <mat-button-toggle-group [value]=\"activeDevice()\" (change)=\"activeDevice.set($event.value)\" [attr.aria-label]=\"tx('editor.devices.target', 'Device to configure')\">\n @for (device of deviceKeys; track device) {\n <mat-button-toggle [value]=\"device\">{{ deviceLabels[device] }}</mat-button-toggle>\n }\n </mat-button-toggle-group>\n @for (device of deviceKeys; track device) {\n @if (activeDevice() === device) {\n <div class=\"card\">\n <div class=\"head\"><strong>{{ deviceLabels[device] }}</strong><button mat-stroked-button type=\"button\" [attr.data-testid]=\"'stack-device-' + device\" [disabled]=\"!hasCanvasWidgets\" (click)=\"stackDevice(device)\"><mat-icon aria-hidden=\"true\">view_agenda</mat-icon>{{ tx('editor.devices.stack', 'Stack widgets') }}</button></div>\n <p class=\"hint\">{{ tx('editor.devices.stackHelp', 'Use full width and natural height, in reading order. This replaces positions and width limits only for this device; other layouts stay unchanged.') }}</p>\n @if (stackedDevices.has(device)) { <p class=\"hint\" role=\"status\">{{ tx('editor.devices.stacked', 'Single-column layout prepared. Save to apply; Reset undoes the changes.') }}</p> }\n <div class=\"grid\">\n <mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.devices.columns', 'Columns') }}</mat-label><input matInput type=\"number\" min=\"1\" [formControl]=\"deviceForms[device].controls.columns\" /></mat-form-field>\n <mat-form-field class=\"page-inset-field\" appearance=\"outline\"><mat-label>{{ tx('editor.layout.paddingInline', 'Page side inset') }}</mat-label><input matInput [formControl]=\"deviceForms[device].controls.paddingInline\" placeholder=\"16px\" /></mat-form-field><mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.devices.gap', 'Gap') }}</mat-label><input matInput [formControl]=\"deviceForms[device].controls.gap\" /></mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.layout.autoRows', 'Auto rows') }}</mat-label><mat-select [formControl]=\"deviceForms[device].controls.autoRows\"><mat-option value=\"\">{{ tx('editor.devices.orientationDefault', 'Default') }}</mat-option><mat-option value=\"fixed\">{{ tx('editor.layout.autoRowsFixed', 'Fixed height') }}</mat-option><mat-option value=\"content\">{{ tx('editor.layout.autoRowsContent', 'Fit content') }}</mat-option></mat-select></mat-form-field>\n </div>\n <details><summary>{{ tx('editor.devices.advanced', 'Advanced overrides') }}</summary><div class=\"grid\">\n <mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.layout.rowUnit', 'Row unit') }}</mat-label><input matInput [formControl]=\"deviceForms[device].controls.rowUnit\" /></mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"wide\"><mat-label>{{ tx('editor.devices.groupingOverridesJson', 'Grouping overrides (JSON)') }}</mat-label><textarea matInput rows=\"4\" [formControl]=\"deviceForms[device].controls.groupingOverrides\"></textarea></mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"wide\"><mat-label>{{ tx('editor.devices.widgetOverridesJson', 'Widget overrides (JSON)') }}</mat-label><textarea matInput rows=\"4\" [formControl]=\"deviceForms[device].controls.widgetOverrides\"></textarea><mat-hint>{{ tx('editor.devices.widgetOverridesHelp', 'Supported per-widget properties on this device: className, span, order, and hidden. Does not override internal widget definition or inputs.') }}</mat-hint></mat-form-field>\n </div></details>\n @if (!stackedDevices.has(device)) { <div class=\"hint\">{{ tx('editor.preview.deviceCanvasHint', 'Existing canvas item overrides are preserved.') }}</div> }\n </div>\n }\n }\n </div>\n </mat-tab>\n <mat-tab [label]=\"tx('editor.tabs.context', 'Context')\"><mat-form-field appearance=\"outline\" class=\"wide\"><mat-label>{{ tx('editor.context.json', 'Context (JSON)') }}</mat-label><textarea matInput rows=\"12\" [formControl]=\"contextControl\"></textarea></mat-form-field></mat-tab>\n <mat-tab [label]=\"tx('editor.tabs.state', 'Page State')\">\n <div class=\"panel\">\n <mat-form-field appearance=\"outline\" class=\"wide\">\n <mat-label>{{ tx('editor.state.stateDefinitionJson', 'Page state (values, schema, derived \u2014 JSON)') }}</mat-label>\n <textarea matInput rows=\"10\" [formControl]=\"stateControl\" data-testid=\"page-state-json\"></textarea>\n </mat-form-field>\n\n <section class=\"panel\" data-testid=\"page-datasources-guided\">\n <div class=\"head\">\n <strong>{{ tx('editor.state.dataSourcesTitle', 'Data Sources') }}</strong>\n <div class=\"studio-actions\">\n @if (dataSourceNodes.controls.length) {\n <button\n mat-button\n type=\"button\"\n data-testid=\"page-state-sync-from-datasources\"\n (click)=\"syncStateValuesFromDataSources()\"\n >\n <mat-icon>sync</mat-icon>{{ tx('editor.state.syncFromDataSources', 'Sync state paths') }}\n </button>\n }\n <button mat-stroked-button type=\"button\" data-testid=\"page-datasources-add\" (click)=\"addDataSourceNode()\">\n <mat-icon>add</mat-icon>{{ tx('editor.state.addDataSource', 'Add data source') }}\n </button>\n </div>\n </div>\n <p class=\"hint\">{{ tx('editor.state.dataSourcesCopy', 'Bind HTTP or resource endpoints directly into page state paths with reactive parameters.') }}</p>\n @if (!dataSourceNodes.controls.length) {\n <p class=\"hint\" data-testid=\"page-datasources-empty\">{{ tx('editor.state.emptyDataSources', 'No declarative data sources configured.') }}</p>\n }\n @for (ds of dataSourceNodes.controls; track $index) {\n <div class=\"card\" [formGroup]=\"ds\" [attr.data-testid]=\"'page-datasource-card-' + $index\">\n <div class=\"head\">\n <span>{{ dataSourceLabel($index) }}</span>\n <button\n praxisIconButton=\"delete\"\n size=\"compact\"\n type=\"button\"\n [attr.data-testid]=\"'page-datasource-remove-' + $index\"\n [attr.aria-label]=\"tx('editor.state.removeDataSourceAria', 'Remove data source')\"\n (click)=\"removeDataSourceNode($index)\"\n ></button>\n </div>\n <div class=\"grid\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.dataSourceId', 'ID') }}</mat-label>\n <input matInput formControlName=\"id\" [attr.data-testid]=\"'page-datasource-id-' + $index\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.dataSourceKind', 'Kind') }}</mat-label>\n <mat-select formControlName=\"kind\" [attr.data-testid]=\"'page-datasource-kind-' + $index\">\n <mat-option value=\"\">{{ tx('editor.common.default', 'Default') }}</mat-option>\n <mat-option value=\"remote-resource\">remote-resource</mat-option>\n <mat-option value=\"stats-query\">stats-query</mat-option>\n <mat-option value=\"raw-http\">raw-http</mat-option>\n </mat-select>\n </mat-form-field>\n <praxis-resource-quick-connect\n formControlName=\"resourcePath\"\n [intent]=\"ds.controls.kind.value === 'stats-query' ? 'stats' : (ds.controls.kind.value === 'raw-http' ? 'any' : 'collection')\"\n [endpointKey]=\"resolveEndpointKey(ds.controls.endpointKey.value)\"\n [label]=\"tx('editor.state.resourcePath', 'Resource path')\"\n [attr.data-testid]=\"'page-datasource-resourcePath-' + $index\"\n />\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.endpointKey', 'Endpoint key') }}</mat-label>\n <input matInput formControlName=\"endpointKey\" [attr.data-testid]=\"'page-datasource-endpointKey-' + $index\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.method', 'HTTP method') }}</mat-label>\n <mat-select formControlName=\"method\" [attr.data-testid]=\"'page-datasource-method-' + $index\">\n <mat-option value=\"\">{{ tx('editor.common.auto', 'Auto') }}</mat-option>\n <mat-option value=\"GET\">GET</mat-option>\n <mat-option value=\"POST\">POST</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.targetStatePath', 'Target state path') }}</mat-label>\n <input\n matInput\n formControlName=\"targetStatePath\"\n [attr.data-testid]=\"'page-datasource-targetStatePath-' + $index\"\n (blur)=\"syncStateValuesFromDataSources()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.errorPolicy', 'Error policy') }}</mat-label>\n <mat-select formControlName=\"errorPolicy\" [attr.data-testid]=\"'page-datasource-errorPolicy-' + $index\">\n <mat-option value=\"\">{{ tx('editor.common.default', 'Default') }}</mat-option>\n <mat-option value=\"propagate\">propagate</mat-option>\n <mat-option value=\"silent\">silent</mat-option>\n <mat-option value=\"fallback\">fallback</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.refreshIntervalMs', 'Refresh interval (ms)') }}</mat-label>\n <input matInput type=\"number\" min=\"0\" formControlName=\"refreshIntervalMs\" [attr.data-testid]=\"'page-datasource-refreshIntervalMs-' + $index\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.autoFetch', 'Auto fetch') }}</mat-label>\n <mat-select formControlName=\"autoFetch\" [attr.data-testid]=\"'page-datasource-autoFetch-' + $index\">\n <mat-option value=\"\">{{ tx('editor.common.default', 'Default') }}</mat-option>\n <mat-option value=\"true\">true</mat-option>\n <mat-option value=\"false\">false</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"wide\">\n <mat-label>{{ tx('editor.state.description', 'Description') }}</mat-label>\n <input matInput formControlName=\"description\" [attr.data-testid]=\"'page-datasource-description-' + $index\" />\n </mat-form-field>\n <div class=\"wide guided-presets-bar\" [attr.data-testid]=\"'page-datasource-guided-presets-' + $index\">\n <span class=\"hint\">{{ tx('editor.state.guidedPresets', 'Guided query & transform templates:') }}</span>\n <div class=\"studio-actions\">\n <button mat-stroked-button type=\"button\" [attr.data-testid]=\"'page-datasource-preset-groupby-' + $index\" (click)=\"applyGuidedDataSourceQuery($index, { mode: 'group-by', groupByField: 'ativo', metricField: 'id', metricOp: 'count' }); applyGuidedDataSourceTransform($index, { kind: 'group-by-map', totalField: 'total', mappingsText: 'true:active, false:inactive', ratioTargetField: 'activeRatePct', ratioNumeratorField: 'active', ratioDenominatorField: 'total', ratioScale: 100, ratioDecimals: 1 })\">\n {{ tx('editor.state.presetGroupByKpi', 'Group-by + Total + Ratio (%)') }}\n </button>\n <button mat-stroked-button type=\"button\" [attr.data-testid]=\"'page-datasource-preset-weighted-' + $index\" (click)=\"applyGuidedDataSourceTransform($index, { kind: 'group-by-map', totalField: 'total', weightedAverageField: 'weightedAvg', weightedAverageDecimals: 2, mappingsText: '1:low, 5:high' })\">\n {{ tx('editor.state.presetWeightedAvg', 'Weighted average') }}\n </button>\n <button mat-stroked-button type=\"button\" [attr.data-testid]=\"'page-datasource-preset-timeseries-' + $index\" (click)=\"applyGuidedDataSourceQuery($index, { mode: 'timeseries', groupByField: 'createdAt', period: 'month', metricField: 'id', metricOp: 'count' }); applyGuidedDataSourceTransform($index, { kind: 'timeseries-last', mappingsText: 'count:latestCount, avg:latestAvg' })\">\n {{ tx('editor.state.presetTimeseriesLast', 'Timeseries latest point') }}\n </button>\n <button mat-stroked-button type=\"button\" [attr.data-testid]=\"'page-datasource-preset-path-' + $index\" (click)=\"applyGuidedDataSourceTransform($index, { kind: 'path', path: 'totalElements' })\">\n {{ tx('editor.state.presetPathExtract', 'Extract path (totalElements)') }}\n </button>\n </div>\n </div>\n <mat-form-field appearance=\"outline\" class=\"wide\">\n <mat-label>{{ tx('editor.state.queryJson', 'Query (JSON)') }}</mat-label>\n <textarea matInput rows=\"3\" formControlName=\"queryJson\" [attr.data-testid]=\"'page-datasource-queryJson-' + $index\"></textarea>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"wide\">\n <mat-label>{{ tx('editor.state.transformJson', 'Transform (JSON)') }}</mat-label>\n <textarea matInput rows=\"3\" formControlName=\"transformJson\" [attr.data-testid]=\"'page-datasource-transformJson-' + $index\"></textarea>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"wide\">\n <mat-label>{{ tx('editor.state.fallbackJson', 'Fallback value (JSON)') }}</mat-label>\n <textarea matInput rows=\"2\" formControlName=\"fallbackJson\" [attr.data-testid]=\"'page-datasource-fallbackJson-' + $index\"></textarea>\n </mat-form-field>\n </div>\n @let preview = getDataSourcePreview($index);\n <div class=\"datasource-preview-box\" [attr.data-testid]=\"'page-datasource-preview-' + $index\">\n <div class=\"head\">\n <strong>{{ tx('editor.state.previewTitle', 'Transform & State Preview') }}</strong>\n <span class=\"previous-height-chip\">{{ preview.sourceKind === 'live' ? tx('editor.state.previewLive', 'Live response') : tx('editor.state.previewSample', 'Simulated sample') }}</span>\n </div>\n @if (preview.status === 'error') {\n <p class=\"limits-error\" role=\"alert\" [attr.data-testid]=\"'page-datasource-preview-error-' + $index\">{{ preview.error }}</p>\n } @else {\n <pre class=\"datasource-preview-json\" [attr.data-testid]=\"'page-datasource-preview-json-' + $index\">{{ preview.formattedJson }}</pre>\n @if (preview.tokens.length) {\n <div class=\"studio-actions\" [attr.data-testid]=\"'page-datasource-preview-tokens-' + $index\">\n <span class=\"hint\">{{ tx('editor.state.availableTokens', 'Click a token to prepare widget binding:') }}</span>\n @for (token of preview.tokens; track token) {\n <button mat-button type=\"button\" (click)=\"binderTokenControl.setValue(token)\"><code>{{ token }}</code></button>\n }\n </div>\n }\n }\n </div>\n </div>\n }\n </section>\n\n <section class=\"card\" data-testid=\"page-derived-measures-guided\">\n <div class=\"head\">\n <strong>{{ tx('editor.state.derivedMeasuresTitle', 'Guided Derived Measures') }}</strong>\n </div>\n <p class=\"hint\">{{ tx('editor.state.derivedMeasuresCopy', 'Create percentages, ratios, weighted averages, or formatted measures from page state paths without writing raw JsonLogic.') }}</p>\n <div class=\"grid\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.derivedTargetPath', 'Measure key') }}</mat-label>\n <input matInput [formControl]=\"derivedPathControl\" placeholder=\"activeRatePct\" data-testid=\"derived-target-path\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.derivedKind', 'Calculation') }}</mat-label>\n <mat-select [formControl]=\"derivedKindControl\" data-testid=\"derived-kind\">\n <mat-option value=\"ratio-percent\">{{ tx('editor.state.derivedRatio', 'Ratio / Percentage') }}</mat-option>\n <mat-option value=\"weighted-average\">{{ tx('editor.state.derivedWeighted', 'Weighted average') }}</mat-option>\n <mat-option value=\"format-value\">{{ tx('editor.state.derivedFormat', 'Formatted value') }}</mat-option>\n <mat-option value=\"template\">{{ tx('editor.state.derivedTemplate', 'State reference') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.derivedNumerator', 'Numerator / Source path') }}</mat-label>\n <input matInput [formControl]=\"derivedNumeratorControl\" placeholder=\"kpis.active\" data-testid=\"derived-numerator\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.derivedDenominator', 'Denominator path') }}</mat-label>\n <input matInput [formControl]=\"derivedDenominatorControl\" placeholder=\"kpis.total\" data-testid=\"derived-denominator\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.derivedFormatKind', 'Format') }}</mat-label>\n <mat-select [formControl]=\"derivedFormatControl\" data-testid=\"derived-format\">\n <mat-option value=\"percent\">percent (%)</mat-option>\n <mat-option value=\"number\">number</mat-option>\n <mat-option value=\"integer\">integer</mat-option>\n <mat-option value=\"currency\">currency (R$)</mat-option>\n <mat-option value=\"boolean\">boolean</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div class=\"studio-actions\">\n <button mat-stroked-button type=\"button\" data-testid=\"derived-add-btn\" (click)=\"addGuidedDerivedMeasure()\">\n <mat-icon aria-hidden=\"true\">functions</mat-icon>{{ tx('editor.state.addDerivedAction', 'Add measure to page state') }}\n </button>\n </div>\n @let derivedPreview = getDerivedStatePreview();\n @if (derivedPreview.entries.length || derivedPreview.diagnostics.length) {\n <div class=\"datasource-preview-box\" data-testid=\"page-derived-preview\">\n @for (diag of derivedPreview.diagnostics; track diag) {\n <p class=\"limits-error\" role=\"alert\">{{ diag }}</p>\n }\n @if (derivedPreview.entries.length) {\n <div class=\"studio-actions\">\n @for (entry of derivedPreview.entries; track entry.key) {\n <button mat-button type=\"button\" (click)=\"binderTokenControl.setValue(entry.token)\">\n <strong>{{ entry.key }}</strong>: {{ entry.formattedValue }} <code>({{ entry.token }})</code>\n </button>\n }\n </div>\n }\n </div>\n }\n </section>\n\n <section class=\"card\" data-testid=\"page-state-component-binder\">\n <div class=\"head\">\n <strong>{{ tx('editor.state.binderTitle', 'Connect State or Measure to Widget') }}</strong>\n </div>\n <p class=\"hint\">{{ tx('editor.state.binderCopy', 'Bind any state path or derived measure directly to a widget input in the draft.') }}</p>\n <div class=\"grid\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.binderToken', 'State token') }}</mat-label>\n <input matInput [formControl]=\"binderTokenControl\" placeholder=\"{{ '{{state.kpis.total}}' }}\" data-testid=\"binder-token\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.binderWidget', 'Target widget') }}</mat-label>\n <mat-select [formControl]=\"binderWidgetControl\" data-testid=\"binder-widget\" panelClass=\"praxis-binder-widget-select-panel\">\n @if (selectedBinderWidget(); as selectedWidget) {\n <mat-select-trigger>\n <span class=\"binder-widget-trigger\">\n <mat-icon aria-hidden=\"true\" class=\"binder-widget-trigger-icon\">{{ selectedWidget.icon }}</mat-icon>\n <span class=\"binder-widget-trigger-text\">\n <strong>{{ selectedWidget.title }}</strong>\n @if (selectedWidget.description) {\n <small> \u00B7 {{ selectedWidget.description }}</small>\n }\n </span>\n </span>\n </mat-select-trigger>\n }\n @for (w of bindablePageWidgets(); track w.key) {\n <mat-option [value]=\"w.key\" [attr.data-widget-key]=\"w.key\" class=\"binder-widget-option\">\n <span class=\"binder-widget-option-row\">\n <mat-icon aria-hidden=\"true\" class=\"binder-widget-option-icon\">{{ w.icon }}</mat-icon>\n <span class=\"binder-widget-option-copy\">\n <strong class=\"binder-widget-option-title\">{{ w.title }}</strong>\n @if (w.description) {\n <small class=\"binder-widget-option-desc\">{{ w.description }}</small>\n }\n </span>\n <span class=\"binder-widget-option-sr-key\" aria-hidden=\"true\">{{ w.key }}</span>\n </span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.binderInputPath', 'Widget input path') }}</mat-label>\n <input matInput [formControl]=\"binderInputPathControl\" placeholder=\"value\" data-testid=\"binder-input-path\" />\n </mat-form-field>\n </div>\n @if (availableStateBindingTokens().length) {\n <div class=\"binder-input-suggestions\" data-testid=\"binder-token-suggestions\">\n <span class=\"hint\">{{ tx('editor.state.binderTokenSuggestions', 'Available tokens:') }}</span>\n @for (availableToken of availableStateBindingTokens(); track availableToken) {\n <button\n type=\"button\"\n class=\"binder-input-suggestion-chip\"\n [class.active]=\"binderTokenControl.value === availableToken\"\n [attr.data-testid]=\"'binder-token-suggestion-' + availableToken\"\n (click)=\"selectBinderTokenSuggestion(availableToken)\"\n >\n <code>{{ availableToken }}</code>\n </button>\n }\n </div>\n }\n <div class=\"binder-input-suggestions\" data-testid=\"binder-pipe-suggestions\">\n <span class=\"hint\">{{ tx('editor.state.binderPipeSuggestions', 'Formatadores CSP-safe (pipes):') }}</span>\n @for (pipeSegment of binderDeclarativePipes; track pipeSegment) {\n <button\n type=\"button\"\n class=\"binder-input-suggestion-chip\"\n [attr.data-testid]=\"'binder-pipe-chip-' + pipeSegment\"\n (click)=\"appendBinderTokenPipe(pipeSegment)\"\n >\n <code>{{ pipeSegment }}</code>\n </button>\n }\n </div>\n @if (binderTokenPipeError(); as pipeErr) {\n <p class=\"studio-status binder-unbound-warning\" role=\"alert\" data-testid=\"binder-pipe-error\">{{ pipeErr }}</p>\n } @else if (binderTokenLivePreview(); as livePreviewVal) {\n <p class=\"hint\" data-testid=\"binder-live-preview\">\n {{ tx('editor.state.binderLivePreview', 'Preview simulado:') }} <strong>{{ livePreviewVal }}</strong>\n </p>\n }\n @if (selectedBinderWidgetInputSuggestions().length) {\n <div class=\"binder-input-suggestions\" data-testid=\"binder-input-suggestions\">\n <span class=\"hint\">{{ tx('editor.state.binderInputSuggestions', 'Suggested paths:') }}</span>\n @for (suggestedPath of selectedBinderWidgetInputSuggestions(); track suggestedPath) {\n <button\n type=\"button\"\n class=\"binder-input-suggestion-chip\"\n [class.active]=\"binderInputPathControl.value === suggestedPath\"\n [attr.data-testid]=\"'binder-input-suggestion-' + suggestedPath\"\n (click)=\"selectBinderInputPathSuggestion(suggestedPath)\"\n >\n <code>{{ suggestedPath }}</code>\n </button>\n }\n </div>\n }\n <div class=\"studio-actions\">\n <button mat-stroked-button type=\"button\" data-testid=\"binder-bind-btn\" (click)=\"bindStateTokenToWidget()\">\n <mat-icon aria-hidden=\"true\">link</mat-icon>{{ tx('editor.state.bindAction', 'Bind token to widget in draft') }}\n </button>\n <button mat-stroked-button type=\"button\" data-testid=\"binder-auto-bind-btn\" (click)=\"autoBindWidgetStatePlaceholders()\">\n <mat-icon aria-hidden=\"true\">auto_fix_high</mat-icon>{{ tx('editor.state.autoBindAction', 'Auto-bind ${...} widget placeholders') }}\n </button>\n </div>\n @if (binderMessage()) {\n <p class=\"studio-status\" role=\"status\" data-testid=\"binder-status-message\">{{ binderMessage() }}</p>\n }\n @if (invalidWidgetStateBindingPipes().length) {\n <div class=\"binder-unbound-warning\" role=\"alert\" data-testid=\"binder-invalid-pipes-warning\">\n <mat-icon aria-hidden=\"true\">error_outline</mat-icon>\n <div class=\"binder-unbound-warning__body\">\n <span>{{ tx('editor.state.invalidWidgetPipesWarning', 'Formatadores (pipes) inv\u00E1lidos detectados:') }}</span>\n <div class=\"binder-unbound-warning__chips\">\n @for (invalidBinding of invalidWidgetStateBindingPipes(); track invalidBinding.widgetKey + ':' + invalidBinding.inputPath) {\n <button\n type=\"button\"\n class=\"binder-unbound-chip\"\n [attr.data-testid]=\"'binder-invalid-pipe-chip-' + invalidBinding.widgetKey + '-' + invalidBinding.inputPath\"\n (click)=\"loadStateBindingIntoForm(invalidBinding)\"\n >\n <strong>{{ invalidBinding.widgetTitle }}</strong>.<code>{{ invalidBinding.inputPath }}</code> (<code>{{ invalidBinding.expression }}</code>)\n </button>\n }\n </div>\n </div>\n </div>\n }\n @if (unboundWidgetStatePlaceholders().length; as unboundCount) {\n <div class=\"binder-unbound-warning\" role=\"status\" data-testid=\"binder-unbound-placeholders-warning\">\n <mat-icon aria-hidden=\"true\">warning_amber</mat-icon>\n <div class=\"binder-unbound-warning__body\">\n <span>{{ tx('editor.state.unboundPlaceholdersWarning', 'Placeholders sem v\u00EDnculo detectados:') }}</span>\n <div class=\"binder-unbound-warning__chips\">\n @for (item of unboundWidgetStatePlaceholders(); track item.widgetKey + ':' + item.placeholder) {\n <button\n type=\"button\"\n class=\"binder-unbound-chip\"\n [attr.data-testid]=\"'binder-unbound-chip-' + item.widgetKey + '-' + item.placeholder\"\n [attr.title]=\"tx('editor.state.selectUnboundPlaceholderHint', 'Clique para preencher widget e caminho no formul\u00E1rio')\"\n (click)=\"selectUnboundPlaceholder(item)\"\n >\n <strong>{{ item.widgetTitle }}</strong> (<code>{{ '${' + item.placeholder + '}' }}</code>)\n </button>\n }\n </div>\n </div>\n </div>\n }\n @if (currentWidgetStateBindings().length) {\n <ul class=\"blocked-widgets-list binder-active-list\" data-testid=\"active-widget-state-bindings\">\n @for (binding of currentWidgetStateBindings(); track binding.widgetKey + ':' + binding.inputPath) {\n @let previewVal = resolveStateTokenPreview(binding.token);\n <li class=\"binder-active-item\">\n <mat-icon aria-hidden=\"true\" class=\"binder-active-icon\">{{ widgetIconForKey(binding.widgetKey) }}</mat-icon>\n <button\n type=\"button\"\n class=\"binder-active-copy binder-active-select-btn\"\n [attr.data-testid]=\"'binder-select-' + binding.widgetKey + '-' + binding.inputPath\"\n [attr.title]=\"tx('editor.state.editBindingHint', 'Click to load binding into form')\"\n (click)=\"loadStateBindingIntoForm(binding)\"\n >\n <strong>{{ binding.widgetTitle }}</strong>.<code>{{ binding.inputPath }}</code> \u2190 <code>{{ binding.token }}</code>\n @if (previewVal !== null) {\n <small class=\"binder-preview-badge\" [attr.data-testid]=\"'binder-preview-' + binding.widgetKey + '-' + binding.inputPath\">= {{ previewVal }}</small>\n }\n </button>\n <button mat-icon-button type=\"button\" [attr.data-testid]=\"'binder-unbind-' + binding.widgetKey + '-' + binding.inputPath\"\n [attr.aria-label]=\"tx('editor.state.unbindAction', 'Remove binding')\"\n (click)=\"unbindStateTokenFromWidget(binding.widgetKey, binding.inputPath)\">\n <mat-icon aria-hidden=\"true\">link_off</mat-icon>\n </button>\n </li>\n }\n </ul>\n }\n </section>\n\n <mat-form-field appearance=\"outline\" class=\"wide\">\n <mat-label>{{ tx('editor.state.dataSourcesJson', 'page.dataSources (JSON)') }}</mat-label>\n <textarea matInput rows=\"8\" [formControl]=\"dataSourcesControl\" [placeholder]=\"tx('editor.hints.dataSourcesExample', '')\" data-testid=\"page-datasources-json\"></textarea>\n </mat-form-field>\n </div>\n </mat-tab>\n <mat-tab [label]=\"tx('editor.limits.tab', 'Widget limits')\">\n <section class=\"limits-panel\" data-testid=\"canvas-limits-editor\">\n @if (workspaceTab() === 3 && activeTab() === 'limits' && canvasLimits.targets.length) {\n <div class=\"selection-identity\" tabindex=\"-1\" data-testid=\"canvas-limits-identity\">\n <mat-icon aria-hidden=\"true\">{{ limitsTargetIcon(limitsTargetControl.value) }}</mat-icon>\n <strong>{{ limitsTargetLabel(limitsTargetControl.value) }}</strong>\n </div>\n }\n\n <p class=\"hint\" id=\"canvas-limits-help\">{{ tx('editor.limits.help', 'Limits use grid columns and rows, not pixels. Leave blank for defaults, or to inherit the base limits on a device.') }}</p>\n @if (canvasLimits.targets.length) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ tx('editor.limits.target', 'Widget and layout') }}</mat-label>\n <mat-select [formControl]=\"limitsTargetControl\" data-testid=\"canvas-limits-target\">\n <mat-select-trigger>\n <span class=\"binder-widget-trigger\">\n <mat-icon aria-hidden=\"true\" class=\"binder-widget-trigger-icon\">{{ limitsTargetIcon(limitsTargetControl.value) }}</mat-icon>\n <span class=\"binder-widget-trigger-text\">{{ limitsTargetLabel(limitsTargetControl.value) }}</span>\n </span>\n </mat-select-trigger>\n @for (target of canvasLimits.targets; track target.scope + ':' + target.key; let index = $index) {\n <mat-option [value]=\"index\">\n <mat-icon aria-hidden=\"true\">{{ limitsTargetIcon(index) }}</mat-icon>\n <span>{{ limitsTargetLabel(index) }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <p class=\"hint\">{{ limitsGeometryLabel() }}</p>\n @if (canvasLimits.form.controls[limitsTargetControl.value]; as limitsForm) {\n <div class=\"limits-fields\" aria-describedby=\"canvas-limits-help\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ tx('editor.limits.minColSpan', 'Minimum width (columns)') }}</mat-label>\n <input matInput [errorStateMatcher]=\"limitsErrorMatcher\" type=\"number\" min=\"1\" step=\"1\" [formControl]=\"limitsForm.controls.minColSpan\" data-testid=\"canvas-limit-minColSpan\">\n <mat-error>{{ limitFieldError(limitsForm.controls.minColSpan) }}</mat-error>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ tx('editor.limits.maxColSpan', 'Maximum width (columns)') }}</mat-label>\n <input matInput [errorStateMatcher]=\"limitsErrorMatcher\" type=\"number\" min=\"1\" step=\"1\" [formControl]=\"limitsForm.controls.maxColSpan\" data-testid=\"canvas-limit-maxColSpan\">\n <mat-error>{{ limitFieldError(limitsForm.controls.maxColSpan) }}</mat-error>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ tx('editor.limits.minRowSpan', 'Minimum height (rows)') }}</mat-label>\n <input matInput [errorStateMatcher]=\"limitsErrorMatcher\" type=\"number\" min=\"1\" step=\"1\" [formControl]=\"limitsForm.controls.minRowSpan\" data-testid=\"canvas-limit-minRowSpan\">\n <mat-error>{{ limitFieldError(limitsForm.controls.minRowSpan) }}</mat-error>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ tx('editor.limits.maxRowSpan', 'Maximum height (rows)') }}</mat-label>\n <input matInput [errorStateMatcher]=\"limitsErrorMatcher\" type=\"number\" min=\"1\" step=\"1\" [formControl]=\"limitsForm.controls.maxRowSpan\" data-testid=\"canvas-limit-maxRowSpan\">\n <mat-error>{{ limitFieldError(limitsForm.controls.maxRowSpan) }}</mat-error>\n </mat-form-field>\n </div>\n @if (limitsUseContentHeight()) {\n <p class=\"limits-notice\"><mat-icon aria-hidden=\"true\">height</mat-icon><span>{{ tx('editor.limits.autoHeight', 'Choose natural height or an adjustable height with internal scrolling.') }}</span></p>\n <mat-button-toggle-group class=\"height-modes\" [formControl]=\"limitsForm.controls.heightMode\" [attr.aria-label]=\"tx('editor.height.mode', 'Widget height')\" data-testid=\"canvas-height-mode\">\n <mat-button-toggle value=\"inherit\">{{ tx('editor.height.inherit', 'Default') }}</mat-button-toggle>\n <mat-button-toggle value=\"auto\">{{ tx('editor.height.auto', 'Automatic') }}</mat-button-toggle>\n <mat-button-toggle value=\"fixed\">{{ tx('editor.height.fixed', 'Adjustable') }}</mat-button-toggle>\n </mat-button-toggle-group>\n @if (limitsForm.controls.heightMode.value === 'fixed') {\n <div class=\"limits-fields\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ tx('editor.height.value', 'Height (px)') }}</mat-label>\n <input matInput type=\"number\" min=\"160\" step=\"1\" [errorStateMatcher]=\"limitsErrorMatcher\" [formControl]=\"limitsForm.controls.heightPx\" data-testid=\"canvas-height-px\">\n <mat-error>{{ limitFieldError(limitsForm.controls.heightPx) }}</mat-error>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ tx('editor.height.top', 'Top inset (px)') }}</mat-label>\n <input matInput type=\"number\" min=\"0\" step=\"1\" [errorStateMatcher]=\"limitsErrorMatcher\" [formControl]=\"limitsForm.controls.offsetTopPx\" data-testid=\"canvas-height-top\">\n <mat-error>{{ limitFieldError(limitsForm.controls.offsetTopPx) }}</mat-error>\n </mat-form-field>\n </div>\n }\n <div class=\"limits-fields\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ tx('editor.height.min', 'Minimum height (px)') }}</mat-label>\n <input matInput type=\"number\" min=\"160\" step=\"1\" [errorStateMatcher]=\"limitsErrorMatcher\" [formControl]=\"limitsForm.controls.minHeightPx\" data-testid=\"canvas-limit-minHeightPx\">\n <mat-error>{{ limitFieldError(limitsForm.controls.minHeightPx) }}</mat-error>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ tx('editor.height.max', 'Maximum height (px)') }}</mat-label>\n <input matInput type=\"number\" min=\"160\" step=\"1\" [errorStateMatcher]=\"limitsErrorMatcher\" [formControl]=\"limitsForm.controls.maxHeightPx\" data-testid=\"canvas-limit-maxHeightPx\">\n <mat-error>{{ limitFieldError(limitsForm.controls.maxHeightPx) }}</mat-error>\n </mat-form-field>\n </div>\n }\n }\n @if (limitsSummaryError) { <p class=\"limits-error\" role=\"alert\">{{ limitsSummaryError }}</p> }\n } @else {\n <p>{{ tx('editor.limits.empty', 'Add widgets to the canvas to configure their limits.') }}</p>\n }\n </section>\n </mat-tab>\n </mat-tab-group></div>\n </mat-tab>\n </mat-tab-group>\n <details class=\"summary-details\">\n <summary>{{ tx('editor.summaryAria', 'Configuration summary') }}</summary>\n <div class=\"summary\">\n @if (!isHostedInSettingsPanel) {\n <span>{{ validationStateLabel }}</span>\n <span>{{ dirtyStateLabel }}</span>\n }\n <span>{{ selectedLayoutPresetLabel }}</span>\n <span>{{ selectedThemePresetLabel }}</span>\n <span>{{ canvasSummaryLabel }}</span>\n </div>\n </details>\n </div>\n", styles: [".page-inset-field{grid-column:1/-1;min-width:0}:host{display:block;min-width:0;max-width:100%}.height-modes{width:fit-content;max-width:100%;flex-wrap:wrap;margin-bottom:16px}.editor,.panel{display:grid;gap:16px;min-width:0;max-width:100%;color:var(--md-sys-color-on-surface, currentColor)}mat-tab-group,mat-form-field{min-width:0;max-width:100%}.head,.summary{display:flex;gap:12px;align-items:center;flex-wrap:wrap}.summary{color:var(--md-sys-color-on-surface-variant, currentColor)}.selection-identity{display:flex;align-items:flex-start;gap:8px;padding-block:8px;background:var(--md-sys-color-surface-container-low, var(--md-sys-color-surface));overflow-wrap:anywhere}.selection-identity mat-icon{flex-shrink:0}:host ::ng-deep .editor-tabs>.mat-mdc-tab-header .mdc-tab{min-width:0;padding-inline:12px}:host ::ng-deep .editor-tabs>.mat-mdc-tab-header .mdc-tab__text-label{max-width:128px;white-space:normal;overflow-wrap:anywhere;text-align:center;letter-spacing:normal;line-height:1.25}.grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(min(220px,100%),1fr))}@container (max-width: 600px){.grid{grid-template-columns:minmax(0,1fr)}.head,.summary{align-items:stretch}.head>button{width:100%}}@media(max-width:600px){.grid{grid-template-columns:minmax(0,1fr)}.head,.summary{align-items:stretch}.head>button{width:100%}}.card{display:grid;gap:12px;padding:16px;border:1px solid var(--md-sys-color-outline-variant, color-mix(in srgb, currentColor 18%, transparent));border-radius:8px;background:var(--md-sys-color-surface-container-low, var(--md-sys-color-surface-container, transparent));color:var(--md-sys-color-on-surface, currentColor)}.limits-panel{display:grid;gap:8px;min-width:0}.limits-fields{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr));gap:4px 16px}.limits-fields mat-form-field{min-width:0}.limits-notice{display:flex;gap:8px;align-items:flex-start;color:var(--md-sys-color-on-surface-variant, currentColor)}.limits-notice mat-icon{flex-shrink:0}.limits-error{color:var(--md-sys-color-error, #b3261e)}.limits-blocking-banner{border-color:var(--md-sys-color-error, #b3261e);gap:8px}.blocked-widgets-list{margin:0;padding-inline-start:20px;display:grid;gap:4px}.previous-heights-summary{display:flex;flex-wrap:wrap;gap:8px;align-items:center;font:var(--mat-sys-body-small, inherit);color:var(--md-sys-color-on-surface-variant, currentColor)}.previous-height-chip{padding:2px 8px;border-radius:999px;background:var(--md-sys-color-surface-container-highest, rgba(128, 128, 128, .14))}.wide{grid-column:1/-1}.hint{color:var(--md-sys-color-on-surface-variant, currentColor);font-size:12px}.eyebrow{color:var(--md-sys-color-primary, currentColor);font-size:11px;font-weight:700;letter-spacing:0;text-transform:uppercase}:host ::ng-deep .mat-mdc-tab-body-content{padding-top:12px;box-sizing:border-box}:host{container-type:inline-size}.studio-panel{display:grid;gap:var(--p-space-4, 16px);padding-block:var(--p-space-5, 20px);min-width:0}.studio-heading h2{font-size:var(--p-font-size-lg, 20px);line-height:1.25;margin:var(--p-space-2, 8px) 0;font-weight:600}.studio-heading p{margin:0;color:var(--md-sys-color-on-surface-variant);line-height:1.5}.studio-panel h3{font-size:var(--p-font-size-md, 16px);margin:0}.studio-panel>button{justify-self:start}.studio-gallery{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--p-space-3, 12px)}.studio-choice{display:flex;flex-direction:column;gap:var(--p-space-2, 8px);min-width:0;padding:var(--p-space-3, 12px);text-align:start;border:1px solid var(--md-sys-color-outline-variant);border-radius:var(--p-radius-lg, 12px);background:var(--md-sys-color-surface);color:var(--md-sys-color-on-surface);font:inherit;cursor:pointer;transition:border-color var(--md-sys-motion-duration-short2, .1s),background-color var(--md-sys-motion-duration-short2, .1s)}.studio-choice:hover:not(:disabled){border-color:var(--md-sys-color-primary);background:var(--md-sys-color-surface-container-low)}.studio-choice.selected{border-color:var(--md-sys-color-primary);box-shadow:inset 0 0 0 1px var(--md-sys-color-primary);background:var(--md-sys-color-surface-container-low)}.studio-choice:focus-visible,.studio-panel summary:focus-visible{outline:3px solid var(--md-sys-color-primary);outline-offset:3px}.studio-choice:disabled{opacity:var(--p-disabled-opacity, .5);cursor:not-allowed}.choice-title{display:flex;align-items:center;justify-content:space-between;gap:var(--p-space-2, 8px);width:100%;font-weight:600;line-height:1.35}.choice-title mat-icon{color:var(--md-sys-color-primary);flex-shrink:0;font-size:20px;height:20px;width:20px}.choice-description{font-size:var(--p-font-size-sm, 13px);line-height:1.5;color:var(--md-sys-color-on-surface-variant)}.choice-meta{font-size:var(--p-font-size-xs, 12px);color:var(--md-sys-color-on-surface-variant);margin-top:auto;padding-top:var(--p-space-1, 4px)}.theme-sample{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));padding:var(--p-space-3, 12px);border-radius:var(--p-radius-md, 8px);min-height:128px;align-self:stretch;margin-bottom:var(--p-space-1, 4px)}.sample-metric,.sample-chart{background:var(--sample-surface);border-radius:var(--sample-radius);box-shadow:var(--sample-shadow);padding:var(--p-space-2, 8px)}.sample-metric b{display:block;font-size:var(--p-font-size-md, 16px)}.sample-metric i{display:block;height:3px;width:55%;margin-top:var(--p-space-1, 4px);background:var(--sample-accent)}.sample-chart{grid-column:1/-1;display:flex;height:40px;align-items:flex-end;gap:var(--p-space-2, 8px)}.sample-chart i{flex:1;background:var(--sample-accent);height:50%;border-radius:var(--p-radius-xs, 2px)}.sample-chart i:nth-child(2){height:85%}.sample-chart i:nth-child(3){height:65%}.sample-chart i:nth-child(4){height:100%}.sample-chart i:nth-child(5){height:75%}.shell-options,.arrangement-rules{border-top:1px solid var(--md-sys-color-outline-variant);padding-top:var(--p-space-3, 12px)}.studio-panel summary{cursor:pointer;font-weight:500;line-height:1.5;padding-block:var(--p-space-2, 8px)}.shell-gallery{margin-top:var(--p-space-4, 16px)}.shell-sample{display:block;align-self:stretch;border:1px solid;overflow:hidden;margin:var(--p-space-2, 8px)}.shell-sample>span{display:flex;gap:var(--p-space-2, 8px);align-items:center;padding:var(--p-space-2, 8px);border-bottom:1px solid transparent}.shell-sample i{height:4px;width:60%;background:var(--md-sys-color-on-surface-variant);opacity:.4}.shell-sample>span:last-child{min-height:30px;flex-direction:column;align-items:flex-start}.shell-sample>span:last-child i:last-child{width:40%}.selection-line{margin:0;font-size:var(--p-font-size-sm, 13px);color:var(--md-sys-color-on-surface-variant)}.studio-actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--p-space-2, 8px)}.distribution-sample{display:grid;grid-template-rows:repeat(2,1fr);gap:var(--p-space-1, 4px);align-self:stretch;height:76px;padding:var(--p-space-2, 8px);background:var(--md-sys-color-surface-container);border-radius:var(--p-radius-md, 8px)}.distribution-sample i{display:grid;place-items:center;background:var(--md-sys-color-secondary-container);color:var(--md-sys-color-on-secondary-container);border-radius:var(--p-radius-sm, 4px);font-size:var(--p-font-size-xs, 12px);font-style:normal}.distribution-sample i:nth-child(n+4){opacity:.65}.selected .distribution-sample i{background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}.arrangement-review{display:grid;gap:var(--p-space-3, 12px);padding-block:var(--p-space-4, 16px);border-top:2px solid var(--md-sys-color-primary)}.review-heading{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:var(--p-space-2, 8px)}.review-heading>span{font-size:var(--p-font-size-sm, 13px);color:var(--md-sys-color-primary)}.canvas-comparison{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--p-space-3, 12px)}.canvas-comparison figure{margin:0;min-width:0}.canvas-comparison figcaption{font-weight:500;margin-bottom:var(--p-space-2, 8px)}.canvas-map{display:grid;gap:2px;height:220px;padding:var(--p-space-2, 8px);border:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface-container-low);border-radius:var(--p-radius-md, 8px)}.canvas-map span{display:grid;place-items:center;min-height:0;min-width:0;overflow:hidden;font-size:var(--p-font-size-xs, 12px);color:var(--md-sys-color-on-secondary-container);background:var(--md-sys-color-secondary-container);border-radius:var(--p-radius-xs, 2px)}.canvas-map .changed{background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}.widget-changes{padding-inline-start:var(--p-space-5, 20px);margin:0}.widget-changes li{padding-block:var(--p-space-2, 8px);overflow-wrap:anywhere}.widget-changes span{display:block;font-size:var(--p-font-size-sm, 13px);color:var(--md-sys-color-on-surface-variant)}.studio-status{border-inline-start:3px solid var(--md-sys-color-primary);background:var(--md-sys-color-surface-container-low);padding:var(--p-space-3, 12px);margin:0;line-height:1.5}@container (max-width:420px){.studio-gallery{grid-template-columns:minmax(0,1fr)}.canvas-map{height:180px}}@media(prefers-reduced-motion:reduce){.studio-choice{transition:none}}.arrangement-feedback:empty{display:none}.arrangement-feedback:focus{outline:none}.arrangement-feedback:focus-visible{outline:2px solid var(--md-sys-color-primary);outline-offset:3px}@container (min-width:620px){.arrangement-gallery{grid-template-columns:repeat(3,minmax(0,1fr))}}:host ::ng-deep .studio-tabs>.mat-mdc-tab-header{position:sticky;top:0;z-index:var(--praxis-layer-sticky-local, 100);background:var(--md-sys-color-surface-container-low, var(--md-sys-color-surface))}:host ::ng-deep .studio-tabs>.mat-mdc-tab-header .mdc-tab{flex:1 1 0;padding-inline:var(--p-space-1, 4px)}.arrangement-feedback{scroll-margin-block-start:var(--p-space-16, 64px)}.filter-owner-banner{border-color:var(--md-sys-color-tertiary, var(--md-sys-color-primary))}.filter-owner-item{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px}.quick-geometry-rows{display:grid;gap:8px}.quick-geometry-row{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px;padding-block:4px;border-bottom:1px solid var(--md-sys-color-outline-variant, rgba(128, 128, 128, .2))}.quick-geometry-label{display:inline-flex;align-items:center;gap:6px;min-width:0}.quick-geometry-label mat-icon{font-size:18px;width:18px;height:18px;flex-shrink:0;color:var(--md-sys-color-primary, currentColor)}.binder-widget-trigger{display:inline-flex;align-items:center;gap:8px;max-width:100%;min-width:0}.binder-widget-trigger-icon{font-size:18px;width:18px;height:18px;flex-shrink:0;color:var(--md-sys-color-primary, currentColor)}.binder-widget-trigger-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.binder-widget-trigger-text small{color:var(--md-sys-color-on-surface-variant, currentColor);font-weight:400}.binder-widget-option-row{position:relative;display:flex;align-items:flex-start;gap:10px;padding-block:4px;min-width:0}.binder-widget-option-icon{font-size:20px;width:20px;height:20px;margin-top:2px;flex-shrink:0;color:var(--md-sys-color-primary, currentColor)}.binder-widget-option-copy{display:flex;flex-direction:column;gap:2px;min-width:0;line-height:1.3}.binder-widget-option-title{font-weight:600;color:var(--md-sys-color-on-surface, currentColor);overflow-wrap:anywhere}.binder-widget-option-desc{font-size:12px;color:var(--md-sys-color-on-surface-variant, currentColor);overflow-wrap:anywhere}.binder-widget-option-sr-key{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;opacity:0;pointer-events:none}.binder-input-suggestions{display:flex;flex-wrap:wrap;align-items:center;gap:6px}.binder-input-suggestion-chip{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;border:1px solid var(--md-sys-color-outline-variant, rgba(128, 128, 128, .3));background:var(--md-sys-color-surface, transparent);color:var(--md-sys-color-on-surface, currentColor);font-size:12px;cursor:pointer;transition:border-color .1s,background-color .1s}.binder-input-suggestion-chip:hover{border-color:var(--md-sys-color-primary, currentColor);background:var(--md-sys-color-surface-container-low, rgba(128, 128, 128, .08))}.binder-input-suggestion-chip.active{border-color:var(--md-sys-color-primary, currentColor);background:var(--md-sys-color-primary-container, rgba(128, 128, 128, .16));color:var(--md-sys-color-on-primary-container, currentColor)}.binder-active-list{list-style:none;padding-inline-start:0;gap:6px}.binder-active-item{display:flex;align-items:center;gap:8px;padding:4px 8px;border-radius:6px;background:var(--md-sys-color-surface, rgba(255, 255, 255, .03))}.binder-active-icon{font-size:18px;width:18px;height:18px;flex-shrink:0;color:var(--md-sys-color-primary, currentColor)}.binder-active-copy{flex:1 1 auto;min-width:0;overflow-wrap:anywhere}.binder-active-select-btn{appearance:none;border:none;background:transparent;color:inherit;font:inherit;text-align:start;padding:2px 4px;border-radius:4px;cursor:pointer}.binder-active-select-btn:hover{background:var(--md-sys-color-surface-container-low, rgba(128, 128, 128, .08))}.binder-active-select-btn:focus-visible{outline:2px solid var(--md-sys-color-primary, currentColor);outline-offset:2px}.binder-preview-badge{display:inline-flex;align-items:center;margin-inline-start:6px;padding:1px 6px;border-radius:999px;font-size:11px;font-weight:600;color:var(--md-sys-color-on-secondary-container, #1d192b);background:var(--md-sys-color-secondary-container, rgba(103, 80, 164, .14))}.binder-unbound-warning{display:flex;align-items:flex-start;gap:8px;padding:8px 10px;border-radius:6px;font-size:12px;border:1px solid var(--md-sys-color-tertiary, rgba(180, 110, 0, .35));background:color-mix(in srgb,var(--md-sys-color-tertiary-container, #ffdea6) 45%,transparent);color:var(--md-sys-color-on-surface, inherit)}.binder-unbound-warning__body{display:grid;gap:6px}.binder-unbound-warning__chips{display:flex;flex-wrap:wrap;gap:6px}.binder-unbound-chip{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:999px;border:1px solid var(--md-sys-color-outline-variant, rgba(128, 128, 128, .35));background:var(--md-sys-color-surface, rgba(255, 255, 255, .65));color:inherit;font:inherit;font-size:11px;cursor:pointer}.binder-unbound-chip:hover{border-color:var(--md-sys-color-primary, currentColor)}.binder-unbound-chip:focus-visible{outline:2px solid var(--md-sys-color-primary, currentColor);outline-offset:2px}.guided-presets-bar{display:grid;gap:6px}.datasource-preview-box{display:grid;gap:8px;padding:12px;border-radius:6px;background:var(--md-sys-color-surface-container, rgba(128, 128, 128, .08))}.datasource-preview-json{margin:0;padding:8px;border-radius:4px;font-size:12px;overflow-x:auto;background:var(--md-sys-color-surface, rgba(0, 0, 0, .04))}.composer-rich-field-option{display:flex;flex-direction:column;gap:2px;padding:2px 0;line-height:1.3;min-width:0}.composer-rich-field-option__header{display:flex;align-items:center;flex-wrap:wrap;gap:6px;min-width:0}.composer-rich-field-option__label{font-weight:500;color:var(--md-sys-color-on-surface, currentColor);overflow:hidden;text-overflow:ellipsis}.composer-rich-field-option__code{font-family:var(--mat-sys-mono-font, monospace);font-size:11px;padding:1px 6px;border-radius:999px;background:var(--md-sys-color-surface-container-highest, rgba(128, 128, 128, .14));color:var(--md-sys-color-on-surface-variant, currentColor)}.composer-rich-field-option__badge{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.03em;padding:1px 6px;border-radius:999px;background:color-mix(in srgb,var(--md-sys-color-primary, #4f46e5) 14%,transparent);color:var(--md-sys-color-primary, currentColor)}.composer-rich-field-option__desc{font-size:11px;color:var(--md-sys-color-on-surface-variant, currentColor);white-space:normal;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}\n"], dependencies: [{ kind: "component", type: PagePresetComparisonComponent, selector: "praxis-page-preset-comparison", inputs: ["expanded", "layoutId", "themeId", "layoutPresetOptions", "canvas"], outputs: ["choose"] }, { 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],[formArray],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: MatAutocompleteModule }, { kind: "component", type: i2$3.MatAutocomplete, selector: "mat-autocomplete", inputs: ["aria-label", "aria-labelledby", "displayWith", "autoActiveFirstOption", "autoSelectActiveOption", "requireSelection", "panelWidth", "disableRipple", "class", "hideSingleSelectionIndicator"], outputs: ["optionSelected", "opened", "closed", "optionActivated"], exportAs: ["matAutocomplete"] }, { kind: "component", type: i3$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: i2$3.MatAutocompleteTrigger, selector: "input[matAutocomplete], textarea[matAutocomplete]", inputs: ["matAutocomplete", "matAutocompletePosition", "matAutocompleteConnectedTo", "autocomplete", "matAutocompleteDisabled"], exportAs: ["matAutocompleteTrigger"] }, { 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: MatButtonToggleModule }, { kind: "directive", type: i4.MatButtonToggleGroup, selector: "mat-button-toggle-group", inputs: ["appearance", "name", "vertical", "value", "multiple", "disabled", "disabledInteractive", "hideSingleSelectionIndicator", "hideMultipleSelectionIndicator"], outputs: ["valueChange", "change"], exportAs: ["matButtonToggleGroup"] }, { kind: "component", type: i4.MatButtonToggle, selector: "mat-button-toggle", inputs: ["aria-label", "aria-labelledby", "id", "name", "value", "tabIndex", "disableRipple", "appearance", "checked", "disabled", "disabledInteractive"], outputs: ["change"], exportAs: ["matButtonToggle"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i7.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i7.MatLabel, selector: "mat-label" }, { kind: "directive", type: i7.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i7.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i7.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$1.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: i3$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: "directive", type: i3$1.MatSelectTrigger, selector: "mat-select-trigger" }, { kind: "ngmodule", type: MatTabsModule }, { kind: "directive", type: i9$1.MatTabContent, selector: "[matTabContent]" }, { kind: "component", type: i9$1.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i9$1.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: PraxisIconButtonComponent, selector: "button[praxisIconButton]", inputs: ["praxisIconButton", "size", "appearance", "presentation", "pressed", "busy"] }, { kind: "component", type: ResourceQuickConnectComponent, selector: "praxis-resource-quick-connect", inputs: ["resourcePath", "endpointKey", "apiUrlEntry", "intent", "resources", "label", "placeholder", "hint", "disabled"], outputs: ["resourcePathChange", "resourceSelected"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
18575
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: DynamicPageConfigEditorComponent, isStandalone: true, selector: "praxis-dynamic-page-config-editor", inputs: { page: "page", layout: "layout", presets: "presets", pagePreset: "pagePreset" }, outputs: { pageChange: "pageChange" }, host: { listeners: { "window:resize": "refreshRenderedValidation()" } }, providers: [providePraxisPageBuilderI18n()], viewQueries: [{ propertyName: "arrangementFeedback", first: true, predicate: ["arrangementFeedback"], descendants: true }], ngImport: i0, template: "<div class=\"editor\">\n @if (!isHostedInSettingsPanel) { <div class=\"head\">\n <div>{{ tx('editor.title', 'Page settings') }}</div>\n <button mat-stroked-button type=\"button\" (click)=\"reset()\">{{ tx('editor.reload', 'Reload') }}</button>\n </div> }\n\n @if (nonLimitError) { <p class=\"limits-error\" role=\"alert\">{{ nonLimitError }}</p> }\n @if (limitsError() || previousAdjustableHeights().length) {\n <section class=\"card limits-blocking-banner\" role=\"region\" [attr.aria-label]=\"tx('editor.limits.blockedBannerTitle', 'Height geometry requires review in Widget limits')\" data-testid=\"preset-geometry-blocking-banner\">\n @if (limitsError()) {\n <div class=\"head\">\n <strong>{{ tx('editor.limits.blockedBannerTitle', 'Height geometry requires review in Widget limits') }}</strong>\n </div>\n <p class=\"hint\">{{ tx('editor.limits.blockedBannerHelp', 'One or more widgets use an adjustable pixel height that exceeds the current usable area or requires review before applying the page or template:') }}</p>\n <ul class=\"blocked-widgets-list\">\n @for (item of blockedGeometryWidgets; track item.scope + ':' + item.key) {\n <li>\n <strong>{{ item.title }}</strong>\n @if (item.title !== item.key && !item.title.endsWith('(' + item.key + ')')) { <code>({{ item.key }})</code> }\n <span> \u00B7 {{ item.scopeLabel }}</span>\n @if (item.heightPx !== null) {\n <span> \u00B7 {{ tx('editor.height.fixed', 'Adjustable') }}: <strong>{{ item.heightPx }}px</strong></span>\n }\n </li>\n }\n </ul>\n <p class=\"limits-error\" role=\"alert\">{{ limitsError() }}</p>\n <div class=\"head\">\n <button mat-stroked-button type=\"button\" data-testid=\"goto-blocked-widget-limits\" (click)=\"navigateToBlockedLimit()\">\n <mat-icon aria-hidden=\"true\">tune</mat-icon>{{ tx('editor.limits.gotoLimitsAction', 'Go to Adjustments \u2192 Widget limits') }}\n </button>\n <button mat-flat-button type=\"button\" data-testid=\"switch-blocked-widgets-auto-height\" (click)=\"switchBlockedWidgetsToAutoHeight()\">\n <mat-icon aria-hidden=\"true\">height</mat-icon>{{ tx('editor.limits.switchToAutoHeightAction', 'Switch affected widgets to Automatic height') }}\n </button>\n </div>\n }\n @if (previousAdjustableHeights().length) {\n <div class=\"previous-heights-summary\" data-testid=\"previous-adjustable-heights-summary\">\n <span>{{ tx('editor.limits.previousHeightsSummary', 'Previous adjustable heights preserved for reference:') }}</span>\n @for (prev of previousAdjustableHeights(); track prev.scope + ':' + prev.key) {\n <span class=\"previous-height-chip\">{{ prev.title }} ({{ prev.scopeLabel }}): <strong>{{ prev.heightPx }}px</strong></span>\n }\n </div>\n }\n </section>\n }\n <mat-tab-group class=\"editor-tabs studio-tabs\" [mat-stretch-tabs]=\"false\" [selectedIndex]=\"workspaceTab()\" (selectedIndexChange)=\"workspaceTab.set($event)\">\n <mat-tab [label]=\"tx('studio.visualTab', 'Visual templates')\"><ng-template matTabContent>\n <section class=\"studio-panel\" data-testid=\"studio-visual\">\n <header class=\"studio-heading\"><span class=\"eyebrow\">{{ tx('studio.visualEyebrow', 'Appearance') }}</span><h2>{{ tx('studio.visualTitle', 'Give the page a visual language') }}</h2><p>{{ tx('studio.visualHelp', 'Choose surfaces, density and chart styling together. Individual widget overrides still take precedence.') }}</p></header>\n <button mat-stroked-button type=\"button\" [attr.aria-pressed]=\"!themePresetControl.value\" (click)=\"themePresetControl.setValue('')\">{{ tx('editor.layout.layoutDefault', 'Layout default') }}</button>\n <p class=\"selection-line\">{{ tx('presets.effectiveTheme', 'Effective page theme') }}: <strong>{{ selectedThemePresetEntry?.label || themePresetControl.value || recommendedThemePresetEntry?.label || tx('presets.hostTheme', 'Host theme') }}</strong></p>\n <div class=\"studio-gallery\">\n @for (theme of themePresetEntries; track theme.id) {\n <button type=\"button\" class=\"studio-choice\" [class.selected]=\"themePresetControl.value === theme.id\" [attr.aria-pressed]=\"themePresetControl.value === theme.id\" [attr.data-theme-preset]=\"theme.id\" (click)=\"themePresetControl.setValue(theme.id)\">\n <span class=\"theme-sample\" aria-hidden=\"true\" [style.background]=\"themeTokens(theme.id)['--pdx-page-bg']\" [style.--sample-accent]=\"themeTokens(theme.id)['--pdx-page-accent']\" [style.--sample-surface]=\"themeTokens(theme.id)['--pdx-page-surface']\" [style.--sample-radius]=\"themeTokens(theme.id)['--pdx-page-card-radius']\" [style.--sample-shadow]=\"themeTokens(theme.id)['--pdx-page-card-shadow']\" [style.gap]=\"themeTokens(theme.id)['--pdx-page-gap']\">\n <span class=\"sample-metric\"><b>128</b><i></i></span><span class=\"sample-metric\"><b>86%</b><i></i></span><span class=\"sample-chart\"><i></i><i></i><i></i><i></i><i></i></span>\n </span>\n <span class=\"choice-title\">{{ theme.label }}@if (themePresetControl.value === theme.id) {<mat-icon aria-hidden=\"true\">check_circle</mat-icon>}</span>\n <span class=\"choice-description\">{{ tx('studio.theme.' + theme.id, theme.label) }}</span>\n <span class=\"choice-meta\">{{ tx('studio.density.' + theme.density, theme.density) }} \u00B7 {{ shellName(theme.shellPreset) }}</span>\n </button>\n }\n </div>\n <p class=\"hint\">{{ tx('studio.samplesHelp', 'Illustrative samples using the preset tokens. Apply to see the result with your content.') }}</p>\n <details class=\"shell-options\"><summary>{{ tx('studio.shellTitle', 'Customize widget surfaces') }} \u00B7 {{ shellPresetEntries.length }}</summary>\n <p>{{ tx('studio.shellHelp', 'Override the page theme\u2019s default widget surface. Per-widget appearance settings keep their priority.') }}</p>\n <button mat-stroked-button type=\"button\" [attr.aria-pressed]=\"!shellPresetControl.value\" (click)=\"shellPresetControl.setValue('')\">{{ tx('editor.layout.inheritShell', 'Inherit appearance') }}</button>\n <div class=\"studio-gallery shell-gallery\">@for (shell of shellPresetEntries; track shell.id) {\n <button type=\"button\" class=\"studio-choice\" [class.selected]=\"shellPresetControl.value === shell.id\" [attr.aria-pressed]=\"shellPresetControl.value === shell.id\" (click)=\"shellPresetControl.setValue(shell.id)\">\n <span class=\"shell-sample\" aria-hidden=\"true\" [style.background]=\"shell.appearance.card?.background\" [style.border-color]=\"shell.appearance.card?.borderColor\" [style.border-radius]=\"shell.appearance.card?.borderRadius\" [style.box-shadow]=\"shell.appearance.card?.shadow\"><span [style.background]=\"shell.appearance.header?.background\" [style.border-color]=\"shell.appearance.header?.borderColor\"><mat-icon>analytics</mat-icon><i></i></span><span [style.background]=\"shell.appearance.body?.background\"><i></i><i></i></span></span>\n <span class=\"choice-title\">{{ shellPresetLabel(shell) }}@if (shellPresetControl.value === shell.id) {<mat-icon aria-hidden=\"true\">check_circle</mat-icon>}</span><span class=\"choice-description\">{{ shellDescription(shell) }}</span>\n </button>\n }</div>\n </details>\n </section>\n </ng-template></mat-tab>\n <mat-tab [label]=\"tx('studio.layoutTab', 'Layout templates')\"><ng-template matTabContent>\n <section class=\"studio-panel\" data-testid=\"studio-layout\">\n <header class=\"studio-heading\"><span class=\"eyebrow\">{{ tx('studio.layoutEyebrow', 'Composition model') }}</span><h2>{{ tx('studio.layoutTitle', 'Choose the structure for your use case') }}</h2><p>{{ tx('studio.layoutHelp', 'Compare the roles and default theme of each model. To redistribute the widgets already on this canvas, open Arrange.') }}</p></header>\n @if (duplicateFilterOwners().length) {\n <section class=\"card filter-owner-banner\" role=\"region\" [attr.aria-label]=\"tx('studio.filterOwnerBannerTitle', 'Duplicate filter region detected')\" data-testid=\"duplicate-filter-owner-banner\">\n <div class=\"head\">\n <strong>{{ tx('studio.filterOwnerBannerTitle', 'Single filter region recommendation') }}</strong>\n </div>\n <p class=\"hint\">{{ tx('studio.filterOwnerBannerHelp', 'A table has both its integrated filter bar and an external filter link active. Choose a single query owner to avoid competing filter regions:') }}</p>\n @for (item of duplicateFilterOwners(); track item.tableKey) {\n <div class=\"filter-owner-item\" [attr.data-testid]=\"'duplicate-filter-owner-' + item.tableKey\">\n <span><strong>{{ item.tableTitle }}</strong> <code>({{ item.tableKey }})</code> \u2190 {{ item.externalSourceLabels.join(', ') }}</span>\n <div class=\"studio-actions\">\n <button mat-stroked-button type=\"button\" [attr.data-testid]=\"'consolidate-filter-table-toolbar-' + item.tableKey\" (click)=\"consolidateFilterOwnership(item.tableKey, 'keep-table-toolbar')\">\n {{ tx('studio.keepTableToolbarFilter', 'Use integrated table filter') }}\n </button>\n <button mat-stroked-button type=\"button\" [attr.data-testid]=\"'consolidate-filter-external-link-' + item.tableKey\" (click)=\"consolidateFilterOwnership(item.tableKey, 'keep-external-link')\">\n {{ tx('studio.keepExternalFilterLink', 'Use external filter bar') }}\n </button>\n </div>\n </div>\n }\n </section>\n }\n <details class=\"card composer-card\" data-testid=\"collection-screen-composer\">\n <summary>{{ tx('studio.composer.title', 'Guided collection screen composer (5-minute flow)') }}</summary>\n <p class=\"hint\">{{ tx('studio.composer.help', 'Assemble a complete collection screen with header, KPI indicators, single filter region, primary collection, and optional detail surface in the draft.') }}</p>\n <div class=\"grid\">\n <praxis-resource-quick-connect\n [formControl]=\"composerResourcePathControl\"\n intent=\"collection\"\n [label]=\"tx('studio.composer.resourcePath', 'Resource path')\"\n (resourceSelected)=\"onComposerResourceSelected($event)\"\n data-testid=\"composer-resource-path\"\n />\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('studio.composer.screenTitle', 'Screen title') }}</mat-label>\n <input matInput [formControl]=\"composerTitleControl\" data-testid=\"composer-title\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('studio.composer.groupField', 'KPI group-by field') }}</mat-label>\n <input\n matInput\n [formControl]=\"composerGroupFieldControl\"\n [matAutocomplete]=\"composerGroupFieldPicker\"\n data-testid=\"composer-group-field\"\n />\n <mat-autocomplete\n #composerGroupFieldPicker=\"matAutocomplete\"\n (optionSelected)=\"composerGroupFieldControl.markAsPristine()\"\n >\n @for (field of filteredComposerGroupFieldOptions(); track field.name) {\n <mat-option [value]=\"field.name\" [attr.data-testid]=\"'composer-group-field-option-' + field.name\">\n <div class=\"composer-rich-field-option\">\n <div class=\"composer-rich-field-option__header\">\n <span class=\"composer-rich-field-option__label\">{{ field.label || field.name }}</span>\n @if (field.label && field.label !== field.name) {\n <code class=\"composer-rich-field-option__code\">{{ field.name }}</code>\n }\n @if (getComposerFieldTypeBadge(field); as badge) {\n <span class=\"composer-rich-field-option__badge\">{{ badge }}</span>\n }\n </div>\n @if (getComposerFieldDescription(field); as desc) {\n <span class=\"composer-rich-field-option__desc\">{{ desc }}</span>\n }\n </div>\n </mat-option>\n }\n </mat-autocomplete>\n @if (composerSelectedResource()?.capabilities?.stats === false) {\n <mat-hint data-testid=\"composer-stats-unsupported-hint\">{{ tx('studio.composer.statsUnsupportedHint', 'This resource does not publish stats/group-by aggregation; summary indicators were disabled automatically.') }}</mat-hint>\n } @else {\n <mat-hint>{{ tx('studio.composer.groupFieldHint', 'Choose the boolean or categorical field used in summary indicators.') }}</mat-hint>\n }\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('studio.composer.filterMode', 'Query filter owner') }}</mat-label>\n <mat-select [formControl]=\"composerFilterModeControl\" data-testid=\"composer-filter-mode\">\n <mat-option value=\"table-integrated\">{{ tx('studio.composer.filterIntegrated', 'Integrated table toolbar') }}</mat-option>\n <mat-option value=\"external-filter-bar\">{{ tx('studio.composer.filterExternal', 'Dedicated filter bar widget') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('studio.composer.collectionKind', 'Primary collection') }}</mat-label>\n <mat-select [formControl]=\"composerCollectionKindControl\" data-testid=\"composer-collection-kind\">\n <mat-option value=\"table\">praxis-Table</mat-option>\n <mat-option value=\"list\">praxis-List</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('studio.composer.detailSurface', 'Secondary detail surface') }}</mat-label>\n <mat-select [formControl]=\"composerDetailSurfaceControl\" data-testid=\"composer-detail-surface\">\n <mat-option value=\"drawer\">Drawer</mat-option>\n <mat-option value=\"modal\">Modal</mat-option>\n <mat-option value=\"none\">{{ tx('studio.composer.noSurface', 'None') }}</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div class=\"studio-actions\">\n <button mat-flat-button type=\"button\" data-testid=\"composer-generate-btn\" (click)=\"composeCollectionScreen()\">\n <mat-icon aria-hidden=\"true\">auto_awesome</mat-icon>{{ tx('studio.composer.generate', 'Generate collection screen in draft') }}\n </button>\n </div>\n @if (composerMessage()) {\n <p class=\"studio-status\" role=\"status\" data-testid=\"composer-status-message\">{{ composerMessage() }}</p>\n }\n </details>\n <div class=\"studio-actions\"><button mat-stroked-button type=\"button\" (click)=\"layoutPresetControl.setValue('')\">{{ tx('studio.inheritLayout', 'Use inherited model') }}</button><button mat-button type=\"button\" (click)=\"workspaceTab.set(2)\">{{ tx('studio.goArrange', 'Arrange existing widgets') }}<mat-icon iconPositionEnd>arrow_forward</mat-icon></button></div>\n <praxis-page-preset-comparison [expanded]=\"true\" [layoutId]=\"layoutPresetControl.value\" [layoutPresetOptions]=\"layoutPresetOptions\" [themeId]=\"themePresetControl.value\" [canvas]=\"true\" (choose)=\"layoutPresetControl.setValue($event)\" />\n </section>\n </ng-template></mat-tab>\n <mat-tab [label]=\"tx('studio.arrangeTab', 'Arrange')\"><ng-template matTabContent>\n <section class=\"studio-panel\" data-testid=\"canvas-arrangement\">\n <header class=\"studio-heading\"><span class=\"eyebrow\">{{ tx('studio.arrangeEyebrow', 'Existing widgets') }} \u00B7 {{ canvasColumnsControl.value }} {{ tx('editor.layout.cols', 'Columns') }}</span><h2>{{ tx('studio.arrangeTitle', 'Find the right balance of space') }}</h2><p>{{ tx('studio.arrangeHelp', 'Choose a distribution, compare the real positions, then use it in the draft. Widgets keep their reading order and content.') }}</p></header>\n <div #arrangementFeedback tabindex=\"-1\" class=\"arrangement-feedback\">\n @if (arrangementPreview(); as preview) {\n <section class=\"arrangement-review\" aria-live=\"polite\" data-testid=\"arrangement-preview\">\n <div class=\"review-heading\"><h3>{{ tx('studio.reviewTitle', 'Compare before using') }}</h3><span>{{ preview.keys.length }} {{ tx('studio.changedWidgets', 'widgets change') }}</span></div>\n <div class=\"canvas-comparison\">\n @for (side of ['before', 'after']; track side) {\n <figure><figcaption>{{ side === 'before' ? tx('studio.before', 'Before') : tx('studio.after', 'After') }}</figcaption>\n <div class=\"canvas-map\" [style.grid-template-columns]=\"'repeat(' + preview.canvas.columns + ', minmax(0, 1fr))'\" [style.grid-template-rows]=\"'repeat(' + previewRowCount(preview.before, preview.canvas) + ', minmax(0, 1fr))'\" role=\"img\" [attr.aria-label]=\"side === 'before' ? tx('studio.before', 'Before') : tx('studio.after', 'After')\">\n @for (item of previewItems(side === 'before' ? preview.before : preview.canvas); track item.key) {\n <span [style.grid-column]=\"item.col + ' / span ' + item.colSpan\" [style.grid-row]=\"item.row + ' / span ' + item.rowSpan\" [class.changed]=\"preview.keys.includes(item.key)\" [matTooltip]=\"item.label\">{{ item.number }}</span>\n }\n </div>\n </figure>\n }\n </div>\n <p class=\"hint\">{{ tx('studio.mapHelp', 'Numbered map of your widgets. Positions and spans are to scale; content height is evaluated on the page.') }}</p>\n <details><summary>{{ tx('studio.widgetDetails', 'Widget positions and sizes') }}</summary><ol class=\"widget-changes\">@for (item of previewItems(preview.canvas); track item.key) {<li><strong>{{ item.label }}</strong><span>{{ tx('studio.before', 'Before') }}: {{ tx('editor.arrange.position', 'Column / row') }} {{ preview.before.items[item.key].col }} / {{ preview.before.items[item.key].row }} \u00B7 {{ tx('studio.gridSize', 'Size in columns \u00D7 rows') }} {{ preview.before.items[item.key].colSpan }} \u00D7 {{ preview.before.items[item.key].rowSpan }}</span><span>{{ tx('studio.after', 'After') }}: {{ tx('editor.arrange.position', 'Column / row') }} {{ item.col }} / {{ item.row }} \u00B7 {{ tx('studio.gridSize', 'Size in columns \u00D7 rows') }} {{ item.colSpan }} \u00D7 {{ item.rowSpan }}</span></li>}</ol></details>\n <button mat-flat-button type=\"button\" data-testid=\"arrangement-use\" (click)=\"useArrangement()\">{{ tx('editor.arrange.use', 'Use arrangement in draft') }}<mat-icon iconPositionEnd>arrow_forward</mat-icon></button>\n </section>\n }\n @if (arrangementMessage()) {<p class=\"studio-status\" role=\"status\">{{ arrangementMessage() }}</p>}\n @if (arrangementUndoAvailable()) {<button mat-stroked-button type=\"button\" data-testid=\"arrangement-undo\" (click)=\"undoArrangement()\"><mat-icon>undo</mat-icon>{{ tx('editor.arrange.undo', 'Undo arrangement') }}</button>}\n </div>\n <div class=\"studio-gallery arrangement-gallery\">\n @for (choice of arrangementChoices; track choice.id) {\n <button type=\"button\" class=\"studio-choice\" [class.selected]=\"arrangementSelection() === choice.id\" [attr.aria-pressed]=\"arrangementSelection() === choice.id\" [disabled]=\"!hasCanvasWidgets || !arrangementFits(choice.id)\" [attr.data-testid]=\"'arrange-' + choice.id\" (click)=\"prepareArrangement(choice.id)\">\n <span class=\"distribution-sample\" aria-hidden=\"true\" [style.grid-template-columns]=\"(arrangementFits(choice.id) ? arrangementWidths(choice.id) : choice.weights).join('fr ') + 'fr'\">@for (weight of choice.weights; track $index) {<i>{{ arrangementWidths(choice.id)[$index] }}</i>}@for (weight of choice.weights; track $index) {<i></i>}</span>\n <span class=\"choice-title\">{{ tx('editor.arrange.' + choice.id, choice.id) }}@if (arrangementSelection() === choice.id) {<mat-icon aria-hidden=\"true\">check_circle</mat-icon>}</span>\n <span class=\"choice-description\">{{ tx('studio.arrange.' + choice.id, choice.id) }}</span>\n <span class=\"choice-meta\">{{ arrangementFits(choice.id) ? arrangementWidths(choice.id).join(' + ') : tx('studio.moreColumns', 'Needs more grid columns') }}</span>\n </button>\n }\n </div>\n @if (hasCanvasWidgets) {\n <details class=\"card quick-geometry-card\" data-testid=\"quick-widget-geometry-list\">\n <summary>{{ tx('studio.quickGeometryTitle', 'Quick widget spans (columns \u00D7 rows)') }}</summary>\n <div class=\"quick-geometry-rows\">\n @for (w of quickCanvasWidgets(); track w.key) {\n <div class=\"quick-geometry-row\" [attr.data-testid]=\"'quick-geometry-row-' + w.key\">\n <span class=\"quick-geometry-label\"><mat-icon aria-hidden=\"true\">{{ w.icon }}</mat-icon><strong>{{ w.label }}</strong> <small>({{ w.colSpan }}\u00D7{{ w.rowSpan }})</small></span>\n <div class=\"studio-actions\">\n @for (span of [3, 4, 6, 12]; track span) {\n <button mat-stroked-button type=\"button\" [attr.data-testid]=\"'quick-colspan-' + w.key + '-' + span\" (click)=\"setWidgetCanvasSpan(w.key, span)\">{{ span }}c</button>\n }\n @for (rSpan of [1, 2, 4]; track rSpan) {\n <button mat-button type=\"button\" [attr.data-testid]=\"'quick-rowspan-' + w.key + '-' + rSpan\" (click)=\"setWidgetCanvasSpan(w.key, w.colSpan, rSpan)\">{{ rSpan }}r</button>\n }\n </div>\n </div>\n }\n </div>\n </details>\n }\n @if (!hasCanvasWidgets) {<p role=\"status\">{{ tx('editor.limits.empty', 'Add widgets to the canvas to configure their limits.') }}</p>}\n <details class=\"arrangement-rules\"><summary>{{ tx('studio.rules', 'Scope, devices and locked widgets') }}</summary><p>{{ tx('editor.arrange.help', 'Base canvas, in current reading order. Inheriting devices follow the base; explicit device positions are preserved. Locked positions stay in place.') }}</p><button mat-button type=\"button\" (click)=\"onSelectedTabIndexChange(5)\">{{ tx('studio.goLimits', 'Review widget limits') }}</button></details>\n </section>\n </ng-template></mat-tab>\n <mat-tab [label]=\"tx('studio.settingsTab', 'Adjustments')\">\n <div class=\"studio-panel\"><mat-tab-group class=\"editor-tabs\" [mat-stretch-tabs]=\"false\" [selectedIndex]=\"selectedTabIndex\" (selectedIndexChange)=\"selectDetailTab($event)\">\n <mat-tab [label]=\"tx('editor.tabs.layout', 'Layout')\"><div class=\"grid\"><mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.layout.cols', 'Columns') }}</mat-label><input matInput type=\"number\" min=\"1\" [formControl]=\"canvasColumnsControl\" /></mat-form-field><mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.layout.rowUnit', 'Row unit') }}</mat-label><input matInput [formControl]=\"canvasRowUnitControl\" /></mat-form-field><mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.layout.gap', 'Gap') }}</mat-label><input matInput [formControl]=\"canvasGapControl\" /></mat-form-field><mat-form-field class=\"page-inset-field\" appearance=\"outline\"><mat-label>{{ tx('editor.layout.paddingInline', 'Page side inset') }}</mat-label><input matInput [formControl]=\"paddingInlineControl\" placeholder=\"24px\" /><button praxisIconButton=\"help_outline\" size=\"dense\" matSuffix type=\"button\" [matTooltip]=\"tx('editor.hints.paddingInline', 'Shared space beside all page widgets. Use a CSS length or theme token; 0px removes it. Leave empty to inherit the host theme.')\" [attr.aria-label]=\"tx('editor.layout.paddingInline', 'Page side inset')\"></button></mat-form-field><mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.layout.autoRows', 'Auto rows') }}</mat-label><mat-select [formControl]=\"canvasAutoRowsControl\"><mat-option value=\"fixed\">{{ tx('editor.layout.autoRowsFixed', 'Fixed height') }}</mat-option><mat-option value=\"content\">{{ tx('editor.layout.autoRowsContent', 'Fit content') }}</mat-option></mat-select></mat-form-field></div></mat-tab>\n <mat-tab [label]=\"tx('editor.tabs.grouping', 'Grouping')\"><div class=\"panel\">\n <p class=\"hint\" data-testid=\"canvas-grouping-notice\">{{ tx('editor.grouping.canvasNotice', 'Canvas positions control this page. Groups below are preserved for flow layouts and do not arrange widgets in the canvas. Use the widget move and resize controls to place them side by side.') }}</p><div class=\"head\"><span>{{ tx('editor.grouping.title', 'Grouping') }}</span><button mat-stroked-button type=\"button\" (click)=\"addGroupingNode()\"><mat-icon>add</mat-icon>{{ tx('editor.grouping.add', 'Add group') }}</button></div>@for (group of groupingNodes.controls; track $index) {<div class=\"card\" [formGroup]=\"group\"><div class=\"head\"><span>{{ groupLabel($index) }}</span><button praxisIconButton=\"delete\" size=\"compact\" type=\"button\" [attr.aria-label]=\"tx('editor.grouping.removeAria', 'Remove group')\" (click)=\"removeGroupingNode($index)\"></button></div><div class=\"grid\"><mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.grouping.kind', 'Kind') }}</mat-label><mat-select formControlName=\"kind\"><mat-option value=\"section\">section</mat-option><mat-option value=\"tabs\">tabs</mat-option><mat-option value=\"hero\">hero</mat-option><mat-option value=\"rail\">rail</mat-option></mat-select></mat-form-field><mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.grouping.id', 'Id') }}</mat-label><input matInput formControlName=\"id\" /></mat-form-field><mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.grouping.label', 'Label') }}</mat-label><input matInput formControlName=\"label\" /></mat-form-field>@if (group.controls.kind.value === 'section') {<mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.grouping.layout', 'Layout') }}</mat-label><mat-select formControlName=\"layout\"><mat-option value=\"\">{{ tx('editor.grouping.layoutDefault', 'Default') }}</mat-option><mat-option value=\"stack\">stack</mat-option><mat-option value=\"grid\">grid</mat-option><mat-option value=\"row\">row</mat-option></mat-select></mat-form-field>}@if (group.controls.kind.value === 'hero') {<mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.grouping.emphasis', 'Emphasis') }}</mat-label><mat-select formControlName=\"emphasis\"><mat-option value=\"\">{{ tx('editor.grouping.layoutDefault', 'Default') }}</mat-option><mat-option value=\"high\">high</mat-option><mat-option value=\"medium\">medium</mat-option></mat-select></mat-form-field>}@if (group.controls.kind.value === 'rail') {<mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.grouping.side', 'Side') }}</mat-label><mat-select formControlName=\"side\"><mat-option value=\"left\">left</mat-option><mat-option value=\"right\">right</mat-option></mat-select></mat-form-field>}@if (group.controls.kind.value === 'tabs') {<mat-form-field appearance=\"outline\" class=\"wide\"><mat-label>{{ tx('editor.grouping.tabsJson', 'Tabs (JSON)') }}</mat-label><textarea matInput rows=\"4\" formControlName=\"tabsJson\"></textarea></mat-form-field>} @else {@if (bindablePageWidgets().length > 0) {<mat-form-field appearance=\"outline\" class=\"wide\"><mat-label>{{ tx('editor.grouping.widgets', 'Widgets in group') }}</mat-label><mat-select multiple [value]=\"groupingSelectedWidgetKeys($index)\" (selectionChange)=\"onGroupingWidgetSelectionChange($index, $event.value)\" [attr.data-testid]=\"'grouping-widget-select-' + $index\" panelClass=\"praxis-binder-widget-select-panel\"><mat-select-trigger><span class=\"binder-widget-trigger\"><mat-icon aria-hidden=\"true\" class=\"binder-widget-trigger-icon\">{{ groupingSelectedWidgetIcon($index) }}</mat-icon><span class=\"binder-widget-trigger-text\">{{ groupingSelectedWidgetsLabel($index) }}</span></span></mat-select-trigger>@for (w of bindablePageWidgets(); track w.key) {<mat-option [value]=\"w.key\" [attr.data-widget-key]=\"w.key\" class=\"binder-widget-option\"><span class=\"binder-widget-option-row\"><mat-icon class=\"binder-widget-option-icon\" aria-hidden=\"true\">{{ w.icon }}</mat-icon><span class=\"binder-widget-option-copy\"><strong class=\"binder-widget-option-title\">{{ w.title }}</strong>@if (w.description) {<small class=\"binder-widget-option-desc\">{{ w.description }}</small>}</span><span class=\"binder-widget-option-sr-key\" aria-hidden=\"true\">{{ w.key }}</span></span></mat-option>}</mat-select></mat-form-field>}<mat-form-field appearance=\"outline\" class=\"wide\"><mat-label>{{ tx('editor.grouping.widgetKeysCsv', 'Widget keys (comma-separated)') }}</mat-label><input matInput formControlName=\"widgetKeysCsv\" /></mat-form-field>}</div></div>}</div></mat-tab>\n <mat-tab [label]=\"tx('editor.tabs.devices', 'Device Layouts')\">\n <div class=\"panel\">\n <mat-button-toggle-group [value]=\"activeDevice()\" (change)=\"activeDevice.set($event.value)\" [attr.aria-label]=\"tx('editor.devices.target', 'Device to configure')\">\n @for (device of deviceKeys; track device) {\n <mat-button-toggle [value]=\"device\">{{ deviceLabels[device] }}</mat-button-toggle>\n }\n </mat-button-toggle-group>\n @for (device of deviceKeys; track device) {\n @if (activeDevice() === device) {\n <div class=\"card\">\n <div class=\"head\"><strong>{{ deviceLabels[device] }}</strong><button mat-stroked-button type=\"button\" [attr.data-testid]=\"'stack-device-' + device\" [disabled]=\"!hasCanvasWidgets\" (click)=\"stackDevice(device)\"><mat-icon aria-hidden=\"true\">view_agenda</mat-icon>{{ tx('editor.devices.stack', 'Stack widgets') }}</button></div>\n <p class=\"hint\">{{ tx('editor.devices.stackHelp', 'Use full width and natural height, in reading order. This replaces positions and width limits only for this device; other layouts stay unchanged.') }}</p>\n @if (stackedDevices.has(device)) { <p class=\"hint\" role=\"status\">{{ tx('editor.devices.stacked', 'Single-column layout prepared. Save to apply; Reset undoes the changes.') }}</p> }\n <div class=\"grid\">\n <mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.devices.columns', 'Columns') }}</mat-label><input matInput type=\"number\" min=\"1\" [formControl]=\"deviceForms[device].controls.columns\" /></mat-form-field>\n <mat-form-field class=\"page-inset-field\" appearance=\"outline\"><mat-label>{{ tx('editor.layout.paddingInline', 'Page side inset') }}</mat-label><input matInput [formControl]=\"deviceForms[device].controls.paddingInline\" placeholder=\"16px\" /></mat-form-field><mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.devices.gap', 'Gap') }}</mat-label><input matInput [formControl]=\"deviceForms[device].controls.gap\" /></mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.layout.autoRows', 'Auto rows') }}</mat-label><mat-select [formControl]=\"deviceForms[device].controls.autoRows\"><mat-option value=\"\">{{ tx('editor.devices.orientationDefault', 'Default') }}</mat-option><mat-option value=\"fixed\">{{ tx('editor.layout.autoRowsFixed', 'Fixed height') }}</mat-option><mat-option value=\"content\">{{ tx('editor.layout.autoRowsContent', 'Fit content') }}</mat-option></mat-select></mat-form-field>\n </div>\n <details><summary>{{ tx('editor.devices.advanced', 'Advanced overrides') }}</summary><div class=\"grid\">\n <mat-form-field appearance=\"outline\"><mat-label>{{ tx('editor.layout.rowUnit', 'Row unit') }}</mat-label><input matInput [formControl]=\"deviceForms[device].controls.rowUnit\" /></mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"wide\"><mat-label>{{ tx('editor.devices.groupingOverridesJson', 'Grouping overrides (JSON)') }}</mat-label><textarea matInput rows=\"4\" [formControl]=\"deviceForms[device].controls.groupingOverrides\"></textarea></mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"wide\"><mat-label>{{ tx('editor.devices.widgetOverridesJson', 'Widget overrides (JSON)') }}</mat-label><textarea matInput rows=\"4\" [formControl]=\"deviceForms[device].controls.widgetOverrides\"></textarea><mat-hint>{{ tx('editor.devices.widgetOverridesHelp', 'Supported per-widget properties on this device: className, span, order, and hidden. Does not override internal widget definition or inputs.') }}</mat-hint></mat-form-field>\n </div></details>\n @if (!stackedDevices.has(device)) { <div class=\"hint\">{{ tx('editor.preview.deviceCanvasHint', 'Existing canvas item overrides are preserved.') }}</div> }\n </div>\n }\n }\n </div>\n </mat-tab>\n <mat-tab [label]=\"tx('editor.tabs.context', 'Context')\"><mat-form-field appearance=\"outline\" class=\"wide\"><mat-label>{{ tx('editor.context.json', 'Context (JSON)') }}</mat-label><textarea matInput rows=\"12\" [formControl]=\"contextControl\"></textarea></mat-form-field></mat-tab>\n <mat-tab [label]=\"tx('editor.tabs.state', 'Page State')\">\n <div class=\"panel\">\n <mat-form-field appearance=\"outline\" class=\"wide\">\n <mat-label>{{ tx('editor.state.stateDefinitionJson', 'Page state (values, schema, derived \u2014 JSON)') }}</mat-label>\n <textarea matInput rows=\"10\" [formControl]=\"stateControl\" data-testid=\"page-state-json\"></textarea>\n </mat-form-field>\n\n <section class=\"panel\" data-testid=\"page-datasources-guided\">\n <div class=\"head\">\n <strong>{{ tx('editor.state.dataSourcesTitle', 'Data Sources') }}</strong>\n <div class=\"studio-actions\">\n @if (dataSourceNodes.controls.length) {\n <button\n mat-button\n type=\"button\"\n data-testid=\"page-state-sync-from-datasources\"\n (click)=\"syncStateValuesFromDataSources()\"\n >\n <mat-icon>sync</mat-icon>{{ tx('editor.state.syncFromDataSources', 'Sync state paths') }}\n </button>\n }\n <button mat-stroked-button type=\"button\" data-testid=\"page-datasources-add\" (click)=\"addDataSourceNode()\">\n <mat-icon>add</mat-icon>{{ tx('editor.state.addDataSource', 'Add data source') }}\n </button>\n </div>\n </div>\n <p class=\"hint\">{{ tx('editor.state.dataSourcesCopy', 'Bind HTTP or resource endpoints directly into page state paths with reactive parameters.') }}</p>\n @if (!dataSourceNodes.controls.length) {\n <p class=\"hint\" data-testid=\"page-datasources-empty\">{{ tx('editor.state.emptyDataSources', 'No declarative data sources configured.') }}</p>\n }\n @for (ds of dataSourceNodes.controls; track $index) {\n <div class=\"card\" [formGroup]=\"ds\" [attr.data-testid]=\"'page-datasource-card-' + $index\">\n <div class=\"head\">\n <span>{{ dataSourceLabel($index) }}</span>\n <button\n praxisIconButton=\"delete\"\n size=\"compact\"\n type=\"button\"\n [attr.data-testid]=\"'page-datasource-remove-' + $index\"\n [attr.aria-label]=\"tx('editor.state.removeDataSourceAria', 'Remove data source')\"\n (click)=\"removeDataSourceNode($index)\"\n ></button>\n </div>\n <div class=\"grid\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.dataSourceId', 'ID') }}</mat-label>\n <input matInput formControlName=\"id\" [attr.data-testid]=\"'page-datasource-id-' + $index\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.dataSourceKind', 'Kind') }}</mat-label>\n <mat-select formControlName=\"kind\" [attr.data-testid]=\"'page-datasource-kind-' + $index\">\n <mat-option value=\"\">{{ tx('editor.common.default', 'Default') }}</mat-option>\n <mat-option value=\"remote-resource\">remote-resource</mat-option>\n <mat-option value=\"stats-query\">stats-query</mat-option>\n <mat-option value=\"raw-http\">raw-http</mat-option>\n </mat-select>\n </mat-form-field>\n <praxis-resource-quick-connect\n formControlName=\"resourcePath\"\n [intent]=\"ds.controls.kind.value === 'stats-query' ? 'stats' : (ds.controls.kind.value === 'raw-http' ? 'any' : 'collection')\"\n [endpointKey]=\"resolveEndpointKey(ds.controls.endpointKey.value)\"\n [label]=\"tx('editor.state.resourcePath', 'Resource path')\"\n [attr.data-testid]=\"'page-datasource-resourcePath-' + $index\"\n />\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.endpointKey', 'Endpoint key') }}</mat-label>\n <input matInput formControlName=\"endpointKey\" [attr.data-testid]=\"'page-datasource-endpointKey-' + $index\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.method', 'HTTP method') }}</mat-label>\n <mat-select formControlName=\"method\" [attr.data-testid]=\"'page-datasource-method-' + $index\">\n <mat-option value=\"\">{{ tx('editor.common.auto', 'Auto') }}</mat-option>\n <mat-option value=\"GET\">GET</mat-option>\n <mat-option value=\"POST\">POST</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.targetStatePath', 'Target state path') }}</mat-label>\n <input\n matInput\n formControlName=\"targetStatePath\"\n [attr.data-testid]=\"'page-datasource-targetStatePath-' + $index\"\n (blur)=\"syncStateValuesFromDataSources()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.errorPolicy', 'Error policy') }}</mat-label>\n <mat-select formControlName=\"errorPolicy\" [attr.data-testid]=\"'page-datasource-errorPolicy-' + $index\">\n <mat-option value=\"\">{{ tx('editor.common.default', 'Default') }}</mat-option>\n <mat-option value=\"propagate\">propagate</mat-option>\n <mat-option value=\"silent\">silent</mat-option>\n <mat-option value=\"fallback\">fallback</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.refreshIntervalMs', 'Refresh interval (ms)') }}</mat-label>\n <input matInput type=\"number\" min=\"0\" formControlName=\"refreshIntervalMs\" [attr.data-testid]=\"'page-datasource-refreshIntervalMs-' + $index\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.autoFetch', 'Auto fetch') }}</mat-label>\n <mat-select formControlName=\"autoFetch\" [attr.data-testid]=\"'page-datasource-autoFetch-' + $index\">\n <mat-option value=\"\">{{ tx('editor.common.default', 'Default') }}</mat-option>\n <mat-option value=\"true\">true</mat-option>\n <mat-option value=\"false\">false</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"wide\">\n <mat-label>{{ tx('editor.state.description', 'Description') }}</mat-label>\n <input matInput formControlName=\"description\" [attr.data-testid]=\"'page-datasource-description-' + $index\" />\n </mat-form-field>\n <div class=\"wide guided-presets-bar\" [attr.data-testid]=\"'page-datasource-guided-presets-' + $index\">\n <span class=\"hint\">{{ tx('editor.state.guidedPresets', 'Guided query & transform templates:') }}</span>\n <div class=\"studio-actions\">\n <button mat-stroked-button type=\"button\" [attr.data-testid]=\"'page-datasource-preset-groupby-' + $index\" (click)=\"applyGuidedDataSourceQuery($index, { mode: 'group-by', groupByField: 'ativo', metricField: 'id', metricOp: 'count' }); applyGuidedDataSourceTransform($index, { kind: 'group-by-map', totalField: 'total', mappingsText: 'true:active, false:inactive', ratioTargetField: 'activeRatePct', ratioNumeratorField: 'active', ratioDenominatorField: 'total', ratioScale: 100, ratioDecimals: 1 })\">\n {{ tx('editor.state.presetGroupByKpi', 'Group-by + Total + Ratio (%)') }}\n </button>\n <button mat-stroked-button type=\"button\" [attr.data-testid]=\"'page-datasource-preset-weighted-' + $index\" (click)=\"applyGuidedDataSourceTransform($index, { kind: 'group-by-map', totalField: 'total', weightedAverageField: 'weightedAvg', weightedAverageDecimals: 2, mappingsText: '1:low, 5:high' })\">\n {{ tx('editor.state.presetWeightedAvg', 'Weighted average') }}\n </button>\n <button mat-stroked-button type=\"button\" [attr.data-testid]=\"'page-datasource-preset-timeseries-' + $index\" (click)=\"applyGuidedDataSourceQuery($index, { mode: 'timeseries', groupByField: 'createdAt', period: 'month', metricField: 'id', metricOp: 'count' }); applyGuidedDataSourceTransform($index, { kind: 'timeseries-last', mappingsText: 'count:latestCount, avg:latestAvg' })\">\n {{ tx('editor.state.presetTimeseriesLast', 'Timeseries latest point') }}\n </button>\n <button mat-stroked-button type=\"button\" [attr.data-testid]=\"'page-datasource-preset-path-' + $index\" (click)=\"applyGuidedDataSourceTransform($index, { kind: 'path', path: 'totalElements' })\">\n {{ tx('editor.state.presetPathExtract', 'Extract path (totalElements)') }}\n </button>\n </div>\n </div>\n <mat-form-field appearance=\"outline\" class=\"wide\">\n <mat-label>{{ tx('editor.state.queryJson', 'Query (JSON)') }}</mat-label>\n <textarea matInput rows=\"3\" formControlName=\"queryJson\" [attr.data-testid]=\"'page-datasource-queryJson-' + $index\"></textarea>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"wide\">\n <mat-label>{{ tx('editor.state.transformJson', 'Transform (JSON)') }}</mat-label>\n <textarea matInput rows=\"3\" formControlName=\"transformJson\" [attr.data-testid]=\"'page-datasource-transformJson-' + $index\"></textarea>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"wide\">\n <mat-label>{{ tx('editor.state.fallbackJson', 'Fallback value (JSON)') }}</mat-label>\n <textarea matInput rows=\"2\" formControlName=\"fallbackJson\" [attr.data-testid]=\"'page-datasource-fallbackJson-' + $index\"></textarea>\n </mat-form-field>\n </div>\n @let preview = getDataSourcePreview($index);\n <div class=\"datasource-preview-box\" [attr.data-testid]=\"'page-datasource-preview-' + $index\">\n <div class=\"head\">\n <strong>{{ tx('editor.state.previewTitle', 'Transform & State Preview') }}</strong>\n <span class=\"previous-height-chip\">{{ preview.sourceKind === 'live' ? tx('editor.state.previewLive', 'Live response') : tx('editor.state.previewSample', 'Simulated sample') }}</span>\n </div>\n @if (preview.status === 'error') {\n <p class=\"limits-error\" role=\"alert\" [attr.data-testid]=\"'page-datasource-preview-error-' + $index\">{{ preview.error }}</p>\n } @else {\n <pre class=\"datasource-preview-json\" [attr.data-testid]=\"'page-datasource-preview-json-' + $index\">{{ preview.formattedJson }}</pre>\n @if (preview.tokens.length) {\n <div class=\"studio-actions\" [attr.data-testid]=\"'page-datasource-preview-tokens-' + $index\">\n <span class=\"hint\">{{ tx('editor.state.availableTokens', 'Click a token to prepare widget binding:') }}</span>\n @for (token of preview.tokens; track token) {\n <button mat-button type=\"button\" (click)=\"binderTokenControl.setValue(token)\"><code>{{ token }}</code></button>\n }\n </div>\n }\n }\n </div>\n </div>\n }\n </section>\n\n <section class=\"card\" data-testid=\"page-derived-measures-guided\">\n <div class=\"head\">\n <strong>{{ tx('editor.state.derivedMeasuresTitle', 'Guided Derived Measures') }}</strong>\n </div>\n <p class=\"hint\">{{ tx('editor.state.derivedMeasuresCopy', 'Create percentages, ratios, weighted averages, or formatted measures from page state paths without writing raw JsonLogic.') }}</p>\n <div class=\"grid\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.derivedTargetPath', 'Measure key') }}</mat-label>\n <input matInput [formControl]=\"derivedPathControl\" placeholder=\"activeRatePct\" data-testid=\"derived-target-path\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.derivedKind', 'Calculation') }}</mat-label>\n <mat-select [formControl]=\"derivedKindControl\" data-testid=\"derived-kind\">\n <mat-option value=\"ratio-percent\">{{ tx('editor.state.derivedRatio', 'Ratio / Percentage') }}</mat-option>\n <mat-option value=\"weighted-average\">{{ tx('editor.state.derivedWeighted', 'Weighted average') }}</mat-option>\n <mat-option value=\"format-value\">{{ tx('editor.state.derivedFormat', 'Formatted value') }}</mat-option>\n <mat-option value=\"template\">{{ tx('editor.state.derivedTemplate', 'State reference') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.derivedNumerator', 'Numerator / Source path') }}</mat-label>\n <input matInput [formControl]=\"derivedNumeratorControl\" placeholder=\"kpis.active\" data-testid=\"derived-numerator\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.derivedDenominator', 'Denominator path') }}</mat-label>\n <input matInput [formControl]=\"derivedDenominatorControl\" placeholder=\"kpis.total\" data-testid=\"derived-denominator\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.derivedFormatKind', 'Format') }}</mat-label>\n <mat-select [formControl]=\"derivedFormatControl\" data-testid=\"derived-format\">\n <mat-option value=\"percent\">percent (%)</mat-option>\n <mat-option value=\"number\">number</mat-option>\n <mat-option value=\"integer\">integer</mat-option>\n <mat-option value=\"currency\">currency (R$)</mat-option>\n <mat-option value=\"boolean\">boolean</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div class=\"studio-actions\">\n <button mat-stroked-button type=\"button\" data-testid=\"derived-add-btn\" (click)=\"addGuidedDerivedMeasure()\">\n <mat-icon aria-hidden=\"true\">functions</mat-icon>{{ tx('editor.state.addDerivedAction', 'Add measure to page state') }}\n </button>\n </div>\n @let derivedPreview = getDerivedStatePreview();\n @if (derivedPreview.entries.length || derivedPreview.diagnostics.length) {\n <div class=\"datasource-preview-box\" data-testid=\"page-derived-preview\">\n @for (diag of derivedPreview.diagnostics; track diag) {\n <p class=\"limits-error\" role=\"alert\">{{ diag }}</p>\n }\n @if (derivedPreview.entries.length) {\n <div class=\"studio-actions\">\n @for (entry of derivedPreview.entries; track entry.key) {\n <button mat-button type=\"button\" (click)=\"binderTokenControl.setValue(entry.token)\">\n <strong>{{ entry.key }}</strong>: {{ entry.formattedValue }} <code>({{ entry.token }})</code>\n </button>\n }\n </div>\n }\n </div>\n }\n </section>\n\n <section class=\"card\" data-testid=\"page-state-component-binder\">\n <div class=\"head\">\n <strong>{{ tx('editor.state.binderTitle', 'Connect State or Measure to Widget') }}</strong>\n </div>\n <p class=\"hint\">{{ tx('editor.state.binderCopy', 'Bind any state path or derived measure directly to a widget input in the draft.') }}</p>\n <div class=\"grid\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.binderToken', 'State token') }}</mat-label>\n <input matInput [formControl]=\"binderTokenControl\" placeholder=\"{{ '{{state.kpis.total}}' }}\" data-testid=\"binder-token\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.binderWidget', 'Target widget') }}</mat-label>\n <mat-select [formControl]=\"binderWidgetControl\" data-testid=\"binder-widget\" panelClass=\"praxis-binder-widget-select-panel\">\n @if (selectedBinderWidget(); as selectedWidget) {\n <mat-select-trigger>\n <span class=\"binder-widget-trigger\">\n <mat-icon aria-hidden=\"true\" class=\"binder-widget-trigger-icon\">{{ selectedWidget.icon }}</mat-icon>\n <span class=\"binder-widget-trigger-text\">\n <strong>{{ selectedWidget.title }}</strong>\n @if (selectedWidget.description) {\n <small> \u00B7 {{ selectedWidget.description }}</small>\n }\n </span>\n </span>\n </mat-select-trigger>\n }\n @for (w of bindablePageWidgets(); track w.key) {\n <mat-option [value]=\"w.key\" [attr.data-widget-key]=\"w.key\" class=\"binder-widget-option\">\n <span class=\"binder-widget-option-row\">\n <mat-icon aria-hidden=\"true\" class=\"binder-widget-option-icon\">{{ w.icon }}</mat-icon>\n <span class=\"binder-widget-option-copy\">\n <strong class=\"binder-widget-option-title\">{{ w.title }}</strong>\n @if (w.description) {\n <small class=\"binder-widget-option-desc\">{{ w.description }}</small>\n }\n </span>\n <span class=\"binder-widget-option-sr-key\" aria-hidden=\"true\">{{ w.key }}</span>\n </span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('editor.state.binderInputPath', 'Widget input path') }}</mat-label>\n <input matInput [formControl]=\"binderInputPathControl\" placeholder=\"value\" data-testid=\"binder-input-path\" />\n </mat-form-field>\n </div>\n @if (availableStateBindingTokens().length) {\n <div class=\"binder-input-suggestions\" data-testid=\"binder-token-suggestions\">\n <span class=\"hint\">{{ tx('editor.state.binderTokenSuggestions', 'Available tokens:') }}</span>\n @for (availableToken of availableStateBindingTokens(); track availableToken) {\n <button\n type=\"button\"\n class=\"binder-input-suggestion-chip\"\n [class.active]=\"binderTokenControl.value === availableToken\"\n [attr.data-testid]=\"'binder-token-suggestion-' + availableToken\"\n (click)=\"selectBinderTokenSuggestion(availableToken)\"\n >\n <code>{{ availableToken }}</code>\n </button>\n }\n </div>\n }\n <div class=\"binder-input-suggestions\" data-testid=\"binder-pipe-suggestions\">\n <span class=\"hint\">{{ tx('editor.state.binderPipeSuggestions', 'Formatadores CSP-safe (pipes):') }}</span>\n @for (pipeSegment of binderDeclarativePipes; track pipeSegment) {\n <button\n type=\"button\"\n class=\"binder-input-suggestion-chip\"\n [attr.data-testid]=\"'binder-pipe-chip-' + pipeSegment\"\n (click)=\"appendBinderTokenPipe(pipeSegment)\"\n >\n <code>{{ pipeSegment }}</code>\n </button>\n }\n </div>\n @if (binderTokenPipeError(); as pipeErr) {\n <p class=\"studio-status binder-unbound-warning\" role=\"alert\" data-testid=\"binder-pipe-error\">{{ pipeErr }}</p>\n } @else if (binderTokenLivePreview(); as livePreviewVal) {\n <p class=\"hint\" data-testid=\"binder-live-preview\">\n {{ tx('editor.state.binderLivePreview', 'Preview simulado:') }} <strong>{{ livePreviewVal }}</strong>\n </p>\n }\n @if (selectedBinderWidgetInputSuggestions().length) {\n <div class=\"binder-input-suggestions\" data-testid=\"binder-input-suggestions\">\n <span class=\"hint\">{{ tx('editor.state.binderInputSuggestions', 'Suggested paths:') }}</span>\n @for (suggestedPath of selectedBinderWidgetInputSuggestions(); track suggestedPath) {\n <button\n type=\"button\"\n class=\"binder-input-suggestion-chip\"\n [class.active]=\"binderInputPathControl.value === suggestedPath\"\n [attr.data-testid]=\"'binder-input-suggestion-' + suggestedPath\"\n (click)=\"selectBinderInputPathSuggestion(suggestedPath)\"\n >\n <code>{{ suggestedPath }}</code>\n </button>\n }\n </div>\n }\n <div class=\"studio-actions\">\n <button mat-stroked-button type=\"button\" data-testid=\"binder-bind-btn\" (click)=\"bindStateTokenToWidget()\">\n <mat-icon aria-hidden=\"true\">link</mat-icon>{{ tx('editor.state.bindAction', 'Bind token to widget in draft') }}\n </button>\n <button mat-stroked-button type=\"button\" data-testid=\"binder-auto-bind-btn\" (click)=\"autoBindWidgetStatePlaceholders()\">\n <mat-icon aria-hidden=\"true\">auto_fix_high</mat-icon>{{ tx('editor.state.autoBindAction', 'Auto-bind ${...} widget placeholders') }}\n </button>\n </div>\n @if (binderMessage()) {\n <p class=\"studio-status\" role=\"status\" data-testid=\"binder-status-message\">{{ binderMessage() }}</p>\n }\n @if (invalidWidgetStateBindingPipes().length) {\n <div class=\"binder-unbound-warning\" role=\"alert\" data-testid=\"binder-invalid-pipes-warning\">\n <mat-icon aria-hidden=\"true\">error_outline</mat-icon>\n <div class=\"binder-unbound-warning__body\">\n <span>{{ tx('editor.state.invalidWidgetPipesWarning', 'Formatadores (pipes) inv\u00E1lidos detectados:') }}</span>\n <div class=\"binder-unbound-warning__chips\">\n @for (invalidBinding of invalidWidgetStateBindingPipes(); track invalidBinding.widgetKey + ':' + invalidBinding.inputPath) {\n <button\n type=\"button\"\n class=\"binder-unbound-chip\"\n [attr.data-testid]=\"'binder-invalid-pipe-chip-' + invalidBinding.widgetKey + '-' + invalidBinding.inputPath\"\n (click)=\"loadStateBindingIntoForm(invalidBinding)\"\n >\n <strong>{{ invalidBinding.widgetTitle }}</strong>.<code>{{ invalidBinding.inputPath }}</code> (<code>{{ invalidBinding.expression }}</code>)\n </button>\n }\n </div>\n </div>\n </div>\n }\n @if (unboundWidgetStatePlaceholders().length; as unboundCount) {\n <div class=\"binder-unbound-warning\" role=\"status\" data-testid=\"binder-unbound-placeholders-warning\">\n <mat-icon aria-hidden=\"true\">warning_amber</mat-icon>\n <div class=\"binder-unbound-warning__body\">\n <span>{{ tx('editor.state.unboundPlaceholdersWarning', 'Placeholders sem v\u00EDnculo detectados:') }}</span>\n <div class=\"binder-unbound-warning__chips\">\n @for (item of unboundWidgetStatePlaceholders(); track item.widgetKey + ':' + item.placeholder) {\n <button\n type=\"button\"\n class=\"binder-unbound-chip\"\n [attr.data-testid]=\"'binder-unbound-chip-' + item.widgetKey + '-' + item.placeholder\"\n [attr.title]=\"tx('editor.state.selectUnboundPlaceholderHint', 'Clique para preencher widget e caminho no formul\u00E1rio')\"\n (click)=\"selectUnboundPlaceholder(item)\"\n >\n <strong>{{ item.widgetTitle }}</strong> (<code>{{ '${' + item.placeholder + '}' }}</code>)\n </button>\n }\n </div>\n </div>\n </div>\n }\n @if (currentWidgetStateBindings().length) {\n <ul class=\"blocked-widgets-list binder-active-list\" data-testid=\"active-widget-state-bindings\">\n @for (binding of currentWidgetStateBindings(); track binding.widgetKey + ':' + binding.inputPath) {\n @let previewVal = resolveStateTokenPreview(binding.token);\n <li class=\"binder-active-item\">\n <mat-icon aria-hidden=\"true\" class=\"binder-active-icon\">{{ widgetIconForKey(binding.widgetKey) }}</mat-icon>\n <button\n type=\"button\"\n class=\"binder-active-copy binder-active-select-btn\"\n [attr.data-testid]=\"'binder-select-' + binding.widgetKey + '-' + binding.inputPath\"\n [attr.title]=\"tx('editor.state.editBindingHint', 'Click to load binding into form')\"\n (click)=\"loadStateBindingIntoForm(binding)\"\n >\n <strong>{{ binding.widgetTitle }}</strong>.<code>{{ binding.inputPath }}</code> \u2190 <code>{{ binding.token }}</code>\n @if (previewVal !== null) {\n <small class=\"binder-preview-badge\" [attr.data-testid]=\"'binder-preview-' + binding.widgetKey + '-' + binding.inputPath\">= {{ previewVal }}</small>\n }\n </button>\n <button mat-icon-button type=\"button\" [attr.data-testid]=\"'binder-unbind-' + binding.widgetKey + '-' + binding.inputPath\"\n [attr.aria-label]=\"tx('editor.state.unbindAction', 'Remove binding')\"\n (click)=\"unbindStateTokenFromWidget(binding.widgetKey, binding.inputPath)\">\n <mat-icon aria-hidden=\"true\">link_off</mat-icon>\n </button>\n </li>\n }\n </ul>\n }\n </section>\n\n <mat-form-field appearance=\"outline\" class=\"wide\">\n <mat-label>{{ tx('editor.state.dataSourcesJson', 'page.dataSources (JSON)') }}</mat-label>\n <textarea matInput rows=\"8\" [formControl]=\"dataSourcesControl\" [placeholder]=\"tx('editor.hints.dataSourcesExample', '')\" data-testid=\"page-datasources-json\"></textarea>\n </mat-form-field>\n </div>\n </mat-tab>\n <mat-tab [label]=\"tx('editor.limits.tab', 'Widget limits')\">\n <section class=\"limits-panel\" data-testid=\"canvas-limits-editor\">\n @if (workspaceTab() === 3 && activeTab() === 'limits' && canvasLimits.targets.length) {\n <div class=\"selection-identity\" tabindex=\"-1\" data-testid=\"canvas-limits-identity\">\n <mat-icon aria-hidden=\"true\">{{ limitsTargetIcon(limitsTargetControl.value) }}</mat-icon>\n <strong>{{ limitsTargetLabel(limitsTargetControl.value) }}</strong>\n </div>\n }\n\n <p class=\"hint\" id=\"canvas-limits-help\">{{ tx('editor.limits.help', 'Limits use grid columns and rows, not pixels. Leave blank for defaults, or to inherit the base limits on a device.') }}</p>\n @if (canvasLimits.targets.length) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ tx('editor.limits.target', 'Widget and layout') }}</mat-label>\n <mat-select [formControl]=\"limitsTargetControl\" data-testid=\"canvas-limits-target\">\n <mat-select-trigger>\n <span class=\"binder-widget-trigger\">\n <mat-icon aria-hidden=\"true\" class=\"binder-widget-trigger-icon\">{{ limitsTargetIcon(limitsTargetControl.value) }}</mat-icon>\n <span class=\"binder-widget-trigger-text\">{{ limitsTargetLabel(limitsTargetControl.value) }}</span>\n </span>\n </mat-select-trigger>\n @for (target of canvasLimits.targets; track target.scope + ':' + target.key; let index = $index) {\n <mat-option [value]=\"index\">\n <mat-icon aria-hidden=\"true\">{{ limitsTargetIcon(index) }}</mat-icon>\n <span>{{ limitsTargetLabel(index) }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <p class=\"hint\">{{ limitsGeometryLabel() }}</p>\n @if (canvasLimits.form.controls[limitsTargetControl.value]; as limitsForm) {\n <div class=\"limits-fields\" aria-describedby=\"canvas-limits-help\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ tx('editor.limits.minColSpan', 'Minimum width (columns)') }}</mat-label>\n <input matInput [errorStateMatcher]=\"limitsErrorMatcher\" type=\"number\" min=\"1\" step=\"1\" [formControl]=\"limitsForm.controls.minColSpan\" data-testid=\"canvas-limit-minColSpan\">\n <mat-error>{{ limitFieldError(limitsForm.controls.minColSpan) }}</mat-error>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ tx('editor.limits.maxColSpan', 'Maximum width (columns)') }}</mat-label>\n <input matInput [errorStateMatcher]=\"limitsErrorMatcher\" type=\"number\" min=\"1\" step=\"1\" [formControl]=\"limitsForm.controls.maxColSpan\" data-testid=\"canvas-limit-maxColSpan\">\n <mat-error>{{ limitFieldError(limitsForm.controls.maxColSpan) }}</mat-error>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ tx('editor.limits.minRowSpan', 'Minimum height (rows)') }}</mat-label>\n <input matInput [errorStateMatcher]=\"limitsErrorMatcher\" type=\"number\" min=\"1\" step=\"1\" [formControl]=\"limitsForm.controls.minRowSpan\" data-testid=\"canvas-limit-minRowSpan\">\n <mat-error>{{ limitFieldError(limitsForm.controls.minRowSpan) }}</mat-error>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ tx('editor.limits.maxRowSpan', 'Maximum height (rows)') }}</mat-label>\n <input matInput [errorStateMatcher]=\"limitsErrorMatcher\" type=\"number\" min=\"1\" step=\"1\" [formControl]=\"limitsForm.controls.maxRowSpan\" data-testid=\"canvas-limit-maxRowSpan\">\n <mat-error>{{ limitFieldError(limitsForm.controls.maxRowSpan) }}</mat-error>\n </mat-form-field>\n </div>\n @if (limitsUseContentHeight()) {\n <p class=\"limits-notice\"><mat-icon aria-hidden=\"true\">height</mat-icon><span>{{ tx('editor.limits.autoHeight', 'Choose natural height or an adjustable height with internal scrolling.') }}</span></p>\n <mat-button-toggle-group class=\"height-modes\" [formControl]=\"limitsForm.controls.heightMode\" [attr.aria-label]=\"tx('editor.height.mode', 'Widget height')\" data-testid=\"canvas-height-mode\">\n <mat-button-toggle value=\"inherit\">{{ tx('editor.height.inherit', 'Default') }}</mat-button-toggle>\n <mat-button-toggle value=\"auto\">{{ tx('editor.height.auto', 'Automatic') }}</mat-button-toggle>\n <mat-button-toggle value=\"fixed\">{{ tx('editor.height.fixed', 'Adjustable') }}</mat-button-toggle>\n </mat-button-toggle-group>\n @if (limitsForm.controls.heightMode.value === 'fixed') {\n <div class=\"limits-fields\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ tx('editor.height.value', 'Height (px)') }}</mat-label>\n <input matInput type=\"number\" min=\"160\" step=\"1\" [errorStateMatcher]=\"limitsErrorMatcher\" [formControl]=\"limitsForm.controls.heightPx\" data-testid=\"canvas-height-px\">\n <mat-error>{{ limitFieldError(limitsForm.controls.heightPx) }}</mat-error>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ tx('editor.height.top', 'Top inset (px)') }}</mat-label>\n <input matInput type=\"number\" min=\"0\" step=\"1\" [errorStateMatcher]=\"limitsErrorMatcher\" [formControl]=\"limitsForm.controls.offsetTopPx\" data-testid=\"canvas-height-top\">\n <mat-error>{{ limitFieldError(limitsForm.controls.offsetTopPx) }}</mat-error>\n </mat-form-field>\n </div>\n }\n <div class=\"limits-fields\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ tx('editor.height.min', 'Minimum height (px)') }}</mat-label>\n <input matInput type=\"number\" min=\"160\" step=\"1\" [errorStateMatcher]=\"limitsErrorMatcher\" [formControl]=\"limitsForm.controls.minHeightPx\" data-testid=\"canvas-limit-minHeightPx\">\n <mat-error>{{ limitFieldError(limitsForm.controls.minHeightPx) }}</mat-error>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ tx('editor.height.max', 'Maximum height (px)') }}</mat-label>\n <input matInput type=\"number\" min=\"160\" step=\"1\" [errorStateMatcher]=\"limitsErrorMatcher\" [formControl]=\"limitsForm.controls.maxHeightPx\" data-testid=\"canvas-limit-maxHeightPx\">\n <mat-error>{{ limitFieldError(limitsForm.controls.maxHeightPx) }}</mat-error>\n </mat-form-field>\n </div>\n }\n }\n @if (limitsSummaryError) { <p class=\"limits-error\" role=\"alert\">{{ limitsSummaryError }}</p> }\n } @else {\n <p>{{ tx('editor.limits.empty', 'Add widgets to the canvas to configure their limits.') }}</p>\n }\n </section>\n </mat-tab>\n </mat-tab-group></div>\n </mat-tab>\n </mat-tab-group>\n <details class=\"summary-details\">\n <summary>{{ tx('editor.summaryAria', 'Configuration summary') }}</summary>\n <div class=\"summary\">\n @if (!isHostedInSettingsPanel) {\n <span>{{ validationStateLabel }}</span>\n <span>{{ dirtyStateLabel }}</span>\n }\n <span>{{ selectedLayoutPresetLabel }}</span>\n <span>{{ selectedThemePresetLabel }}</span>\n <span>{{ canvasSummaryLabel }}</span>\n </div>\n </details>\n </div>\n", styles: [".page-inset-field{grid-column:1/-1;min-width:0}:host{display:block;min-width:0;max-width:100%}.height-modes{width:fit-content;max-width:100%;flex-wrap:wrap;margin-bottom:16px}.editor,.panel{display:grid;gap:16px;min-width:0;max-width:100%;color:var(--md-sys-color-on-surface, currentColor)}mat-tab-group,mat-form-field{min-width:0;max-width:100%}.head,.summary{display:flex;gap:12px;align-items:center;flex-wrap:wrap}.summary{color:var(--md-sys-color-on-surface-variant, currentColor)}.selection-identity{display:flex;align-items:flex-start;gap:8px;padding-block:8px;background:var(--md-sys-color-surface-container-low, var(--md-sys-color-surface));overflow-wrap:anywhere}.selection-identity mat-icon{flex-shrink:0}:host ::ng-deep .editor-tabs>.mat-mdc-tab-header .mdc-tab{min-width:0;padding-inline:12px}:host ::ng-deep .editor-tabs>.mat-mdc-tab-header .mdc-tab__text-label{max-width:128px;white-space:normal;overflow-wrap:anywhere;text-align:center;letter-spacing:normal;line-height:1.25}.grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(min(220px,100%),1fr))}@container (max-width: 600px){.grid{grid-template-columns:minmax(0,1fr)}.head,.summary{align-items:stretch}.head>button{width:100%}}@media(max-width:600px){.grid{grid-template-columns:minmax(0,1fr)}.head,.summary{align-items:stretch}.head>button{width:100%}}.card{display:grid;gap:12px;padding:16px;border:1px solid var(--md-sys-color-outline-variant, color-mix(in srgb, currentColor 18%, transparent));border-radius:8px;background:var(--md-sys-color-surface-container-low, var(--md-sys-color-surface-container, transparent));color:var(--md-sys-color-on-surface, currentColor)}.limits-panel{display:grid;gap:8px;min-width:0}.limits-fields{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr));gap:4px 16px}.limits-fields mat-form-field{min-width:0}.limits-notice{display:flex;gap:8px;align-items:flex-start;color:var(--md-sys-color-on-surface-variant, currentColor)}.limits-notice mat-icon{flex-shrink:0}.limits-error{color:var(--md-sys-color-error, #b3261e)}.limits-blocking-banner{border-color:var(--md-sys-color-error, #b3261e);gap:8px}.blocked-widgets-list{margin:0;padding-inline-start:20px;display:grid;gap:4px}.previous-heights-summary{display:flex;flex-wrap:wrap;gap:8px;align-items:center;font:var(--mat-sys-body-small, inherit);color:var(--md-sys-color-on-surface-variant, currentColor)}.previous-height-chip{padding:2px 8px;border-radius:999px;background:var(--md-sys-color-surface-container-highest, rgba(128, 128, 128, .14))}.wide{grid-column:1/-1}.hint{color:var(--md-sys-color-on-surface-variant, currentColor);font-size:12px}.eyebrow{color:var(--md-sys-color-primary, currentColor);font-size:11px;font-weight:700;letter-spacing:0;text-transform:uppercase}:host ::ng-deep .mat-mdc-tab-body-content{padding-top:12px;box-sizing:border-box}:host{container-type:inline-size}.studio-panel{display:grid;gap:var(--p-space-4, 16px);padding-block:var(--p-space-5, 20px);min-width:0}.studio-heading h2{font-size:var(--p-font-size-lg, 20px);line-height:1.25;margin:var(--p-space-2, 8px) 0;font-weight:600}.studio-heading p{margin:0;color:var(--md-sys-color-on-surface-variant);line-height:1.5}.studio-panel h3{font-size:var(--p-font-size-md, 16px);margin:0}.studio-panel>button{justify-self:start}.studio-gallery{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--p-space-3, 12px)}.studio-choice{display:flex;flex-direction:column;gap:var(--p-space-2, 8px);min-width:0;padding:var(--p-space-3, 12px);text-align:start;border:1px solid var(--md-sys-color-outline-variant);border-radius:var(--p-radius-lg, 12px);background:var(--md-sys-color-surface);color:var(--md-sys-color-on-surface);font:inherit;cursor:pointer;transition:border-color var(--md-sys-motion-duration-short2, .1s),background-color var(--md-sys-motion-duration-short2, .1s)}.studio-choice:hover:not(:disabled){border-color:var(--md-sys-color-primary);background:var(--md-sys-color-surface-container-low)}.studio-choice.selected{border-color:var(--md-sys-color-primary);box-shadow:inset 0 0 0 1px var(--md-sys-color-primary);background:var(--md-sys-color-surface-container-low)}.studio-choice:focus-visible,.studio-panel summary:focus-visible{outline:3px solid var(--md-sys-color-primary);outline-offset:3px}.studio-choice:disabled{opacity:var(--p-disabled-opacity, .5);cursor:not-allowed}.choice-title{display:flex;align-items:center;justify-content:space-between;gap:var(--p-space-2, 8px);width:100%;font-weight:600;line-height:1.35}.choice-title mat-icon{color:var(--md-sys-color-primary);flex-shrink:0;font-size:20px;height:20px;width:20px}.choice-description{font-size:var(--p-font-size-sm, 13px);line-height:1.5;color:var(--md-sys-color-on-surface-variant)}.choice-meta{font-size:var(--p-font-size-xs, 12px);color:var(--md-sys-color-on-surface-variant);margin-top:auto;padding-top:var(--p-space-1, 4px)}.theme-sample{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));padding:var(--p-space-3, 12px);border-radius:var(--p-radius-md, 8px);min-height:128px;align-self:stretch;margin-bottom:var(--p-space-1, 4px)}.sample-metric,.sample-chart{background:var(--sample-surface);border-radius:var(--sample-radius);box-shadow:var(--sample-shadow);padding:var(--p-space-2, 8px)}.sample-metric b{display:block;font-size:var(--p-font-size-md, 16px)}.sample-metric i{display:block;height:3px;width:55%;margin-top:var(--p-space-1, 4px);background:var(--sample-accent)}.sample-chart{grid-column:1/-1;display:flex;height:40px;align-items:flex-end;gap:var(--p-space-2, 8px)}.sample-chart i{flex:1;background:var(--sample-accent);height:50%;border-radius:var(--p-radius-xs, 2px)}.sample-chart i:nth-child(2){height:85%}.sample-chart i:nth-child(3){height:65%}.sample-chart i:nth-child(4){height:100%}.sample-chart i:nth-child(5){height:75%}.shell-options,.arrangement-rules{border-top:1px solid var(--md-sys-color-outline-variant);padding-top:var(--p-space-3, 12px)}.studio-panel summary{cursor:pointer;font-weight:500;line-height:1.5;padding-block:var(--p-space-2, 8px)}.shell-gallery{margin-top:var(--p-space-4, 16px)}.shell-sample{display:block;align-self:stretch;border:1px solid;overflow:hidden;margin:var(--p-space-2, 8px)}.shell-sample>span{display:flex;gap:var(--p-space-2, 8px);align-items:center;padding:var(--p-space-2, 8px);border-bottom:1px solid transparent}.shell-sample i{height:4px;width:60%;background:var(--md-sys-color-on-surface-variant);opacity:.4}.shell-sample>span:last-child{min-height:30px;flex-direction:column;align-items:flex-start}.shell-sample>span:last-child i:last-child{width:40%}.selection-line{margin:0;font-size:var(--p-font-size-sm, 13px);color:var(--md-sys-color-on-surface-variant)}.studio-actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--p-space-2, 8px)}.distribution-sample{display:grid;grid-template-rows:repeat(2,1fr);gap:var(--p-space-1, 4px);align-self:stretch;height:76px;padding:var(--p-space-2, 8px);background:var(--md-sys-color-surface-container);border-radius:var(--p-radius-md, 8px)}.distribution-sample i{display:grid;place-items:center;background:var(--md-sys-color-secondary-container);color:var(--md-sys-color-on-secondary-container);border-radius:var(--p-radius-sm, 4px);font-size:var(--p-font-size-xs, 12px);font-style:normal}.distribution-sample i:nth-child(n+4){opacity:.65}.selected .distribution-sample i{background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}.arrangement-review{display:grid;gap:var(--p-space-3, 12px);padding-block:var(--p-space-4, 16px);border-top:2px solid var(--md-sys-color-primary)}.review-heading{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:var(--p-space-2, 8px)}.review-heading>span{font-size:var(--p-font-size-sm, 13px);color:var(--md-sys-color-primary)}.canvas-comparison{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--p-space-3, 12px)}.canvas-comparison figure{margin:0;min-width:0}.canvas-comparison figcaption{font-weight:500;margin-bottom:var(--p-space-2, 8px)}.canvas-map{display:grid;gap:2px;height:220px;padding:var(--p-space-2, 8px);border:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface-container-low);border-radius:var(--p-radius-md, 8px)}.canvas-map span{display:grid;place-items:center;min-height:0;min-width:0;overflow:hidden;font-size:var(--p-font-size-xs, 12px);color:var(--md-sys-color-on-secondary-container);background:var(--md-sys-color-secondary-container);border-radius:var(--p-radius-xs, 2px)}.canvas-map .changed{background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}.widget-changes{padding-inline-start:var(--p-space-5, 20px);margin:0}.widget-changes li{padding-block:var(--p-space-2, 8px);overflow-wrap:anywhere}.widget-changes span{display:block;font-size:var(--p-font-size-sm, 13px);color:var(--md-sys-color-on-surface-variant)}.studio-status{border-inline-start:3px solid var(--md-sys-color-primary);background:var(--md-sys-color-surface-container-low);padding:var(--p-space-3, 12px);margin:0;line-height:1.5}@container (max-width:420px){.studio-gallery{grid-template-columns:minmax(0,1fr)}.canvas-map{height:180px}}@media(prefers-reduced-motion:reduce){.studio-choice{transition:none}}.arrangement-feedback:empty{display:none}.arrangement-feedback:focus{outline:none}.arrangement-feedback:focus-visible{outline:2px solid var(--md-sys-color-primary);outline-offset:3px}@container (min-width:620px){.arrangement-gallery{grid-template-columns:repeat(3,minmax(0,1fr))}}:host ::ng-deep .studio-tabs>.mat-mdc-tab-header{position:sticky;top:0;z-index:var(--praxis-layer-sticky-local, 100);background:var(--md-sys-color-surface-container-low, var(--md-sys-color-surface))}:host ::ng-deep .studio-tabs>.mat-mdc-tab-header .mdc-tab{flex:1 1 0;padding-inline:var(--p-space-1, 4px)}.arrangement-feedback{scroll-margin-block-start:var(--p-space-16, 64px)}.filter-owner-banner{border-color:var(--md-sys-color-tertiary, var(--md-sys-color-primary))}.filter-owner-item{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px}.quick-geometry-rows{display:grid;gap:8px}.quick-geometry-row{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px;padding-block:4px;border-bottom:1px solid var(--md-sys-color-outline-variant, rgba(128, 128, 128, .2))}.quick-geometry-label{display:inline-flex;align-items:center;gap:6px;min-width:0}.quick-geometry-label mat-icon{font-size:18px;width:18px;height:18px;flex-shrink:0;color:var(--md-sys-color-primary, currentColor)}.binder-widget-trigger{display:inline-flex;align-items:center;gap:8px;max-width:100%;min-width:0}.binder-widget-trigger-icon{font-size:18px;width:18px;height:18px;flex-shrink:0;color:var(--md-sys-color-primary, currentColor)}.binder-widget-trigger-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.binder-widget-trigger-text small{color:var(--md-sys-color-on-surface-variant, currentColor);font-weight:400}.binder-widget-option-row{position:relative;display:flex;align-items:flex-start;gap:10px;padding-block:4px;min-width:0}.binder-widget-option-icon{font-size:20px;width:20px;height:20px;margin-top:2px;flex-shrink:0;color:var(--md-sys-color-primary, currentColor)}.binder-widget-option-copy{display:flex;flex-direction:column;gap:2px;min-width:0;line-height:1.3}.binder-widget-option-title{font-weight:600;color:var(--md-sys-color-on-surface, currentColor);overflow-wrap:anywhere}.binder-widget-option-desc{font-size:12px;color:var(--md-sys-color-on-surface-variant, currentColor);overflow-wrap:anywhere}.binder-widget-option-sr-key{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;opacity:0;pointer-events:none}.binder-input-suggestions{display:flex;flex-wrap:wrap;align-items:center;gap:6px}.binder-input-suggestion-chip{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;border:1px solid var(--md-sys-color-outline-variant, rgba(128, 128, 128, .3));background:var(--md-sys-color-surface, transparent);color:var(--md-sys-color-on-surface, currentColor);font-size:12px;cursor:pointer;transition:border-color .1s,background-color .1s}.binder-input-suggestion-chip:hover{border-color:var(--md-sys-color-primary, currentColor);background:var(--md-sys-color-surface-container-low, rgba(128, 128, 128, .08))}.binder-input-suggestion-chip.active{border-color:var(--md-sys-color-primary, currentColor);background:var(--md-sys-color-primary-container, rgba(128, 128, 128, .16));color:var(--md-sys-color-on-primary-container, currentColor)}.binder-active-list{list-style:none;padding-inline-start:0;gap:6px}.binder-active-item{display:flex;align-items:center;gap:8px;padding:4px 8px;border-radius:6px;background:var(--md-sys-color-surface, rgba(255, 255, 255, .03))}.binder-active-icon{font-size:18px;width:18px;height:18px;flex-shrink:0;color:var(--md-sys-color-primary, currentColor)}.binder-active-copy{flex:1 1 auto;min-width:0;overflow-wrap:anywhere}.binder-active-select-btn{appearance:none;border:none;background:transparent;color:inherit;font:inherit;text-align:start;padding:2px 4px;border-radius:4px;cursor:pointer}.binder-active-select-btn:hover{background:var(--md-sys-color-surface-container-low, rgba(128, 128, 128, .08))}.binder-active-select-btn:focus-visible{outline:2px solid var(--md-sys-color-primary, currentColor);outline-offset:2px}.binder-preview-badge{display:inline-flex;align-items:center;margin-inline-start:6px;padding:1px 6px;border-radius:999px;font-size:11px;font-weight:600;color:var(--md-sys-color-on-secondary-container, #1d192b);background:var(--md-sys-color-secondary-container, rgba(103, 80, 164, .14))}.binder-unbound-warning{display:flex;align-items:flex-start;gap:8px;padding:8px 10px;border-radius:6px;font-size:12px;border:1px solid var(--md-sys-color-tertiary, rgba(180, 110, 0, .35));background:color-mix(in srgb,var(--md-sys-color-tertiary-container, #ffdea6) 45%,transparent);color:var(--md-sys-color-on-surface, inherit)}.binder-unbound-warning__body{display:grid;gap:6px}.binder-unbound-warning__chips{display:flex;flex-wrap:wrap;gap:6px}.binder-unbound-chip{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:999px;border:1px solid var(--md-sys-color-outline-variant, rgba(128, 128, 128, .35));background:var(--md-sys-color-surface, rgba(255, 255, 255, .65));color:inherit;font:inherit;font-size:11px;cursor:pointer}.binder-unbound-chip:hover{border-color:var(--md-sys-color-primary, currentColor)}.binder-unbound-chip:focus-visible{outline:2px solid var(--md-sys-color-primary, currentColor);outline-offset:2px}.guided-presets-bar{display:grid;gap:6px}.datasource-preview-box{display:grid;gap:8px;padding:12px;border-radius:6px;background:var(--md-sys-color-surface-container, rgba(128, 128, 128, .08))}.datasource-preview-json{margin:0;padding:8px;border-radius:4px;font-size:12px;overflow-x:auto;background:var(--md-sys-color-surface, rgba(0, 0, 0, .04))}.composer-rich-field-option{display:flex;flex-direction:column;gap:2px;padding:2px 0;line-height:1.3;min-width:0}.composer-rich-field-option__header{display:flex;align-items:center;flex-wrap:wrap;gap:6px;min-width:0}.composer-rich-field-option__label{font-weight:500;color:var(--md-sys-color-on-surface, currentColor);overflow:hidden;text-overflow:ellipsis}.composer-rich-field-option__code{font-family:var(--mat-sys-mono-font, monospace);font-size:11px;padding:1px 6px;border-radius:999px;background:var(--md-sys-color-surface-container-highest, rgba(128, 128, 128, .14));color:var(--md-sys-color-on-surface-variant, currentColor)}.composer-rich-field-option__badge{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.03em;padding:1px 6px;border-radius:999px;background:color-mix(in srgb,var(--md-sys-color-primary, #4f46e5) 14%,transparent);color:var(--md-sys-color-primary, currentColor)}.composer-rich-field-option__desc{font-size:11px;color:var(--md-sys-color-on-surface-variant, currentColor);white-space:normal;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}\n"], dependencies: [{ kind: "component", type: PagePresetComparisonComponent, selector: "praxis-page-preset-comparison", inputs: ["expanded", "layoutId", "themeId", "layoutPresetOptions", "canvas"], outputs: ["choose"] }, { 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],[formArray],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: MatAutocompleteModule }, { kind: "component", type: i2$3.MatAutocomplete, selector: "mat-autocomplete", inputs: ["aria-label", "aria-labelledby", "displayWith", "autoActiveFirstOption", "autoSelectActiveOption", "requireSelection", "panelWidth", "disableRipple", "class", "hideSingleSelectionIndicator"], outputs: ["optionSelected", "opened", "closed", "optionActivated"], exportAs: ["matAutocomplete"] }, { kind: "component", type: i3$2.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: i2$3.MatAutocompleteTrigger, selector: "input[matAutocomplete], textarea[matAutocomplete]", inputs: ["matAutocomplete", "matAutocompletePosition", "matAutocompleteConnectedTo", "autocomplete", "matAutocompleteDisabled"], exportAs: ["matAutocompleteTrigger"] }, { 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: MatButtonToggleModule }, { kind: "directive", type: i4.MatButtonToggleGroup, selector: "mat-button-toggle-group", inputs: ["appearance", "name", "vertical", "value", "multiple", "disabled", "disabledInteractive", "hideSingleSelectionIndicator", "hideMultipleSelectionIndicator"], outputs: ["valueChange", "change"], exportAs: ["matButtonToggleGroup"] }, { kind: "component", type: i4.MatButtonToggle, selector: "mat-button-toggle", inputs: ["aria-label", "aria-labelledby", "id", "name", "value", "tabIndex", "disableRipple", "appearance", "checked", "disabled", "disabledInteractive"], outputs: ["change"], exportAs: ["matButtonToggle"] }, { 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.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i3$1.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3$1.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i4$1.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: i3$2.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: "directive", type: i3$2.MatSelectTrigger, selector: "mat-select-trigger" }, { kind: "ngmodule", type: MatTabsModule }, { kind: "directive", type: i9.MatTabContent, selector: "[matTabContent]" }, { kind: "component", type: i9.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i9.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: PraxisIconButtonComponent, selector: "button[praxisIconButton]", inputs: ["praxisIconButton", "size", "appearance", "presentation", "pressed", "busy"] }, { kind: "component", type: ResourceQuickConnectComponent, selector: "praxis-resource-quick-connect", inputs: ["resourcePath", "endpointKey", "apiUrlEntry", "intent", "resources", "label", "placeholder", "hint", "disabled"], outputs: ["resourcePathChange", "resourceSelected"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
18464
18576
  }
18465
18577
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicPageConfigEditorComponent, decorators: [{
18466
18578
  type: Component,
@@ -20398,7 +20510,7 @@ class PageConfigEditorComponent {
20398
20510
  </div>
20399
20511
  }
20400
20512
  </div>
20401
- `, isInline: true, styles: [".page-inset-field{grid-column:1 / -1;min-width:0}.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{margin-right:-4px}.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: "component", type: PagePresetComparisonComponent, selector: "praxis-page-preset-comparison", inputs: ["expanded", "layoutId", "themeId", "layoutPresetOptions", "canvas"], outputs: ["choose"] }, { 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],[formArray],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: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i7.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i7.MatLabel, selector: "mat-label" }, { kind: "directive", type: i7.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: i2$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i3$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: i3$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i9$1.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i9$1.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: PraxisIconButtonComponent, selector: "button[praxisIconButton]", inputs: ["praxisIconButton", "size", "appearance", "presentation", "pressed", "busy"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
20513
+ `, isInline: true, styles: [".page-inset-field{grid-column:1 / -1;min-width:0}.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{margin-right:-4px}.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: "component", type: PagePresetComparisonComponent, selector: "praxis-page-preset-comparison", inputs: ["expanded", "layoutId", "themeId", "layoutPresetOptions", "canvas"], outputs: ["choose"] }, { 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],[formArray],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: "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: i4$1.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: i2$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i3$2.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: i3$2.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i9.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i9.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: PraxisIconButtonComponent, selector: "button[praxisIconButton]", inputs: ["praxisIconButton", "size", "appearance", "presentation", "pressed", "busy"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
20402
20514
  }
20403
20515
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PageConfigEditorComponent, decorators: [{
20404
20516
  type: Component,
@@ -31250,13 +31362,14 @@ class DynamicPageBuilderComponent {
31250
31362
  return metadataHints;
31251
31363
  }
31252
31364
  switch (componentId) {
31365
+ case 'praxis-crud':
31366
+ return { recommendedCols: 12, recommendedRows: 6 };
31253
31367
  case 'praxis-table':
31254
31368
  return { recommendedCols: 8, recommendedRows: 6 };
31255
31369
  case 'praxis-filter':
31256
31370
  return { recommendedCols: 12, recommendedRows: 3 };
31257
31371
  case 'praxis-chart':
31258
31372
  case 'praxis-list':
31259
- case 'praxis-crud':
31260
31373
  return { recommendedCols: 8, recommendedRows: 5 };
31261
31374
  default:
31262
31375
  return undefined;
@@ -31772,7 +31885,7 @@ class DynamicPageBuilderComponent {
31772
31885
  emptyText: this.tx('agentic.projectKnowledgeCockpit.timelineEmpty', 'No safe timeline events published for this change-set yet.'),
31773
31886
  });
31774
31887
  const timelineNode = document.nodes.find((node) => node.type === 'timeline');
31775
- return timelineNode?.type === 'timeline' ? timelineNode.items ?? [] : [];
31888
+ return timelineNode?.type === 'timeline' ? (timelineNode.items ?? []) : [];
31776
31889
  }
31777
31890
  trackProjectKnowledgeTimelineItem(index, item) {
31778
31891
  return item.id || `${index}:${item.title || item.badge || 'timeline'}`;
@@ -34411,7 +34524,7 @@ class DynamicPageBuilderComponent {
34411
34524
  if (!metadataInputs?.length || !widgetKey)
34412
34525
  return;
34413
34526
  const inputNames = new Set(metadataInputs.map((input) => input.name));
34414
- for (const identityInput of ['componentInstanceId', 'tabsId', 'formId']) {
34527
+ for (const identityInput of ['componentInstanceId', 'tabsId', 'formId', 'crudId']) {
34415
34528
  if (inputNames.has(identityInput)
34416
34529
  && inputs[identityInput] === undefined
34417
34530
  && presetInputs?.[identityInput] === undefined) {
@@ -34623,6 +34736,7 @@ class DynamicPageBuilderComponent {
34623
34736
 
34624
34737
  <div class="builder-governance-persistence" data-testid="dashboard-layout-status" role="status"
34625
34738
  aria-live="polite" aria-atomic="true"
34739
+ [class.busy]="isGovernanceBusy()"
34626
34740
  [class.customized]="isLayoutCustomized()" [class.governed]="!isLayoutCustomized()">
34627
34741
  <span>{{ governancePersistenceLabel() }}</span>
34628
34742
  @if (governancePersistence().status === 'saved' && governanceLastSavedAt(); as saved) {
@@ -35196,7 +35310,7 @@ class DynamicPageBuilderComponent {
35196
35310
  }
35197
35311
  </praxis-floating-toolbar>
35198
35312
  </div>
35199
- `, isInline: true, styles: [":host{display:block;position:relative;min-height:var(--pdx-page-builder-min-height, 420px)}.builder-shell{display:block;position:relative;min-height:inherit;height:100%;box-sizing:border-box}.builder-shell--bottom-toolbar{padding-block-end:calc(80px + env(safe-area-inset-bottom,0px))}.builder-shell:has(.pdx-shell.fullscreen) praxis-floating-toolbar{display:none}.builder-governance-toolbar{display:flex;justify-content:space-between;align-items:center;padding:10px 18px;margin-bottom:16px;border-radius:14px;background:var(--surface-gradient, color-mix(in srgb, var(--md-sys-color-surface-container, #1e293b) 95%, transparent));border:1px solid var(--border, color-mix(in srgb, var(--md-sys-color-outline-variant, #334155) 60%, transparent));gap:16px;flex-wrap:wrap}.builder-governance-brand{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.builder-governance-status-indicator{display:inline-flex;align-items:center;gap:8px;padding:3px 10px;border-radius:9999px;background:color-mix(in srgb,var(--ready, #22c55e) 12%,transparent);border:1px solid color-mix(in srgb,var(--ready, #22c55e) 28%,transparent)}.builder-governance-dot-pulse{width:7px;height:7px;border-radius:50%;background:var(--ready, #22c55e);box-shadow:0 0 8px var(--ready, #22c55e);animation:builderGovernancePulse 2s infinite cubic-bezier(.4,0,.6,1)}.builder-governance-title{font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--ready, #22c55e)}.builder-governance-badge{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:8px;background:color-mix(in srgb,var(--primary, #3b82f6) 12%,transparent);border:1px solid color-mix(in srgb,var(--primary, #3b82f6) 24%,transparent);font-size:11px;font-weight:600;color:var(--primary, #3b82f6)}.builder-governance-badge .material-symbols-outlined{font-size:14px}.builder-governance-persistence{display:inline-flex;align-items:center;gap:6px;padding:3px 10px;border-radius:9999px;font-size:11px;font-weight:600;letter-spacing:.02em;border:1px solid transparent;transition:all .2s ease}.builder-governance-persistence.governed{background:color-mix(in srgb,var(--muted, #64748b) 15%,transparent);border-color:color-mix(in srgb,var(--border, #334155) 80%,transparent);color:var(--muted-foreground, #94a3b8)}.builder-governance-persistence.customized{background:color-mix(in srgb,var(--primary, #3b82f6) 14%,transparent);border-color:color-mix(in srgb,var(--primary, #3b82f6) 35%,transparent);color:var(--primary, #3b82f6)}.builder-governance-persistence .builder-governance-saved-time{opacity:.75;font-weight:400}.builder-governance-actions{display:flex;align-items:center;gap:10px;margin-left:auto}.builder-governance-btn{display:inline-flex;align-items:center;gap:6px;padding:7px 13px;border-radius:9px;font-size:12px;font-weight:600;cursor:pointer;border:1px solid var(--border, #334155);background:var(--surface, #1e293b);color:var(--foreground, #f8fafc);transition:all .15s ease}.builder-governance-btn:hover:not(:disabled){border-color:var(--primary, #3b82f6);background:color-mix(in srgb,var(--primary, #3b82f6) 10%,var(--surface, #1e293b))}.builder-governance-btn:disabled{opacity:.5;cursor:not-allowed}.builder-governance-btn--reset{border-color:color-mix(in srgb,var(--warning, #eab308) 40%,transparent);color:var(--warning, #eab308)}.builder-governance-btn--reset:hover:not(:disabled){border-color:var(--warning, #eab308);background:color-mix(in srgb,var(--warning, #eab308) 12%,transparent)}.builder-governance-btn .material-symbols-outlined,.builder-governance-toggle-btn .material-symbols-outlined{font-size:16px}.builder-governance-toggle-btn{display:inline-flex;align-items:center;gap:7px;padding:7px 15px;border-radius:9px;font-size:12px;font-weight:600;cursor:pointer;border:1px solid var(--border, #334155);background:var(--surface, #1e293b);color:var(--foreground, #f8fafc);transition:all .2s cubic-bezier(.16,1,.3,1)}.builder-governance-toggle-btn:hover:not(:disabled){border-color:var(--primary, #3b82f6);transform:translateY(-1px);box-shadow:0 4px 12px color-mix(in srgb,var(--primary, #3b82f6) 20%,transparent)}.builder-governance-toggle-btn.active{background:linear-gradient(135deg,var(--primary, #3b82f6),color-mix(in srgb,var(--primary, #3b82f6) 80%,black));border-color:var(--primary, #3b82f6);color:#fff;box-shadow:0 0 16px color-mix(in srgb,var(--primary, #3b82f6) 35%,transparent)}.builder-governance-toggle-btn:disabled{opacity:.5;cursor:not-allowed}@keyframes builderGovernancePulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.85)}}.builder-authoring-lineage-alert{display:grid;grid-template-columns:auto minmax(0,1fr);gap:10px;margin:0 0 14px;padding:12px 14px;border:1px solid var(--md-sys-color-error, #b3261e);border-radius:10px;background:var(--md-sys-color-error-container, #f9dedc);color:var(--md-sys-color-on-error-container, #410e0b)}.builder-authoring-lineage-alert strong{display:block}.builder-authoring-lineage-alert p{margin:4px 0 0}.builder-authoring-lineage-alert ul{margin:8px 0 0;padding-inline-start:20px}@media(forced-colors:active){.builder-authoring-lineage-alert{border:2px solid CanvasText;background:Canvas;color:CanvasText}}.agentic-diagnostics-panel{position:absolute;z-index:21;right:16px;width:min(520px,calc(100% - 32px));max-height:min(440px,calc(100% - 32px));overflow:auto;padding:12px;border:1px solid rgba(107,114,128,.28);border-radius:8px;background:#fffffff5;color:#111827;box-shadow:0 14px 36px #0000003d}.agentic-diagnostics-panel--collapsed{width:min(280px,calc(100% - 32px));max-height:58px;overflow:hidden;padding:10px 12px;pointer-events:auto}.agentic-diagnostics-panel__header{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:13px;font-weight:700}.agentic-diagnostics-panel__header-actions{display:flex;align-items:center;gap:8px}.agentic-diagnostics-panel__badge{padding:2px 6px;border:1px solid rgba(107,114,128,.28);border-radius:8px;font-size:11px;font-weight:600;color:#4b5563}.agentic-diagnostics-panel__toggle,.shared-rule-cockpit__toggle{appearance:none;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 18%,transparent);border-radius:999px;background:color-mix(in srgb,var(--md-sys-color-surface-container-lowest) 84%,transparent);color:var(--md-sys-color-primary);cursor:pointer;font-size:11px;font-weight:700;line-height:1;padding:6px 9px;white-space:nowrap}.agentic-diagnostics-panel__description{margin:8px 0 10px;color:#4b5563;font-size:12px;line-height:1.4}.agentic-diagnostics-panel pre{margin:0;white-space:pre-wrap;overflow-wrap:anywhere;font-size:11px;line-height:1.45;color:#1f2937}.agentic-diagnostics-panel__health{display:grid;gap:8px;margin-bottom:12px}.agentic-diagnostics-panel__health h3,.agentic-diagnostics-panel__health p{margin:0}.agentic-diagnostics-panel__health h3{font-size:13px}.agentic-diagnostics-panel__health p{color:#4b5563;font-size:11px}.agentic-diagnostics-panel__metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:0}.agentic-diagnostics-panel__metrics div{padding:8px;border-radius:8px;background:#f3f4f6}.agentic-diagnostics-panel__metrics dt{color:#4b5563;font-size:10px}.agentic-diagnostics-panel__metrics dd{margin:3px 0 0;font-size:14px;font-weight:700}.agentic-diagnostics-panel__health-table{width:100%;border-collapse:collapse;font-size:10px}.agentic-diagnostics-panel__health-table caption{margin-bottom:4px;text-align:left;font-weight:700}.agentic-diagnostics-panel__health-table th,.agentic-diagnostics-panel__health-table td{padding:5px 4px;border-bottom:1px solid #e5e7eb;text-align:left}.agentic-diagnostics-panel__raw summary{margin-bottom:8px;cursor:pointer;font-size:11px;font-weight:700}@media(max-width:520px){.agentic-diagnostics-panel__metrics{grid-template-columns:repeat(2,minmax(0,1fr))}}praxis-ai-assistant-shell{position:fixed;inset:0;z-index:140;pointer-events:none}@media(min-width:1100px){.builder-shell--agentic-review-rail{box-sizing:border-box;padding-right:464px}}.builder-shell__agentic-toggle{position:relative}.builder-shell__agentic-toggle--open,.builder-shell__agentic-toggle--minimized{outline:2px solid rgba(96,165,250,.52);outline-offset:2px}.builder-shell__agentic-toggle--minimized:after{content:\"\";position:absolute;top:6px;right:6px;width:8px;height:8px;border:2px solid var(--md-sys-color-surface, #111827);border-radius:999px;background:#34d399}.builder-shell__connections-toggle--open{outline:2px solid color-mix(in srgb,var(--md-sys-color-primary, #6750a4) 52%,transparent);outline-offset:2px}.builder-shell__connections-toggle{position:relative}.builder-shell__connections-toggle--recommended:not(.builder-shell__connections-toggle--open){outline:2px solid rgba(34,211,238,.48);outline-offset:2px}.builder-shell__connections-toggle-badge{position:absolute;top:5px;right:5px;width:9px;height:9px;border:2px solid var(--md-sys-color-surface, #111827);border-radius:999px;background:#22d3ee;box-shadow:0 0 0 3px #22d3ee2e}.shared-rule-cockpit{position:absolute;z-index:120;left:16px;bottom:88px;width:min(720px,calc(100% - 32px));max-height:min(560px,calc(100vh - 140px));padding:14px;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 24%,transparent);border-radius:12px;background:linear-gradient(135deg,color-mix(in srgb,var(--md-sys-color-primary-container) 24%,var(--md-sys-color-surface-container-high) 76%),color-mix(in srgb,var(--md-sys-color-tertiary-container) 18%,var(--md-sys-color-surface-container) 82%));color:var(--md-sys-color-on-surface);box-shadow:0 18px 46px color-mix(in srgb,var(--md-sys-color-shadow, #000) 24%,transparent);overflow:auto;overscroll-behavior:contain}.shared-rule-cockpit--collapsed{width:min(360px,calc(100% - 32px));max-height:72px;overflow:hidden;padding:10px 12px}.shared-rule-cockpit__header{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.shared-rule-cockpit__header-actions{display:flex;align-items:center;gap:8px}.shared-rule-cockpit__header div{display:grid;gap:2px}.shared-rule-cockpit__header strong{font-size:14px;line-height:1.2}.shared-rule-cockpit__header span{color:var(--md-sys-color-on-surface-variant);font-size:12px;overflow-wrap:anywhere}.shared-rule-cockpit__badge{flex:0 0 auto;max-width:220px;padding:3px 8px;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 20%,transparent);border-radius:999px;background:color-mix(in srgb,var(--md-sys-color-surface-container-lowest) 74%,transparent);color:var(--md-sys-color-primary);font-size:11px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.shared-rule-cockpit__description,.shared-rule-cockpit__status,.shared-rule-cockpit__error{margin:10px 0 0;color:var(--md-sys-color-on-surface-variant);font-size:12px;line-height:1.45}.shared-rule-cockpit__error{color:var(--md-sys-color-error);font-weight:600}.shared-rule-cockpit__actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}.shared-rule-cockpit__facts{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin:12px 0 0}.shared-rule-cockpit__facts div{min-width:0;padding:8px;border-radius:10px;background:color-mix(in srgb,var(--md-sys-color-surface-container-lowest) 72%,transparent)}.shared-rule-cockpit__facts dt{color:var(--md-sys-color-on-surface-variant);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.04em}.shared-rule-cockpit__facts dd{margin:4px 0 0;color:var(--md-sys-color-on-surface);font-size:12px;overflow-wrap:anywhere}.shared-rule-cockpit__enforcement-matrix{display:grid;gap:10px;margin-top:12px;padding:10px;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 14%,transparent);border-radius:12px;background:color-mix(in srgb,var(--md-sys-color-surface-container-lowest) 58%,transparent)}.shared-rule-cockpit__matrix-header{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}.shared-rule-cockpit__matrix-header div{display:grid;gap:2px}.shared-rule-cockpit__matrix-header strong{color:var(--md-sys-color-on-surface);font-size:12px}.shared-rule-cockpit__matrix-header span,.shared-rule-cockpit__matrix-source{color:var(--md-sys-color-on-surface-variant);font-size:11px;line-height:1.35;overflow-wrap:anywhere}.shared-rule-cockpit__matrix-badge{flex:0 0 auto;padding:3px 8px;border-radius:999px;background:#16a34a1f;color:#166534;font-weight:800;white-space:nowrap}.shared-rule-cockpit__projection-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.shared-rule-cockpit__projection{display:grid;gap:3px;min-width:0;padding:9px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 52%,transparent);border-radius:11px;background:color-mix(in srgb,var(--md-sys-color-surface-container-low) 82%,transparent)}.shared-rule-cockpit__projection[data-status=ready]{border-color:color-mix(in srgb,var(--md-sys-color-tertiary) 24%,transparent);background:color-mix(in srgb,var(--md-sys-color-tertiary-container) 32%,var(--md-sys-color-surface-container-low) 68%)}.shared-rule-cockpit__projection span{color:var(--md-sys-color-primary);font-size:10px;font-weight:800;letter-spacing:.04em;text-transform:uppercase;overflow-wrap:anywhere}.shared-rule-cockpit__projection strong{color:var(--md-sys-color-on-surface);font-size:12px}.shared-rule-cockpit__projection small,.shared-rule-cockpit__projection em{color:var(--md-sys-color-on-surface-variant);font-size:11px;font-style:normal;overflow-wrap:anywhere}.shared-rule-cockpit__matrix-source{margin:0}.shared-rule-cockpit__timeline{display:grid;gap:8px;max-height:190px;margin:12px 0 0;padding:0;overflow:auto;list-style:none}.shared-rule-cockpit__timeline-item{display:grid;grid-template-columns:auto minmax(0,1fr);gap:10px;padding:10px;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 12%,transparent);border-radius:12px;background:color-mix(in srgb,var(--md-sys-color-surface-container-lowest) 76%,transparent)}.shared-rule-cockpit__timeline-icon{display:grid;width:24px;height:24px;place-items:center;border-radius:999px;background:#1e40af1a;color:var(--md-sys-color-primary);font-family:Material Icons;font-size:16px;line-height:1}.shared-rule-cockpit__timeline-item strong{color:var(--md-sys-color-on-surface);font-size:12px;line-height:1.35}.shared-rule-cockpit__timeline-item p,.shared-rule-cockpit__timeline-item small,.shared-rule-cockpit__timeline-item time,.shared-rule-cockpit__timeline-empty{display:block;margin:3px 0 0;color:var(--md-sys-color-on-surface-variant);font-size:11px;line-height:1.35;overflow-wrap:anywhere}.shared-rule-cockpit__timeline-badge{display:inline-flex;margin-left:6px;padding:2px 6px;border-radius:999px;background:color-mix(in srgb,var(--md-sys-color-secondary-container) 42%,transparent);color:var(--md-sys-color-on-secondary-container);font-size:10px;font-weight:700}.shared-rule-cockpit__timeline-empty{padding:10px;border-radius:12px;background:color-mix(in srgb,var(--md-sys-color-surface-container-lowest) 64%,transparent)}@media(max-width:720px){.shared-rule-cockpit{bottom:72px;max-height:calc(100vh - 112px)}.shared-rule-cockpit__facts,.shared-rule-cockpit__projection-grid{grid-template-columns:1fr}}\n"], dependencies: [{ 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.MatMiniFabButton, selector: "button[mat-mini-fab], a[mat-mini-fab], button[matMiniFab], a[matMiniFab]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3.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", "authoringCapabilities", "showPageSettingsButton", "shellEditorComponent", "pageEditorComponent", "autoPersist", "pageIdentity", "componentInstanceId", "showWidgetAssistantButton"], outputs: ["pageChange", "persistenceChange", "pageAuthoringChange", "widgetEvent", "widgetSelectionChange", "widgetAssistantRequested", "widgetDiagnosticsChange"] }, { kind: "component", type: FloatingToolbarComponent, selector: "praxis-floating-toolbar", inputs: ["visible", "canUndo", "canRedo", "showSave", "showPreview", "showAdd", "showSettings", "editingEnabled", "previewActive", "connectionMode", "modeLabel", "addLabel", "saveLabel", "undoLabel", "redoLabel", "settingsLabel", "previewLabel", "exitPreviewLabel"], outputs: ["add", "undo", "redo", "settings", "preview", "save"] }, { kind: "component", type: ConnectionEditorComponent, selector: "praxis-connection-editor", inputs: ["open", "page"], outputs: ["pageChange", "focusWidget", "openPageSettings", "close"] }, { kind: "component", type: PraxisAiAssistantShellComponent, selector: "praxis-ai-assistant-shell", inputs: ["labels", "mode", "state", "contextItems", "attachments", "messages", "quickReplies", "recommendedIntents", "prompt", "statusText", "errorText", "testIdPrefix", "panelTestId", "submitTestId", "applyTestId", "primaryAction", "secondaryActions", "governanceActions", "busy", "canSubmit", "canApply", "submitOnEnter", "showAttachAction", "enablePastedAttachments", "enableFileAttachments", "attachmentAccept", "attachmentMultiple", "voiceInputMode", "voiceLanguage", "draggable", "resizable", "minWidth", "minHeight", "margin", "layout"], outputs: ["promptChange", "submitPrompt", "apply", "retryTurn", "cancelTurn", "shellAction", "close", "attach", "attachmentsPasted", "attachmentsSelected", "removeAttachment", "messageAction", "editMessage", "resendMessage", "quickReply", "recommendedIntent", "layoutChange"] }] });
35313
+ `, isInline: true, styles: [":host{display:block;position:relative;min-height:var(--pdx-page-builder-min-height, 420px)}.builder-shell{display:block;position:relative;min-height:inherit;height:100%;box-sizing:border-box}.builder-shell--bottom-toolbar{padding-block-end:calc(80px + env(safe-area-inset-bottom,0px))}.builder-shell:has(.pdx-shell.fullscreen) praxis-floating-toolbar{display:none}.builder-governance-toolbar{display:flex;justify-content:space-between;align-items:center;padding:10px 18px;margin-bottom:16px;border-radius:14px;background:var(--surface-gradient, color-mix(in srgb, var(--md-sys-color-surface-container, #1e293b) 95%, transparent));border:1px solid var(--border, color-mix(in srgb, var(--md-sys-color-outline-variant, #334155) 60%, transparent));gap:16px;flex-wrap:wrap}.builder-governance-brand{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.builder-governance-status-indicator{display:inline-flex;align-items:center;gap:8px;padding:3px 10px;border-radius:9999px;background:color-mix(in srgb,var(--ready, #22c55e) 12%,transparent);border:1px solid color-mix(in srgb,var(--ready, #22c55e) 28%,transparent)}.builder-governance-dot-pulse{width:7px;height:7px;border-radius:50%;background:var(--ready, #22c55e);box-shadow:0 0 8px var(--ready, #22c55e);animation:builderGovernancePulse 2s infinite cubic-bezier(.4,0,.6,1)}.builder-governance-title{font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--ready, #22c55e)}.builder-governance-badge{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:8px;background:color-mix(in srgb,var(--primary, #3b82f6) 12%,transparent);border:1px solid color-mix(in srgb,var(--primary, #3b82f6) 24%,transparent);font-size:11px;font-weight:600;color:var(--primary, #3b82f6)}.builder-governance-badge .material-symbols-outlined{font-size:14px}.builder-governance-persistence{display:inline-flex;align-items:center;gap:6px;padding:3px 10px;border-radius:9999px;font-size:11px;font-weight:600;letter-spacing:.02em;border:1px solid transparent;transition:all .2s ease}.builder-governance-persistence.governed{background:color-mix(in srgb,var(--muted, #64748b) 15%,transparent);border-color:color-mix(in srgb,var(--border, #334155) 80%,transparent);color:var(--muted-foreground, #94a3b8)}.builder-governance-persistence.customized{background:color-mix(in srgb,var(--primary, #3b82f6) 14%,transparent);border-color:color-mix(in srgb,var(--primary, #3b82f6) 35%,transparent);color:var(--primary, #3b82f6)}.builder-governance-persistence .builder-governance-saved-time{opacity:.75;font-weight:400}.builder-governance-actions{display:flex;align-items:center;gap:10px;margin-left:auto}.builder-governance-btn{display:inline-flex;align-items:center;gap:6px;padding:7px 13px;border-radius:9px;font-size:12px;font-weight:600;cursor:pointer;border:1px solid var(--border, #334155);background:var(--surface, #1e293b);color:var(--foreground, #f8fafc);transition:all .15s ease}.builder-governance-btn:hover:not(:disabled){border-color:var(--primary, #3b82f6);background:color-mix(in srgb,var(--primary, #3b82f6) 10%,var(--surface, #1e293b))}.builder-governance-btn:disabled{opacity:.5;cursor:not-allowed}.builder-governance-btn--reset{border-color:color-mix(in srgb,var(--warning, #eab308) 40%,transparent);color:var(--warning, #eab308)}.builder-governance-btn--reset:hover:not(:disabled){border-color:var(--warning, #eab308);background:color-mix(in srgb,var(--warning, #eab308) 12%,transparent)}.builder-governance-btn .material-symbols-outlined,.builder-governance-toggle-btn .material-symbols-outlined{font-size:16px}.builder-governance-toggle-btn{display:inline-flex;align-items:center;gap:7px;padding:7px 15px;border-radius:9px;font-size:12px;font-weight:600;cursor:pointer;border:1px solid var(--border, #334155);background:var(--surface, #1e293b);color:var(--foreground, #f8fafc);transition:all .2s cubic-bezier(.16,1,.3,1)}.builder-governance-toggle-btn:hover:not(:disabled){border-color:var(--primary, #3b82f6);transform:translateY(-1px);box-shadow:0 4px 12px color-mix(in srgb,var(--primary, #3b82f6) 20%,transparent)}.builder-governance-toggle-btn.active{background:linear-gradient(135deg,var(--primary, #3b82f6),color-mix(in srgb,var(--primary, #3b82f6) 80%,black));border-color:var(--primary, #3b82f6);color:#fff;box-shadow:0 0 16px color-mix(in srgb,var(--primary, #3b82f6) 35%,transparent)}.builder-governance-toggle-btn:disabled{opacity:.5;cursor:not-allowed}@keyframes builderGovernancePulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.85)}}.builder-authoring-lineage-alert{display:grid;grid-template-columns:auto minmax(0,1fr);gap:10px;margin:0 0 14px;padding:12px 14px;border:1px solid var(--md-sys-color-error, #b3261e);border-radius:10px;background:var(--md-sys-color-error-container, #f9dedc);color:var(--md-sys-color-on-error-container, #410e0b)}.builder-authoring-lineage-alert strong{display:block}.builder-authoring-lineage-alert p{margin:4px 0 0}.builder-authoring-lineage-alert ul{margin:8px 0 0;padding-inline-start:20px}@media(forced-colors:active){.builder-authoring-lineage-alert{border:2px solid CanvasText;background:Canvas;color:CanvasText}}.agentic-diagnostics-panel{position:absolute;z-index:21;right:16px;width:min(520px,calc(100% - 32px));max-height:min(440px,calc(100% - 32px));overflow:auto;padding:12px;border:1px solid rgba(107,114,128,.28);border-radius:8px;background:#fffffff5;color:#111827;box-shadow:0 14px 36px #0000003d}.agentic-diagnostics-panel--collapsed{width:min(280px,calc(100% - 32px));max-height:58px;overflow:hidden;padding:10px 12px;pointer-events:auto}.agentic-diagnostics-panel__header{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:13px;font-weight:700}.agentic-diagnostics-panel__header-actions{display:flex;align-items:center;gap:8px}.agentic-diagnostics-panel__badge{padding:2px 6px;border:1px solid rgba(107,114,128,.28);border-radius:8px;font-size:11px;font-weight:600;color:#4b5563}.agentic-diagnostics-panel__toggle,.shared-rule-cockpit__toggle{appearance:none;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 18%,transparent);border-radius:999px;background:color-mix(in srgb,var(--md-sys-color-surface-container-lowest) 84%,transparent);color:var(--md-sys-color-primary);cursor:pointer;font-size:11px;font-weight:700;line-height:1;padding:6px 9px;white-space:nowrap}.agentic-diagnostics-panel__description{margin:8px 0 10px;color:#4b5563;font-size:12px;line-height:1.4}.agentic-diagnostics-panel pre{margin:0;white-space:pre-wrap;overflow-wrap:anywhere;font-size:11px;line-height:1.45;color:#1f2937}.agentic-diagnostics-panel__health{display:grid;gap:8px;margin-bottom:12px}.agentic-diagnostics-panel__health h3,.agentic-diagnostics-panel__health p{margin:0}.agentic-diagnostics-panel__health h3{font-size:13px}.agentic-diagnostics-panel__health p{color:#4b5563;font-size:11px}.agentic-diagnostics-panel__metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:0}.agentic-diagnostics-panel__metrics div{padding:8px;border-radius:8px;background:#f3f4f6}.agentic-diagnostics-panel__metrics dt{color:#4b5563;font-size:10px}.agentic-diagnostics-panel__metrics dd{margin:3px 0 0;font-size:14px;font-weight:700}.agentic-diagnostics-panel__health-table{width:100%;border-collapse:collapse;font-size:10px}.agentic-diagnostics-panel__health-table caption{margin-bottom:4px;text-align:left;font-weight:700}.agentic-diagnostics-panel__health-table th,.agentic-diagnostics-panel__health-table td{padding:5px 4px;border-bottom:1px solid #e5e7eb;text-align:left}.agentic-diagnostics-panel__raw summary{margin-bottom:8px;cursor:pointer;font-size:11px;font-weight:700}@media(max-width:520px){.agentic-diagnostics-panel__metrics{grid-template-columns:repeat(2,minmax(0,1fr))}}praxis-ai-assistant-shell{position:fixed;inset:0;z-index:140;pointer-events:none}@media(min-width:1100px){.builder-shell--agentic-review-rail{box-sizing:border-box;padding-right:464px}}.builder-shell__agentic-toggle{position:relative}.builder-shell__agentic-toggle--open,.builder-shell__agentic-toggle--minimized{outline:2px solid rgba(96,165,250,.52);outline-offset:2px}.builder-shell__agentic-toggle--minimized:after{content:\"\";position:absolute;top:6px;right:6px;width:8px;height:8px;border:2px solid var(--md-sys-color-surface, #111827);border-radius:999px;background:#34d399}.builder-shell__connections-toggle--open{outline:2px solid color-mix(in srgb,var(--md-sys-color-primary, #6750a4) 52%,transparent);outline-offset:2px}.builder-shell__connections-toggle{position:relative}.builder-shell__connections-toggle--recommended:not(.builder-shell__connections-toggle--open){outline:2px solid rgba(34,211,238,.48);outline-offset:2px}.builder-shell__connections-toggle-badge{position:absolute;top:5px;right:5px;width:9px;height:9px;border:2px solid var(--md-sys-color-surface, #111827);border-radius:999px;background:#22d3ee;box-shadow:0 0 0 3px #22d3ee2e}.shared-rule-cockpit{position:absolute;z-index:120;left:16px;bottom:88px;width:min(720px,calc(100% - 32px));max-height:min(560px,calc(100vh - 140px));padding:14px;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 24%,transparent);border-radius:12px;background:linear-gradient(135deg,color-mix(in srgb,var(--md-sys-color-primary-container) 24%,var(--md-sys-color-surface-container-high) 76%),color-mix(in srgb,var(--md-sys-color-tertiary-container) 18%,var(--md-sys-color-surface-container) 82%));color:var(--md-sys-color-on-surface);box-shadow:0 18px 46px color-mix(in srgb,var(--md-sys-color-shadow, #000) 24%,transparent);overflow:auto;overscroll-behavior:contain}.shared-rule-cockpit--collapsed{width:min(360px,calc(100% - 32px));max-height:72px;overflow:hidden;padding:10px 12px}.shared-rule-cockpit__header{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.shared-rule-cockpit__header-actions{display:flex;align-items:center;gap:8px}.shared-rule-cockpit__header div{display:grid;gap:2px}.shared-rule-cockpit__header strong{font-size:14px;line-height:1.2}.shared-rule-cockpit__header span{color:var(--md-sys-color-on-surface-variant);font-size:12px;overflow-wrap:anywhere}.shared-rule-cockpit__badge{flex:0 0 auto;max-width:220px;padding:3px 8px;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 20%,transparent);border-radius:999px;background:color-mix(in srgb,var(--md-sys-color-surface-container-lowest) 74%,transparent);color:var(--md-sys-color-primary);font-size:11px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.shared-rule-cockpit__description,.shared-rule-cockpit__status,.shared-rule-cockpit__error{margin:10px 0 0;color:var(--md-sys-color-on-surface-variant);font-size:12px;line-height:1.45}.shared-rule-cockpit__status,.builder-governance-persistence.busy>span:first-child{font-weight:550;color:var(--pdx-text-shimmer-base, var(--md-sys-color-on-surface-variant))}@supports ((-webkit-background-clip: text) or (background-clip: text)){.shared-rule-cockpit__status,.builder-governance-persistence.busy>span:first-child{background-image:var( --pdx-text-shimmer-gradient, linear-gradient( 105deg, color-mix(in oklab, var(--md-sys-color-on-surface-variant, #64748b) 82%, var(--md-sys-color-on-surface, #0f172a) 18%) 0%, color-mix(in oklab, var(--md-sys-color-on-surface-variant, #64748b) 82%, var(--md-sys-color-on-surface, #0f172a) 18%) 26%, color-mix(in oklab, var(--md-sys-color-tertiary, #6366f1) 74%, #a855f7 26%) 40%, var(--pdx-text-shimmer-peak, var(--md-sys-color-on-surface, #ffffff)) 50%, color-mix(in oklab, var(--md-sys-color-primary, #006a6a) 74%, #ec4899 26%) 60%, color-mix(in oklab, var(--md-sys-color-on-surface-variant, #64748b) 82%, var(--md-sys-color-on-surface, #0f172a) 18%) 74%, color-mix(in oklab, var(--md-sys-color-on-surface-variant, #64748b) 82%, var(--md-sys-color-on-surface, #0f172a) 18%) 100% ) );background-size:300% 100%;background-position:100% 50%;background-repeat:no-repeat;-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent;animation:sharedRuleCockpitTextShimmer var(--pdx-text-shimmer-duration, 2.6s) linear infinite;will-change:background-position}}@media(prefers-reduced-motion:reduce),(forced-colors:active),print{.shared-rule-cockpit__status,.builder-governance-persistence.busy>span:first-child{animation:none!important;background-image:none!important;-webkit-text-fill-color:currentColor!important;color:var(--md-sys-color-primary)!important;will-change:auto}}@media(forced-colors:active){.shared-rule-cockpit__status,.builder-governance-persistence.busy>span:first-child{color:CanvasText!important;-webkit-text-fill-color:CanvasText!important}}@keyframes sharedRuleCockpitTextShimmer{0%{background-position:100% 50%}to{background-position:0% 50%}}.shared-rule-cockpit__error{color:var(--md-sys-color-error);font-weight:600}.shared-rule-cockpit__actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}.shared-rule-cockpit__facts{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin:12px 0 0}.shared-rule-cockpit__facts div{min-width:0;padding:8px;border-radius:10px;background:color-mix(in srgb,var(--md-sys-color-surface-container-lowest) 72%,transparent)}.shared-rule-cockpit__facts dt{color:var(--md-sys-color-on-surface-variant);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.04em}.shared-rule-cockpit__facts dd{margin:4px 0 0;color:var(--md-sys-color-on-surface);font-size:12px;overflow-wrap:anywhere}.shared-rule-cockpit__enforcement-matrix{display:grid;gap:10px;margin-top:12px;padding:10px;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 14%,transparent);border-radius:12px;background:color-mix(in srgb,var(--md-sys-color-surface-container-lowest) 58%,transparent)}.shared-rule-cockpit__matrix-header{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}.shared-rule-cockpit__matrix-header div{display:grid;gap:2px}.shared-rule-cockpit__matrix-header strong{color:var(--md-sys-color-on-surface);font-size:12px}.shared-rule-cockpit__matrix-header span,.shared-rule-cockpit__matrix-source{color:var(--md-sys-color-on-surface-variant);font-size:11px;line-height:1.35;overflow-wrap:anywhere}.shared-rule-cockpit__matrix-badge{flex:0 0 auto;padding:3px 8px;border-radius:999px;background:#16a34a1f;color:#166534;font-weight:800;white-space:nowrap}.shared-rule-cockpit__projection-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.shared-rule-cockpit__projection{display:grid;gap:3px;min-width:0;padding:9px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 52%,transparent);border-radius:11px;background:color-mix(in srgb,var(--md-sys-color-surface-container-low) 82%,transparent)}.shared-rule-cockpit__projection[data-status=ready]{border-color:color-mix(in srgb,var(--md-sys-color-tertiary) 24%,transparent);background:color-mix(in srgb,var(--md-sys-color-tertiary-container) 32%,var(--md-sys-color-surface-container-low) 68%)}.shared-rule-cockpit__projection span{color:var(--md-sys-color-primary);font-size:10px;font-weight:800;letter-spacing:.04em;text-transform:uppercase;overflow-wrap:anywhere}.shared-rule-cockpit__projection strong{color:var(--md-sys-color-on-surface);font-size:12px}.shared-rule-cockpit__projection small,.shared-rule-cockpit__projection em{color:var(--md-sys-color-on-surface-variant);font-size:11px;font-style:normal;overflow-wrap:anywhere}.shared-rule-cockpit__matrix-source{margin:0}.shared-rule-cockpit__timeline{display:grid;gap:8px;max-height:190px;margin:12px 0 0;padding:0;overflow:auto;list-style:none}.shared-rule-cockpit__timeline-item{display:grid;grid-template-columns:auto minmax(0,1fr);gap:10px;padding:10px;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 12%,transparent);border-radius:12px;background:color-mix(in srgb,var(--md-sys-color-surface-container-lowest) 76%,transparent)}.shared-rule-cockpit__timeline-icon{display:grid;width:24px;height:24px;place-items:center;border-radius:999px;background:#1e40af1a;color:var(--md-sys-color-primary);font-family:Material Icons;font-size:16px;line-height:1}.shared-rule-cockpit__timeline-item strong{color:var(--md-sys-color-on-surface);font-size:12px;line-height:1.35}.shared-rule-cockpit__timeline-item p,.shared-rule-cockpit__timeline-item small,.shared-rule-cockpit__timeline-item time,.shared-rule-cockpit__timeline-empty{display:block;margin:3px 0 0;color:var(--md-sys-color-on-surface-variant);font-size:11px;line-height:1.35;overflow-wrap:anywhere}.shared-rule-cockpit__timeline-badge{display:inline-flex;margin-left:6px;padding:2px 6px;border-radius:999px;background:color-mix(in srgb,var(--md-sys-color-secondary-container) 42%,transparent);color:var(--md-sys-color-on-secondary-container);font-size:10px;font-weight:700}.shared-rule-cockpit__timeline-empty{padding:10px;border-radius:12px;background:color-mix(in srgb,var(--md-sys-color-surface-container-lowest) 64%,transparent)}@media(max-width:720px){.shared-rule-cockpit{bottom:72px;max-height:calc(100vh - 112px)}.shared-rule-cockpit__facts,.shared-rule-cockpit__projection-grid{grid-template-columns:1fr}}\n"], dependencies: [{ 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.MatMiniFabButton, selector: "button[mat-mini-fab], a[mat-mini-fab], button[matMiniFab], a[matMiniFab]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3.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", "authoringCapabilities", "showPageSettingsButton", "shellEditorComponent", "pageEditorComponent", "autoPersist", "pageIdentity", "componentInstanceId", "showWidgetAssistantButton", "blockMutations"], outputs: ["pageChange", "persistenceChange", "pageAuthoringChange", "widgetEvent", "widgetSelectionChange", "widgetAssistantRequested", "widgetDiagnosticsChange"] }, { kind: "component", type: FloatingToolbarComponent, selector: "praxis-floating-toolbar", inputs: ["visible", "canUndo", "canRedo", "showSave", "showPreview", "showAdd", "showSettings", "editingEnabled", "previewActive", "connectionMode", "modeLabel", "addLabel", "saveLabel", "undoLabel", "redoLabel", "settingsLabel", "previewLabel", "exitPreviewLabel"], outputs: ["add", "undo", "redo", "settings", "preview", "save"] }, { kind: "component", type: ConnectionEditorComponent, selector: "praxis-connection-editor", inputs: ["open", "page"], outputs: ["pageChange", "focusWidget", "openPageSettings", "close"] }, { kind: "component", type: PraxisAiAssistantShellComponent, selector: "praxis-ai-assistant-shell", inputs: ["labels", "mode", "state", "contextItems", "attachments", "messages", "quickReplies", "recommendedIntents", "prompt", "statusText", "errorText", "testIdPrefix", "panelTestId", "submitTestId", "applyTestId", "primaryAction", "secondaryActions", "governanceActions", "busy", "canSubmit", "canApply", "submitOnEnter", "showAttachAction", "enablePastedAttachments", "enableFileAttachments", "attachmentAccept", "attachmentMultiple", "voiceInputMode", "voiceLanguage", "draggable", "resizable", "minWidth", "minHeight", "margin", "layout"], outputs: ["promptChange", "submitPrompt", "apply", "retryTurn", "cancelTurn", "shellAction", "close", "attach", "attachmentsPasted", "attachmentsSelected", "removeAttachment", "messageAction", "editMessage", "resendMessage", "quickReply", "recommendedIntent", "layoutChange"] }] });
35200
35314
  }
35201
35315
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicPageBuilderComponent, decorators: [{
35202
35316
  type: Component,
@@ -35233,6 +35347,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
35233
35347
 
35234
35348
  <div class="builder-governance-persistence" data-testid="dashboard-layout-status" role="status"
35235
35349
  aria-live="polite" aria-atomic="true"
35350
+ [class.busy]="isGovernanceBusy()"
35236
35351
  [class.customized]="isLayoutCustomized()" [class.governed]="!isLayoutCustomized()">
35237
35352
  <span>{{ governancePersistenceLabel() }}</span>
35238
35353
  @if (governancePersistence().status === 'saved' && governanceLastSavedAt(); as saved) {
@@ -35806,7 +35921,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
35806
35921
  }
35807
35922
  </praxis-floating-toolbar>
35808
35923
  </div>
35809
- `, styles: [":host{display:block;position:relative;min-height:var(--pdx-page-builder-min-height, 420px)}.builder-shell{display:block;position:relative;min-height:inherit;height:100%;box-sizing:border-box}.builder-shell--bottom-toolbar{padding-block-end:calc(80px + env(safe-area-inset-bottom,0px))}.builder-shell:has(.pdx-shell.fullscreen) praxis-floating-toolbar{display:none}.builder-governance-toolbar{display:flex;justify-content:space-between;align-items:center;padding:10px 18px;margin-bottom:16px;border-radius:14px;background:var(--surface-gradient, color-mix(in srgb, var(--md-sys-color-surface-container, #1e293b) 95%, transparent));border:1px solid var(--border, color-mix(in srgb, var(--md-sys-color-outline-variant, #334155) 60%, transparent));gap:16px;flex-wrap:wrap}.builder-governance-brand{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.builder-governance-status-indicator{display:inline-flex;align-items:center;gap:8px;padding:3px 10px;border-radius:9999px;background:color-mix(in srgb,var(--ready, #22c55e) 12%,transparent);border:1px solid color-mix(in srgb,var(--ready, #22c55e) 28%,transparent)}.builder-governance-dot-pulse{width:7px;height:7px;border-radius:50%;background:var(--ready, #22c55e);box-shadow:0 0 8px var(--ready, #22c55e);animation:builderGovernancePulse 2s infinite cubic-bezier(.4,0,.6,1)}.builder-governance-title{font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--ready, #22c55e)}.builder-governance-badge{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:8px;background:color-mix(in srgb,var(--primary, #3b82f6) 12%,transparent);border:1px solid color-mix(in srgb,var(--primary, #3b82f6) 24%,transparent);font-size:11px;font-weight:600;color:var(--primary, #3b82f6)}.builder-governance-badge .material-symbols-outlined{font-size:14px}.builder-governance-persistence{display:inline-flex;align-items:center;gap:6px;padding:3px 10px;border-radius:9999px;font-size:11px;font-weight:600;letter-spacing:.02em;border:1px solid transparent;transition:all .2s ease}.builder-governance-persistence.governed{background:color-mix(in srgb,var(--muted, #64748b) 15%,transparent);border-color:color-mix(in srgb,var(--border, #334155) 80%,transparent);color:var(--muted-foreground, #94a3b8)}.builder-governance-persistence.customized{background:color-mix(in srgb,var(--primary, #3b82f6) 14%,transparent);border-color:color-mix(in srgb,var(--primary, #3b82f6) 35%,transparent);color:var(--primary, #3b82f6)}.builder-governance-persistence .builder-governance-saved-time{opacity:.75;font-weight:400}.builder-governance-actions{display:flex;align-items:center;gap:10px;margin-left:auto}.builder-governance-btn{display:inline-flex;align-items:center;gap:6px;padding:7px 13px;border-radius:9px;font-size:12px;font-weight:600;cursor:pointer;border:1px solid var(--border, #334155);background:var(--surface, #1e293b);color:var(--foreground, #f8fafc);transition:all .15s ease}.builder-governance-btn:hover:not(:disabled){border-color:var(--primary, #3b82f6);background:color-mix(in srgb,var(--primary, #3b82f6) 10%,var(--surface, #1e293b))}.builder-governance-btn:disabled{opacity:.5;cursor:not-allowed}.builder-governance-btn--reset{border-color:color-mix(in srgb,var(--warning, #eab308) 40%,transparent);color:var(--warning, #eab308)}.builder-governance-btn--reset:hover:not(:disabled){border-color:var(--warning, #eab308);background:color-mix(in srgb,var(--warning, #eab308) 12%,transparent)}.builder-governance-btn .material-symbols-outlined,.builder-governance-toggle-btn .material-symbols-outlined{font-size:16px}.builder-governance-toggle-btn{display:inline-flex;align-items:center;gap:7px;padding:7px 15px;border-radius:9px;font-size:12px;font-weight:600;cursor:pointer;border:1px solid var(--border, #334155);background:var(--surface, #1e293b);color:var(--foreground, #f8fafc);transition:all .2s cubic-bezier(.16,1,.3,1)}.builder-governance-toggle-btn:hover:not(:disabled){border-color:var(--primary, #3b82f6);transform:translateY(-1px);box-shadow:0 4px 12px color-mix(in srgb,var(--primary, #3b82f6) 20%,transparent)}.builder-governance-toggle-btn.active{background:linear-gradient(135deg,var(--primary, #3b82f6),color-mix(in srgb,var(--primary, #3b82f6) 80%,black));border-color:var(--primary, #3b82f6);color:#fff;box-shadow:0 0 16px color-mix(in srgb,var(--primary, #3b82f6) 35%,transparent)}.builder-governance-toggle-btn:disabled{opacity:.5;cursor:not-allowed}@keyframes builderGovernancePulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.85)}}.builder-authoring-lineage-alert{display:grid;grid-template-columns:auto minmax(0,1fr);gap:10px;margin:0 0 14px;padding:12px 14px;border:1px solid var(--md-sys-color-error, #b3261e);border-radius:10px;background:var(--md-sys-color-error-container, #f9dedc);color:var(--md-sys-color-on-error-container, #410e0b)}.builder-authoring-lineage-alert strong{display:block}.builder-authoring-lineage-alert p{margin:4px 0 0}.builder-authoring-lineage-alert ul{margin:8px 0 0;padding-inline-start:20px}@media(forced-colors:active){.builder-authoring-lineage-alert{border:2px solid CanvasText;background:Canvas;color:CanvasText}}.agentic-diagnostics-panel{position:absolute;z-index:21;right:16px;width:min(520px,calc(100% - 32px));max-height:min(440px,calc(100% - 32px));overflow:auto;padding:12px;border:1px solid rgba(107,114,128,.28);border-radius:8px;background:#fffffff5;color:#111827;box-shadow:0 14px 36px #0000003d}.agentic-diagnostics-panel--collapsed{width:min(280px,calc(100% - 32px));max-height:58px;overflow:hidden;padding:10px 12px;pointer-events:auto}.agentic-diagnostics-panel__header{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:13px;font-weight:700}.agentic-diagnostics-panel__header-actions{display:flex;align-items:center;gap:8px}.agentic-diagnostics-panel__badge{padding:2px 6px;border:1px solid rgba(107,114,128,.28);border-radius:8px;font-size:11px;font-weight:600;color:#4b5563}.agentic-diagnostics-panel__toggle,.shared-rule-cockpit__toggle{appearance:none;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 18%,transparent);border-radius:999px;background:color-mix(in srgb,var(--md-sys-color-surface-container-lowest) 84%,transparent);color:var(--md-sys-color-primary);cursor:pointer;font-size:11px;font-weight:700;line-height:1;padding:6px 9px;white-space:nowrap}.agentic-diagnostics-panel__description{margin:8px 0 10px;color:#4b5563;font-size:12px;line-height:1.4}.agentic-diagnostics-panel pre{margin:0;white-space:pre-wrap;overflow-wrap:anywhere;font-size:11px;line-height:1.45;color:#1f2937}.agentic-diagnostics-panel__health{display:grid;gap:8px;margin-bottom:12px}.agentic-diagnostics-panel__health h3,.agentic-diagnostics-panel__health p{margin:0}.agentic-diagnostics-panel__health h3{font-size:13px}.agentic-diagnostics-panel__health p{color:#4b5563;font-size:11px}.agentic-diagnostics-panel__metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:0}.agentic-diagnostics-panel__metrics div{padding:8px;border-radius:8px;background:#f3f4f6}.agentic-diagnostics-panel__metrics dt{color:#4b5563;font-size:10px}.agentic-diagnostics-panel__metrics dd{margin:3px 0 0;font-size:14px;font-weight:700}.agentic-diagnostics-panel__health-table{width:100%;border-collapse:collapse;font-size:10px}.agentic-diagnostics-panel__health-table caption{margin-bottom:4px;text-align:left;font-weight:700}.agentic-diagnostics-panel__health-table th,.agentic-diagnostics-panel__health-table td{padding:5px 4px;border-bottom:1px solid #e5e7eb;text-align:left}.agentic-diagnostics-panel__raw summary{margin-bottom:8px;cursor:pointer;font-size:11px;font-weight:700}@media(max-width:520px){.agentic-diagnostics-panel__metrics{grid-template-columns:repeat(2,minmax(0,1fr))}}praxis-ai-assistant-shell{position:fixed;inset:0;z-index:140;pointer-events:none}@media(min-width:1100px){.builder-shell--agentic-review-rail{box-sizing:border-box;padding-right:464px}}.builder-shell__agentic-toggle{position:relative}.builder-shell__agentic-toggle--open,.builder-shell__agentic-toggle--minimized{outline:2px solid rgba(96,165,250,.52);outline-offset:2px}.builder-shell__agentic-toggle--minimized:after{content:\"\";position:absolute;top:6px;right:6px;width:8px;height:8px;border:2px solid var(--md-sys-color-surface, #111827);border-radius:999px;background:#34d399}.builder-shell__connections-toggle--open{outline:2px solid color-mix(in srgb,var(--md-sys-color-primary, #6750a4) 52%,transparent);outline-offset:2px}.builder-shell__connections-toggle{position:relative}.builder-shell__connections-toggle--recommended:not(.builder-shell__connections-toggle--open){outline:2px solid rgba(34,211,238,.48);outline-offset:2px}.builder-shell__connections-toggle-badge{position:absolute;top:5px;right:5px;width:9px;height:9px;border:2px solid var(--md-sys-color-surface, #111827);border-radius:999px;background:#22d3ee;box-shadow:0 0 0 3px #22d3ee2e}.shared-rule-cockpit{position:absolute;z-index:120;left:16px;bottom:88px;width:min(720px,calc(100% - 32px));max-height:min(560px,calc(100vh - 140px));padding:14px;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 24%,transparent);border-radius:12px;background:linear-gradient(135deg,color-mix(in srgb,var(--md-sys-color-primary-container) 24%,var(--md-sys-color-surface-container-high) 76%),color-mix(in srgb,var(--md-sys-color-tertiary-container) 18%,var(--md-sys-color-surface-container) 82%));color:var(--md-sys-color-on-surface);box-shadow:0 18px 46px color-mix(in srgb,var(--md-sys-color-shadow, #000) 24%,transparent);overflow:auto;overscroll-behavior:contain}.shared-rule-cockpit--collapsed{width:min(360px,calc(100% - 32px));max-height:72px;overflow:hidden;padding:10px 12px}.shared-rule-cockpit__header{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.shared-rule-cockpit__header-actions{display:flex;align-items:center;gap:8px}.shared-rule-cockpit__header div{display:grid;gap:2px}.shared-rule-cockpit__header strong{font-size:14px;line-height:1.2}.shared-rule-cockpit__header span{color:var(--md-sys-color-on-surface-variant);font-size:12px;overflow-wrap:anywhere}.shared-rule-cockpit__badge{flex:0 0 auto;max-width:220px;padding:3px 8px;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 20%,transparent);border-radius:999px;background:color-mix(in srgb,var(--md-sys-color-surface-container-lowest) 74%,transparent);color:var(--md-sys-color-primary);font-size:11px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.shared-rule-cockpit__description,.shared-rule-cockpit__status,.shared-rule-cockpit__error{margin:10px 0 0;color:var(--md-sys-color-on-surface-variant);font-size:12px;line-height:1.45}.shared-rule-cockpit__error{color:var(--md-sys-color-error);font-weight:600}.shared-rule-cockpit__actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}.shared-rule-cockpit__facts{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin:12px 0 0}.shared-rule-cockpit__facts div{min-width:0;padding:8px;border-radius:10px;background:color-mix(in srgb,var(--md-sys-color-surface-container-lowest) 72%,transparent)}.shared-rule-cockpit__facts dt{color:var(--md-sys-color-on-surface-variant);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.04em}.shared-rule-cockpit__facts dd{margin:4px 0 0;color:var(--md-sys-color-on-surface);font-size:12px;overflow-wrap:anywhere}.shared-rule-cockpit__enforcement-matrix{display:grid;gap:10px;margin-top:12px;padding:10px;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 14%,transparent);border-radius:12px;background:color-mix(in srgb,var(--md-sys-color-surface-container-lowest) 58%,transparent)}.shared-rule-cockpit__matrix-header{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}.shared-rule-cockpit__matrix-header div{display:grid;gap:2px}.shared-rule-cockpit__matrix-header strong{color:var(--md-sys-color-on-surface);font-size:12px}.shared-rule-cockpit__matrix-header span,.shared-rule-cockpit__matrix-source{color:var(--md-sys-color-on-surface-variant);font-size:11px;line-height:1.35;overflow-wrap:anywhere}.shared-rule-cockpit__matrix-badge{flex:0 0 auto;padding:3px 8px;border-radius:999px;background:#16a34a1f;color:#166534;font-weight:800;white-space:nowrap}.shared-rule-cockpit__projection-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.shared-rule-cockpit__projection{display:grid;gap:3px;min-width:0;padding:9px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 52%,transparent);border-radius:11px;background:color-mix(in srgb,var(--md-sys-color-surface-container-low) 82%,transparent)}.shared-rule-cockpit__projection[data-status=ready]{border-color:color-mix(in srgb,var(--md-sys-color-tertiary) 24%,transparent);background:color-mix(in srgb,var(--md-sys-color-tertiary-container) 32%,var(--md-sys-color-surface-container-low) 68%)}.shared-rule-cockpit__projection span{color:var(--md-sys-color-primary);font-size:10px;font-weight:800;letter-spacing:.04em;text-transform:uppercase;overflow-wrap:anywhere}.shared-rule-cockpit__projection strong{color:var(--md-sys-color-on-surface);font-size:12px}.shared-rule-cockpit__projection small,.shared-rule-cockpit__projection em{color:var(--md-sys-color-on-surface-variant);font-size:11px;font-style:normal;overflow-wrap:anywhere}.shared-rule-cockpit__matrix-source{margin:0}.shared-rule-cockpit__timeline{display:grid;gap:8px;max-height:190px;margin:12px 0 0;padding:0;overflow:auto;list-style:none}.shared-rule-cockpit__timeline-item{display:grid;grid-template-columns:auto minmax(0,1fr);gap:10px;padding:10px;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 12%,transparent);border-radius:12px;background:color-mix(in srgb,var(--md-sys-color-surface-container-lowest) 76%,transparent)}.shared-rule-cockpit__timeline-icon{display:grid;width:24px;height:24px;place-items:center;border-radius:999px;background:#1e40af1a;color:var(--md-sys-color-primary);font-family:Material Icons;font-size:16px;line-height:1}.shared-rule-cockpit__timeline-item strong{color:var(--md-sys-color-on-surface);font-size:12px;line-height:1.35}.shared-rule-cockpit__timeline-item p,.shared-rule-cockpit__timeline-item small,.shared-rule-cockpit__timeline-item time,.shared-rule-cockpit__timeline-empty{display:block;margin:3px 0 0;color:var(--md-sys-color-on-surface-variant);font-size:11px;line-height:1.35;overflow-wrap:anywhere}.shared-rule-cockpit__timeline-badge{display:inline-flex;margin-left:6px;padding:2px 6px;border-radius:999px;background:color-mix(in srgb,var(--md-sys-color-secondary-container) 42%,transparent);color:var(--md-sys-color-on-secondary-container);font-size:10px;font-weight:700}.shared-rule-cockpit__timeline-empty{padding:10px;border-radius:12px;background:color-mix(in srgb,var(--md-sys-color-surface-container-lowest) 64%,transparent)}@media(max-width:720px){.shared-rule-cockpit{bottom:72px;max-height:calc(100vh - 112px)}.shared-rule-cockpit__facts,.shared-rule-cockpit__projection-grid{grid-template-columns:1fr}}\n"] }]
35924
+ `, styles: [":host{display:block;position:relative;min-height:var(--pdx-page-builder-min-height, 420px)}.builder-shell{display:block;position:relative;min-height:inherit;height:100%;box-sizing:border-box}.builder-shell--bottom-toolbar{padding-block-end:calc(80px + env(safe-area-inset-bottom,0px))}.builder-shell:has(.pdx-shell.fullscreen) praxis-floating-toolbar{display:none}.builder-governance-toolbar{display:flex;justify-content:space-between;align-items:center;padding:10px 18px;margin-bottom:16px;border-radius:14px;background:var(--surface-gradient, color-mix(in srgb, var(--md-sys-color-surface-container, #1e293b) 95%, transparent));border:1px solid var(--border, color-mix(in srgb, var(--md-sys-color-outline-variant, #334155) 60%, transparent));gap:16px;flex-wrap:wrap}.builder-governance-brand{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.builder-governance-status-indicator{display:inline-flex;align-items:center;gap:8px;padding:3px 10px;border-radius:9999px;background:color-mix(in srgb,var(--ready, #22c55e) 12%,transparent);border:1px solid color-mix(in srgb,var(--ready, #22c55e) 28%,transparent)}.builder-governance-dot-pulse{width:7px;height:7px;border-radius:50%;background:var(--ready, #22c55e);box-shadow:0 0 8px var(--ready, #22c55e);animation:builderGovernancePulse 2s infinite cubic-bezier(.4,0,.6,1)}.builder-governance-title{font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--ready, #22c55e)}.builder-governance-badge{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:8px;background:color-mix(in srgb,var(--primary, #3b82f6) 12%,transparent);border:1px solid color-mix(in srgb,var(--primary, #3b82f6) 24%,transparent);font-size:11px;font-weight:600;color:var(--primary, #3b82f6)}.builder-governance-badge .material-symbols-outlined{font-size:14px}.builder-governance-persistence{display:inline-flex;align-items:center;gap:6px;padding:3px 10px;border-radius:9999px;font-size:11px;font-weight:600;letter-spacing:.02em;border:1px solid transparent;transition:all .2s ease}.builder-governance-persistence.governed{background:color-mix(in srgb,var(--muted, #64748b) 15%,transparent);border-color:color-mix(in srgb,var(--border, #334155) 80%,transparent);color:var(--muted-foreground, #94a3b8)}.builder-governance-persistence.customized{background:color-mix(in srgb,var(--primary, #3b82f6) 14%,transparent);border-color:color-mix(in srgb,var(--primary, #3b82f6) 35%,transparent);color:var(--primary, #3b82f6)}.builder-governance-persistence .builder-governance-saved-time{opacity:.75;font-weight:400}.builder-governance-actions{display:flex;align-items:center;gap:10px;margin-left:auto}.builder-governance-btn{display:inline-flex;align-items:center;gap:6px;padding:7px 13px;border-radius:9px;font-size:12px;font-weight:600;cursor:pointer;border:1px solid var(--border, #334155);background:var(--surface, #1e293b);color:var(--foreground, #f8fafc);transition:all .15s ease}.builder-governance-btn:hover:not(:disabled){border-color:var(--primary, #3b82f6);background:color-mix(in srgb,var(--primary, #3b82f6) 10%,var(--surface, #1e293b))}.builder-governance-btn:disabled{opacity:.5;cursor:not-allowed}.builder-governance-btn--reset{border-color:color-mix(in srgb,var(--warning, #eab308) 40%,transparent);color:var(--warning, #eab308)}.builder-governance-btn--reset:hover:not(:disabled){border-color:var(--warning, #eab308);background:color-mix(in srgb,var(--warning, #eab308) 12%,transparent)}.builder-governance-btn .material-symbols-outlined,.builder-governance-toggle-btn .material-symbols-outlined{font-size:16px}.builder-governance-toggle-btn{display:inline-flex;align-items:center;gap:7px;padding:7px 15px;border-radius:9px;font-size:12px;font-weight:600;cursor:pointer;border:1px solid var(--border, #334155);background:var(--surface, #1e293b);color:var(--foreground, #f8fafc);transition:all .2s cubic-bezier(.16,1,.3,1)}.builder-governance-toggle-btn:hover:not(:disabled){border-color:var(--primary, #3b82f6);transform:translateY(-1px);box-shadow:0 4px 12px color-mix(in srgb,var(--primary, #3b82f6) 20%,transparent)}.builder-governance-toggle-btn.active{background:linear-gradient(135deg,var(--primary, #3b82f6),color-mix(in srgb,var(--primary, #3b82f6) 80%,black));border-color:var(--primary, #3b82f6);color:#fff;box-shadow:0 0 16px color-mix(in srgb,var(--primary, #3b82f6) 35%,transparent)}.builder-governance-toggle-btn:disabled{opacity:.5;cursor:not-allowed}@keyframes builderGovernancePulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.85)}}.builder-authoring-lineage-alert{display:grid;grid-template-columns:auto minmax(0,1fr);gap:10px;margin:0 0 14px;padding:12px 14px;border:1px solid var(--md-sys-color-error, #b3261e);border-radius:10px;background:var(--md-sys-color-error-container, #f9dedc);color:var(--md-sys-color-on-error-container, #410e0b)}.builder-authoring-lineage-alert strong{display:block}.builder-authoring-lineage-alert p{margin:4px 0 0}.builder-authoring-lineage-alert ul{margin:8px 0 0;padding-inline-start:20px}@media(forced-colors:active){.builder-authoring-lineage-alert{border:2px solid CanvasText;background:Canvas;color:CanvasText}}.agentic-diagnostics-panel{position:absolute;z-index:21;right:16px;width:min(520px,calc(100% - 32px));max-height:min(440px,calc(100% - 32px));overflow:auto;padding:12px;border:1px solid rgba(107,114,128,.28);border-radius:8px;background:#fffffff5;color:#111827;box-shadow:0 14px 36px #0000003d}.agentic-diagnostics-panel--collapsed{width:min(280px,calc(100% - 32px));max-height:58px;overflow:hidden;padding:10px 12px;pointer-events:auto}.agentic-diagnostics-panel__header{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:13px;font-weight:700}.agentic-diagnostics-panel__header-actions{display:flex;align-items:center;gap:8px}.agentic-diagnostics-panel__badge{padding:2px 6px;border:1px solid rgba(107,114,128,.28);border-radius:8px;font-size:11px;font-weight:600;color:#4b5563}.agentic-diagnostics-panel__toggle,.shared-rule-cockpit__toggle{appearance:none;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 18%,transparent);border-radius:999px;background:color-mix(in srgb,var(--md-sys-color-surface-container-lowest) 84%,transparent);color:var(--md-sys-color-primary);cursor:pointer;font-size:11px;font-weight:700;line-height:1;padding:6px 9px;white-space:nowrap}.agentic-diagnostics-panel__description{margin:8px 0 10px;color:#4b5563;font-size:12px;line-height:1.4}.agentic-diagnostics-panel pre{margin:0;white-space:pre-wrap;overflow-wrap:anywhere;font-size:11px;line-height:1.45;color:#1f2937}.agentic-diagnostics-panel__health{display:grid;gap:8px;margin-bottom:12px}.agentic-diagnostics-panel__health h3,.agentic-diagnostics-panel__health p{margin:0}.agentic-diagnostics-panel__health h3{font-size:13px}.agentic-diagnostics-panel__health p{color:#4b5563;font-size:11px}.agentic-diagnostics-panel__metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:0}.agentic-diagnostics-panel__metrics div{padding:8px;border-radius:8px;background:#f3f4f6}.agentic-diagnostics-panel__metrics dt{color:#4b5563;font-size:10px}.agentic-diagnostics-panel__metrics dd{margin:3px 0 0;font-size:14px;font-weight:700}.agentic-diagnostics-panel__health-table{width:100%;border-collapse:collapse;font-size:10px}.agentic-diagnostics-panel__health-table caption{margin-bottom:4px;text-align:left;font-weight:700}.agentic-diagnostics-panel__health-table th,.agentic-diagnostics-panel__health-table td{padding:5px 4px;border-bottom:1px solid #e5e7eb;text-align:left}.agentic-diagnostics-panel__raw summary{margin-bottom:8px;cursor:pointer;font-size:11px;font-weight:700}@media(max-width:520px){.agentic-diagnostics-panel__metrics{grid-template-columns:repeat(2,minmax(0,1fr))}}praxis-ai-assistant-shell{position:fixed;inset:0;z-index:140;pointer-events:none}@media(min-width:1100px){.builder-shell--agentic-review-rail{box-sizing:border-box;padding-right:464px}}.builder-shell__agentic-toggle{position:relative}.builder-shell__agentic-toggle--open,.builder-shell__agentic-toggle--minimized{outline:2px solid rgba(96,165,250,.52);outline-offset:2px}.builder-shell__agentic-toggle--minimized:after{content:\"\";position:absolute;top:6px;right:6px;width:8px;height:8px;border:2px solid var(--md-sys-color-surface, #111827);border-radius:999px;background:#34d399}.builder-shell__connections-toggle--open{outline:2px solid color-mix(in srgb,var(--md-sys-color-primary, #6750a4) 52%,transparent);outline-offset:2px}.builder-shell__connections-toggle{position:relative}.builder-shell__connections-toggle--recommended:not(.builder-shell__connections-toggle--open){outline:2px solid rgba(34,211,238,.48);outline-offset:2px}.builder-shell__connections-toggle-badge{position:absolute;top:5px;right:5px;width:9px;height:9px;border:2px solid var(--md-sys-color-surface, #111827);border-radius:999px;background:#22d3ee;box-shadow:0 0 0 3px #22d3ee2e}.shared-rule-cockpit{position:absolute;z-index:120;left:16px;bottom:88px;width:min(720px,calc(100% - 32px));max-height:min(560px,calc(100vh - 140px));padding:14px;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 24%,transparent);border-radius:12px;background:linear-gradient(135deg,color-mix(in srgb,var(--md-sys-color-primary-container) 24%,var(--md-sys-color-surface-container-high) 76%),color-mix(in srgb,var(--md-sys-color-tertiary-container) 18%,var(--md-sys-color-surface-container) 82%));color:var(--md-sys-color-on-surface);box-shadow:0 18px 46px color-mix(in srgb,var(--md-sys-color-shadow, #000) 24%,transparent);overflow:auto;overscroll-behavior:contain}.shared-rule-cockpit--collapsed{width:min(360px,calc(100% - 32px));max-height:72px;overflow:hidden;padding:10px 12px}.shared-rule-cockpit__header{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.shared-rule-cockpit__header-actions{display:flex;align-items:center;gap:8px}.shared-rule-cockpit__header div{display:grid;gap:2px}.shared-rule-cockpit__header strong{font-size:14px;line-height:1.2}.shared-rule-cockpit__header span{color:var(--md-sys-color-on-surface-variant);font-size:12px;overflow-wrap:anywhere}.shared-rule-cockpit__badge{flex:0 0 auto;max-width:220px;padding:3px 8px;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 20%,transparent);border-radius:999px;background:color-mix(in srgb,var(--md-sys-color-surface-container-lowest) 74%,transparent);color:var(--md-sys-color-primary);font-size:11px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.shared-rule-cockpit__description,.shared-rule-cockpit__status,.shared-rule-cockpit__error{margin:10px 0 0;color:var(--md-sys-color-on-surface-variant);font-size:12px;line-height:1.45}.shared-rule-cockpit__status,.builder-governance-persistence.busy>span:first-child{font-weight:550;color:var(--pdx-text-shimmer-base, var(--md-sys-color-on-surface-variant))}@supports ((-webkit-background-clip: text) or (background-clip: text)){.shared-rule-cockpit__status,.builder-governance-persistence.busy>span:first-child{background-image:var( --pdx-text-shimmer-gradient, linear-gradient( 105deg, color-mix(in oklab, var(--md-sys-color-on-surface-variant, #64748b) 82%, var(--md-sys-color-on-surface, #0f172a) 18%) 0%, color-mix(in oklab, var(--md-sys-color-on-surface-variant, #64748b) 82%, var(--md-sys-color-on-surface, #0f172a) 18%) 26%, color-mix(in oklab, var(--md-sys-color-tertiary, #6366f1) 74%, #a855f7 26%) 40%, var(--pdx-text-shimmer-peak, var(--md-sys-color-on-surface, #ffffff)) 50%, color-mix(in oklab, var(--md-sys-color-primary, #006a6a) 74%, #ec4899 26%) 60%, color-mix(in oklab, var(--md-sys-color-on-surface-variant, #64748b) 82%, var(--md-sys-color-on-surface, #0f172a) 18%) 74%, color-mix(in oklab, var(--md-sys-color-on-surface-variant, #64748b) 82%, var(--md-sys-color-on-surface, #0f172a) 18%) 100% ) );background-size:300% 100%;background-position:100% 50%;background-repeat:no-repeat;-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent;animation:sharedRuleCockpitTextShimmer var(--pdx-text-shimmer-duration, 2.6s) linear infinite;will-change:background-position}}@media(prefers-reduced-motion:reduce),(forced-colors:active),print{.shared-rule-cockpit__status,.builder-governance-persistence.busy>span:first-child{animation:none!important;background-image:none!important;-webkit-text-fill-color:currentColor!important;color:var(--md-sys-color-primary)!important;will-change:auto}}@media(forced-colors:active){.shared-rule-cockpit__status,.builder-governance-persistence.busy>span:first-child{color:CanvasText!important;-webkit-text-fill-color:CanvasText!important}}@keyframes sharedRuleCockpitTextShimmer{0%{background-position:100% 50%}to{background-position:0% 50%}}.shared-rule-cockpit__error{color:var(--md-sys-color-error);font-weight:600}.shared-rule-cockpit__actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}.shared-rule-cockpit__facts{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin:12px 0 0}.shared-rule-cockpit__facts div{min-width:0;padding:8px;border-radius:10px;background:color-mix(in srgb,var(--md-sys-color-surface-container-lowest) 72%,transparent)}.shared-rule-cockpit__facts dt{color:var(--md-sys-color-on-surface-variant);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.04em}.shared-rule-cockpit__facts dd{margin:4px 0 0;color:var(--md-sys-color-on-surface);font-size:12px;overflow-wrap:anywhere}.shared-rule-cockpit__enforcement-matrix{display:grid;gap:10px;margin-top:12px;padding:10px;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 14%,transparent);border-radius:12px;background:color-mix(in srgb,var(--md-sys-color-surface-container-lowest) 58%,transparent)}.shared-rule-cockpit__matrix-header{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}.shared-rule-cockpit__matrix-header div{display:grid;gap:2px}.shared-rule-cockpit__matrix-header strong{color:var(--md-sys-color-on-surface);font-size:12px}.shared-rule-cockpit__matrix-header span,.shared-rule-cockpit__matrix-source{color:var(--md-sys-color-on-surface-variant);font-size:11px;line-height:1.35;overflow-wrap:anywhere}.shared-rule-cockpit__matrix-badge{flex:0 0 auto;padding:3px 8px;border-radius:999px;background:#16a34a1f;color:#166534;font-weight:800;white-space:nowrap}.shared-rule-cockpit__projection-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.shared-rule-cockpit__projection{display:grid;gap:3px;min-width:0;padding:9px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 52%,transparent);border-radius:11px;background:color-mix(in srgb,var(--md-sys-color-surface-container-low) 82%,transparent)}.shared-rule-cockpit__projection[data-status=ready]{border-color:color-mix(in srgb,var(--md-sys-color-tertiary) 24%,transparent);background:color-mix(in srgb,var(--md-sys-color-tertiary-container) 32%,var(--md-sys-color-surface-container-low) 68%)}.shared-rule-cockpit__projection span{color:var(--md-sys-color-primary);font-size:10px;font-weight:800;letter-spacing:.04em;text-transform:uppercase;overflow-wrap:anywhere}.shared-rule-cockpit__projection strong{color:var(--md-sys-color-on-surface);font-size:12px}.shared-rule-cockpit__projection small,.shared-rule-cockpit__projection em{color:var(--md-sys-color-on-surface-variant);font-size:11px;font-style:normal;overflow-wrap:anywhere}.shared-rule-cockpit__matrix-source{margin:0}.shared-rule-cockpit__timeline{display:grid;gap:8px;max-height:190px;margin:12px 0 0;padding:0;overflow:auto;list-style:none}.shared-rule-cockpit__timeline-item{display:grid;grid-template-columns:auto minmax(0,1fr);gap:10px;padding:10px;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 12%,transparent);border-radius:12px;background:color-mix(in srgb,var(--md-sys-color-surface-container-lowest) 76%,transparent)}.shared-rule-cockpit__timeline-icon{display:grid;width:24px;height:24px;place-items:center;border-radius:999px;background:#1e40af1a;color:var(--md-sys-color-primary);font-family:Material Icons;font-size:16px;line-height:1}.shared-rule-cockpit__timeline-item strong{color:var(--md-sys-color-on-surface);font-size:12px;line-height:1.35}.shared-rule-cockpit__timeline-item p,.shared-rule-cockpit__timeline-item small,.shared-rule-cockpit__timeline-item time,.shared-rule-cockpit__timeline-empty{display:block;margin:3px 0 0;color:var(--md-sys-color-on-surface-variant);font-size:11px;line-height:1.35;overflow-wrap:anywhere}.shared-rule-cockpit__timeline-badge{display:inline-flex;margin-left:6px;padding:2px 6px;border-radius:999px;background:color-mix(in srgb,var(--md-sys-color-secondary-container) 42%,transparent);color:var(--md-sys-color-on-secondary-container);font-size:10px;font-weight:700}.shared-rule-cockpit__timeline-empty{padding:10px;border-radius:12px;background:color-mix(in srgb,var(--md-sys-color-surface-container-lowest) 64%,transparent)}@media(max-width:720px){.shared-rule-cockpit{bottom:72px;max-height:calc(100vh - 112px)}.shared-rule-cockpit__facts,.shared-rule-cockpit__projection-grid{grid-template-columns:1fr}}\n"] }]
35810
35925
  }], ctorParameters: () => [{ type: i1.MatDialog }, { type: undefined, decorators: [{
35811
35926
  type: Optional
35812
35927
  }, {
@@ -36004,6 +36119,2009 @@ function providePraxisPageBuilderWidgets(widgets) {
36004
36119
  return providers;
36005
36120
  }
36006
36121
 
36122
+ /** Reusable save-as journey. Feed save() only from the builder's pageSaveRequested event. */
36123
+ class PraxisPageSaveAsComponent {
36124
+ adapter = input.required(...(ngDevMode ? [{ debugName: "adapter" }] : /* istanbul ignore next */ []));
36125
+ /** Monotonic host view/session identity; UI invalidation only, never an authorization grant. */
36126
+ contextKey = input.required(...(ngDevMode ? [{ debugName: "contextKey" }] : /* istanbul ignore next */ []));
36127
+ canSave = input(false, ...(ngDevMode ? [{ debugName: "canSave" }] : /* istanbul ignore next */ []));
36128
+ initialTitle = input('', ...(ngDevMode ? [{ debugName: "initialTitle" }] : /* istanbul ignore next */ []));
36129
+ initialMenuLabel = input('', ...(ngDevMode ? [{ debugName: "initialMenuLabel" }] : /* istanbul ignore next */ []));
36130
+ initialIcon = input('web', ...(ngDevMode ? [{ debugName: "initialIcon" }] : /* istanbul ignore next */ []));
36131
+ titleMaxLength = input(160, ...(ngDevMode ? [{ debugName: "titleMaxLength" }] : /* istanbul ignore next */ []));
36132
+ menuMaxLength = input(160, ...(ngDevMode ? [{ debugName: "menuMaxLength" }] : /* istanbul ignore next */ []));
36133
+ saveRequested = output();
36134
+ saved = output();
36135
+ saveFailed = output();
36136
+ busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
36137
+ choosingIcon = signal(false, ...(ngDevMode ? [{ debugName: "choosingIcon" }] : /* istanbul ignore next */ []));
36138
+ result = signal(null, ...(ngDevMode ? [{ debugName: "result" }] : /* istanbul ignore next */ []));
36139
+ error = signal(false, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
36140
+ iconError = signal(false, ...(ngDevMode ? [{ debugName: "iconError" }] : /* istanbul ignore next */ []));
36141
+ title = new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.pattern(/\S/), Validators.maxLength(160)] });
36142
+ menu = new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.pattern(/\S/), Validators.maxLength(160)] });
36143
+ icon = signal('web', ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
36144
+ i18n = inject(PraxisI18nService);
36145
+ picker = inject(IconPickerService);
36146
+ generation = 0;
36147
+ attempt;
36148
+ destroyed = false;
36149
+ constructor() {
36150
+ effect(() => {
36151
+ this.contextKey();
36152
+ this.adapter();
36153
+ const title = this.initialTitle(), menu = this.initialMenuLabel(), icon = this.initialIcon();
36154
+ ++this.generation;
36155
+ this.attempt = undefined;
36156
+ this.busy.set(false);
36157
+ this.choosingIcon.set(false);
36158
+ this.result.set(null);
36159
+ this.error.set(false);
36160
+ this.iconError.set(false);
36161
+ this.title.setValidators([Validators.required, Validators.pattern(/\S/), Validators.maxLength(this.limit(this.titleMaxLength()))]);
36162
+ this.menu.setValidators([Validators.required, Validators.pattern(/\S/), Validators.maxLength(this.limit(this.menuMaxLength()))]);
36163
+ this.title.setValue(title);
36164
+ this.menu.setValue(menu);
36165
+ this.icon.set(icon);
36166
+ });
36167
+ inject(DestroyRef).onDestroy(() => { this.destroyed = true; ++this.generation; });
36168
+ }
36169
+ text(key) { return resolvePraxisPageBuilderText(this.i18n, `saveAs.${key}`, key); }
36170
+ destination() { return { label: this.menu.value.trim(), icon: this.icon() }; }
36171
+ clearResult() { this.result.set(null); }
36172
+ requestSave() {
36173
+ this.title.markAsTouched();
36174
+ this.menu.markAsTouched();
36175
+ if (this.available())
36176
+ this.saveRequested.emit();
36177
+ }
36178
+ async chooseIcon() {
36179
+ if (this.busy() || this.choosingIcon())
36180
+ return;
36181
+ const generation = this.generation;
36182
+ const context = this.contextKey(), adapter = this.adapter();
36183
+ this.choosingIcon.set(true);
36184
+ try {
36185
+ const selected = await this.picker.openDialog({ value: this.icon() });
36186
+ if (!this.current(generation, context, adapter) || selected === undefined)
36187
+ return;
36188
+ // The menu profile accepts Material names, not URLs or family-prefixed tokens.
36189
+ if (!this.validIcon(selected)) {
36190
+ this.iconError.set(true);
36191
+ return;
36192
+ }
36193
+ this.iconError.set(false);
36194
+ this.icon.set(selected);
36195
+ }
36196
+ catch {
36197
+ if (this.current(generation, context, adapter))
36198
+ this.iconError.set(true);
36199
+ }
36200
+ finally {
36201
+ if (this.current(generation, context, adapter))
36202
+ this.choosingIcon.set(false);
36203
+ }
36204
+ }
36205
+ async save(definition) {
36206
+ if (!this.available())
36207
+ return;
36208
+ const adapter = this.adapter(), context = this.contextKey(), generation = this.generation;
36209
+ this.busy.set(true);
36210
+ this.error.set(false);
36211
+ try {
36212
+ const details = { definition: structuredClone(definition), presentation: { title: this.title.value.trim(), icon: this.icon() }, menuLabel: this.menu.value.trim() };
36213
+ const signature = JSON.stringify(details);
36214
+ if (this.attempt?.signature !== signature) {
36215
+ this.attempt = { signature, request: { creationKey: crypto.randomUUID(), ...details } };
36216
+ }
36217
+ // An adapter cannot mutate the retained retry snapshot or the source document.
36218
+ const receipt = await adapter.save(structuredClone(this.attempt.request));
36219
+ if (!this.current(generation, context, adapter))
36220
+ return;
36221
+ if (!receipt || typeof receipt.pageId !== 'string' || !receipt.pageId.trim() || !this.localRoute(receipt.routePath))
36222
+ throw new Error('Invalid save-as receipt');
36223
+ const result = { pageId: receipt.pageId, routePath: receipt.routePath };
36224
+ this.result.set(result);
36225
+ this.saved.emit(result);
36226
+ }
36227
+ catch (error) {
36228
+ if (this.current(generation, context, adapter)) {
36229
+ this.error.set(true);
36230
+ this.saveFailed.emit(error);
36231
+ }
36232
+ }
36233
+ finally {
36234
+ if (this.current(generation, context, adapter))
36235
+ this.busy.set(false);
36236
+ }
36237
+ }
36238
+ available() {
36239
+ return !this.destroyed && !this.busy() && !this.choosingIcon() && !!this.contextKey().trim() && this.canSave()
36240
+ && this.title.valid && this.menu.valid && !!this.title.value.trim() && !!this.menu.value.trim() && this.validIcon(this.icon());
36241
+ }
36242
+ current(generation, context, adapter) {
36243
+ return !this.destroyed && generation === this.generation && context === this.contextKey() && adapter === this.adapter();
36244
+ }
36245
+ limit(value) { return Number.isInteger(value) && value > 0 && value <= 160 ? value : 0; }
36246
+ validIcon(value) { return /^[a-z][a-z0-9_]{0,63}$/.test(value); }
36247
+ localRoute(value) {
36248
+ if (typeof value !== 'string' || !value.startsWith('/') || value.startsWith('//') || /[\\\s\u0000-\u001f\u007f]/.test(value))
36249
+ return false;
36250
+ try {
36251
+ return new URL(value, 'https://praxis.invalid').origin === 'https://praxis.invalid' && !/%(?:2f|5c|0[0-9a-f]|1[0-9a-f]|7f)/i.test(value);
36252
+ }
36253
+ catch {
36254
+ return false;
36255
+ }
36256
+ }
36257
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisPageSaveAsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
36258
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisPageSaveAsComponent, isStandalone: true, selector: "praxis-page-save-as", inputs: { adapter: { classPropertyName: "adapter", publicName: "adapter", isSignal: true, isRequired: true, transformFunction: null }, contextKey: { classPropertyName: "contextKey", publicName: "contextKey", isSignal: true, isRequired: true, transformFunction: null }, canSave: { classPropertyName: "canSave", publicName: "canSave", isSignal: true, isRequired: false, transformFunction: null }, initialTitle: { classPropertyName: "initialTitle", publicName: "initialTitle", isSignal: true, isRequired: false, transformFunction: null }, initialMenuLabel: { classPropertyName: "initialMenuLabel", publicName: "initialMenuLabel", isSignal: true, isRequired: false, transformFunction: null }, initialIcon: { classPropertyName: "initialIcon", publicName: "initialIcon", isSignal: true, isRequired: false, transformFunction: null }, titleMaxLength: { classPropertyName: "titleMaxLength", publicName: "titleMaxLength", isSignal: true, isRequired: false, transformFunction: null }, menuMaxLength: { classPropertyName: "menuMaxLength", publicName: "menuMaxLength", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { saveRequested: "saveRequested", saved: "saved", saveFailed: "saveFailed" }, providers: [providePraxisPageBuilderI18n()], ngImport: i0, template: "<div class=\"save-as\" [attr.aria-busy]=\"busy() || choosingIcon()\">\n <mat-form-field appearance=\"outline\"><mat-label>{{ text('title') }}</mat-label>\n <input matInput data-testid=\"page-save-as-title\" [formControl]=\"title\" [maxlength]=\"titleMaxLength()\" />\n <mat-error>{{ text('invalidName') }}</mat-error>\n </mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>{{ text('menu') }}</mat-label>\n <input matInput data-testid=\"page-save-as-menu\" [formControl]=\"menu\" [maxlength]=\"menuMaxLength()\" />\n <mat-hint>{{ text('menuHint') }}</mat-hint>\n <mat-error>{{ text('invalidName') }}</mat-error>\n </mat-form-field>\n <button mat-stroked-button type=\"button\" data-testid=\"page-save-as-icon\" [attr.data-icon]=\"icon()\" [disabled]=\"busy() || choosingIcon()\" (click)=\"chooseIcon()\"><mat-icon aria-hidden=\"true\">{{ icon() }}</mat-icon>{{ text('icon') }}</button>\n <button mat-flat-button type=\"button\" data-testid=\"page-save-as-submit\" [disabled]=\"busy() || choosingIcon() || !canSave() || title.invalid || menu.invalid\" (click)=\"requestSave()\">{{ text(busy() ? 'saving' : 'save') }}</button>\n</div>\n@if (iconError()) { <p role=\"alert\">{{ text('invalidIcon') }}</p> }\n@if (error()) { <p role=\"alert\">{{ text('failed') }}</p> }\n@if (result(); as confirmed) {\n <div class=\"save-result\" role=\"status\"><span>{{ text('saved') }}</span><a mat-button data-testid=\"page-save-as-link\" [routerLink]=\"confirmed.routePath\">{{ text('open') }}</a><code>{{ confirmed.routePath }}</code></div>\n}\n", styles: [":host{display:block;min-width:0}.save-as{display:flex;flex-wrap:wrap;align-items:start;gap:16px}mat-form-field{flex:1 1 180px;min-width:0}button{margin-top:7px;min-height:44px}p[role=alert]{color:var(--mat-sys-error)}.save-result{display:flex;flex-wrap:wrap;align-items:center;gap:12px;color:var(--mat-sys-primary)}code{overflow-wrap:anywhere;font-size:12px}@media(max-width:700px){.save-as{display:grid;grid-template-columns:minmax(0,1fr);gap:6px}button{margin-block:4px 12px}}\n"], dependencies: [{ 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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i1$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: 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.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i3$1.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i4$1.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: i2$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatDialogModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
36259
+ }
36260
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisPageSaveAsComponent, decorators: [{
36261
+ type: Component,
36262
+ args: [{ selector: 'praxis-page-save-as', imports: [ReactiveFormsModule, RouterLink, MatButtonModule, MatFormFieldModule, MatInputModule, MatIconModule, MatDialogModule], providers: [providePraxisPageBuilderI18n()], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"save-as\" [attr.aria-busy]=\"busy() || choosingIcon()\">\n <mat-form-field appearance=\"outline\"><mat-label>{{ text('title') }}</mat-label>\n <input matInput data-testid=\"page-save-as-title\" [formControl]=\"title\" [maxlength]=\"titleMaxLength()\" />\n <mat-error>{{ text('invalidName') }}</mat-error>\n </mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>{{ text('menu') }}</mat-label>\n <input matInput data-testid=\"page-save-as-menu\" [formControl]=\"menu\" [maxlength]=\"menuMaxLength()\" />\n <mat-hint>{{ text('menuHint') }}</mat-hint>\n <mat-error>{{ text('invalidName') }}</mat-error>\n </mat-form-field>\n <button mat-stroked-button type=\"button\" data-testid=\"page-save-as-icon\" [attr.data-icon]=\"icon()\" [disabled]=\"busy() || choosingIcon()\" (click)=\"chooseIcon()\"><mat-icon aria-hidden=\"true\">{{ icon() }}</mat-icon>{{ text('icon') }}</button>\n <button mat-flat-button type=\"button\" data-testid=\"page-save-as-submit\" [disabled]=\"busy() || choosingIcon() || !canSave() || title.invalid || menu.invalid\" (click)=\"requestSave()\">{{ text(busy() ? 'saving' : 'save') }}</button>\n</div>\n@if (iconError()) { <p role=\"alert\">{{ text('invalidIcon') }}</p> }\n@if (error()) { <p role=\"alert\">{{ text('failed') }}</p> }\n@if (result(); as confirmed) {\n <div class=\"save-result\" role=\"status\"><span>{{ text('saved') }}</span><a mat-button data-testid=\"page-save-as-link\" [routerLink]=\"confirmed.routePath\">{{ text('open') }}</a><code>{{ confirmed.routePath }}</code></div>\n}\n", styles: [":host{display:block;min-width:0}.save-as{display:flex;flex-wrap:wrap;align-items:start;gap:16px}mat-form-field{flex:1 1 180px;min-width:0}button{margin-top:7px;min-height:44px}p[role=alert]{color:var(--mat-sys-error)}.save-result{display:flex;flex-wrap:wrap;align-items:center;gap:12px;color:var(--mat-sys-primary)}code{overflow-wrap:anywhere;font-size:12px}@media(max-width:700px){.save-as{display:grid;grid-template-columns:minmax(0,1fr);gap:6px}button{margin-block:4px 12px}}\n"] }]
36263
+ }], ctorParameters: () => [], propDecorators: { adapter: [{ type: i0.Input, args: [{ isSignal: true, alias: "adapter", required: true }] }], contextKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "contextKey", required: true }] }], canSave: [{ type: i0.Input, args: [{ isSignal: true, alias: "canSave", required: false }] }], initialTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialTitle", required: false }] }], initialMenuLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialMenuLabel", required: false }] }], initialIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialIcon", required: false }] }], titleMaxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleMaxLength", required: false }] }], menuMaxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuMaxLength", required: false }] }], saveRequested: [{ type: i0.Output, args: ["saveRequested"] }], saved: [{ type: i0.Output, args: ["saved"] }], saveFailed: [{ type: i0.Output, args: ["saveFailed"] }] } });
36264
+
36265
+ /** Copies the current local authoring snapshot. Allocation/provenance belongs to the destination adapter. */
36266
+ class PraxisPageCopyActionComponent {
36267
+ page = input(...(ngDevMode ? [undefined, { debugName: "page" }] : /* istanbul ignore next */ []));
36268
+ canCopy = input(false, ...(ngDevMode ? [{ debugName: "canCopy" }] : /* istanbul ignore next */ []));
36269
+ copyRequested = output();
36270
+ error = signal(false, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
36271
+ i18n = inject(PraxisI18nService);
36272
+ text(key) { return resolvePraxisPageBuilderText(this.i18n, `saveAs.${key}`, key); }
36273
+ copy() {
36274
+ const page = this.page();
36275
+ if (!this.canCopy() || !page)
36276
+ return;
36277
+ this.error.set(false);
36278
+ let snapshot;
36279
+ try {
36280
+ snapshot = structuredClone(page);
36281
+ }
36282
+ catch {
36283
+ this.error.set(true);
36284
+ return;
36285
+ }
36286
+ this.copyRequested.emit(snapshot);
36287
+ }
36288
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisPageCopyActionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
36289
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisPageCopyActionComponent, isStandalone: true, selector: "praxis-page-copy-action", inputs: { page: { classPropertyName: "page", publicName: "page", isSignal: true, isRequired: false, transformFunction: null }, canCopy: { classPropertyName: "canCopy", publicName: "canCopy", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { copyRequested: "copyRequested" }, providers: [providePraxisPageBuilderI18n()], ngImport: i0, template: `
36290
+ <button mat-flat-button type="button" data-testid="page-copy-action" [disabled]="!canCopy() || !page()" (click)="copy()">
36291
+ <mat-icon aria-hidden="true">content_copy</mat-icon>{{ text('copy') }}
36292
+ </button>
36293
+ @if (error()) { <p role="alert">{{ text('copyFailed') }}</p> }
36294
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
36295
+ }
36296
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisPageCopyActionComponent, decorators: [{
36297
+ type: Component,
36298
+ args: [{
36299
+ selector: 'praxis-page-copy-action',
36300
+ imports: [MatButtonModule, MatIconModule],
36301
+ providers: [providePraxisPageBuilderI18n()],
36302
+ changeDetection: ChangeDetectionStrategy.OnPush,
36303
+ template: `
36304
+ <button mat-flat-button type="button" data-testid="page-copy-action" [disabled]="!canCopy() || !page()" (click)="copy()">
36305
+ <mat-icon aria-hidden="true">content_copy</mat-icon>{{ text('copy') }}
36306
+ </button>
36307
+ @if (error()) { <p role="alert">{{ text('copyFailed') }}</p> }
36308
+ `,
36309
+ }]
36310
+ }], propDecorators: { page: [{ type: i0.Input, args: [{ isSignal: true, alias: "page", required: false }] }], canCopy: [{ type: i0.Input, args: [{ isSignal: true, alias: "canCopy", required: false }] }], copyRequested: [{ type: i0.Output, args: ["copyRequested"] }] } });
36311
+
36312
+ const GOVERNED_PAGE_CAPABILITY_KEYS = Object.freeze({
36313
+ authorPages: 'praxis.config.pages.author',
36314
+ submitForReview: 'praxis.config.pages.submit',
36315
+ approveReleases: 'praxis.config.pages.approve',
36316
+ publishReleases: 'praxis.config.pages.publish',
36317
+ manageNavigationGroups: 'praxis.config.navigation.manage',
36318
+ });
36319
+ /**
36320
+ * Derives governed page capabilities from canonical server-issued capability keys.
36321
+ * Never fabricates approval or publication authority when a capability is absent.
36322
+ */
36323
+ function deriveGovernedPageCapabilities(grantedCapabilities) {
36324
+ const granted = new Set(grantedCapabilities
36325
+ ? Array.from(grantedCapabilities, item => String(item ?? '').trim())
36326
+ : []);
36327
+ const hasWildcard = granted.has('*') || granted.has('praxis.config.*');
36328
+ return {
36329
+ canAuthorPages: hasWildcard ||
36330
+ granted.has(GOVERNED_PAGE_CAPABILITY_KEYS.authorPages) ||
36331
+ granted.has('ui.authoring.configure') ||
36332
+ granted.has('ui-layout:write') ||
36333
+ granted.has('pages:write'),
36334
+ canSubmitForReview: hasWildcard ||
36335
+ granted.has(GOVERNED_PAGE_CAPABILITY_KEYS.submitForReview) ||
36336
+ granted.has('ui.authoring.configure') ||
36337
+ granted.has('ui-layout:submit') ||
36338
+ granted.has('pages:submit'),
36339
+ canApproveReleases: hasWildcard ||
36340
+ granted.has(GOVERNED_PAGE_CAPABILITY_KEYS.approveReleases) ||
36341
+ granted.has('ui.layout.publish') ||
36342
+ granted.has('ui-layout:approve') ||
36343
+ granted.has('pages:approve'),
36344
+ canPublishReleases: hasWildcard ||
36345
+ granted.has(GOVERNED_PAGE_CAPABILITY_KEYS.publishReleases) ||
36346
+ granted.has('ui.layout.publish') ||
36347
+ granted.has('ui-layout:publish') ||
36348
+ granted.has('pages:publish'),
36349
+ canManageNavigationGroups: hasWildcard ||
36350
+ granted.has(GOVERNED_PAGE_CAPABILITY_KEYS.manageNavigationGroups) ||
36351
+ granted.has(GOVERNED_PAGE_CAPABILITY_KEYS.authorPages) ||
36352
+ granted.has('ui.authoring.configure'),
36353
+ };
36354
+ }
36355
+ function createCorporateGovernedPagePersistenceAdapter(lifecycle) {
36356
+ return {
36357
+ mode: 'corporate',
36358
+ createPage: command => lifecycle.createPage(command),
36359
+ readPageByCreationKey: creationKey => lifecycle.readPageByCreationKey(creationKey),
36360
+ readPageCatalog: options => lifecycle.readPageCatalog(options),
36361
+ readPublishedPage: (pageId, ifNoneMatch) => lifecycle.readPublishedPage(pageId, ifNoneMatch),
36362
+ readDraftByCreationKey: (target, creationKey) => lifecycle.readDraftByCreationKey(target, creationKey),
36363
+ readDraft: (target, draftId, ifNoneMatch) => lifecycle.readDraft(target, draftId, ifNoneMatch),
36364
+ readReview: (target, releaseId, ifNoneMatch) => lifecycle.readReview(target, releaseId, ifNoneMatch),
36365
+ readHead: (target, ifNoneMatch) => lifecycle.readHead(target, ifNoneMatch),
36366
+ createDraft: (target, idempotencyKey) => lifecycle.createDraft(target, idempotencyKey),
36367
+ createRevision: (target, draftId, draftEtag, command) => lifecycle.createRevision(target, draftId, draftEtag, command),
36368
+ createAssignment: (target, draftId, draftEtag, command) => lifecycle.createAssignment(target, draftId, draftEtag, command),
36369
+ freezeRelease: (target, draftId, draftEtag, command) => lifecycle.freezeRelease(target, draftId, draftEtag, command),
36370
+ submit: (target, releaseId, reviewEtag) => lifecycle.submit(target, releaseId, reviewEtag),
36371
+ approve: (target, releaseId, reviewEtag, command) => lifecycle.approve(target, releaseId, reviewEtag, command),
36372
+ publish: (target, condition, command) => lifecycle.publish(target, condition, command),
36373
+ };
36374
+ }
36375
+ function createLocalPageSaveAsAdapter(handler) {
36376
+ return {
36377
+ mode: 'local',
36378
+ save: request => handler(request),
36379
+ };
36380
+ }
36381
+ const LEGACY_PUBLISHED_PAGE_STORAGE_PREFIX = 'published-page:';
36382
+
36383
+ const DEFAULT_CORPORATE_NAVIGATION_GROUPS_TARGET = Object.freeze({
36384
+ componentType: 'praxis-app-shell',
36385
+ componentId: 'b9debe04-41ba-498b-86aa-cb30a464fb82',
36386
+ });
36387
+ class GovernedPageNavigationController {
36388
+ persistence;
36389
+ observeTargetDoc;
36390
+ navigationGroupsTarget;
36391
+ registeredNamespace;
36392
+ allowedIcons;
36393
+ pageSize;
36394
+ maxItemsPerLevel;
36395
+ idGenerator;
36396
+ epoch = 0;
36397
+ contextKey;
36398
+ groupsSub = null;
36399
+ pageSubs = new Map();
36400
+ pageDocuments = new Map();
36401
+ expandedGroups = new Set();
36402
+ fencedAttempts = new Map();
36403
+ latestGroupsDoc = createEmptyNavigationGroupsAuthoringDocument();
36404
+ latestCatalogEntries = [];
36405
+ stateSubject;
36406
+ snapshot$;
36407
+ constructor(config) {
36408
+ this.persistence = config.persistence;
36409
+ this.navigationGroupsTarget =
36410
+ config.navigationGroupsTarget ?? DEFAULT_CORPORATE_NAVIGATION_GROUPS_TARGET;
36411
+ this.registeredNamespace = config.registeredNamespace ?? '/pages';
36412
+ this.allowedIcons = new Set(Array.from(config.allowedIcons ?? GOVERNED_PAGE_DEFAULT_ALLOWED_ICONS, icon => String(icon ?? '').trim()));
36413
+ this.pageSize = Math.max(1, Math.min(200, Math.trunc(config.pageSize ?? 50)));
36414
+ this.maxItemsPerLevel = Math.max(1, Math.min(200, Math.trunc(config.maxItemsPerLevel ?? 200)));
36415
+ this.contextKey = config.initialContextKey ?? 'default';
36416
+ this.idGenerator =
36417
+ config.idGenerator ??
36418
+ (() => typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
36419
+ ? crypto.randomUUID()
36420
+ : `00000000-0000-4000-8000-${Date.now().toString(16).padStart(12, '0').slice(-12)}`);
36421
+ if (config.observeTargetDocument) {
36422
+ this.observeTargetDoc = config.observeTargetDocument;
36423
+ }
36424
+ else if (config.effectiveLayoutService) {
36425
+ const effectiveLayout = config.effectiveLayoutService;
36426
+ this.observeTargetDoc = (target) => new Observable(subscriber => {
36427
+ let cancelled = false;
36428
+ void effectiveLayout
36429
+ .resolve(target)
36430
+ .then(response => {
36431
+ if (!cancelled) {
36432
+ subscriber.next(response.document ?? null);
36433
+ subscriber.complete();
36434
+ }
36435
+ })
36436
+ .catch((err) => {
36437
+ if (!cancelled) {
36438
+ subscriber.error(err);
36439
+ }
36440
+ });
36441
+ return () => {
36442
+ cancelled = true;
36443
+ };
36444
+ });
36445
+ }
36446
+ this.stateSubject = new BehaviorSubject({
36447
+ epoch: this.epoch,
36448
+ contextKey: this.contextKey,
36449
+ catalogEntries: [],
36450
+ groupsDocument: this.latestGroupsDoc,
36451
+ menuNodes: [],
36452
+ expandedGroupIds: [],
36453
+ loading: false,
36454
+ diagnostics: [],
36455
+ fencedAttempts: {},
36456
+ lastError: null,
36457
+ });
36458
+ this.snapshot$ = this.stateSubject.asObservable();
36459
+ }
36460
+ get snapshot() {
36461
+ return this.stateSubject.value;
36462
+ }
36463
+ setContextKey(nextContextKey) {
36464
+ const normalized = String(nextContextKey ?? '').trim() || 'default';
36465
+ this.contextKey = normalized;
36466
+ this.invalidate();
36467
+ }
36468
+ invalidate() {
36469
+ this.epoch += 1;
36470
+ this.teardownSubscriptions();
36471
+ this.pageDocuments.clear();
36472
+ this.latestCatalogEntries = [];
36473
+ this.latestGroupsDoc = createEmptyNavigationGroupsAuthoringDocument();
36474
+ this.fencedAttempts.clear();
36475
+ this.publishSnapshot({
36476
+ epoch: this.epoch,
36477
+ contextKey: this.contextKey,
36478
+ catalogEntries: [],
36479
+ groupsDocument: this.latestGroupsDoc,
36480
+ menuNodes: [],
36481
+ expandedGroupIds: Array.from(this.expandedGroups),
36482
+ loading: false,
36483
+ diagnostics: [],
36484
+ fencedAttempts: {},
36485
+ lastError: null,
36486
+ });
36487
+ }
36488
+ destroy() {
36489
+ this.epoch += 1;
36490
+ this.teardownSubscriptions();
36491
+ this.stateSubject.complete();
36492
+ }
36493
+ async refresh() {
36494
+ const activeEpoch = ++this.epoch;
36495
+ const activeContextKey = this.contextKey;
36496
+ this.teardownSubscriptions();
36497
+ this.pageDocuments.clear();
36498
+ this.publishSnapshot({
36499
+ ...this.snapshot,
36500
+ epoch: activeEpoch,
36501
+ contextKey: activeContextKey,
36502
+ loading: true,
36503
+ lastError: null,
36504
+ });
36505
+ this.bindGroupsObservation(activeEpoch, activeContextKey);
36506
+ try {
36507
+ const entries = await this.readAllCatalogEntries(activeEpoch, activeContextKey);
36508
+ if (this.isStale(activeEpoch, activeContextKey)) {
36509
+ return this.snapshot;
36510
+ }
36511
+ this.latestCatalogEntries = entries;
36512
+ await this.hydrateCatalogPages(entries, activeEpoch, activeContextKey);
36513
+ if (this.isStale(activeEpoch, activeContextKey)) {
36514
+ return this.snapshot;
36515
+ }
36516
+ this.recomputeProjection(activeEpoch, activeContextKey, false);
36517
+ return this.snapshot;
36518
+ }
36519
+ catch (error) {
36520
+ if (this.isStale(activeEpoch, activeContextKey)) {
36521
+ return this.snapshot;
36522
+ }
36523
+ this.publishSnapshot({
36524
+ ...this.snapshot,
36525
+ loading: false,
36526
+ lastError: error instanceof Error ? error.message : String(error),
36527
+ });
36528
+ return this.snapshot;
36529
+ }
36530
+ }
36531
+ toggleGroupExpanded(groupId) {
36532
+ const normalized = String(groupId ?? '').trim().toLowerCase();
36533
+ if (!normalized) {
36534
+ return;
36535
+ }
36536
+ if (this.expandedGroups.has(normalized)) {
36537
+ this.expandedGroups.delete(normalized);
36538
+ }
36539
+ else {
36540
+ this.expandedGroups.add(normalized);
36541
+ this.reobserveGroupChildren(normalized, this.epoch, this.contextKey);
36542
+ }
36543
+ this.publishSnapshot({
36544
+ ...this.snapshot,
36545
+ expandedGroupIds: Array.from(this.expandedGroups),
36546
+ });
36547
+ }
36548
+ clearFencedAttempt(attemptKey) {
36549
+ this.fencedAttempts.delete(attemptKey);
36550
+ this.publishSnapshot({
36551
+ ...this.snapshot,
36552
+ fencedAttempts: Object.fromEntries(this.fencedAttempts),
36553
+ });
36554
+ }
36555
+ async upsertAndPublishNavigationGroup(input) {
36556
+ const activeEpoch = this.epoch;
36557
+ const activeContextKey = this.contextKey;
36558
+ const attemptKey = `navigation-group:${input.entry.id}`;
36559
+ if (this.fencedAttempts.has(attemptKey)) {
36560
+ throw new Error(`Navigation group operation is fenced pending reconciliation: ${attemptKey}`);
36561
+ }
36562
+ const updated = upsertNavigationGroupEntry(this.latestGroupsDoc, input.entry, {
36563
+ allowedIcons: this.allowedIcons,
36564
+ });
36565
+ if (!updated.ok) {
36566
+ throw new Error(updated.diagnostics[0]?.code ?? 'NAVIGATION_GROUPS_PROFILE_INVALID');
36567
+ }
36568
+ const draftCreationKey = input.draftCreationKey ?? this.idGenerator();
36569
+ const revisionCommandRef = input.revisionCommandRef ?? this.idGenerator();
36570
+ const assignmentCommandRef = input.assignmentCommandRef ?? this.idGenerator();
36571
+ const freezeCommandRef = input.freezeCommandRef ?? this.idGenerator();
36572
+ const reason = input.reason?.trim() || 'Publish navigation group update';
36573
+ try {
36574
+ const createdDraft = await this.persistence.createDraft(this.navigationGroupsTarget, draftCreationKey);
36575
+ if (this.isStale(activeEpoch, activeContextKey)) {
36576
+ throw new UiLayoutLifecycleContractError('CONTEXT_OBSOLETE');
36577
+ }
36578
+ let draftEtag = createdDraft.etag;
36579
+ const draftId = createdDraft.body.draft.draftRef;
36580
+ const revisionReceipt = await this.persistence.createRevision(this.navigationGroupsTarget, draftId, draftEtag, {
36581
+ commandRef: revisionCommandRef,
36582
+ target: this.navigationGroupsTarget,
36583
+ authoringDocument: updated.value,
36584
+ reason,
36585
+ });
36586
+ if (this.isStale(activeEpoch, activeContextKey)) {
36587
+ throw new UiLayoutLifecycleContractError('CONTEXT_OBSOLETE');
36588
+ }
36589
+ if (revisionReceipt.etag === draftEtag) {
36590
+ throw new Error('Navigation groups draft ETag did not progress after revision.');
36591
+ }
36592
+ draftEtag = revisionReceipt.etag;
36593
+ const assignmentReceipt = await this.persistence.createAssignment(this.navigationGroupsTarget, draftId, draftEtag, {
36594
+ commandRef: assignmentCommandRef,
36595
+ target: this.navigationGroupsTarget,
36596
+ layerClass: input.layerClass ?? 'TENANT',
36597
+ selector: input.selector,
36598
+ priority: input.priority ?? 100,
36599
+ contributionKey: input.contributionKey ?? 'corporate:navigation-groups',
36600
+ });
36601
+ if (this.isStale(activeEpoch, activeContextKey)) {
36602
+ throw new UiLayoutLifecycleContractError('CONTEXT_OBSOLETE');
36603
+ }
36604
+ if (assignmentReceipt.etag === draftEtag) {
36605
+ throw new Error('Navigation groups draft ETag did not progress after assignment.');
36606
+ }
36607
+ draftEtag = assignmentReceipt.etag;
36608
+ const freezeReceipt = await this.persistence.freezeRelease(this.navigationGroupsTarget, draftId, draftEtag, { commandRef: freezeCommandRef });
36609
+ if (this.isStale(activeEpoch, activeContextKey)) {
36610
+ throw new UiLayoutLifecycleContractError('CONTEXT_OBSOLETE');
36611
+ }
36612
+ if (freezeReceipt.etag === draftEtag) {
36613
+ throw new Error('Navigation groups draft ETag did not progress after freeze.');
36614
+ }
36615
+ const releaseId = freezeReceipt.body.release.releaseRef;
36616
+ let reviewEtag = freezeReceipt.body.review.reviewEtag;
36617
+ const submitReceipt = await this.persistence.submit(this.navigationGroupsTarget, releaseId, reviewEtag);
36618
+ if (this.isStale(activeEpoch, activeContextKey)) {
36619
+ throw new UiLayoutLifecycleContractError('CONTEXT_OBSOLETE');
36620
+ }
36621
+ if (submitReceipt.etag === reviewEtag) {
36622
+ throw new Error('Navigation groups review ETag did not progress after submit.');
36623
+ }
36624
+ reviewEtag = submitReceipt.etag;
36625
+ if (submitReceipt.body.state !== 'APPROVED') {
36626
+ const approveReceipt = await this.persistence.approve(this.navigationGroupsTarget, releaseId, reviewEtag, {
36627
+ reason,
36628
+ });
36629
+ if (this.isStale(activeEpoch, activeContextKey)) {
36630
+ throw new UiLayoutLifecycleContractError('CONTEXT_OBSOLETE');
36631
+ }
36632
+ if (approveReceipt.etag === reviewEtag) {
36633
+ throw new Error('Navigation groups review ETag did not progress after approve.');
36634
+ }
36635
+ }
36636
+ let existingHeadReleaseRef = null;
36637
+ let existingHeadEtag = null;
36638
+ try {
36639
+ const headResult = await this.persistence.readHead(this.navigationGroupsTarget);
36640
+ if (this.isStale(activeEpoch, activeContextKey)) {
36641
+ throw new UiLayoutLifecycleContractError('CONTEXT_OBSOLETE');
36642
+ }
36643
+ if ('body' in headResult) {
36644
+ existingHeadReleaseRef = headResult.body.activeReleaseRef;
36645
+ existingHeadEtag = headResult.etag;
36646
+ }
36647
+ }
36648
+ catch (headError) {
36649
+ if (this.isStale(activeEpoch, activeContextKey)) {
36650
+ throw new UiLayoutLifecycleContractError('CONTEXT_OBSOLETE');
36651
+ }
36652
+ if (!(headError instanceof UiLayoutLifecycleHttpError && headError.status === 404)) {
36653
+ throw headError;
36654
+ }
36655
+ }
36656
+ if (existingHeadReleaseRef !== releaseId) {
36657
+ const condition = existingHeadEtag
36658
+ ? { kind: 'match', etag: existingHeadEtag }
36659
+ : { kind: 'first-publish' };
36660
+ await this.persistence.publish(this.navigationGroupsTarget, condition, {
36661
+ releaseId,
36662
+ reason,
36663
+ });
36664
+ }
36665
+ this.latestGroupsDoc = updated.value;
36666
+ this.recomputeProjection(activeEpoch, activeContextKey, false);
36667
+ return updated.value;
36668
+ }
36669
+ catch (error) {
36670
+ if (error instanceof UiLayoutLifecycleContractError &&
36671
+ error.code === 'COMMAND_OUTCOME_UNCERTAIN') {
36672
+ this.fencedAttempts.set(attemptKey, draftCreationKey);
36673
+ this.publishSnapshot({
36674
+ ...this.snapshot,
36675
+ fencedAttempts: Object.fromEntries(this.fencedAttempts),
36676
+ lastError: error.message,
36677
+ });
36678
+ }
36679
+ throw error;
36680
+ }
36681
+ }
36682
+ async readAllCatalogEntries(activeEpoch, activeContextKey) {
36683
+ const collected = [];
36684
+ const seenPageIds = new Set();
36685
+ const seenCursors = new Set();
36686
+ let cursor = null;
36687
+ while (collected.length < this.maxItemsPerLevel) {
36688
+ const response = await this.persistence.readPageCatalog({
36689
+ cursor,
36690
+ limit: Math.min(this.pageSize, this.maxItemsPerLevel - collected.length),
36691
+ });
36692
+ if (this.isStale(activeEpoch, activeContextKey)) {
36693
+ return [];
36694
+ }
36695
+ for (const item of response.items ?? []) {
36696
+ const normalizedId = String(item?.pageId ?? '').trim().toLowerCase();
36697
+ if (!normalizedId || seenPageIds.has(normalizedId)) {
36698
+ continue;
36699
+ }
36700
+ seenPageIds.add(normalizedId);
36701
+ collected.push(item);
36702
+ if (collected.length >= this.maxItemsPerLevel) {
36703
+ break;
36704
+ }
36705
+ }
36706
+ const nextCursor = response.nextCursor?.trim() || null;
36707
+ if (!nextCursor) {
36708
+ break;
36709
+ }
36710
+ if (seenCursors.has(nextCursor)) {
36711
+ throw new Error(`Detected pagination cursor cycle in page catalog: ${nextCursor}`);
36712
+ }
36713
+ seenCursors.add(nextCursor);
36714
+ cursor = nextCursor;
36715
+ }
36716
+ return collected;
36717
+ }
36718
+ async hydrateCatalogPages(entries, activeEpoch, activeContextKey) {
36719
+ for (const entry of entries) {
36720
+ if (this.isStale(activeEpoch, activeContextKey)) {
36721
+ return;
36722
+ }
36723
+ const pageId = String(entry.pageId ?? '').trim().toLowerCase();
36724
+ if (!pageId) {
36725
+ continue;
36726
+ }
36727
+ try {
36728
+ const published = await this.persistence.readPublishedPage(pageId);
36729
+ if (this.isStale(activeEpoch, activeContextKey)) {
36730
+ return;
36731
+ }
36732
+ if ('body' in published) {
36733
+ const decoded = decodePageAuthoringDocument(published.body.document);
36734
+ if (decoded.ok) {
36735
+ this.pageDocuments.set(pageId, decoded.value);
36736
+ }
36737
+ }
36738
+ }
36739
+ catch {
36740
+ // Individual page read failure does not abort the rest of the catalog projection.
36741
+ }
36742
+ this.bindPageObservation(pageId, activeEpoch, activeContextKey);
36743
+ }
36744
+ }
36745
+ bindGroupsObservation(activeEpoch, activeContextKey) {
36746
+ if (!this.observeTargetDoc) {
36747
+ return;
36748
+ }
36749
+ this.groupsSub?.unsubscribe();
36750
+ this.groupsSub = this.observeTargetDoc(this.navigationGroupsTarget).subscribe({
36751
+ next: rawDoc => {
36752
+ if (this.isStale(activeEpoch, activeContextKey) || !rawDoc) {
36753
+ return;
36754
+ }
36755
+ const decoded = decodeNavigationGroupsAuthoringDocument(rawDoc, {
36756
+ allowedIcons: this.allowedIcons,
36757
+ });
36758
+ if (decoded.ok) {
36759
+ this.latestGroupsDoc = decoded.value;
36760
+ this.recomputeProjection(activeEpoch, activeContextKey, false);
36761
+ }
36762
+ },
36763
+ });
36764
+ }
36765
+ bindPageObservation(pageId, activeEpoch, activeContextKey) {
36766
+ if (!this.observeTargetDoc) {
36767
+ return;
36768
+ }
36769
+ this.pageSubs.get(pageId)?.unsubscribe();
36770
+ let target;
36771
+ try {
36772
+ target = buildGovernedPageTarget(pageId);
36773
+ }
36774
+ catch {
36775
+ return;
36776
+ }
36777
+ const sub = this.observeTargetDoc(target).subscribe({
36778
+ next: rawDoc => {
36779
+ if (this.isStale(activeEpoch, activeContextKey) || !rawDoc) {
36780
+ return;
36781
+ }
36782
+ const decoded = decodePageAuthoringDocument(rawDoc);
36783
+ if (decoded.ok) {
36784
+ this.pageDocuments.set(pageId, decoded.value);
36785
+ this.recomputeProjection(activeEpoch, activeContextKey, false);
36786
+ }
36787
+ },
36788
+ });
36789
+ this.pageSubs.set(pageId, sub);
36790
+ }
36791
+ reobserveGroupChildren(groupId, activeEpoch, activeContextKey) {
36792
+ for (const [pageId, doc] of this.pageDocuments.entries()) {
36793
+ if (doc.config.navigation.some(binding => binding.groupId.toLowerCase() === groupId)) {
36794
+ this.bindPageObservation(pageId, activeEpoch, activeContextKey);
36795
+ }
36796
+ }
36797
+ }
36798
+ recomputeProjection(activeEpoch, activeContextKey, loading) {
36799
+ if (this.isStale(activeEpoch, activeContextKey)) {
36800
+ return;
36801
+ }
36802
+ const diagnostics = [];
36803
+ const groups = this.latestGroupsDoc.config.groups.slice(0, this.maxItemsPerLevel);
36804
+ const groupNodesById = new Map();
36805
+ for (const group of groups) {
36806
+ groupNodesById.set(group.id.toLowerCase(), {
36807
+ id: group.id.toLowerCase(),
36808
+ kind: 'group',
36809
+ label: group.label,
36810
+ icon: group.icon,
36811
+ order: group.order,
36812
+ children: [],
36813
+ });
36814
+ }
36815
+ for (const [pageId, doc] of this.pageDocuments.entries()) {
36816
+ const routePath = buildPublishedPageIdentity(pageId, this.registeredNamespace).routePath;
36817
+ for (const binding of doc.config.navigation) {
36818
+ if (!this.allowedIcons.has(binding.icon)) {
36819
+ diagnostics.push({
36820
+ code: 'NAVIGATION_ICON_NOT_ALLOWED',
36821
+ path: `/pages/${pageId}/navigation/${binding.id}/icon`,
36822
+ });
36823
+ continue;
36824
+ }
36825
+ const parentGroup = groupNodesById.get(binding.groupId.toLowerCase());
36826
+ if (!parentGroup) {
36827
+ continue;
36828
+ }
36829
+ parentGroup.children ??= [];
36830
+ if (parentGroup.children.length < this.maxItemsPerLevel) {
36831
+ parentGroup.children.push({
36832
+ id: binding.id.toLowerCase(),
36833
+ kind: 'page',
36834
+ label: binding.label,
36835
+ icon: binding.icon,
36836
+ order: binding.order,
36837
+ pageId,
36838
+ routePath,
36839
+ });
36840
+ }
36841
+ }
36842
+ }
36843
+ const rootNodes = [];
36844
+ for (const group of groups) {
36845
+ const node = groupNodesById.get(group.id.toLowerCase());
36846
+ if (!node) {
36847
+ continue;
36848
+ }
36849
+ rootNodes.push(node);
36850
+ }
36851
+ const sortNodes = (nodes) => {
36852
+ nodes.sort((left, right) => left.order !== right.order
36853
+ ? left.order - right.order
36854
+ : left.label.localeCompare(right.label));
36855
+ for (const item of nodes) {
36856
+ if (item.children?.length) {
36857
+ sortNodes(item.children);
36858
+ }
36859
+ }
36860
+ };
36861
+ sortNodes(rootNodes);
36862
+ const validation = validateRuntimeNavigationMenuNodes(rootNodes, {
36863
+ allowedIcons: this.allowedIcons,
36864
+ });
36865
+ if (!validation.ok) {
36866
+ diagnostics.push(...validation.diagnostics);
36867
+ }
36868
+ this.publishSnapshot({
36869
+ epoch: activeEpoch,
36870
+ contextKey: activeContextKey,
36871
+ catalogEntries: [...this.latestCatalogEntries],
36872
+ groupsDocument: this.latestGroupsDoc,
36873
+ menuNodes: validation.ok ? validation.value : rootNodes,
36874
+ expandedGroupIds: Array.from(this.expandedGroups),
36875
+ loading,
36876
+ diagnostics,
36877
+ fencedAttempts: Object.fromEntries(this.fencedAttempts),
36878
+ lastError: null,
36879
+ });
36880
+ }
36881
+ isStale(epoch, contextKey) {
36882
+ return epoch !== this.epoch || contextKey !== this.contextKey;
36883
+ }
36884
+ teardownSubscriptions() {
36885
+ this.groupsSub?.unsubscribe();
36886
+ this.groupsSub = null;
36887
+ for (const sub of this.pageSubs.values()) {
36888
+ sub.unsubscribe();
36889
+ }
36890
+ this.pageSubs.clear();
36891
+ }
36892
+ publishSnapshot(next) {
36893
+ this.stateSubject.next(next);
36894
+ }
36895
+ }
36896
+
36897
+ const DEFAULT_GOVERNED_PAGE_CHILD_VALIDATORS = Object.freeze({
36898
+ 'praxis-dynamic-form': validateDynamicFormPageWidget,
36899
+ 'praxis-table': validateTablePageWidget,
36900
+ });
36901
+ const DEFAULT_GOVERNED_PAGE_PREVIEW_ADAPTERS = Object.freeze({
36902
+ 'praxis-dynamic-form': DYNAMIC_FORM_GOVERNED_PAGE_PREVIEW_ADAPTER,
36903
+ 'praxis-table': TABLE_GOVERNED_PAGE_PREVIEW_ADAPTER,
36904
+ });
36905
+ const CANONICAL_UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
36906
+ function isPlainRecord(value) {
36907
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
36908
+ }
36909
+ function parseLegacySourceReference(value) {
36910
+ if (!isPlainRecord(value) || typeof value['kind'] !== 'string') {
36911
+ return null;
36912
+ }
36913
+ if (value['kind'] === 'registered-native') {
36914
+ const sourceId = typeof value['sourceId'] === 'string' ? value['sourceId'].trim() : '';
36915
+ const revision = typeof value['revision'] === 'string' ? value['revision'].trim() : '';
36916
+ if (!sourceId || !revision) {
36917
+ return null;
36918
+ }
36919
+ return {
36920
+ kind: 'registered-native',
36921
+ sourceId,
36922
+ revision,
36923
+ };
36924
+ }
36925
+ if (value['kind'] === 'published-page' || value['kind'] === 'published-page-copy') {
36926
+ const pageId = typeof value['pageId'] === 'string' ? value['pageId'].trim().toLowerCase() : '';
36927
+ const releaseId = typeof value['releaseId'] === 'string' ? value['releaseId'].trim().toLowerCase() : '';
36928
+ const revisionId = typeof value['revisionId'] === 'string' ? value['revisionId'].trim().toLowerCase() : '';
36929
+ if (!CANONICAL_UUID_PATTERN.test(pageId) ||
36930
+ !CANONICAL_UUID_PATTERN.test(releaseId) ||
36931
+ !CANONICAL_UUID_PATTERN.test(revisionId)) {
36932
+ return null;
36933
+ }
36934
+ return {
36935
+ kind: 'published-page',
36936
+ pageId,
36937
+ releaseId,
36938
+ revisionId,
36939
+ };
36940
+ }
36941
+ return null;
36942
+ }
36943
+ function normalizeLegacyCandidateDocument(rawPayload) {
36944
+ if (!isPlainRecord(rawPayload)) {
36945
+ return rawPayload;
36946
+ }
36947
+ if (isPlainRecord(rawPayload['document'])) {
36948
+ return normalizeLegacyCandidateDocument(rawPayload['document']);
36949
+ }
36950
+ if (rawPayload['schemaVersion'] !== undefined || rawPayload['kind'] !== undefined) {
36951
+ return rawPayload;
36952
+ }
36953
+ if (isPlainRecord(rawPayload['config'])) {
36954
+ return {
36955
+ kind: 'praxis.dynamic-page.editor',
36956
+ version: 1,
36957
+ config: rawPayload['config'],
36958
+ };
36959
+ }
36960
+ const presentation = isPlainRecord(rawPayload['presentation'])
36961
+ ? rawPayload['presentation']
36962
+ : {
36963
+ title: typeof rawPayload['title'] === 'string' ? rawPayload['title'] : '',
36964
+ icon: typeof rawPayload['icon'] === 'string' ? rawPayload['icon'] : 'article',
36965
+ ...(typeof rawPayload['description'] === 'string'
36966
+ ? { description: rawPayload['description'] }
36967
+ : {}),
36968
+ };
36969
+ const navigation = Array.isArray(rawPayload['navigation'])
36970
+ ? rawPayload['navigation']
36971
+ : [];
36972
+ const definition = isPlainRecord(rawPayload['definition'])
36973
+ ? rawPayload['definition']
36974
+ : Object.fromEntries(Object.entries(rawPayload).filter(([key]) => key !== 'presentation' &&
36975
+ key !== 'navigation' &&
36976
+ key !== 'title' &&
36977
+ key !== 'icon' &&
36978
+ key !== 'description' &&
36979
+ key !== 'source' &&
36980
+ key !== 'childSources'));
36981
+ return {
36982
+ kind: 'praxis.dynamic-page.editor',
36983
+ version: 1,
36984
+ config: {
36985
+ presentation,
36986
+ navigation,
36987
+ definition,
36988
+ },
36989
+ };
36990
+ }
36991
+ /**
36992
+ * Inspects a legacy `published-page:<uuid>` payload against the closed P0 governed page profile.
36993
+ * Validates page source, authoring document structure, and child widget sources while preserving
36994
+ * an exportable artifact for incompatible legacy records.
36995
+ */
36996
+ function inspectLegacyPublishedPagePayload(request, childValidators = DEFAULT_GOVERNED_PAGE_CHILD_VALIDATORS) {
36997
+ const rawId = String(request.pageId ?? '').trim();
36998
+ const normalizedPageId = rawId
36999
+ .replace(new RegExp(`^${LEGACY_PUBLISHED_PAGE_STORAGE_PREFIX}`, 'i'), '')
37000
+ .trim()
37001
+ .toLowerCase();
37002
+ const storageKey = `${LEGACY_PUBLISHED_PAGE_STORAGE_PREFIX}${normalizedPageId}`;
37003
+ const routePath = `/pages/${normalizedPageId}`;
37004
+ const diagnostics = [];
37005
+ let target = {
37006
+ componentType: 'praxis-dynamic-page',
37007
+ componentId: normalizedPageId,
37008
+ };
37009
+ try {
37010
+ target = buildGovernedPageTarget(normalizedPageId);
37011
+ }
37012
+ catch {
37013
+ diagnostics.push({
37014
+ code: 'PAGE_ID_INVALID',
37015
+ path: '/pageId',
37016
+ });
37017
+ }
37018
+ const rawRecord = isPlainRecord(request.rawPayload) ? request.rawPayload : null;
37019
+ const resolvedSource = parseLegacySourceReference(request.source) ??
37020
+ parseLegacySourceReference(rawRecord?.['source']);
37021
+ if (!resolvedSource) {
37022
+ diagnostics.push({
37023
+ code: 'LEGACY_PAGE_SOURCE_INVALID',
37024
+ path: '/source',
37025
+ });
37026
+ }
37027
+ const candidateDoc = normalizeLegacyCandidateDocument(request.rawPayload);
37028
+ const validation = validatePageAuthoringDocument(candidateDoc, childValidators);
37029
+ if (!validation.ok) {
37030
+ diagnostics.push(...validation.diagnostics);
37031
+ }
37032
+ const rawChildSources = Array.isArray(request.childSources)
37033
+ ? request.childSources
37034
+ : Array.isArray(rawRecord?.['childSources'])
37035
+ ? rawRecord['childSources']
37036
+ : [];
37037
+ const childSourceMap = new Map();
37038
+ rawChildSources.forEach((entry, index) => {
37039
+ if (!isPlainRecord(entry)) {
37040
+ diagnostics.push({
37041
+ code: 'LEGACY_WIDGET_SOURCE_INVALID',
37042
+ path: `/childSources/${index}`,
37043
+ });
37044
+ return;
37045
+ }
37046
+ const widgetKey = typeof entry['widgetKey'] === 'string' ? entry['widgetKey'].trim().toLowerCase() : '';
37047
+ const parsedSource = parseLegacySourceReference(entry['source']);
37048
+ if (!widgetKey || !parsedSource || parsedSource.kind !== 'registered-native') {
37049
+ diagnostics.push({
37050
+ code: 'LEGACY_WIDGET_SOURCE_INVALID',
37051
+ path: `/childSources/${index}`,
37052
+ });
37053
+ return;
37054
+ }
37055
+ childSourceMap.set(widgetKey, {
37056
+ widgetKey,
37057
+ source: parsedSource,
37058
+ });
37059
+ });
37060
+ if (validation.ok) {
37061
+ validation.value.config.definition.widgets.forEach((widget, index) => {
37062
+ const normalizedKey = widget.key.toLowerCase();
37063
+ if (!childSourceMap.has(normalizedKey)) {
37064
+ diagnostics.push({
37065
+ code: 'LEGACY_WIDGET_SOURCE_MISSING',
37066
+ path: `/config/definition/widgets/${index}/key`,
37067
+ });
37068
+ }
37069
+ });
37070
+ }
37071
+ const compatible = diagnostics.length === 0 && validation.ok && resolvedSource !== null;
37072
+ const authoringDocument = compatible && validation.ok ? validation.value : null;
37073
+ const childSources = Array.from(childSourceMap.values());
37074
+ return {
37075
+ storageKey,
37076
+ pageId: normalizedPageId,
37077
+ routePath,
37078
+ target,
37079
+ compatible,
37080
+ source: resolvedSource,
37081
+ childSources,
37082
+ authoringDocument,
37083
+ diagnostics,
37084
+ exportableArtifact: {
37085
+ schemaVersion: '1.0.0',
37086
+ kind: 'legacy-published-page-export',
37087
+ storageKey,
37088
+ pageId: normalizedPageId,
37089
+ routePath,
37090
+ compatible,
37091
+ diagnostics,
37092
+ rawPayload: request.rawPayload,
37093
+ },
37094
+ };
37095
+ }
37096
+ const DEFAULT_CAPABILITIES = Object.freeze({
37097
+ canAuthorPages: true,
37098
+ canSubmitForReview: true,
37099
+ canApproveReleases: false,
37100
+ canPublishReleases: false,
37101
+ canManageNavigationGroups: true,
37102
+ });
37103
+ class GovernedPageWorkflowController {
37104
+ persistence;
37105
+ childValidators;
37106
+ previewAdapters;
37107
+ idGenerator;
37108
+ contextGeneration = 0;
37109
+ contextKey;
37110
+ fencedOperations = new Map();
37111
+ childSourcesByWidgetId = new Map();
37112
+ stateSubject;
37113
+ snapshot$;
37114
+ constructor(config) {
37115
+ this.persistence = config.persistence;
37116
+ this.childValidators = config.childValidators ?? DEFAULT_GOVERNED_PAGE_CHILD_VALIDATORS;
37117
+ this.previewAdapters = config.previewAdapters ?? DEFAULT_GOVERNED_PAGE_PREVIEW_ADAPTERS;
37118
+ this.contextKey = config.initialContextKey ?? 'default';
37119
+ this.idGenerator =
37120
+ config.idGenerator ??
37121
+ (() => typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
37122
+ ? crypto.randomUUID()
37123
+ : `00000000-0000-4000-8000-${Date.now().toString(16).padStart(12, '0').slice(-12)}`);
37124
+ this.stateSubject = new BehaviorSubject({
37125
+ contextGeneration: this.contextGeneration,
37126
+ contextKey: this.contextKey,
37127
+ pageId: null,
37128
+ rootTarget: null,
37129
+ source: null,
37130
+ draftWorkspace: null,
37131
+ draftEtag: null,
37132
+ workingDocument: null,
37133
+ baselineDocument: null,
37134
+ publishedSnapshot: null,
37135
+ publishedEtag: null,
37136
+ childSources: [],
37137
+ reviewReceipt: null,
37138
+ reviewEtag: null,
37139
+ headReceipt: null,
37140
+ headEtag: null,
37141
+ preview: null,
37142
+ dirty: false,
37143
+ busy: false,
37144
+ capabilities: config.initialCapabilities ?? DEFAULT_CAPABILITIES,
37145
+ fencedOperations: {},
37146
+ lastError: null,
37147
+ });
37148
+ this.snapshot$ = this.stateSubject.asObservable();
37149
+ }
37150
+ get snapshot() {
37151
+ return this.stateSubject.value;
37152
+ }
37153
+ setCapabilities(capabilities) {
37154
+ this.publishSnapshot({
37155
+ ...this.snapshot,
37156
+ capabilities: { ...capabilities },
37157
+ });
37158
+ }
37159
+ /**
37160
+ * Advances monotonic context generation on context switch (including A -> B -> A),
37161
+ * invalidating any in-flight callbacks from previous generations.
37162
+ */
37163
+ setContextKey(nextContextKey) {
37164
+ const normalized = String(nextContextKey ?? '').trim() || 'default';
37165
+ this.contextGeneration += 1;
37166
+ this.contextKey = normalized;
37167
+ this.fencedOperations.clear();
37168
+ this.childSourcesByWidgetId.clear();
37169
+ this.publishSnapshot({
37170
+ contextGeneration: this.contextGeneration,
37171
+ contextKey: this.contextKey,
37172
+ pageId: null,
37173
+ rootTarget: null,
37174
+ source: null,
37175
+ draftWorkspace: null,
37176
+ draftEtag: null,
37177
+ workingDocument: null,
37178
+ baselineDocument: null,
37179
+ publishedSnapshot: null,
37180
+ publishedEtag: null,
37181
+ childSources: [],
37182
+ reviewReceipt: null,
37183
+ reviewEtag: null,
37184
+ headReceipt: null,
37185
+ headEtag: null,
37186
+ preview: null,
37187
+ dirty: false,
37188
+ busy: false,
37189
+ capabilities: this.snapshot.capabilities,
37190
+ fencedOperations: {},
37191
+ lastError: null,
37192
+ });
37193
+ }
37194
+ destroy() {
37195
+ this.contextGeneration += 1;
37196
+ this.stateSubject.complete();
37197
+ }
37198
+ async createNativePage(request) {
37199
+ this.requireCapability('canAuthorPages', 'Creating a governed page requires authoring capability.');
37200
+ const creationKey = request.creationKey?.trim() || this.idGenerator();
37201
+ return this.executePageCreation({
37202
+ creationKey,
37203
+ title: request.title,
37204
+ source: {
37205
+ kind: 'registered-native',
37206
+ sourceId: request.sourceId,
37207
+ revision: request.revision,
37208
+ },
37209
+ });
37210
+ }
37211
+ async copyPublishedPage(request) {
37212
+ this.requireCapability('canAuthorPages', 'Copying a published page requires authoring capability.');
37213
+ const activeGen = this.contextGeneration;
37214
+ const activeCtx = this.contextKey;
37215
+ const creationKey = request.creationKey?.trim() || this.idGenerator();
37216
+ this.setBusy(true);
37217
+ try {
37218
+ const publishedRead = await this.persistence.readPublishedPage(request.sourcePageId);
37219
+ this.assertActiveContext(activeGen, activeCtx);
37220
+ if ('status' in publishedRead) {
37221
+ throw this.fail('TRANSPORT_ERROR', 'Unexpected 304 when reading published source page.');
37222
+ }
37223
+ const sourceRef = publishedRead.body.copySource;
37224
+ return await this.executePageCreation({
37225
+ creationKey,
37226
+ title: request.title,
37227
+ source: sourceRef,
37228
+ }, activeGen, activeCtx);
37229
+ }
37230
+ catch (error) {
37231
+ throw this.handleOperationError(error, activeGen, activeCtx);
37232
+ }
37233
+ finally {
37234
+ if (!this.isStale(activeGen, activeCtx)) {
37235
+ this.setBusy(false);
37236
+ }
37237
+ }
37238
+ }
37239
+ /**
37240
+ * Explicitly imports a compatible legacy `published-page:<uuid>` record into a governed draft
37241
+ * preserving the `/pages/<uuid>` address (`{ componentType: 'praxis-dynamic-page', componentId: pageId }`).
37242
+ * Never fabricates approval or publication state and never deletes legacy storage keys.
37243
+ */
37244
+ async importLegacyPublishedPage(request) {
37245
+ this.requireCapability('canAuthorPages', 'Importing a legacy published page into a governed draft requires authoring capability.');
37246
+ const inspection = inspectLegacyPublishedPagePayload(request, this.childValidators);
37247
+ if (!inspection.compatible || !inspection.authoringDocument || !inspection.source) {
37248
+ throw this.fail('VALIDATION_FAILED', 'Legacy published page cannot be imported into the closed P0 governed profile.', inspection.diagnostics);
37249
+ }
37250
+ const activeGen = this.contextGeneration;
37251
+ const activeCtx = this.contextKey;
37252
+ const idempotencyKey = request.idempotencyKey?.trim() || this.idGenerator();
37253
+ const attemptKey = `import-legacy-page:${inspection.pageId}`;
37254
+ this.assertNotFenced(attemptKey);
37255
+ this.setBusy(true);
37256
+ try {
37257
+ const draftReceipt = await this.persistence.createDraft(inspection.target, idempotencyKey);
37258
+ this.assertActiveContext(activeGen, activeCtx);
37259
+ const commandRef = this.idGenerator();
37260
+ const revisionReceipt = await this.persistence.createRevision(inspection.target, draftReceipt.body.draft.draftRef, draftReceipt.etag, {
37261
+ commandRef,
37262
+ target: inspection.target,
37263
+ authoringDocument: inspection.authoringDocument,
37264
+ reason: request.reason?.trim() ||
37265
+ `Import legacy ${inspection.storageKey} into governed draft workspace`,
37266
+ ...(inspection.childSources.length > 0
37267
+ ? { childSources: [...inspection.childSources] }
37268
+ : {}),
37269
+ });
37270
+ this.assertActiveContext(activeGen, activeCtx);
37271
+ if (revisionReceipt.etag === draftReceipt.etag) {
37272
+ throw this.fail('ETAG_NOT_PROGRESSED', 'Draft ETag did not progress after importing legacy page revision.');
37273
+ }
37274
+ this.childSourcesByWidgetId.clear();
37275
+ for (const childSource of inspection.childSources) {
37276
+ this.childSourcesByWidgetId.set(childSource.widgetKey.toLowerCase(), childSource);
37277
+ }
37278
+ this.applyDraftWorkspaceReceipt(inspection.pageId, inspection.target, revisionReceipt.body.nextDraft, revisionReceipt.etag, inspection.source);
37279
+ await this.refreshPublishedAndLifecycleMetadata(inspection.pageId, inspection.target, revisionReceipt.body.nextDraft.frozenReleaseRef, activeGen, activeCtx);
37280
+ return this.snapshot;
37281
+ }
37282
+ catch (error) {
37283
+ throw this.handleOperationError(error, activeGen, activeCtx, {
37284
+ operation: 'import-legacy-page',
37285
+ attemptKey,
37286
+ creationKey: idempotencyKey,
37287
+ source: inspection.source,
37288
+ childSources: [...inspection.childSources],
37289
+ });
37290
+ }
37291
+ finally {
37292
+ if (!this.isStale(activeGen, activeCtx)) {
37293
+ this.setBusy(false);
37294
+ }
37295
+ }
37296
+ }
37297
+ async openDraftWorkspace(pageId, draftId, options) {
37298
+ const activeGen = this.contextGeneration;
37299
+ const activeCtx = this.contextKey;
37300
+ const rootTarget = buildGovernedPageTarget(pageId);
37301
+ this.setBusy(true);
37302
+ try {
37303
+ const draftResult = await this.persistence.readDraft(rootTarget, draftId);
37304
+ this.assertActiveContext(activeGen, activeCtx);
37305
+ if ('status' in draftResult) {
37306
+ return this.snapshot;
37307
+ }
37308
+ this.applyDraftWorkspaceReceipt(pageId.trim().toLowerCase(), rootTarget, draftResult.body, draftResult.etag, options?.source ?? this.snapshot.source);
37309
+ await this.refreshPublishedAndLifecycleMetadata(pageId.trim().toLowerCase(), rootTarget, draftResult.body.frozenReleaseRef, activeGen, activeCtx);
37310
+ return this.snapshot;
37311
+ }
37312
+ catch (error) {
37313
+ throw this.handleOperationError(error, activeGen, activeCtx);
37314
+ }
37315
+ finally {
37316
+ if (!this.isStale(activeGen, activeCtx)) {
37317
+ this.setBusy(false);
37318
+ }
37319
+ }
37320
+ }
37321
+ updateWorkingDocument(nextDocument) {
37322
+ this.requireCapability('canAuthorPages', 'Editing a governed page draft requires authoring capability.');
37323
+ const decoded = decodePageAuthoringDocument(nextDocument);
37324
+ if (!decoded.ok) {
37325
+ throw this.fail('VALIDATION_FAILED', 'Invalid governed page authoring document.', decoded.diagnostics);
37326
+ }
37327
+ const baseline = this.snapshot.baselineDocument;
37328
+ const dirty = !baseline || uiLayoutJsonIdentity(baseline) !== uiLayoutJsonIdentity(decoded.value);
37329
+ this.publishSnapshot({
37330
+ ...this.snapshot,
37331
+ workingDocument: decoded.value,
37332
+ dirty,
37333
+ lastError: null,
37334
+ });
37335
+ return decoded.value;
37336
+ }
37337
+ updatePresentation(presentation) {
37338
+ const current = this.requireWorkingDocument();
37339
+ return this.updateWorkingDocument({
37340
+ ...current,
37341
+ config: {
37342
+ ...current.config,
37343
+ presentation: {
37344
+ title: presentation.title,
37345
+ icon: presentation.icon,
37346
+ ...(presentation.description !== undefined
37347
+ ? { description: presentation.description }
37348
+ : {}),
37349
+ },
37350
+ },
37351
+ });
37352
+ }
37353
+ upsertNavigationBinding(binding, options) {
37354
+ this.requireCapability('canAuthorPages', 'Binding page navigation requires authoring capability.');
37355
+ const current = this.requireWorkingDocument();
37356
+ const updated = upsertPageNavigationBinding(current, binding, options);
37357
+ if (!updated.ok) {
37358
+ throw this.fail('VALIDATION_FAILED', 'Invalid governed page navigation binding.', updated.diagnostics);
37359
+ }
37360
+ return this.updateWorkingDocument(updated.value);
37361
+ }
37362
+ /**
37363
+ * Updates a child widget's inputs inside the page's authoring document and
37364
+ * records its governed child source so child widgets remain under page authorship.
37365
+ */
37366
+ updateChildWidgetInputs(widgetKey, inputs, childSource) {
37367
+ this.requireCapability('canAuthorPages', 'Editing child widgets requires page authoring capability.');
37368
+ const current = this.requireWorkingDocument();
37369
+ const normalizedKey = String(widgetKey ?? '').trim().toLowerCase();
37370
+ const widgetIndex = current.config.definition.widgets.findIndex(widget => widget.key.toLowerCase() === normalizedKey);
37371
+ if (widgetIndex < 0) {
37372
+ throw this.fail('VALIDATION_FAILED', `Widget "${widgetKey}" does not exist on the governed page.`);
37373
+ }
37374
+ const nextWidgets = current.config.definition.widgets.map((widget, idx) => idx === widgetIndex
37375
+ ? {
37376
+ ...widget,
37377
+ definition: {
37378
+ ...widget.definition,
37379
+ inputs: JSON.parse(JSON.stringify(inputs)),
37380
+ },
37381
+ }
37382
+ : widget);
37383
+ if (childSource) {
37384
+ this.childSourcesByWidgetId.set(normalizedKey, {
37385
+ widgetKey: normalizedKey,
37386
+ source: childSource.source,
37387
+ });
37388
+ }
37389
+ const updated = this.updateWorkingDocument({
37390
+ ...current,
37391
+ config: {
37392
+ ...current.config,
37393
+ definition: {
37394
+ ...current.config.definition,
37395
+ widgets: nextWidgets,
37396
+ },
37397
+ },
37398
+ });
37399
+ this.publishSnapshot({
37400
+ ...this.snapshot,
37401
+ childSources: Array.from(this.childSourcesByWidgetId.values()),
37402
+ });
37403
+ return updated;
37404
+ }
37405
+ buildInteractivePreview(baseContext = {}) {
37406
+ const working = this.requireWorkingDocument();
37407
+ const pageId = this.snapshot.pageId;
37408
+ if (!pageId) {
37409
+ throw this.fail('DRAFT_NOT_LOADED', 'Cannot materialize preview before loading a governed page.');
37410
+ }
37411
+ const previewResult = materializeGovernedPagePreview(working, pageId, this.previewAdapters, baseContext);
37412
+ if (!previewResult.ok) {
37413
+ const hasUncertified = previewResult.diagnostics.some(diagnostic => diagnostic.code === 'PAGE_PREVIEW_WIDGET_UNCERTIFIED');
37414
+ throw this.fail(hasUncertified ? 'PREVIEW_UNCERTIFIED' : 'VALIDATION_FAILED', hasUncertified
37415
+ ? 'Interactive preview blocked: one or more widgets do not have a certified preview adapter.'
37416
+ : 'Interactive preview failed validation.', previewResult.diagnostics);
37417
+ }
37418
+ this.publishSnapshot({
37419
+ ...this.snapshot,
37420
+ preview: previewResult.value,
37421
+ lastError: null,
37422
+ });
37423
+ return previewResult.value;
37424
+ }
37425
+ async saveDraftRevision(reason = 'Save governed page draft revision') {
37426
+ this.requireCapability('canAuthorPages', 'Saving a draft revision requires authoring capability.');
37427
+ const activeGen = this.contextGeneration;
37428
+ const activeCtx = this.contextKey;
37429
+ const { rootTarget, draftWorkspace, draftEtag, pageId } = this.requireLoadedDraft();
37430
+ const working = this.requireWorkingDocument();
37431
+ const attemptKey = `save-revision:${draftWorkspace.draft.draftRef}`;
37432
+ this.assertNotFenced(attemptKey);
37433
+ const validated = validatePageAuthoringDocument(working, this.childValidators);
37434
+ if (!validated.ok) {
37435
+ throw this.fail('VALIDATION_FAILED', 'Governed page authoring document failed validation.', validated.diagnostics);
37436
+ }
37437
+ const commandRef = this.idGenerator();
37438
+ const childSources = Array.from(this.childSourcesByWidgetId.values());
37439
+ let activeDraftRef = draftWorkspace.draft.draftRef;
37440
+ let activeDraftEtag = draftEtag;
37441
+ this.setBusy(true);
37442
+ try {
37443
+ // A frozen release is immutable: editing after freeze opens a fresh draft workspace
37444
+ // and never carries over a previous release's approval or submission state.
37445
+ if (draftWorkspace.frozenReleaseRef !== null ||
37446
+ draftWorkspace.draft.state === 'RELEASED') {
37447
+ const reopened = await this.persistence.createDraft(rootTarget, this.idGenerator());
37448
+ this.assertActiveContext(activeGen, activeCtx);
37449
+ activeDraftRef = reopened.body.draft.draftRef;
37450
+ activeDraftEtag = reopened.etag;
37451
+ }
37452
+ const receipt = await this.persistence.createRevision(rootTarget, activeDraftRef, activeDraftEtag, {
37453
+ commandRef,
37454
+ target: rootTarget,
37455
+ authoringDocument: validated.value,
37456
+ reason,
37457
+ ...(childSources.length > 0 ? { childSources } : {}),
37458
+ });
37459
+ this.assertActiveContext(activeGen, activeCtx);
37460
+ if (receipt.etag === activeDraftEtag) {
37461
+ throw this.fail('ETAG_NOT_PROGRESSED', 'Draft ETag did not progress after saving revision.');
37462
+ }
37463
+ this.applyDraftWorkspaceReceipt(pageId, rootTarget, receipt.body.nextDraft, receipt.etag, this.snapshot.source);
37464
+ return this.snapshot;
37465
+ }
37466
+ catch (error) {
37467
+ throw this.handleOperationError(error, activeGen, activeCtx, {
37468
+ operation: 'save-revision',
37469
+ attemptKey,
37470
+ commandRef,
37471
+ draftId: activeDraftRef,
37472
+ });
37473
+ }
37474
+ finally {
37475
+ if (!this.isStale(activeGen, activeCtx)) {
37476
+ this.setBusy(false);
37477
+ }
37478
+ }
37479
+ }
37480
+ async assignScope(input) {
37481
+ this.requireCapability('canAuthorPages', 'Assigning governed page scope requires authoring capability.');
37482
+ const activeGen = this.contextGeneration;
37483
+ const activeCtx = this.contextKey;
37484
+ const { rootTarget, draftWorkspace, draftEtag, pageId } = this.requireLoadedDraft();
37485
+ const attemptKey = `assign-scope:${draftWorkspace.draft.draftRef}`;
37486
+ this.assertNotFenced(attemptKey);
37487
+ const commandRef = this.idGenerator();
37488
+ let activeDraftRef = draftWorkspace.draft.draftRef;
37489
+ let activeDraftEtag = draftEtag;
37490
+ this.setBusy(true);
37491
+ try {
37492
+ if (draftWorkspace.frozenReleaseRef !== null ||
37493
+ draftWorkspace.draft.state === 'RELEASED') {
37494
+ const reopened = await this.persistence.createDraft(rootTarget, this.idGenerator());
37495
+ this.assertActiveContext(activeGen, activeCtx);
37496
+ activeDraftRef = reopened.body.draft.draftRef;
37497
+ activeDraftEtag = reopened.etag;
37498
+ const working = this.snapshot.workingDocument;
37499
+ if (working) {
37500
+ const childSources = Array.from(this.childSourcesByWidgetId.values());
37501
+ const revReceipt = await this.persistence.createRevision(rootTarget, activeDraftRef, activeDraftEtag, {
37502
+ commandRef: this.idGenerator(),
37503
+ target: rootTarget,
37504
+ authoringDocument: working,
37505
+ reason: 'Reopen draft revision before updating scope assignment',
37506
+ ...(childSources.length > 0 ? { childSources } : {}),
37507
+ });
37508
+ this.assertActiveContext(activeGen, activeCtx);
37509
+ if (revReceipt.etag === activeDraftEtag) {
37510
+ throw this.fail('ETAG_NOT_PROGRESSED', 'Draft ETag did not progress after saving revision.');
37511
+ }
37512
+ activeDraftEtag = revReceipt.etag;
37513
+ }
37514
+ }
37515
+ const receipt = await this.persistence.createAssignment(rootTarget, activeDraftRef, activeDraftEtag, {
37516
+ commandRef,
37517
+ target: rootTarget,
37518
+ layerClass: input.layerClass,
37519
+ selector: input.selector,
37520
+ priority: input.priority ?? 100,
37521
+ contributionKey: input.contributionKey ?? rootTarget.componentId,
37522
+ });
37523
+ this.assertActiveContext(activeGen, activeCtx);
37524
+ if (receipt.etag === activeDraftEtag) {
37525
+ throw this.fail('ETAG_NOT_PROGRESSED', 'Draft ETag did not progress after scope assignment.');
37526
+ }
37527
+ this.applyDraftWorkspaceReceipt(pageId, rootTarget, receipt.body.nextDraft, receipt.etag, this.snapshot.source);
37528
+ return this.snapshot;
37529
+ }
37530
+ catch (error) {
37531
+ throw this.handleOperationError(error, activeGen, activeCtx, {
37532
+ operation: 'assign-scope',
37533
+ attemptKey,
37534
+ commandRef,
37535
+ draftId: activeDraftRef,
37536
+ });
37537
+ }
37538
+ finally {
37539
+ if (!this.isStale(activeGen, activeCtx)) {
37540
+ this.setBusy(false);
37541
+ }
37542
+ }
37543
+ }
37544
+ async saveAndSubmitForReview(options = {}) {
37545
+ this.requireCapability('canAuthorPages', 'Submitting a governed page requires authoring capability.');
37546
+ this.requireCapability('canSubmitForReview', 'Submitting a governed page for review requires submit capability.');
37547
+ const activeGen = this.contextGeneration;
37548
+ const activeCtx = this.contextKey;
37549
+ const reason = options.reason?.trim() || 'Submit governed page for review';
37550
+ const rootTargetEntry = this.snapshot.draftWorkspace?.targets[0];
37551
+ if (this.snapshot.dirty || !rootTargetEntry?.selectedRevision) {
37552
+ await this.saveDraftRevision(reason);
37553
+ this.assertActiveContext(activeGen, activeCtx);
37554
+ }
37555
+ const updatedTargetEntry = this.snapshot.draftWorkspace?.targets[0];
37556
+ const existingAssignment = updatedTargetEntry?.selectedAssignment;
37557
+ const isSameAssignment = Boolean(existingAssignment) &&
37558
+ Boolean(options.assignment) &&
37559
+ existingAssignment?.layerClass === options.assignment?.layerClass &&
37560
+ existingAssignment?.priority === (options.assignment?.priority ?? 100) &&
37561
+ uiLayoutJsonIdentity(existingAssignment?.selector ?? null) ===
37562
+ uiLayoutJsonIdentity(options.assignment?.selector ?? null);
37563
+ if (!existingAssignment || (options.assignment && !isSameAssignment)) {
37564
+ if (!options.assignment) {
37565
+ throw this.fail('VALIDATION_FAILED', 'Scope assignment selector is required before freezing and submitting a release.');
37566
+ }
37567
+ await this.assignScope(options.assignment);
37568
+ this.assertActiveContext(activeGen, activeCtx);
37569
+ }
37570
+ const { rootTarget, draftWorkspace, draftEtag, pageId } = this.requireLoadedDraft();
37571
+ const freezeAttemptKey = `freeze-release:${draftWorkspace.draft.draftRef}`;
37572
+ this.assertNotFenced(freezeAttemptKey);
37573
+ let releaseId = draftWorkspace.frozenReleaseRef;
37574
+ let reviewEtag = this.snapshot.reviewEtag;
37575
+ this.setBusy(true);
37576
+ try {
37577
+ if (!releaseId || !reviewEtag) {
37578
+ const freezeCommandRef = this.idGenerator();
37579
+ try {
37580
+ const freezeReceipt = await this.persistence.freezeRelease(rootTarget, draftWorkspace.draft.draftRef, draftEtag, { commandRef: freezeCommandRef });
37581
+ this.assertActiveContext(activeGen, activeCtx);
37582
+ if (freezeReceipt.etag === draftEtag) {
37583
+ throw this.fail('ETAG_NOT_PROGRESSED', 'Draft ETag did not progress after freezing release.');
37584
+ }
37585
+ this.applyDraftWorkspaceReceipt(pageId, rootTarget, freezeReceipt.body.nextDraft, freezeReceipt.etag, this.snapshot.source);
37586
+ releaseId = freezeReceipt.body.release.releaseRef;
37587
+ reviewEtag = freezeReceipt.body.review.reviewEtag;
37588
+ this.publishSnapshot({
37589
+ ...this.snapshot,
37590
+ reviewReceipt: freezeReceipt.body.review,
37591
+ reviewEtag,
37592
+ });
37593
+ }
37594
+ catch (error) {
37595
+ throw this.handleOperationError(error, activeGen, activeCtx, {
37596
+ operation: 'freeze-release',
37597
+ attemptKey: freezeAttemptKey,
37598
+ commandRef: freezeCommandRef,
37599
+ draftId: draftWorkspace.draft.draftRef,
37600
+ });
37601
+ }
37602
+ }
37603
+ const submitAttemptKey = `submit-review:${releaseId}`;
37604
+ this.assertNotFenced(submitAttemptKey);
37605
+ try {
37606
+ const submitReceipt = await this.persistence.submit(rootTarget, releaseId, reviewEtag);
37607
+ this.assertActiveContext(activeGen, activeCtx);
37608
+ if (submitReceipt.etag === reviewEtag) {
37609
+ throw this.fail('ETAG_NOT_PROGRESSED', 'Review ETag did not progress after submitting release for review.');
37610
+ }
37611
+ this.publishSnapshot({
37612
+ ...this.snapshot,
37613
+ reviewReceipt: submitReceipt.body,
37614
+ reviewEtag: submitReceipt.etag,
37615
+ lastError: null,
37616
+ });
37617
+ return this.snapshot;
37618
+ }
37619
+ catch (error) {
37620
+ throw this.handleOperationError(error, activeGen, activeCtx, {
37621
+ operation: 'submit-review',
37622
+ attemptKey: submitAttemptKey,
37623
+ releaseId,
37624
+ });
37625
+ }
37626
+ }
37627
+ finally {
37628
+ if (!this.isStale(activeGen, activeCtx)) {
37629
+ this.setBusy(false);
37630
+ }
37631
+ }
37632
+ }
37633
+ async approveRelease(reason = 'Approve governed page release') {
37634
+ this.requireCapability('canApproveReleases', 'Approving a governed page release requires approve capability.');
37635
+ const activeGen = this.contextGeneration;
37636
+ const activeCtx = this.contextKey;
37637
+ const { rootTarget } = this.requireLoadedDraft();
37638
+ const releaseId = this.snapshot.reviewReceipt?.releaseRef ?? this.snapshot.draftWorkspace?.frozenReleaseRef;
37639
+ const reviewEtag = this.snapshot.reviewEtag;
37640
+ if (!releaseId || !reviewEtag) {
37641
+ throw this.fail('RELEASE_NOT_FROZEN', 'Cannot approve before a release is frozen and submitted.');
37642
+ }
37643
+ if (this.snapshot.reviewReceipt?.state === 'APPROVED') {
37644
+ return this.snapshot;
37645
+ }
37646
+ const attemptKey = `approve-release:${releaseId}`;
37647
+ this.assertNotFenced(attemptKey);
37648
+ this.setBusy(true);
37649
+ try {
37650
+ const receipt = await this.persistence.approve(rootTarget, releaseId, reviewEtag, {
37651
+ reason: reason.trim() || 'Approve governed page release',
37652
+ });
37653
+ this.assertActiveContext(activeGen, activeCtx);
37654
+ if (receipt.etag === reviewEtag) {
37655
+ throw this.fail('ETAG_NOT_PROGRESSED', 'Review ETag did not progress after approving release.');
37656
+ }
37657
+ this.publishSnapshot({
37658
+ ...this.snapshot,
37659
+ reviewReceipt: receipt.body,
37660
+ reviewEtag: receipt.etag,
37661
+ lastError: null,
37662
+ });
37663
+ return this.snapshot;
37664
+ }
37665
+ catch (error) {
37666
+ throw this.handleOperationError(error, activeGen, activeCtx, {
37667
+ operation: 'approve-release',
37668
+ attemptKey,
37669
+ releaseId,
37670
+ });
37671
+ }
37672
+ finally {
37673
+ if (!this.isStale(activeGen, activeCtx)) {
37674
+ this.setBusy(false);
37675
+ }
37676
+ }
37677
+ }
37678
+ async publishRelease(reason = 'Publish governed page release') {
37679
+ this.requireCapability('canPublishReleases', 'Publishing a governed page release requires publish capability.');
37680
+ const activeGen = this.contextGeneration;
37681
+ const activeCtx = this.contextKey;
37682
+ const { rootTarget, pageId } = this.requireLoadedDraft();
37683
+ const releaseId = this.snapshot.reviewReceipt?.releaseRef ?? this.snapshot.draftWorkspace?.frozenReleaseRef;
37684
+ if (!releaseId) {
37685
+ throw this.fail('RELEASE_NOT_FROZEN', 'Cannot publish before a release is frozen and approved.');
37686
+ }
37687
+ const attemptKey = `publish-release:${releaseId}`;
37688
+ this.assertNotFenced(attemptKey);
37689
+ this.setBusy(true);
37690
+ try {
37691
+ let currentHead = this.snapshot.headReceipt;
37692
+ let currentHeadEtag = this.snapshot.headEtag;
37693
+ try {
37694
+ const headRead = await this.persistence.readHead(rootTarget);
37695
+ this.assertActiveContext(activeGen, activeCtx);
37696
+ if ('body' in headRead) {
37697
+ currentHead = headRead.body;
37698
+ currentHeadEtag = headRead.etag;
37699
+ }
37700
+ }
37701
+ catch (headError) {
37702
+ this.assertActiveContext(activeGen, activeCtx);
37703
+ if (headError instanceof UiLayoutLifecycleHttpError && headError.status === 404) {
37704
+ currentHead = null;
37705
+ currentHeadEtag = null;
37706
+ }
37707
+ else {
37708
+ throw headError;
37709
+ }
37710
+ }
37711
+ if (currentHead?.activeReleaseRef === releaseId) {
37712
+ this.publishSnapshot({
37713
+ ...this.snapshot,
37714
+ headReceipt: currentHead,
37715
+ headEtag: currentHeadEtag,
37716
+ lastError: null,
37717
+ });
37718
+ return this.snapshot;
37719
+ }
37720
+ const condition = currentHeadEtag
37721
+ ? { kind: 'match', etag: currentHeadEtag }
37722
+ : { kind: 'first-publish' };
37723
+ const publishReceipt = await this.persistence.publish(rootTarget, condition, {
37724
+ releaseId,
37725
+ reason: reason.trim() || 'Publish governed page release',
37726
+ });
37727
+ this.assertActiveContext(activeGen, activeCtx);
37728
+ let publishedSnapshot = this.snapshot.publishedSnapshot;
37729
+ let publishedEtag = this.snapshot.publishedEtag;
37730
+ try {
37731
+ const publishedRead = await this.persistence.readPublishedPage(pageId);
37732
+ this.assertActiveContext(activeGen, activeCtx);
37733
+ if ('body' in publishedRead) {
37734
+ publishedSnapshot = publishedRead.body;
37735
+ publishedEtag = publishedRead.etag;
37736
+ }
37737
+ }
37738
+ catch {
37739
+ // Published snapshot refresh is best-effort after head advancement.
37740
+ }
37741
+ this.publishSnapshot({
37742
+ ...this.snapshot,
37743
+ headReceipt: publishReceipt.body,
37744
+ headEtag: publishReceipt.etag,
37745
+ publishedSnapshot,
37746
+ publishedEtag,
37747
+ lastError: null,
37748
+ });
37749
+ return this.snapshot;
37750
+ }
37751
+ catch (error) {
37752
+ throw this.handleOperationError(error, activeGen, activeCtx, {
37753
+ operation: 'publish-release',
37754
+ attemptKey,
37755
+ releaseId,
37756
+ });
37757
+ }
37758
+ finally {
37759
+ if (!this.isStale(activeGen, activeCtx)) {
37760
+ this.setBusy(false);
37761
+ }
37762
+ }
37763
+ }
37764
+ async approveAndPublish(reason = 'Approve and publish governed page release') {
37765
+ this.requireCapability('canApproveReleases', 'Approve capability is required.');
37766
+ this.requireCapability('canPublishReleases', 'Publish capability is required.');
37767
+ await this.approveRelease(reason);
37768
+ return this.publishRelease(reason);
37769
+ }
37770
+ /**
37771
+ * Reconciles a fenced operation after an ambiguous network failure (`COMMAND_OUTCOME_UNCERTAIN`)
37772
+ * using the canonical read endpoints before unlocking retries.
37773
+ */
37774
+ async reconcileUncertainOperation(attemptKey) {
37775
+ const fenced = this.fencedOperations.get(attemptKey);
37776
+ if (!fenced) {
37777
+ return this.snapshot;
37778
+ }
37779
+ const activeGen = this.contextGeneration;
37780
+ const activeCtx = this.contextKey;
37781
+ this.setBusy(true);
37782
+ try {
37783
+ if (fenced.operation === 'create-page' && fenced.creationKey) {
37784
+ const receipt = await this.persistence.readPageByCreationKey(fenced.creationKey);
37785
+ this.assertActiveContext(activeGen, activeCtx);
37786
+ const draftRead = await this.persistence.readDraft(receipt.body.target, receipt.body.draftId);
37787
+ this.assertActiveContext(activeGen, activeCtx);
37788
+ if ('status' in draftRead) {
37789
+ throw this.fail('TRANSPORT_ERROR', 'Unexpected 304 when reading created page draft workspace.');
37790
+ }
37791
+ this.fencedOperations.delete(attemptKey);
37792
+ this.applyDraftWorkspaceReceipt(receipt.body.pageId, receipt.body.target, draftRead.body, draftRead.etag, receipt.body.source);
37793
+ await this.refreshPublishedAndLifecycleMetadata(receipt.body.pageId, receipt.body.target, draftRead.body.frozenReleaseRef, activeGen, activeCtx);
37794
+ return this.snapshot;
37795
+ }
37796
+ if (fenced.operation === 'import-legacy-page' && fenced.creationKey) {
37797
+ const legacyPageId = fenced.attemptKey.replace(/^import-legacy-page:/, '');
37798
+ const legacyTarget = buildGovernedPageTarget(legacyPageId);
37799
+ const draftLookup = await this.persistence.readDraftByCreationKey(legacyTarget, fenced.creationKey);
37800
+ this.assertActiveContext(activeGen, activeCtx);
37801
+ const draftRead = await this.persistence.readDraft(legacyTarget, draftLookup.body.draftRef);
37802
+ this.assertActiveContext(activeGen, activeCtx);
37803
+ if ('status' in draftRead) {
37804
+ throw this.fail('TRANSPORT_ERROR', 'Unexpected 304 when reading imported legacy draft workspace.');
37805
+ }
37806
+ if (fenced.childSources) {
37807
+ this.childSourcesByWidgetId.clear();
37808
+ for (const childSource of fenced.childSources) {
37809
+ this.childSourcesByWidgetId.set(childSource.widgetKey.toLowerCase(), childSource);
37810
+ }
37811
+ }
37812
+ this.fencedOperations.delete(attemptKey);
37813
+ this.applyDraftWorkspaceReceipt(legacyPageId, legacyTarget, draftRead.body, draftRead.etag, fenced.source ?? this.snapshot.source);
37814
+ await this.refreshPublishedAndLifecycleMetadata(legacyPageId, legacyTarget, draftRead.body.frozenReleaseRef, activeGen, activeCtx);
37815
+ return this.snapshot;
37816
+ }
37817
+ const { rootTarget, pageId } = this.requireLoadedDraft();
37818
+ if ((fenced.operation === 'save-revision' ||
37819
+ fenced.operation === 'assign-scope' ||
37820
+ fenced.operation === 'freeze-release') &&
37821
+ fenced.draftId) {
37822
+ const draftRead = await this.persistence.readDraft(rootTarget, fenced.draftId);
37823
+ this.assertActiveContext(activeGen, activeCtx);
37824
+ if ('body' in draftRead) {
37825
+ this.fencedOperations.delete(attemptKey);
37826
+ this.applyDraftWorkspaceReceipt(pageId, rootTarget, draftRead.body, draftRead.etag, this.snapshot.source);
37827
+ if (draftRead.body.frozenReleaseRef) {
37828
+ await this.refreshPublishedAndLifecycleMetadata(pageId, rootTarget, draftRead.body.frozenReleaseRef, activeGen, activeCtx);
37829
+ }
37830
+ }
37831
+ return this.snapshot;
37832
+ }
37833
+ if ((fenced.operation === 'submit-review' || fenced.operation === 'approve-release') &&
37834
+ fenced.releaseId) {
37835
+ const reviewRead = await this.persistence.readReview(rootTarget, fenced.releaseId);
37836
+ this.assertActiveContext(activeGen, activeCtx);
37837
+ if ('body' in reviewRead) {
37838
+ this.fencedOperations.delete(attemptKey);
37839
+ this.publishSnapshot({
37840
+ ...this.snapshot,
37841
+ reviewReceipt: reviewRead.body,
37842
+ reviewEtag: reviewRead.etag,
37843
+ fencedOperations: Object.fromEntries(this.fencedOperations),
37844
+ lastError: null,
37845
+ });
37846
+ }
37847
+ return this.snapshot;
37848
+ }
37849
+ if (fenced.operation === 'publish-release') {
37850
+ const headRead = await this.persistence.readHead(rootTarget);
37851
+ this.assertActiveContext(activeGen, activeCtx);
37852
+ if ('body' in headRead) {
37853
+ this.fencedOperations.delete(attemptKey);
37854
+ this.publishSnapshot({
37855
+ ...this.snapshot,
37856
+ headReceipt: headRead.body,
37857
+ headEtag: headRead.etag,
37858
+ fencedOperations: Object.fromEntries(this.fencedOperations),
37859
+ lastError: null,
37860
+ });
37861
+ }
37862
+ return this.snapshot;
37863
+ }
37864
+ return this.snapshot;
37865
+ }
37866
+ catch (error) {
37867
+ throw this.handleOperationError(error, activeGen, activeCtx);
37868
+ }
37869
+ finally {
37870
+ if (!this.isStale(activeGen, activeCtx)) {
37871
+ this.setBusy(false);
37872
+ }
37873
+ }
37874
+ }
37875
+ /**
37876
+ * Reloads server state after a 409 Conflict or 412 Precondition Failed response.
37877
+ */
37878
+ async reloadAfterConflict() {
37879
+ const { pageId, draftWorkspace } = this.requireLoadedDraft();
37880
+ return this.openDraftWorkspace(pageId, draftWorkspace.draft.draftRef, {
37881
+ source: this.snapshot.source,
37882
+ });
37883
+ }
37884
+ async executePageCreation(command, activeGen = this.contextGeneration, activeCtx = this.contextKey) {
37885
+ const attemptKey = `create-page:${command.creationKey}`;
37886
+ this.assertNotFenced(attemptKey);
37887
+ this.setBusy(true);
37888
+ try {
37889
+ const receipt = await this.persistence.createPage(command);
37890
+ this.assertActiveContext(activeGen, activeCtx);
37891
+ const draftRead = await this.persistence.readDraft(receipt.body.target, receipt.body.draftId);
37892
+ this.assertActiveContext(activeGen, activeCtx);
37893
+ if ('status' in draftRead) {
37894
+ throw this.fail('TRANSPORT_ERROR', 'Unexpected 304 when reading created page draft workspace.');
37895
+ }
37896
+ this.childSourcesByWidgetId.clear();
37897
+ this.applyDraftWorkspaceReceipt(receipt.body.pageId, receipt.body.target, draftRead.body, draftRead.etag, receipt.body.source);
37898
+ await this.refreshPublishedAndLifecycleMetadata(receipt.body.pageId, receipt.body.target, draftRead.body.frozenReleaseRef, activeGen, activeCtx);
37899
+ return receipt.body;
37900
+ }
37901
+ catch (error) {
37902
+ throw this.handleOperationError(error, activeGen, activeCtx, {
37903
+ operation: 'create-page',
37904
+ attemptKey,
37905
+ creationKey: command.creationKey,
37906
+ });
37907
+ }
37908
+ finally {
37909
+ if (!this.isStale(activeGen, activeCtx)) {
37910
+ this.setBusy(false);
37911
+ }
37912
+ }
37913
+ }
37914
+ applyDraftWorkspaceReceipt(pageId, rootTarget, draftWorkspace, draftEtag, source) {
37915
+ const primaryTarget = draftWorkspace.targets.find(target => target.target.componentType === rootTarget.componentType &&
37916
+ target.target.componentId === rootTarget.componentId) ?? draftWorkspace.targets[0];
37917
+ const workingDecoded = primaryTarget
37918
+ ? decodePageAuthoringDocument(primaryTarget.working.document)
37919
+ : null;
37920
+ const baselineDecoded = primaryTarget
37921
+ ? decodePageAuthoringDocument(primaryTarget.baseline.document)
37922
+ : null;
37923
+ const workingDocument = workingDecoded?.ok ? workingDecoded.value : this.snapshot.workingDocument;
37924
+ const baselineDocument = baselineDecoded?.ok
37925
+ ? baselineDecoded.value
37926
+ : this.snapshot.baselineDocument;
37927
+ const hasFrozenRelease = Boolean(draftWorkspace.frozenReleaseRef);
37928
+ const keepReviewReceipt = hasFrozenRelease &&
37929
+ this.snapshot.reviewReceipt?.releaseRef === draftWorkspace.frozenReleaseRef;
37930
+ this.publishSnapshot({
37931
+ ...this.snapshot,
37932
+ pageId,
37933
+ rootTarget,
37934
+ source,
37935
+ draftWorkspace,
37936
+ draftEtag,
37937
+ workingDocument,
37938
+ baselineDocument,
37939
+ reviewReceipt: keepReviewReceipt ? this.snapshot.reviewReceipt : null,
37940
+ reviewEtag: keepReviewReceipt ? this.snapshot.reviewEtag : null,
37941
+ childSources: Array.from(this.childSourcesByWidgetId.values()),
37942
+ dirty: false,
37943
+ fencedOperations: Object.fromEntries(this.fencedOperations),
37944
+ lastError: null,
37945
+ });
37946
+ }
37947
+ async refreshPublishedAndLifecycleMetadata(pageId, rootTarget, frozenReleaseRef, activeGen, activeCtx) {
37948
+ let publishedSnapshot = this.snapshot.publishedSnapshot;
37949
+ let publishedEtag = this.snapshot.publishedEtag;
37950
+ try {
37951
+ const publishedRead = await this.persistence.readPublishedPage(pageId);
37952
+ if (this.isStale(activeGen, activeCtx)) {
37953
+ return;
37954
+ }
37955
+ if ('body' in publishedRead) {
37956
+ publishedSnapshot = publishedRead.body;
37957
+ publishedEtag = publishedRead.etag;
37958
+ }
37959
+ }
37960
+ catch {
37961
+ // Newly created pages may not have a published release yet (404).
37962
+ }
37963
+ let headReceipt = this.snapshot.headReceipt;
37964
+ let headEtag = this.snapshot.headEtag;
37965
+ try {
37966
+ const headRead = await this.persistence.readHead(rootTarget);
37967
+ if (this.isStale(activeGen, activeCtx)) {
37968
+ return;
37969
+ }
37970
+ if ('body' in headRead) {
37971
+ headReceipt = headRead.body;
37972
+ headEtag = headRead.etag;
37973
+ }
37974
+ }
37975
+ catch {
37976
+ // Unreleased pages may not have a head receipt yet.
37977
+ }
37978
+ let reviewReceipt = this.snapshot.reviewReceipt;
37979
+ let reviewEtag = this.snapshot.reviewEtag;
37980
+ if (frozenReleaseRef) {
37981
+ try {
37982
+ const reviewRead = await this.persistence.readReview(rootTarget, frozenReleaseRef);
37983
+ if (this.isStale(activeGen, activeCtx)) {
37984
+ return;
37985
+ }
37986
+ if ('body' in reviewRead) {
37987
+ reviewReceipt = reviewRead.body;
37988
+ reviewEtag = reviewRead.etag;
37989
+ }
37990
+ }
37991
+ catch {
37992
+ // Review receipt may be unavailable if not yet created.
37993
+ }
37994
+ }
37995
+ this.publishSnapshot({
37996
+ ...this.snapshot,
37997
+ publishedSnapshot,
37998
+ publishedEtag,
37999
+ headReceipt,
38000
+ headEtag,
38001
+ reviewReceipt,
38002
+ reviewEtag,
38003
+ });
38004
+ }
38005
+ requireCapability(key, message) {
38006
+ if (!this.snapshot.capabilities[key]) {
38007
+ throw this.fail('CAPABILITY_DENIED', message);
38008
+ }
38009
+ }
38010
+ requireWorkingDocument() {
38011
+ const doc = this.snapshot.workingDocument;
38012
+ if (!doc) {
38013
+ throw this.fail('DRAFT_NOT_LOADED', 'No governed page authoring document is currently loaded.');
38014
+ }
38015
+ return doc;
38016
+ }
38017
+ requireLoadedDraft() {
38018
+ const { pageId, rootTarget, draftWorkspace, draftEtag } = this.snapshot;
38019
+ if (!pageId || !rootTarget || !draftWorkspace || !draftEtag) {
38020
+ throw this.fail('DRAFT_NOT_LOADED', 'No governed page draft workspace is currently loaded.');
38021
+ }
38022
+ return { pageId, rootTarget, draftWorkspace, draftEtag };
38023
+ }
38024
+ assertNotFenced(attemptKey) {
38025
+ if (this.fencedOperations.has(attemptKey)) {
38026
+ throw this.fail('OPERATION_FENCED', `Operation "${attemptKey}" is fenced due to an uncertain command outcome. Reconcile before retrying.`);
38027
+ }
38028
+ }
38029
+ isStale(generation, contextKey) {
38030
+ return generation !== this.contextGeneration || contextKey !== this.contextKey;
38031
+ }
38032
+ assertActiveContext(generation, contextKey) {
38033
+ if (this.isStale(generation, contextKey)) {
38034
+ throw new UiLayoutLifecycleContractError('CONTEXT_OBSOLETE');
38035
+ }
38036
+ }
38037
+ handleOperationError(error, generation, contextKey, fenceContext) {
38038
+ if (this.isStale(generation, contextKey)) {
38039
+ return {
38040
+ code: 'CONTEXT_OBSOLETE',
38041
+ message: 'Operation response discarded because the runtime context changed.',
38042
+ };
38043
+ }
38044
+ if (error instanceof UiLayoutLifecycleContractError ||
38045
+ this.isLifecycleContractErrorLike(error)) {
38046
+ if (error.code === 'COMMAND_OUTCOME_UNCERTAIN' && fenceContext) {
38047
+ this.fencedOperations.set(fenceContext.attemptKey, {
38048
+ ...fenceContext,
38049
+ occurredAt: new Date().toISOString(),
38050
+ });
38051
+ }
38052
+ const mapped = {
38053
+ code: error.code === 'COMMAND_OUTCOME_UNCERTAIN'
38054
+ ? 'COMMAND_OUTCOME_UNCERTAIN'
38055
+ : error.code === 'CONTEXT_OBSOLETE'
38056
+ ? 'CONTEXT_OBSOLETE'
38057
+ : 'TRANSPORT_ERROR',
38058
+ message: error.message,
38059
+ };
38060
+ this.publishSnapshot({
38061
+ ...this.snapshot,
38062
+ fencedOperations: Object.fromEntries(this.fencedOperations),
38063
+ lastError: mapped,
38064
+ });
38065
+ return mapped;
38066
+ }
38067
+ if (error instanceof UiLayoutLifecycleHttpError) {
38068
+ const code = error.status === 403
38069
+ ? 'CAPABILITY_DENIED'
38070
+ : error.status === 409
38071
+ ? 'CONFLICT'
38072
+ : error.status === 412
38073
+ ? 'PRECONDITION_FAILED'
38074
+ : 'TRANSPORT_ERROR';
38075
+ const mapped = {
38076
+ code,
38077
+ message: error.message,
38078
+ status: error.status,
38079
+ };
38080
+ this.publishSnapshot({ ...this.snapshot, lastError: mapped });
38081
+ return mapped;
38082
+ }
38083
+ if (this.isWorkflowError(error)) {
38084
+ this.publishSnapshot({ ...this.snapshot, lastError: error });
38085
+ return error;
38086
+ }
38087
+ const fallback = {
38088
+ code: 'TRANSPORT_ERROR',
38089
+ message: error instanceof Error ? error.message : String(error),
38090
+ };
38091
+ this.publishSnapshot({ ...this.snapshot, lastError: fallback });
38092
+ return fallback;
38093
+ }
38094
+ fail(code, message, diagnostics) {
38095
+ const err = {
38096
+ code,
38097
+ message,
38098
+ ...(diagnostics ? { diagnostics } : {}),
38099
+ };
38100
+ this.publishSnapshot({ ...this.snapshot, lastError: err });
38101
+ return err;
38102
+ }
38103
+ isLifecycleContractErrorLike(value) {
38104
+ return (value instanceof Error &&
38105
+ value.name === 'UiLayoutLifecycleContractError' &&
38106
+ 'code' in value &&
38107
+ typeof value.code === 'string');
38108
+ }
38109
+ isWorkflowError(value) {
38110
+ return (value !== null &&
38111
+ typeof value === 'object' &&
38112
+ !(value instanceof Error) &&
38113
+ 'code' in value &&
38114
+ 'message' in value &&
38115
+ typeof value.code === 'string');
38116
+ }
38117
+ setBusy(busy) {
38118
+ this.publishSnapshot({ ...this.snapshot, busy });
38119
+ }
38120
+ publishSnapshot(next) {
38121
+ this.stateSubject.next(next);
38122
+ }
38123
+ }
38124
+
36007
38125
  /*
36008
38126
  * Public API Surface of @praxisui/page-builder
36009
38127
  */
@@ -36013,4 +38131,4 @@ function providePraxisPageBuilderWidgets(widgets) {
36013
38131
  * Generated bundle index. Do not edit.
36014
38132
  */
36015
38133
 
36016
- export { ComponentPaletteDialogComponent, ConfirmDialogComponent, ConnectionEditorComponent, DynamicPageBuilderComponent, DynamicPageConfigEditorComponent, FloatingToolbarComponent, PAGE_BUILDER_AGENTIC_AUTHORING_OPTIONS, PAGE_BUILDER_AI_CAPABILITIES, PAGE_BUILDER_COMPONENT_CONTEXT_PACK, PAGE_BUILDER_WIDGET_AI_CATALOGS, PLACEHOLDER, PRAXIS_PAGE_BUILDER_AUTHORING_MANIFEST, PRAXIS_PAGE_BUILDER_COMPONENT_METADATA, PageBuilderAgenticAuthoringService, PageConfigEditorComponent, TileToolbarComponent, UiCompositionResourcePreflightService, WidgetShellEditorComponent, clearWidgetAiCatalogs, compileUiCompositionPlan, getPageAiCatalog, getWidgetAiCapabilities, isComponentDocMeta, preflightUiCompositionPlan, preflightUiCompositionPlanResources, providePageBuilderWidgetAiCatalogs, providePraxisPageBuilderMetadata, providePraxisPageBuilderWidgets, registerWidgetAiCatalog, registerWidgetAiCatalogs, resolveUiCompositionPlanTemplate, validateUiCompositionPlan, verifyUiCompositionAuthoringSource };
38134
+ export { ComponentPaletteDialogComponent, ConfirmDialogComponent, ConnectionEditorComponent, DEFAULT_CORPORATE_NAVIGATION_GROUPS_TARGET, DEFAULT_GOVERNED_PAGE_CHILD_VALIDATORS, DEFAULT_GOVERNED_PAGE_PREVIEW_ADAPTERS, DynamicPageBuilderComponent, DynamicPageConfigEditorComponent, FloatingToolbarComponent, GOVERNED_PAGE_CAPABILITY_KEYS, GovernedPageNavigationController, GovernedPageWorkflowController, LEGACY_PUBLISHED_PAGE_STORAGE_PREFIX, PAGE_BUILDER_AGENTIC_AUTHORING_OPTIONS, PAGE_BUILDER_AI_CAPABILITIES, PAGE_BUILDER_COMPONENT_CONTEXT_PACK, PAGE_BUILDER_WIDGET_AI_CATALOGS, PLACEHOLDER, PRAXIS_PAGE_BUILDER_AUTHORING_MANIFEST, PRAXIS_PAGE_BUILDER_COMPONENT_METADATA, PageBuilderAgenticAuthoringService, PageConfigEditorComponent, PraxisPageCopyActionComponent, PraxisPageSaveAsComponent, TileToolbarComponent, UiCompositionResourcePreflightService, WidgetShellEditorComponent, clearWidgetAiCatalogs, compileUiCompositionPlan, createCorporateGovernedPagePersistenceAdapter, createLocalPageSaveAsAdapter, deriveGovernedPageCapabilities, getPageAiCatalog, getWidgetAiCapabilities, inspectLegacyPublishedPagePayload, isComponentDocMeta, preflightUiCompositionPlan, preflightUiCompositionPlanResources, providePageBuilderWidgetAiCatalogs, providePraxisPageBuilderMetadata, providePraxisPageBuilderWidgets, registerWidgetAiCatalog, registerWidgetAiCatalogs, resolveUiCompositionPlanTemplate, validateUiCompositionPlan, verifyUiCompositionAuthoringSource };