@praxisui/page-builder 10.0.0-rc.36 → 10.0.0-rc.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -0
- package/ai/component-registry.json +14 -6
- package/fesm2022/praxisui-page-builder.mjs +825 -188
- package/package.json +4 -4
- package/types/praxisui-page-builder.d.ts +194 -80
|
@@ -7,33 +7,34 @@ 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 } from '@praxisui/core';
|
|
11
11
|
import * as i3 from '@angular/material/tooltip';
|
|
12
12
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
13
|
-
import
|
|
14
|
-
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
15
|
-
import * as i8 from '@angular/material/input';
|
|
16
|
-
import { MatInputModule } from '@angular/material/input';
|
|
13
|
+
import { A11yModule, CdkTrapFocus } from '@angular/cdk/a11y';
|
|
17
14
|
import * as i1$1 from '@angular/forms';
|
|
18
15
|
import { FormsModule, FormGroup, FormArray, FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
|
|
19
16
|
import * as i1$2 from '@angular/common';
|
|
20
17
|
import { CommonModule, NgTemplateOutlet } from '@angular/common';
|
|
21
|
-
import * as i4 from '@angular/material/
|
|
18
|
+
import * as i4 from '@angular/material/button-toggle';
|
|
19
|
+
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
|
20
|
+
import * as i5 from '@angular/material/checkbox';
|
|
22
21
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
|
23
|
-
import * as i3$1 from '@angular/material/
|
|
22
|
+
import * as i3$1 from '@angular/material/form-field';
|
|
23
|
+
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
24
|
+
import * as i4$1 from '@angular/material/input';
|
|
25
|
+
import { MatInputModule } from '@angular/material/input';
|
|
26
|
+
import * as i3$2 from '@angular/material/select';
|
|
24
27
|
import { MatSelectModule } from '@angular/material/select';
|
|
25
28
|
import { BehaviorSubject, merge, timeout, map, catchError, throwError, firstValueFrom, from, share, concatMap, of, timer, takeUntil, Subject, tap, filter, take, isObservable } from 'rxjs';
|
|
26
29
|
import { SETTINGS_PANEL_DATA } from '@praxisui/settings-panel';
|
|
27
30
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
28
|
-
import { CdkTrapFocus } from '@angular/cdk/a11y';
|
|
29
31
|
import * as i2$3 from '@angular/material/autocomplete';
|
|
30
32
|
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
|
31
|
-
import * as i5 from '@angular/material/button-toggle';
|
|
32
|
-
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
|
33
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';
|
|
37
38
|
|
|
38
39
|
const PLACEHOLDER = 1;
|
|
39
40
|
|
|
@@ -363,6 +364,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
363
364
|
}] } });
|
|
364
365
|
|
|
365
366
|
const PRAXIS_PAGE_BUILDER_EN_US = {
|
|
367
|
+
'praxis.pageBuilder.shell.interactionHighlight.label': 'Selection highlight',
|
|
368
|
+
'praxis.pageBuilder.shell.interactionHighlight.inherit': 'Inherit',
|
|
369
|
+
'praxis.pageBuilder.shell.interactionHighlight.standard': 'Standard',
|
|
370
|
+
'praxis.pageBuilder.shell.interactionHighlight.gradient': 'Gradient',
|
|
371
|
+
'praxis.pageBuilder.shell.interactionHighlight.help': 'The preview simulates selection while editing. Inherit uses the preset or platform default.',
|
|
366
372
|
"praxis.pageBuilder.studio.gridSize": "Size in columns × rows",
|
|
367
373
|
"praxis.pageBuilder.studio.visualTab": "Visual templates",
|
|
368
374
|
"praxis.pageBuilder.studio.layoutTab": "Layout templates",
|
|
@@ -1526,6 +1532,18 @@ const PRAXIS_PAGE_BUILDER_EN_US = {
|
|
|
1526
1532
|
'praxis.pageBuilder.palette.componentSingular': "component",
|
|
1527
1533
|
'praxis.pageBuilder.palette.componentPlural': "components",
|
|
1528
1534
|
'praxis.pageBuilder.palette.done': "Done",
|
|
1535
|
+
'praxis.pageBuilder.palette.clearSearchAria': "Clear search",
|
|
1536
|
+
'praxis.pageBuilder.palette.availableSingular': "available",
|
|
1537
|
+
'praxis.pageBuilder.palette.kbdInsert': "insert",
|
|
1538
|
+
'praxis.pageBuilder.palette.kbdQueue': "queue",
|
|
1539
|
+
'praxis.pageBuilder.palette.kbdNavigate': "navigate",
|
|
1540
|
+
'praxis.pageBuilder.palette.categoryAria': "Filter catalog by category",
|
|
1541
|
+
'praxis.pageBuilder.palette.category.all': "All",
|
|
1542
|
+
'praxis.pageBuilder.palette.category.widgets': "Page widgets",
|
|
1543
|
+
'praxis.pageBuilder.palette.category.presets': "Presets",
|
|
1544
|
+
'praxis.pageBuilder.palette.category.fields': "Fields",
|
|
1545
|
+
'praxis.pageBuilder.palette.emptySearchHint': "Try another search term or clear active filters to view the full catalog.",
|
|
1546
|
+
'praxis.pageBuilder.palette.clearFilters': "Clear filters",
|
|
1529
1547
|
'praxis.pageBuilder.drawer.contextSwitcherAria': "Switch edited widget without closing the drawer",
|
|
1530
1548
|
'praxis.pageBuilder.drawer.prevWidgetAria': "Previous widget",
|
|
1531
1549
|
'praxis.pageBuilder.drawer.nextWidgetAria': "Next widget",
|
|
@@ -1533,9 +1551,27 @@ const PRAXIS_PAGE_BUILDER_EN_US = {
|
|
|
1533
1551
|
'praxis.pageBuilder.editor.state.syncDataSourcesToState': "Sync target paths to state.values",
|
|
1534
1552
|
'praxis.pageBuilder.studio.composer.groupFieldHint': "Choose the boolean or categorical field used in summary indicators.",
|
|
1535
1553
|
'praxis.pageBuilder.studio.composer.statsUnsupportedHint': "This resource does not publish stats/group-by aggregation; summary indicators were disabled automatically.",
|
|
1554
|
+
'praxis.pageBuilder.saveAs.title': 'Page name',
|
|
1555
|
+
'praxis.pageBuilder.saveAs.menu': 'Menu name',
|
|
1556
|
+
'praxis.pageBuilder.saveAs.menuHint': 'Use an existing menu name or create a new one.',
|
|
1557
|
+
'praxis.pageBuilder.saveAs.icon': 'Choose menu icon',
|
|
1558
|
+
'praxis.pageBuilder.saveAs.save': 'Save as new page',
|
|
1559
|
+
'praxis.pageBuilder.saveAs.saving': 'Saving…',
|
|
1560
|
+
'praxis.pageBuilder.saveAs.saved': 'Page and menu saved.',
|
|
1561
|
+
'praxis.pageBuilder.saveAs.open': 'Open saved page',
|
|
1562
|
+
'praxis.pageBuilder.saveAs.failed': 'Could not confirm the saved page. Review the destination and try again.',
|
|
1563
|
+
'praxis.pageBuilder.saveAs.invalidIcon': 'Choose a Material Icons name without a family prefix.',
|
|
1564
|
+
'praxis.pageBuilder.saveAs.invalidName': 'Enter a valid name within the allowed length.',
|
|
1565
|
+
'praxis.pageBuilder.saveAs.copy': 'Create page copy',
|
|
1566
|
+
'praxis.pageBuilder.saveAs.copyFailed': 'Could not prepare a copy of this page.',
|
|
1536
1567
|
};
|
|
1537
1568
|
|
|
1538
1569
|
const PRAXIS_PAGE_BUILDER_PT_BR = {
|
|
1570
|
+
'praxis.pageBuilder.shell.interactionHighlight.label': 'Destaque de seleção',
|
|
1571
|
+
'praxis.pageBuilder.shell.interactionHighlight.inherit': 'Herdar',
|
|
1572
|
+
'praxis.pageBuilder.shell.interactionHighlight.standard': 'Padrão',
|
|
1573
|
+
'praxis.pageBuilder.shell.interactionHighlight.gradient': 'Gradiente',
|
|
1574
|
+
'praxis.pageBuilder.shell.interactionHighlight.help': 'A prévia simula a seleção durante a edição. Herdar usa o preset ou o padrão da plataforma.',
|
|
1539
1575
|
"praxis.pageBuilder.studio.gridSize": "Tamanho em colunas × linhas",
|
|
1540
1576
|
"praxis.pageBuilder.studio.visualTab": "Templates visuais",
|
|
1541
1577
|
"praxis.pageBuilder.studio.layoutTab": "Templates de layout",
|
|
@@ -2699,6 +2735,18 @@ const PRAXIS_PAGE_BUILDER_PT_BR = {
|
|
|
2699
2735
|
'praxis.pageBuilder.palette.componentSingular': "componente",
|
|
2700
2736
|
'praxis.pageBuilder.palette.componentPlural': "componentes",
|
|
2701
2737
|
'praxis.pageBuilder.palette.done': "Concluir",
|
|
2738
|
+
'praxis.pageBuilder.palette.clearSearchAria': "Limpar busca",
|
|
2739
|
+
'praxis.pageBuilder.palette.availableSingular': "disponível",
|
|
2740
|
+
'praxis.pageBuilder.palette.kbdInsert': "inserir",
|
|
2741
|
+
'praxis.pageBuilder.palette.kbdQueue': "fila",
|
|
2742
|
+
'praxis.pageBuilder.palette.kbdNavigate': "navegar",
|
|
2743
|
+
'praxis.pageBuilder.palette.categoryAria': "Filtrar catálogo por categoria",
|
|
2744
|
+
'praxis.pageBuilder.palette.category.all': "Todos",
|
|
2745
|
+
'praxis.pageBuilder.palette.category.widgets': "Widgets de página",
|
|
2746
|
+
'praxis.pageBuilder.palette.category.presets': "Predefinições",
|
|
2747
|
+
'praxis.pageBuilder.palette.category.fields': "Campos",
|
|
2748
|
+
'praxis.pageBuilder.palette.emptySearchHint': "Tente outro termo ou limpe os filtros ativos para exibir todo o catálogo.",
|
|
2749
|
+
'praxis.pageBuilder.palette.clearFilters': "Limpar filtros",
|
|
2702
2750
|
'praxis.pageBuilder.drawer.contextSwitcherAria': "Alternar widget editado sem fechar a gaveta",
|
|
2703
2751
|
'praxis.pageBuilder.drawer.prevWidgetAria': "Widget anterior",
|
|
2704
2752
|
'praxis.pageBuilder.drawer.nextWidgetAria': "Próximo widget",
|
|
@@ -2706,6 +2754,19 @@ const PRAXIS_PAGE_BUILDER_PT_BR = {
|
|
|
2706
2754
|
'praxis.pageBuilder.editor.state.syncDataSourcesToState': "Sincronizar targetPaths em state.values",
|
|
2707
2755
|
'praxis.pageBuilder.studio.composer.groupFieldHint': "Escolha o campo booleano ou categórico usado nos indicadores de resumo.",
|
|
2708
2756
|
'praxis.pageBuilder.studio.composer.statsUnsupportedHint': "Este recurso não publica agregação stats/group-by; os indicadores de resumo foram desativados automaticamente.",
|
|
2757
|
+
'praxis.pageBuilder.saveAs.title': 'Nome da página',
|
|
2758
|
+
'praxis.pageBuilder.saveAs.menu': 'Nome do menu',
|
|
2759
|
+
'praxis.pageBuilder.saveAs.menuHint': 'Use o nome de um menu existente ou crie outro.',
|
|
2760
|
+
'praxis.pageBuilder.saveAs.icon': 'Escolher ícone do menu',
|
|
2761
|
+
'praxis.pageBuilder.saveAs.save': 'Salvar como nova página',
|
|
2762
|
+
'praxis.pageBuilder.saveAs.saving': 'Salvando…',
|
|
2763
|
+
'praxis.pageBuilder.saveAs.saved': 'Página e menu salvos.',
|
|
2764
|
+
'praxis.pageBuilder.saveAs.open': 'Abrir página salva',
|
|
2765
|
+
'praxis.pageBuilder.saveAs.failed': 'Não foi possível confirmar a página salva. Confira o destino e tente novamente.',
|
|
2766
|
+
'praxis.pageBuilder.saveAs.invalidIcon': 'Escolha um nome Material Icons sem prefixo de família.',
|
|
2767
|
+
'praxis.pageBuilder.saveAs.invalidName': 'Informe um nome válido dentro do limite permitido.',
|
|
2768
|
+
'praxis.pageBuilder.saveAs.copy': 'Criar cópia da página',
|
|
2769
|
+
'praxis.pageBuilder.saveAs.copyFailed': 'Não foi possível preparar a cópia desta página.',
|
|
2709
2770
|
};
|
|
2710
2771
|
|
|
2711
2772
|
function createPraxisPageBuilderI18nConfig(options = {}) {
|
|
@@ -2740,6 +2801,18 @@ function resolvePraxisPageBuilderText(i18n, key, fallback) {
|
|
|
2740
2801
|
return i18n.t(namespacedKey, undefined, fallback) || fallback;
|
|
2741
2802
|
}
|
|
2742
2803
|
|
|
2804
|
+
/**
|
|
2805
|
+
* Studio Component Palette Dialog for the Praxis Page Builder.
|
|
2806
|
+
*
|
|
2807
|
+
* Provides three enterprise authoring modes:
|
|
2808
|
+
* 1. **Single Insertion**: Immediate selection and dialog dismissal (`Enter` or card click).
|
|
2809
|
+
* 2. **Continuous Multi-Insert (`keepOpen`)**: Inserts directly onto the canvas without closing the dialog.
|
|
2810
|
+
* 3. **Batch Staging (`batchMode`)**: Stages multiple components with live grid coordinate preview (`L·C`)
|
|
2811
|
+
* and commits them atomically via `onInsertBatch`.
|
|
2812
|
+
*
|
|
2813
|
+
* Automatically prioritizes governed page-level widgets and presets ahead of primitive form field controls
|
|
2814
|
+
* and exposes quick category filter pills for large enterprise catalogs.
|
|
2815
|
+
*/
|
|
2743
2816
|
class ComponentPaletteDialogComponent {
|
|
2744
2817
|
dialogRef;
|
|
2745
2818
|
registry;
|
|
@@ -2748,6 +2821,7 @@ class ComponentPaletteDialogComponent {
|
|
|
2748
2821
|
hostElement = inject((ElementRef));
|
|
2749
2822
|
queueSeq = 0;
|
|
2750
2823
|
query = signal('', ...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
|
|
2824
|
+
selectedCategory = signal('all', ...(ngDevMode ? [{ debugName: "selectedCategory" }] : /* istanbul ignore next */ []));
|
|
2751
2825
|
selectedColSpan = signal(undefined, ...(ngDevMode ? [{ debugName: "selectedColSpan" }] : /* istanbul ignore next */ []));
|
|
2752
2826
|
batchMode = signal(false, ...(ngDevMode ? [{ debugName: "batchMode" }] : /* istanbul ignore next */ []));
|
|
2753
2827
|
keepOpen = signal(false, ...(ngDevMode ? [{ debugName: "keepOpen" }] : /* istanbul ignore next */ []));
|
|
@@ -2763,15 +2837,53 @@ class ComponentPaletteDialogComponent {
|
|
|
2763
2837
|
return 0;
|
|
2764
2838
|
return new Set(items.map((item) => item.rowPreview)).size;
|
|
2765
2839
|
}, ...(ngDevMode ? [{ debugName: "stagedRowCount" }] : /* istanbul ignore next */ []));
|
|
2766
|
-
spanOptions =
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2840
|
+
spanOptions = computed(() => {
|
|
2841
|
+
const cols = this.canvasColumns();
|
|
2842
|
+
if (cols === 12) {
|
|
2843
|
+
return [
|
|
2844
|
+
{ label: 'Auto', value: undefined },
|
|
2845
|
+
{ label: '25% (3 col)', value: 3 },
|
|
2846
|
+
{ label: '33% (4 col)', value: 4 },
|
|
2847
|
+
{ label: '50% (6 col)', value: 6 },
|
|
2848
|
+
{ label: '100% (12 col)', value: 12 },
|
|
2849
|
+
];
|
|
2850
|
+
}
|
|
2851
|
+
const q1 = Math.max(1, Math.round(cols * 0.25));
|
|
2852
|
+
const q3 = Math.max(1, Math.round(cols / 3));
|
|
2853
|
+
const half = Math.max(1, Math.round(cols * 0.5));
|
|
2854
|
+
const seen = new Set();
|
|
2855
|
+
const dynamic = [{ label: 'Auto', value: undefined }];
|
|
2856
|
+
for (const [pct, val] of [[25, q1], [33, q3], [50, half], [100, cols]]) {
|
|
2857
|
+
if (!seen.has(val)) {
|
|
2858
|
+
seen.add(val);
|
|
2859
|
+
dynamic.push({ label: `${pct}% (${val} col)`, value: val });
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
return dynamic;
|
|
2863
|
+
}, ...(ngDevMode ? [{ debugName: "spanOptions" }] : /* istanbul ignore next */ []));
|
|
2773
2864
|
cycleSpans = [3, 4, 6, 8, 12];
|
|
2774
2865
|
all = signal([], ...(ngDevMode ? [{ debugName: "all" }] : /* istanbul ignore next */ []));
|
|
2866
|
+
allowedCatalog = computed(() => this.applyStructuralConstraints(), ...(ngDevMode ? [{ debugName: "allowedCatalog" }] : /* istanbul ignore next */ []));
|
|
2867
|
+
categoryOptions = computed(() => {
|
|
2868
|
+
const entries = this.allowedCatalog();
|
|
2869
|
+
const counts = {
|
|
2870
|
+
all: entries.length,
|
|
2871
|
+
widget: 0,
|
|
2872
|
+
preset: 0,
|
|
2873
|
+
field: 0,
|
|
2874
|
+
};
|
|
2875
|
+
for (const entry of entries) {
|
|
2876
|
+
counts[entry.category] += 1;
|
|
2877
|
+
}
|
|
2878
|
+
const options = [
|
|
2879
|
+
{ id: 'all', label: this.tx('palette.category.all', 'Todos'), count: counts.all },
|
|
2880
|
+
{ id: 'widget', label: this.tx('palette.category.widgets', 'Widgets de página'), count: counts.widget },
|
|
2881
|
+
{ id: 'preset', label: this.tx('palette.category.presets', 'Predefinições'), count: counts.preset },
|
|
2882
|
+
{ id: 'field', label: this.tx('palette.category.fields', 'Campos'), count: counts.field },
|
|
2883
|
+
];
|
|
2884
|
+
return options.filter((opt) => opt.id === 'all' || opt.count > 0);
|
|
2885
|
+
}, ...(ngDevMode ? [{ debugName: "categoryOptions" }] : /* istanbul ignore next */ []));
|
|
2886
|
+
hasMultipleCategories = computed(() => this.categoryOptions().length > 2, ...(ngDevMode ? [{ debugName: "hasMultipleCategories" }] : /* istanbul ignore next */ []));
|
|
2775
2887
|
filtered = computed(() => this.applyFilters(), ...(ngDevMode ? [{ debugName: "filtered" }] : /* istanbul ignore next */ []));
|
|
2776
2888
|
density = computed(() => {
|
|
2777
2889
|
const n = this.filtered().length;
|
|
@@ -2816,8 +2928,8 @@ class ComponentPaletteDialogComponent {
|
|
|
2816
2928
|
}
|
|
2817
2929
|
resolveEffectiveCols(selection) {
|
|
2818
2930
|
const columns = this.canvasColumns();
|
|
2819
|
-
if (typeof selection.colSpan === 'number' && selection.colSpan
|
|
2820
|
-
return Math.trunc(selection.colSpan);
|
|
2931
|
+
if (typeof selection.colSpan === 'number' && Number.isFinite(selection.colSpan) && selection.colSpan >= 1) {
|
|
2932
|
+
return Math.min(columns, Math.trunc(selection.colSpan));
|
|
2821
2933
|
}
|
|
2822
2934
|
const metaHints = this.registry.get(selection.componentId)?.layoutHints;
|
|
2823
2935
|
if (metaHints?.recommendedCols && metaHints.recommendedCols >= 1) {
|
|
@@ -2825,11 +2937,11 @@ class ComponentPaletteDialogComponent {
|
|
|
2825
2937
|
}
|
|
2826
2938
|
switch (selection.componentId) {
|
|
2827
2939
|
case 'praxis-filter':
|
|
2940
|
+
case 'praxis-crud':
|
|
2828
2941
|
return columns;
|
|
2829
2942
|
case 'praxis-table':
|
|
2830
2943
|
case 'praxis-chart':
|
|
2831
2944
|
case 'praxis-list':
|
|
2832
|
-
case 'praxis-crud':
|
|
2833
2945
|
return Math.min(columns, 8);
|
|
2834
2946
|
default:
|
|
2835
2947
|
return Math.min(columns, 4);
|
|
@@ -2842,12 +2954,12 @@ class ComponentPaletteDialogComponent {
|
|
|
2842
2954
|
}
|
|
2843
2955
|
switch (selection.componentId) {
|
|
2844
2956
|
case 'praxis-table':
|
|
2957
|
+
case 'praxis-crud':
|
|
2845
2958
|
return 6;
|
|
2846
2959
|
case 'praxis-filter':
|
|
2847
2960
|
return 3;
|
|
2848
2961
|
case 'praxis-chart':
|
|
2849
2962
|
case 'praxis-list':
|
|
2850
|
-
case 'praxis-crud':
|
|
2851
2963
|
return 5;
|
|
2852
2964
|
default:
|
|
2853
2965
|
return 2;
|
|
@@ -2905,6 +3017,12 @@ class ComponentPaletteDialogComponent {
|
|
|
2905
3017
|
enqueue(entry, event) {
|
|
2906
3018
|
event?.stopPropagation();
|
|
2907
3019
|
event?.preventDefault();
|
|
3020
|
+
if (!this.batchMode()) {
|
|
3021
|
+
this.batchMode.set(true);
|
|
3022
|
+
if (this.keepOpen()) {
|
|
3023
|
+
this.keepOpen.set(false);
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
2908
3026
|
const selection = this.buildPayload(entry);
|
|
2909
3027
|
const effectiveCols = this.resolveEffectiveCols(selection);
|
|
2910
3028
|
const effectiveRows = this.resolveEffectiveRows(selection);
|
|
@@ -2975,7 +3093,7 @@ class ComponentPaletteDialogComponent {
|
|
|
2975
3093
|
this.dialogRef.close(selections[0]);
|
|
2976
3094
|
}
|
|
2977
3095
|
select(entry) {
|
|
2978
|
-
if (this.batchMode()) {
|
|
3096
|
+
if (this.batchMode() || (!this.keepOpen() && this.queuedItems().length > 0)) {
|
|
2979
3097
|
this.enqueue(entry);
|
|
2980
3098
|
return;
|
|
2981
3099
|
}
|
|
@@ -2988,9 +3106,37 @@ class ComponentPaletteDialogComponent {
|
|
|
2988
3106
|
this.dialogRef.close(payload);
|
|
2989
3107
|
}
|
|
2990
3108
|
handleSearchKeydown(event) {
|
|
2991
|
-
if (event.key
|
|
3109
|
+
if (event.key === 'ArrowDown') {
|
|
3110
|
+
this.focusPaletteCard(0, event);
|
|
3111
|
+
return;
|
|
3112
|
+
}
|
|
3113
|
+
if (event.key === 'Escape' && this.query()) {
|
|
3114
|
+
event.preventDefault();
|
|
3115
|
+
event.stopPropagation();
|
|
3116
|
+
this.clearSearch();
|
|
2992
3117
|
return;
|
|
2993
|
-
|
|
3118
|
+
}
|
|
3119
|
+
if (event.key === 'Enter') {
|
|
3120
|
+
const results = this.filtered();
|
|
3121
|
+
if (results.length === 1 || (this.query().trim().length > 0 && results.length > 0)) {
|
|
3122
|
+
event.preventDefault();
|
|
3123
|
+
if (event.shiftKey) {
|
|
3124
|
+
this.enqueue(results[0], event);
|
|
3125
|
+
}
|
|
3126
|
+
else {
|
|
3127
|
+
this.select(results[0]);
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
clearSearch() {
|
|
3133
|
+
this.query.set('');
|
|
3134
|
+
this.searchInput()?.focus();
|
|
3135
|
+
}
|
|
3136
|
+
resetFilters() {
|
|
3137
|
+
this.query.set('');
|
|
3138
|
+
this.selectedCategory.set('all');
|
|
3139
|
+
this.searchInput()?.focus();
|
|
2994
3140
|
}
|
|
2995
3141
|
handleCardKeydown(event, index, entry) {
|
|
2996
3142
|
if (entry && event.shiftKey && (event.key === 'Enter' || event.key === ' ')) {
|
|
@@ -3046,20 +3192,27 @@ class ComponentPaletteDialogComponent {
|
|
|
3046
3192
|
}
|
|
3047
3193
|
// Create effect in injection context (field initializer is allowed)
|
|
3048
3194
|
_adjustSizeEffect = effect(() => {
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
if (n <= 4)
|
|
3052
|
-
width = '560px';
|
|
3053
|
-
else if (n <= 8)
|
|
3054
|
-
width = '680px';
|
|
3195
|
+
void this.filtered().length;
|
|
3196
|
+
const width = '780px';
|
|
3055
3197
|
const container = this.dialogRef._containerInstance;
|
|
3056
3198
|
if (container && container._config) {
|
|
3057
3199
|
container._config.width = width;
|
|
3200
|
+
container._config.maxWidth = '94vw';
|
|
3201
|
+
}
|
|
3202
|
+
const host = this.hostElement?.nativeElement;
|
|
3203
|
+
const dialogContainer = host?.closest('.mat-mdc-dialog-container');
|
|
3204
|
+
if (dialogContainer) {
|
|
3205
|
+
dialogContainer.style.setProperty('--mat-dialog-container-max-width', width);
|
|
3206
|
+
dialogContainer.style.maxWidth = `min(${width}, 94vw)`;
|
|
3058
3207
|
}
|
|
3208
|
+
const overlayPane = host?.closest('.cdk-overlay-pane');
|
|
3209
|
+
if (overlayPane) {
|
|
3210
|
+
overlayPane.style.maxWidth = '94vw';
|
|
3211
|
+
}
|
|
3212
|
+
this.dialogRef.updateSize?.(width);
|
|
3059
3213
|
}, ...(ngDevMode ? [{ debugName: "_adjustSizeEffect" }] : /* istanbul ignore next */ []));
|
|
3060
|
-
|
|
3214
|
+
applyStructuralConstraints() {
|
|
3061
3215
|
const list = (this.all() || []);
|
|
3062
|
-
const q = (this.query() || '').toLowerCase();
|
|
3063
3216
|
const allowIds = new Set((this.data?.allowedWidgetIds || []).map((s) => s.toLowerCase()));
|
|
3064
3217
|
const allowTags = new Set((this.data?.allowedWidgetTags || []).map((s) => s.toLowerCase()));
|
|
3065
3218
|
const allowPresets = new Set((this.data?.allowedPresetIds || []).map((s) => s.toLowerCase()));
|
|
@@ -3068,12 +3221,9 @@ class ComponentPaletteDialogComponent {
|
|
|
3068
3221
|
const meta = metadataById.get(entry.componentId);
|
|
3069
3222
|
if (!meta)
|
|
3070
3223
|
return false;
|
|
3071
|
-
if (q && !entry.searchText.includes(q)) {
|
|
3072
|
-
return false;
|
|
3073
|
-
}
|
|
3074
3224
|
if (allowIds.size > 0 && !allowIds.has((meta.id || '').toLowerCase()))
|
|
3075
3225
|
return false;
|
|
3076
|
-
if (allowTags.size > 0 && !meta.tags?.some(t => allowTags.has(t.toLowerCase())))
|
|
3226
|
+
if (allowTags.size > 0 && !meta.tags?.some((t) => allowTags.has(t.toLowerCase())))
|
|
3077
3227
|
return false;
|
|
3078
3228
|
if (entry.presetId && allowPresets.size > 0 && !allowPresets.has(entry.presetId.toLowerCase()))
|
|
3079
3229
|
return false;
|
|
@@ -3082,11 +3232,33 @@ class ComponentPaletteDialogComponent {
|
|
|
3082
3232
|
return true;
|
|
3083
3233
|
});
|
|
3084
3234
|
}
|
|
3235
|
+
applyFilters() {
|
|
3236
|
+
const list = this.allowedCatalog();
|
|
3237
|
+
const q = (this.query() || '').toLowerCase().trim();
|
|
3238
|
+
const cat = this.selectedCategory();
|
|
3239
|
+
return list.filter((entry) => {
|
|
3240
|
+
if (cat !== 'all' && entry.category !== cat) {
|
|
3241
|
+
return false;
|
|
3242
|
+
}
|
|
3243
|
+
if (q && !entry.searchText.includes(q)) {
|
|
3244
|
+
return false;
|
|
3245
|
+
}
|
|
3246
|
+
return true;
|
|
3247
|
+
});
|
|
3248
|
+
}
|
|
3249
|
+
isFieldMetadata(meta) {
|
|
3250
|
+
const tags = new Set((meta.tags ?? []).map((tag) => tag.toLowerCase()));
|
|
3251
|
+
return tags.has('field') || meta.lib === '@praxisui/dynamic-fields';
|
|
3252
|
+
}
|
|
3085
3253
|
buildEntries(metadata) {
|
|
3086
|
-
|
|
3254
|
+
const pageTier = [];
|
|
3255
|
+
const fieldTier = [];
|
|
3256
|
+
for (const meta of metadata) {
|
|
3257
|
+
const isField = this.isFieldMetadata(meta);
|
|
3087
3258
|
const baseEntry = {
|
|
3088
3259
|
key: meta.id,
|
|
3089
3260
|
componentId: meta.id,
|
|
3261
|
+
category: isField ? 'field' : 'widget',
|
|
3090
3262
|
title: meta.friendlyName || meta.id,
|
|
3091
3263
|
description: meta.description || meta.selector,
|
|
3092
3264
|
icon: meta.icon || 'widgets',
|
|
@@ -3097,6 +3269,7 @@ class ComponentPaletteDialogComponent {
|
|
|
3097
3269
|
key: `${meta.id}::${preset.id}`,
|
|
3098
3270
|
componentId: meta.id,
|
|
3099
3271
|
presetId: preset.id,
|
|
3272
|
+
category: 'preset',
|
|
3100
3273
|
title: `${meta.friendlyName || meta.id} · ${preset.label}`,
|
|
3101
3274
|
description: preset.description || meta.description || meta.selector,
|
|
3102
3275
|
icon: preset.icon || meta.icon || 'widgets',
|
|
@@ -3105,8 +3278,10 @@ class ComponentPaletteDialogComponent {
|
|
|
3105
3278
|
shell: preset.shell ? JSON.parse(JSON.stringify(preset.shell)) : undefined,
|
|
3106
3279
|
inputs: preset.inputs ? JSON.parse(JSON.stringify(preset.inputs)) : undefined,
|
|
3107
3280
|
}));
|
|
3108
|
-
|
|
3109
|
-
|
|
3281
|
+
const bucket = isField ? fieldTier : pageTier;
|
|
3282
|
+
bucket.push(baseEntry, ...presets);
|
|
3283
|
+
}
|
|
3284
|
+
return [...pageTier, ...fieldTier];
|
|
3110
3285
|
}
|
|
3111
3286
|
buildBadges(meta, isPreset) {
|
|
3112
3287
|
const badges = [];
|
|
@@ -3179,49 +3354,92 @@ class ComponentPaletteDialogComponent {
|
|
|
3179
3354
|
}
|
|
3180
3355
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ComponentPaletteDialogComponent, deps: [{ token: i1.MatDialogRef }, { token: i2$2.ComponentMetadataRegistry }, { token: i2$2.PraxisI18nService }, { token: MAT_DIALOG_DATA }], target: i0.ɵɵFactoryTarget.Component });
|
|
3181
3356
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: ComponentPaletteDialogComponent, isStandalone: true, selector: "praxis-component-palette-dialog", providers: [providePraxisPageBuilderI18n()], ngImport: i0, template: `
|
|
3182
|
-
<h2 mat-dialog-title>
|
|
3357
|
+
<h2 mat-dialog-title class="pdx-dialog-header">
|
|
3183
3358
|
<div class="dlg-title">
|
|
3184
|
-
<
|
|
3359
|
+
<div class="dlg-icon-wrap" aria-hidden="true">
|
|
3360
|
+
<mat-icon class="dlg-icon" [praxisIcon]="'widgets'"></mat-icon>
|
|
3361
|
+
</div>
|
|
3185
3362
|
<div class="dlg-texts">
|
|
3186
|
-
<div class="dlg-head">
|
|
3187
|
-
|
|
3363
|
+
<div class="dlg-head-row">
|
|
3364
|
+
<span class="dlg-head">{{ data?.title || tx('palette.title', 'Inserir componente') }}</span>
|
|
3365
|
+
<span class="dlg-count-pill" aria-live="polite">{{ filtered().length }} {{ filtered().length === 1 ? tx('palette.availableSingular', 'disponível') : tx('palette.available', 'disponíveis') }}</span>
|
|
3366
|
+
</div>
|
|
3367
|
+
<div class="dlg-sub">{{ tx('palette.subtitle', 'Escolha um componente para adicionar à página') }}</div>
|
|
3188
3368
|
</div>
|
|
3189
3369
|
</div>
|
|
3190
3370
|
</h2>
|
|
3191
3371
|
<div mat-dialog-content class="pdx-palette-content">
|
|
3192
|
-
<
|
|
3193
|
-
<
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3372
|
+
<div class="pdx-search-shell">
|
|
3373
|
+
<label class="visually-hidden" for="pdx-palette-search-input">
|
|
3374
|
+
{{ tx('palette.searchLabel', 'Buscar componente') }}
|
|
3375
|
+
</label>
|
|
3376
|
+
<div class="pdx-palette-search pdx-search-bar">
|
|
3377
|
+
<mat-icon class="pdx-search-icon" [praxisIcon]="'search'" aria-hidden="true"></mat-icon>
|
|
3378
|
+
<input
|
|
3379
|
+
id="pdx-palette-search-input"
|
|
3380
|
+
class="pdx-search-input"
|
|
3381
|
+
type="text"
|
|
3382
|
+
role="searchbox"
|
|
3383
|
+
cdkFocusInitial
|
|
3384
|
+
autocomplete="off"
|
|
3385
|
+
[ngModel]="query()"
|
|
3386
|
+
(ngModelChange)="query.set($event)"
|
|
3387
|
+
(keydown)="handleSearchKeydown($event)"
|
|
3388
|
+
[placeholder]="tx('palette.searchPlaceholder', 'Nome, capability ou readiness')"
|
|
3389
|
+
data-testid="component-palette-search"
|
|
3203
3390
|
/>
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3391
|
+
@if (query()) {
|
|
3392
|
+
<button
|
|
3393
|
+
type="button"
|
|
3394
|
+
class="pdx-search-clear"
|
|
3395
|
+
[attr.aria-label]="tx('palette.clearSearchAria', 'Limpar busca')"
|
|
3396
|
+
(click)="clearSearch()"
|
|
3397
|
+
>
|
|
3398
|
+
<mat-icon [praxisIcon]="'close'" aria-hidden="true"></mat-icon>
|
|
3399
|
+
</button>
|
|
3400
|
+
}
|
|
3401
|
+
<kbd class="pdx-search-kbd" aria-hidden="true">↓</kbd>
|
|
3402
|
+
</div>
|
|
3403
|
+
</div>
|
|
3404
|
+
|
|
3405
|
+
@if (hasMultipleCategories()) {
|
|
3406
|
+
<div class="pdx-category-tabs" role="group" [attr.aria-label]="tx('palette.categoryAria', 'Filtrar catálogo por categoria')" data-testid="component-palette-categories">
|
|
3407
|
+
@for (cat of categoryOptions(); track cat.id) {
|
|
3209
3408
|
<button
|
|
3210
|
-
mat-stroked-button
|
|
3211
3409
|
type="button"
|
|
3212
|
-
class="pdx-
|
|
3213
|
-
[class.selected]="
|
|
3214
|
-
[attr.aria-pressed]="
|
|
3215
|
-
[attr.data-testid]="'palette-
|
|
3216
|
-
(click)="
|
|
3410
|
+
class="pdx-category-pill"
|
|
3411
|
+
[class.selected]="selectedCategory() === cat.id"
|
|
3412
|
+
[attr.aria-pressed]="selectedCategory() === cat.id"
|
|
3413
|
+
[attr.data-testid]="'palette-category-' + cat.id"
|
|
3414
|
+
(click)="selectedCategory.set(cat.id)"
|
|
3217
3415
|
>
|
|
3218
|
-
{{
|
|
3416
|
+
<span>{{ cat.label }}</span>
|
|
3417
|
+
<span class="pdx-category-count">{{ cat.count }}</span>
|
|
3219
3418
|
</button>
|
|
3220
3419
|
}
|
|
3221
3420
|
</div>
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3423
|
+
<div class="pdx-palette-toolbar" data-testid="component-palette-toolbar">
|
|
3424
|
+
<div class="pdx-palette-span-group" role="group" [attr.aria-label]="tx('palette.initialWidth', 'Largura inicial')">
|
|
3425
|
+
<span class="pdx-palette-toolbar-label">{{ tx('palette.initialWidth', 'Largura inicial') }}</span>
|
|
3426
|
+
<div class="pdx-span-segmented">
|
|
3427
|
+
@for (opt of spanOptions(); track opt.value) {
|
|
3428
|
+
<button
|
|
3429
|
+
type="button"
|
|
3430
|
+
class="pdx-span-chip"
|
|
3431
|
+
[class.selected]="selectedColSpan() === opt.value"
|
|
3432
|
+
[attr.aria-pressed]="selectedColSpan() === opt.value"
|
|
3433
|
+
[attr.data-testid]="'palette-colspan-' + (opt.value ?? 'auto')"
|
|
3434
|
+
(click)="selectedColSpan.set(opt.value)"
|
|
3435
|
+
>
|
|
3436
|
+
{{ opt.label }}
|
|
3437
|
+
</button>
|
|
3438
|
+
}
|
|
3439
|
+
</div>
|
|
3440
|
+
</div>
|
|
3222
3441
|
<div class="pdx-palette-mode-actions">
|
|
3223
3442
|
<button
|
|
3224
|
-
mat-stroked-button
|
|
3225
3443
|
type="button"
|
|
3226
3444
|
class="pdx-batch-mode-chip"
|
|
3227
3445
|
[class.selected]="batchMode()"
|
|
@@ -3230,13 +3448,12 @@ class ComponentPaletteDialogComponent {
|
|
|
3230
3448
|
(click)="toggleBatchMode()"
|
|
3231
3449
|
>
|
|
3232
3450
|
<mat-icon [praxisIcon]="batchMode() ? 'playlist_add_check' : 'playlist_add'" aria-hidden="true"></mat-icon>
|
|
3233
|
-
{{ tx('palette.batchMode', 'Montar em lote') }}
|
|
3451
|
+
<span>{{ tx('palette.batchMode', 'Montar em lote') }}</span>
|
|
3234
3452
|
@if (queuedItems().length > 0) {
|
|
3235
3453
|
<span class="pdx-inserted-badge" data-testid="palette-queued-count">{{ queuedItems().length }}</span>
|
|
3236
3454
|
}
|
|
3237
3455
|
</button>
|
|
3238
3456
|
<button
|
|
3239
|
-
mat-stroked-button
|
|
3240
3457
|
type="button"
|
|
3241
3458
|
class="pdx-keep-open-chip"
|
|
3242
3459
|
[class.selected]="keepOpen()"
|
|
@@ -3245,13 +3462,14 @@ class ComponentPaletteDialogComponent {
|
|
|
3245
3462
|
(click)="toggleKeepOpen()"
|
|
3246
3463
|
>
|
|
3247
3464
|
<mat-icon [praxisIcon]="keepOpen() ? 'check_box' : 'check_box_outline_blank'" aria-hidden="true"></mat-icon>
|
|
3248
|
-
{{ tx('palette.keepOpen', 'Manter aberta') }}
|
|
3465
|
+
<span>{{ tx('palette.keepOpen', 'Manter aberta') }}</span>
|
|
3249
3466
|
@if (insertedCount() > 0) {
|
|
3250
3467
|
<span class="pdx-inserted-badge" data-testid="palette-inserted-count">+{{ insertedCount() }}</span>
|
|
3251
3468
|
}
|
|
3252
3469
|
</button>
|
|
3253
3470
|
</div>
|
|
3254
3471
|
</div>
|
|
3472
|
+
|
|
3255
3473
|
@if (queuedItems().length > 0) {
|
|
3256
3474
|
<div class="pdx-staging-tray" data-testid="palette-staging-tray" role="region" [attr.aria-label]="tx('palette.stagingTrayAria', 'Bandeja de composição em lote')">
|
|
3257
3475
|
<div class="pdx-staging-header">
|
|
@@ -3299,19 +3517,20 @@ class ComponentPaletteDialogComponent {
|
|
|
3299
3517
|
</div>
|
|
3300
3518
|
</div>
|
|
3301
3519
|
}
|
|
3520
|
+
|
|
3302
3521
|
@if (filtered().length) {
|
|
3303
|
-
<div [class]="'pdx-grid ' + density()">
|
|
3522
|
+
<div [class]="'pdx-grid ' + density() + (filtered().length === 1 ? ' single-entry' : '')">
|
|
3304
3523
|
@for (entry of filtered(); track trackById($index, entry)) {
|
|
3305
3524
|
<div class="pdx-card-item">
|
|
3306
3525
|
<button
|
|
3307
3526
|
#paletteCard
|
|
3308
|
-
class="pdx-card
|
|
3527
|
+
class="pdx-card"
|
|
3309
3528
|
type="button"
|
|
3310
3529
|
[attr.aria-label]="tx('palette.addAria', 'Adicionar') + ' ' + entry.title"
|
|
3311
3530
|
(click)="select(entry)"
|
|
3312
3531
|
(keydown)="handleCardKeydown($event, $index, entry)"
|
|
3313
|
-
|
|
3314
|
-
<div class="pdx-card-icon">
|
|
3532
|
+
>
|
|
3533
|
+
<div class="pdx-card-icon" aria-hidden="true">
|
|
3315
3534
|
<mat-icon [praxisIcon]="entry.icon"></mat-icon>
|
|
3316
3535
|
</div>
|
|
3317
3536
|
<div class="pdx-card-body">
|
|
@@ -3326,9 +3545,9 @@ class ComponentPaletteDialogComponent {
|
|
|
3326
3545
|
class="pdx-card-badge"
|
|
3327
3546
|
[class]="'pdx-card-badge ' + badge.tone"
|
|
3328
3547
|
[title]="badge.description"
|
|
3329
|
-
|
|
3548
|
+
>
|
|
3330
3549
|
<mat-icon [praxisIcon]="badge.icon" aria-hidden="true"></mat-icon>
|
|
3331
|
-
{{ badge.label }}
|
|
3550
|
+
<span>{{ badge.label }}</span>
|
|
3332
3551
|
</span>
|
|
3333
3552
|
}
|
|
3334
3553
|
</div>
|
|
@@ -3343,83 +3562,150 @@ class ComponentPaletteDialogComponent {
|
|
|
3343
3562
|
[title]="tx('palette.queueAddTooltip', 'Adicionar à fila de composição em lote (Shift+Enter)')"
|
|
3344
3563
|
(click)="enqueue(entry, $event)"
|
|
3345
3564
|
>
|
|
3346
|
-
|
|
3565
|
+
<span aria-hidden="true">+</span>
|
|
3566
|
+
<span>{{ tx('palette.queueAdd', 'Fila') }}</span>
|
|
3347
3567
|
</button>
|
|
3348
3568
|
</div>
|
|
3349
3569
|
}
|
|
3350
3570
|
</div>
|
|
3351
3571
|
} @else {
|
|
3352
|
-
<div class="pdx-empty">
|
|
3353
|
-
<
|
|
3354
|
-
|
|
3572
|
+
<div class="pdx-empty" role="status" aria-live="polite">
|
|
3573
|
+
<div class="pdx-empty-icon-wrap" aria-hidden="true">
|
|
3574
|
+
<mat-icon [praxisIcon]="'search_off'"></mat-icon>
|
|
3575
|
+
</div>
|
|
3576
|
+
<div class="pdx-empty-title">{{ tx('palette.empty', 'Nenhum componente disponível') }}</div>
|
|
3577
|
+
@if (query() || selectedCategory() !== 'all') {
|
|
3578
|
+
<div class="pdx-empty-sub">{{ tx('palette.emptySearchHint', 'Tente outro termo ou limpe os filtros ativos para exibir todo o catálogo.') }}</div>
|
|
3579
|
+
<button
|
|
3580
|
+
type="button"
|
|
3581
|
+
class="pdx-empty-reset-btn"
|
|
3582
|
+
data-testid="palette-empty-reset-filters"
|
|
3583
|
+
(click)="resetFilters()"
|
|
3584
|
+
>
|
|
3585
|
+
<mat-icon [praxisIcon]="'filter_alt_off'" aria-hidden="true"></mat-icon>
|
|
3586
|
+
<span>{{ tx('palette.clearFilters', 'Limpar filtros') }}</span>
|
|
3587
|
+
</button>
|
|
3588
|
+
}
|
|
3355
3589
|
</div>
|
|
3356
3590
|
}
|
|
3357
|
-
|
|
3358
3591
|
</div>
|
|
3359
|
-
<div mat-dialog-actions align="end">
|
|
3360
|
-
<
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
<
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
(
|
|
3370
|
-
>
|
|
3371
|
-
{{ tx('palette.commitBatchPrefix', 'Inserir') }} {{ queuedItems().length }} {{ queuedItems().length === 1 ? tx('palette.componentSingular', 'componente') : tx('palette.componentPlural', 'componentes') }}
|
|
3592
|
+
<div mat-dialog-actions class="pdx-dialog-footer" align="end">
|
|
3593
|
+
<div class="pdx-footer-hints" aria-hidden="true">
|
|
3594
|
+
<span class="pdx-kbd-hint"><kbd>↵</kbd> {{ tx('palette.kbdInsert', 'inserir') }}</span>
|
|
3595
|
+
<span class="pdx-kbd-sep">·</span>
|
|
3596
|
+
<span class="pdx-kbd-hint"><kbd>Shift+↵</kbd> {{ tx('palette.kbdQueue', 'fila') }}</span>
|
|
3597
|
+
<span class="pdx-kbd-sep">·</span>
|
|
3598
|
+
<span class="pdx-kbd-hint"><kbd>↑↓</kbd> {{ tx('palette.kbdNavigate', 'navegar') }}</span>
|
|
3599
|
+
</div>
|
|
3600
|
+
<div class="pdx-footer-actions">
|
|
3601
|
+
<button mat-stroked-button color="primary" class="pdx-footer-cancel-btn" mat-dialog-close data-testid="component-palette-close">
|
|
3602
|
+
{{ queuedItems().length > 0 ? tx('palette.discardQueue', 'Descartar fila') : (insertedCount() > 0 ? tx('palette.done', 'Concluir') : tx('palette.cancel', 'Cancelar')) }}
|
|
3372
3603
|
</button>
|
|
3373
|
-
|
|
3604
|
+
@if (queuedItems().length > 0) {
|
|
3605
|
+
<button
|
|
3606
|
+
mat-flat-button
|
|
3607
|
+
color="primary"
|
|
3608
|
+
type="button"
|
|
3609
|
+
class="pdx-footer-commit-btn"
|
|
3610
|
+
data-testid="component-palette-commit-batch"
|
|
3611
|
+
(click)="commitBatch()"
|
|
3612
|
+
>
|
|
3613
|
+
{{ tx('palette.commitBatchPrefix', 'Inserir') }} {{ queuedItems().length }} {{ queuedItems().length === 1 ? tx('palette.componentSingular', 'componente') : tx('palette.componentPlural', 'componentes') }}
|
|
3614
|
+
</button>
|
|
3615
|
+
}
|
|
3616
|
+
</div>
|
|
3374
3617
|
</div>
|
|
3375
|
-
`, isInline: true, styles: [":host{display:block}h2[mat-dialog-title]{margin:0;padding:12px 24px 10px;border-bottom:1px solid var(--md-sys-color-outline-variant)}.dlg-title{display:flex;align-items:center;gap:10px}.dlg-icon{color:var(--md-sys-color-primary)}.dlg-head{font-size:18px;font-weight:600;line-height:1.2}.dlg-sub{font-size:12px;opacity:.85;color:var(--md-sys-color-on-surface-variant)}.pdx-palette-content{min-width:480px;max-width:920px;padding-top:8px}.pdx-palette-search{width:100%;margin-bottom:8px}.pdx-palette-toolbar{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px;margin-bottom:12px}.pdx-palette-span-group,.pdx-palette-mode-actions{display:flex;flex-wrap:wrap;align-items:center;gap:6px}.pdx-palette-toolbar-label{font-size:12px;color:var(--md-sys-color-on-surface-variant);margin-right:4px}.pdx-span-chip,.pdx-keep-open-chip,.pdx-batch-mode-chip{min-height:32px;padding:0 10px;font-size:12px;border-radius:999px}.pdx-span-chip.selected,.pdx-keep-open-chip.selected,.pdx-batch-mode-chip.selected{background:var(--md-sys-color-secondary-container);color:var(--md-sys-color-on-secondary-container);border-color:var(--md-sys-color-primary)}.pdx-inserted-badge{display:inline-flex;align-items:center;justify-content:center;margin-left:6px;padding:1px 6px;border-radius:999px;background:var(--md-sys-color-primary);color:var(--md-sys-color-on-primary);font-size:11px;font-weight:700}.pdx-staging-tray{margin-bottom:12px;padding:10px 12px;border-radius:12px;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 38%,var(--md-sys-color-outline-variant));background:color-mix(in srgb,var(--md-sys-color-primary-container) 22%,var(--md-sys-color-surface-container-low))}.pdx-staging-header{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;font-size:12px}.pdx-staging-summary{display:flex;align-items:center;gap:6px;color:var(--md-sys-color-on-surface)}.pdx-staging-summary mat-icon{width:16px;height:16px;font-size:16px;color:var(--md-sys-color-primary)}.pdx-staging-clear{min-height:26px;padding:0 8px;font-size:11px}.pdx-staging-list{display:flex;flex-wrap:wrap;gap:6px;max-height:112px;overflow-y:auto}.pdx-staging-chip{display:inline-flex;align-items:center;gap:5px;padding:3px 8px;border-radius:999px;border:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface);font-size:11px;line-height:1.2}.pdx-staging-chip mat-icon{width:14px;height:14px;font-size:14px;color:var(--md-sys-color-primary)}.pdx-staging-index{font-weight:700;color:var(--md-sys-color-primary)}.pdx-staging-title{max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600}.pdx-staging-coords{appearance:none;border:1px solid transparent;padding:1px 6px;border-radius:6px;background:var(--md-sys-color-surface-container-highest);color:var(--md-sys-color-on-surface-variant);font-family:monospace;font-size:10px;cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease}.pdx-staging-coords:hover,.pdx-staging-coords:focus-visible{background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container);border-color:var(--md-sys-color-primary)}.pdx-staging-remove{appearance:none;border:0;background:transparent;color:var(--md-sys-color-on-surface-variant);cursor:pointer;font-size:14px;line-height:1;padding:0 2px;border-radius:50%}.pdx-staging-remove:hover{color:var(--md-sys-color-error)}.pdx-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px}.pdx-grid.dense{grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px}.pdx-grid.roomy{grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px}.pdx-card-item{position:relative;display:flex;flex-direction:column}.pdx-card{appearance:none;position:relative;width:100%;flex:1;display:grid;grid-template-columns:44px 1fr;grid-template-rows:auto auto;gap:10px;padding:14px;border:1px solid transparent;border-radius:14px;cursor:pointer;outline:none;color:inherit;font:inherit;min-height:92px;text-align:left;background:var(--md-sys-color-surface);border-color:var(--md-sys-color-outline-variant);transition:box-shadow .2s ease,background .25s ease,transform .06s ease}.pdx-grid.dense .pdx-card{padding:10px;border-radius:12px;min-height:84px}.pdx-grid.roomy .pdx-card{padding:16px;border-radius:16px;min-height:100px}.pdx-card:focus,.pdx-card:hover{box-shadow:var(--mat-elevation-level4);background:var(--md-sys-color-surface-container-low);border-color:var(--md-sys-color-primary)}.pdx-card:focus-visible{outline:2px solid var(--md-sys-color-primary);outline-offset:2px}.pdx-card:active{transform:translateY(1px)}.pdx-card-icon{grid-row:span 2;display:flex;align-items:center;justify-content:center;color:var(--md-sys-color-primary)}.pdx-card-head-row{display:flex;align-items:center;justify-content:space-between;gap:6px;padding-right:48px}.pdx-card-title{font-weight:600;line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pdx-card-queue-btn{appearance:none;position:absolute;top:10px;right:10px;z-index:2;flex-shrink:0;padding:2px 7px;border-radius:999px;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 40%,var(--md-sys-color-outline-variant));background:color-mix(in srgb,var(--md-sys-color-primary-container) 45%,var(--md-sys-color-surface));color:var(--md-sys-color-primary);font-size:10px;font-weight:700;line-height:1.2;cursor:pointer;transition:background .15s ease,color .15s ease}.pdx-card-queue-btn:hover,.pdx-card-queue-btn:focus-visible{background:var(--md-sys-color-primary);color:var(--md-sys-color-on-primary)}.pdx-card-desc{color:var(--md-sys-color-on-surface-variant);display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;word-break:break-word}.pdx-card-badges{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}.pdx-card-badge{display:inline-flex;align-items:center;gap:4px;min-height:22px;padding:2px 8px;border-radius:999px;border:1px solid var(--md-sys-color-outline-variant);color:var(--md-sys-color-on-surface-variant);background:var(--md-sys-color-surface-container-highest);font-size:11px;line-height:1.2;font-weight:600;letter-spacing:.01em}.pdx-card-badge mat-icon{width:14px;height:14px;font-size:14px}.pdx-card-badge.ready{color:var(--md-sys-color-primary);border-color:color-mix(in srgb,var(--md-sys-color-primary) 45%,transparent);background:color-mix(in srgb,var(--md-sys-color-primary-container) 72%,transparent)}.pdx-card-badge.info{color:var(--md-sys-color-tertiary);border-color:color-mix(in srgb,var(--md-sys-color-tertiary) 38%,transparent);background:color-mix(in srgb,var(--md-sys-color-tertiary-container) 68%,transparent)}.pdx-card-badge.warning{color:var(--md-sys-color-error);border-color:color-mix(in srgb,var(--md-sys-color-error) 35%,transparent);background:color-mix(in srgb,var(--md-sys-color-error-container) 62%,transparent)}.pdx-empty{padding:24px;display:grid;place-items:center;color:var(--md-sys-color-on-surface-variant);gap:8px}@media(max-width:600px){.pdx-palette-content{min-width:320px}.pdx-grid{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}}\n"], dependencies: [{ kind: "ngmodule", type: 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.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatDialogModule }, { kind: "directive", type: i1.MatDialogClose, selector: "[mat-dialog-close], [matDialogClose]", inputs: ["aria-label", "type", "mat-dialog-close", "matDialogClose"], exportAs: ["matDialogClose"] }, { kind: "directive", type: i1.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i1.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i1.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i6.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i6.MatLabel, selector: "mat-label" }, { kind: "directive", type: i6.MatPrefix, selector: "[matPrefix], [matIconPrefix], [matTextPrefix]", inputs: ["matTextPrefix"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i8.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: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3618
|
+
`, isInline: true, styles: [":host{display:block;color:var(--md-sys-color-on-surface)}.visually-hidden:where(:not(:focus-within,:active)){position:absolute!important;clip-path:inset(50%)!important;overflow:hidden!important;width:1px!important;height:1px!important;margin:-1px!important;padding:0!important;border:0!important;white-space:nowrap!important}h2[mat-dialog-title].pdx-dialog-header{margin:0;padding:18px 24px 16px;border-bottom:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 70%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface-container-low) 55%,var(--md-sys-color-surface))}h2[mat-dialog-title].pdx-dialog-header:before{display:none;content:none}.dlg-title{display:flex;align-items:center;gap:14px}.dlg-icon-wrap{width:40px;height:40px;flex-shrink:0;border-radius:12px;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--md-sys-color-primary-container) 62%,var(--md-sys-color-surface));border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 26%,transparent);color:var(--md-sys-color-primary)}.dlg-icon{width:22px;height:22px;font-size:22px;color:var(--md-sys-color-primary)}.dlg-texts{min-width:0;flex:1;display:flex;flex-direction:column;gap:3px}.dlg-head-row{display:flex;align-items:center;flex-wrap:wrap;gap:10px}.dlg-head{font-size:1.125rem;font-weight:700;letter-spacing:-.015em;line-height:1.25;color:var(--md-sys-color-on-surface)}.dlg-count-pill{display:inline-flex;align-items:center;padding:2px 9px;border-radius:999px;font-size:.72rem;font-weight:600;line-height:1.3;color:var(--md-sys-color-on-secondary-container);background:color-mix(in srgb,var(--md-sys-color-secondary-container) 78%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 60%,transparent)}.dlg-sub{font-size:.8125rem;line-height:1.35;color:var(--md-sys-color-on-surface-variant)}.pdx-palette-content{min-width:560px;max-width:920px;padding:16px 24px 18px!important;display:flex;flex-direction:column;gap:12px}.pdx-search-shell{width:100%}.pdx-search-bar{width:100%;min-height:44px;padding:0 12px 0 14px;border-radius:12px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 90%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface-container-low) 75%,var(--md-sys-color-surface));display:flex;align-items:center;gap:10px;box-sizing:border-box;transition:border-color .16s ease,box-shadow .16s ease,background-color .16s ease}.pdx-search-bar:hover{border-color:color-mix(in srgb,var(--md-sys-color-on-surface) 32%,var(--md-sys-color-outline-variant))}.pdx-search-bar:focus-within{background:var(--md-sys-color-surface);border-color:var(--md-sys-color-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--md-sys-color-primary) 14%,transparent)}.pdx-search-icon{width:20px;height:20px;font-size:20px;flex-shrink:0;color:var(--md-sys-color-on-surface-variant);transition:color .16s ease}.pdx-search-bar:focus-within .pdx-search-icon{color:var(--md-sys-color-primary)}.pdx-search-input{flex:1;min-width:0;border:0;outline:none;background:transparent;color:var(--md-sys-color-on-surface);font:inherit;font-size:.925rem;line-height:1.4;padding:8px 0}.pdx-search-input::placeholder{color:color-mix(in srgb,var(--md-sys-color-on-surface-variant) 78%,transparent)}.pdx-search-input::-webkit-search-cancel-button{-webkit-appearance:none;appearance:none}.pdx-search-clear{appearance:none;width:26px;height:26px;border-radius:7px;border:0;background:transparent;color:var(--md-sys-color-on-surface-variant);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:0;transition:background-color .15s ease,color .15s ease}.pdx-search-clear mat-icon{width:16px;height:16px;font-size:16px}.pdx-search-clear:hover,.pdx-search-clear:focus-visible{background:var(--md-sys-color-surface-container-highest);color:var(--md-sys-color-on-surface);outline:none}.pdx-search-kbd{display:inline-flex;align-items:center;justify-content:center;min-width:22px;height:22px;padding:0 6px;border-radius:6px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.72rem;font-weight:600;color:var(--md-sys-color-on-surface-variant);background:var(--md-sys-color-surface-container-high);border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 80%,transparent);flex-shrink:0}.pdx-category-tabs{display:flex;flex-wrap:wrap;align-items:center;gap:6px}.pdx-category-pill{appearance:none;min-height:28px;padding:0 10px;border-radius:999px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 75%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface-container-low) 65%,var(--md-sys-color-surface));color:var(--md-sys-color-on-surface-variant);font-family:inherit;font-size:.75rem;font-weight:600;display:inline-flex;align-items:center;gap:6px;cursor:pointer;transition:background-color .15s ease,color .15s ease,border-color .15s ease}.pdx-category-pill:hover{color:var(--md-sys-color-on-surface);border-color:color-mix(in srgb,var(--md-sys-color-primary) 35%,var(--md-sys-color-outline-variant))}.pdx-category-pill:focus-visible{outline:2px solid var(--md-sys-color-primary);outline-offset:2px}.pdx-category-pill.selected{background:color-mix(in srgb,var(--md-sys-color-primary-container) 78%,var(--md-sys-color-surface));color:var(--md-sys-color-on-primary-container);border-color:color-mix(in srgb,var(--md-sys-color-primary) 52%,transparent);font-weight:700}.pdx-category-count{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:16px;padding:0 5px;border-radius:999px;font-size:.675rem;font-weight:700;background:color-mix(in srgb,var(--md-sys-color-surface-container-highest) 75%,transparent);color:currentColor}.pdx-category-pill.selected .pdx-category-count{background:color-mix(in srgb,var(--md-sys-color-primary) 22%,transparent)}.pdx-palette-toolbar{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:10px 14px;padding:2px 0 10px;border-bottom:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 55%,transparent)}.pdx-palette-span-group{display:flex;flex-direction:column;align-items:flex-start;gap:5px}.pdx-palette-toolbar-label{font-size:.68rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--md-sys-color-on-surface-variant);white-space:nowrap;padding-left:2px}.pdx-span-segmented{display:inline-flex;flex-wrap:wrap;align-items:center;gap:2px;padding:3px;border-radius:9px;background:color-mix(in srgb,var(--md-sys-color-surface-container-highest) 60%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 55%,transparent)}.pdx-span-chip{appearance:none;min-height:28px;padding:0 8px;font-family:inherit;font-size:.74rem;font-weight:600;line-height:1;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--md-sys-color-on-surface-variant);cursor:pointer;white-space:nowrap;transition:background-color .15s ease,color .15s ease,border-color .15s ease,box-shadow .15s ease}.pdx-span-chip:hover{color:var(--md-sys-color-on-surface);background:color-mix(in srgb,var(--md-sys-color-surface) 65%,transparent)}.pdx-span-chip:focus-visible{outline:2px solid var(--md-sys-color-primary);outline-offset:1px}.pdx-span-chip.selected{background:var(--md-sys-color-secondary-container);color:var(--md-sys-color-on-secondary-container);border-color:color-mix(in srgb,var(--md-sys-color-primary) 42%,transparent);font-weight:700;box-shadow:0 1px 2px #0000000f}.pdx-palette-mode-actions{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin-left:auto}.pdx-keep-open-chip,.pdx-batch-mode-chip{appearance:none;min-height:30px;padding:0 10px;font-family:inherit;font-size:.75rem;font-weight:600;border-radius:8px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 85%,transparent);background:var(--md-sys-color-surface);color:var(--md-sys-color-on-surface-variant);display:inline-flex;align-items:center;gap:5px;cursor:pointer;white-space:nowrap;transition:background-color .15s ease,color .15s ease,border-color .15s ease}.pdx-keep-open-chip mat-icon,.pdx-batch-mode-chip mat-icon{width:16px;height:16px;font-size:16px;color:currentColor}.pdx-keep-open-chip:hover,.pdx-batch-mode-chip:hover{color:var(--md-sys-color-on-surface);border-color:color-mix(in srgb,var(--md-sys-color-primary) 40%,var(--md-sys-color-outline-variant));background:var(--md-sys-color-surface-container-low)}.pdx-keep-open-chip:focus-visible,.pdx-batch-mode-chip:focus-visible{outline:2px solid var(--md-sys-color-primary);outline-offset:2px}.pdx-keep-open-chip.selected,.pdx-batch-mode-chip.selected{background:var(--md-sys-color-secondary-container);color:var(--md-sys-color-on-secondary-container);border-color:var(--md-sys-color-primary);font-weight:700}.pdx-inserted-badge{display:inline-flex;align-items:center;justify-content:center;margin-left:2px;padding:1px 6px;border-radius:999px;background:var(--md-sys-color-primary);color:var(--md-sys-color-on-primary);font-size:10px;font-weight:700;line-height:1.3}.pdx-staging-tray{padding:10px 12px;border-radius:12px;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 38%,var(--md-sys-color-outline-variant));background:color-mix(in srgb,var(--md-sys-color-primary-container) 22%,var(--md-sys-color-surface-container-low))}.pdx-staging-header{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;font-size:12px}.pdx-staging-summary{display:flex;align-items:center;gap:6px;color:var(--md-sys-color-on-surface)}.pdx-staging-summary mat-icon{width:16px;height:16px;font-size:16px;color:var(--md-sys-color-primary)}.pdx-staging-clear{min-height:26px;padding:0 8px;font-size:11px}.pdx-staging-list{display:flex;flex-wrap:wrap;gap:6px;max-height:112px;overflow-y:auto}.pdx-staging-chip{display:inline-flex;align-items:center;gap:5px;padding:3px 8px;border-radius:999px;border:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface);font-size:11px;line-height:1.2}.pdx-staging-chip mat-icon{width:14px;height:14px;font-size:14px;color:var(--md-sys-color-primary)}.pdx-staging-index{font-weight:700;color:var(--md-sys-color-primary)}.pdx-staging-title{max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600}.pdx-staging-coords{appearance:none;border:1px solid transparent;padding:2px 6px;border-radius:6px;background:var(--md-sys-color-surface-container-highest);color:var(--md-sys-color-on-surface-variant);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:10px;cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease}.pdx-staging-coords:hover,.pdx-staging-coords:focus-visible{background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container);border-color:var(--md-sys-color-primary)}.pdx-staging-remove{appearance:none;border:0;background:transparent;color:var(--md-sys-color-on-surface-variant);cursor:pointer;font-size:14px;line-height:1;padding:0 3px;border-radius:50%}.pdx-staging-remove:hover{color:var(--md-sys-color-error)}.pdx-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}.pdx-grid.dense{grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:10px}.pdx-grid.roomy{grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:14px}.pdx-grid.single-entry{grid-template-columns:minmax(0,1fr)}.pdx-card-item{position:relative;display:flex;flex-direction:column}.pdx-card{appearance:none;position:relative;width:100%;flex:1;display:grid;grid-template-columns:44px minmax(0,1fr);align-items:start;gap:14px;padding:16px;border-radius:14px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 85%,transparent);cursor:pointer;outline:none;color:inherit;font:inherit;min-height:96px;text-align:left;background:var(--md-sys-color-surface);box-shadow:0 1px 2px #0000000a;transition:box-shadow .18s ease,background-color .18s ease,border-color .18s ease,transform .08s ease}.pdx-grid.dense .pdx-card{padding:12px 14px;border-radius:12px;min-height:86px}.pdx-grid.roomy .pdx-card{padding:16px 18px;border-radius:14px;min-height:100px}.pdx-card:focus,.pdx-card:hover{box-shadow:0 6px 18px #00000014;background:color-mix(in srgb,var(--md-sys-color-surface-container-low) 70%,var(--md-sys-color-surface));border-color:var(--md-sys-color-primary)}.pdx-card:focus-visible{outline:2px solid var(--md-sys-color-primary);outline-offset:2px}.pdx-card:active{transform:translateY(1px)}.pdx-card-icon{width:44px;height:44px;border-radius:12px;align-self:start;display:flex;align-items:center;justify-content:center;color:var(--md-sys-color-primary);background:color-mix(in srgb,var(--md-sys-color-primary-container) 50%,var(--md-sys-color-surface-container-low));border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 22%,transparent);transition:background-color .18s ease,border-color .18s ease,transform .18s ease}.pdx-card:hover .pdx-card-icon,.pdx-card:focus-visible .pdx-card-icon{background:color-mix(in srgb,var(--md-sys-color-primary-container) 82%,var(--md-sys-color-surface));border-color:color-mix(in srgb,var(--md-sys-color-primary) 42%,transparent)}.pdx-card-icon mat-icon{width:22px;height:22px;font-size:22px}.pdx-card-body{min-width:0;display:flex;flex-direction:column;gap:4px}.pdx-card-head-row{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;padding-right:64px}.pdx-card-title{font-size:.9375rem;font-weight:650;letter-spacing:-.01em;line-height:1.3;color:var(--md-sys-color-on-surface);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pdx-card-queue-btn{appearance:none;position:absolute;top:14px;right:14px;z-index:2;flex-shrink:0;min-height:26px;min-width:52px;padding:0 10px;border-radius:999px;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 36%,var(--md-sys-color-outline-variant));background:color-mix(in srgb,var(--md-sys-color-primary-container) 42%,var(--md-sys-color-surface));color:var(--md-sys-color-primary);font-family:inherit;font-size:.6875rem;font-weight:700;line-height:1;display:inline-flex;align-items:center;justify-content:center;gap:3px;cursor:pointer;transition:background-color .15s ease,color .15s ease,border-color .15s ease}.pdx-card-queue-btn:hover,.pdx-card-queue-btn:focus-visible{background:var(--md-sys-color-primary);color:var(--md-sys-color-on-primary);border-color:var(--md-sys-color-primary);outline:none}.pdx-card-desc{font-size:.8125rem;line-height:1.48;color:var(--md-sys-color-on-surface-variant);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;word-break:break-word}.pdx-card-badges{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}.pdx-card-badge{display:inline-flex;align-items:center;gap:4px;min-height:22px;padding:2px 8px;border-radius:6px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 85%,transparent);color:var(--md-sys-color-on-surface-variant);background:var(--md-sys-color-surface-container-high);font-size:.6875rem;line-height:1.2;font-weight:600;letter-spacing:.01em}.pdx-card-badge mat-icon{width:13px;height:13px;font-size:13px;flex-shrink:0}.pdx-card-badge.ready{color:color-mix(in srgb,var(--md-sys-color-on-primary-container) 75%,var(--md-sys-color-primary));border-color:color-mix(in srgb,var(--md-sys-color-primary) 36%,transparent);background:color-mix(in srgb,var(--md-sys-color-primary-container) 68%,transparent)}.pdx-card-badge.info{color:color-mix(in srgb,var(--md-sys-color-on-tertiary-container) 75%,var(--md-sys-color-tertiary));border-color:color-mix(in srgb,var(--md-sys-color-tertiary) 34%,transparent);background:color-mix(in srgb,var(--md-sys-color-tertiary-container) 62%,transparent)}.pdx-card-badge.warning{color:color-mix(in srgb,var(--md-sys-color-on-error-container) 75%,var(--md-sys-color-error));border-color:color-mix(in srgb,var(--md-sys-color-error) 34%,transparent);background:color-mix(in srgb,var(--md-sys-color-error-container) 60%,transparent)}.pdx-empty{padding:36px 24px;border-radius:14px;border:1px dashed color-mix(in srgb,var(--md-sys-color-outline-variant) 80%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface-container-low) 50%,transparent);display:grid;place-items:center;color:var(--md-sys-color-on-surface-variant);gap:10px;text-align:center}.pdx-empty-icon-wrap{width:44px;height:44px;border-radius:12px;display:flex;align-items:center;justify-content:center;background:var(--md-sys-color-surface-container-high);color:var(--md-sys-color-on-surface-variant)}.pdx-empty-title{font-size:.875rem;font-weight:600;color:var(--md-sys-color-on-surface)}.pdx-empty-sub{font-size:.8rem;max-width:380px;line-height:1.4;color:var(--md-sys-color-on-surface-variant)}.pdx-empty-reset-btn{appearance:none;margin-top:4px;min-height:32px;padding:0 12px;border-radius:8px;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 42%,var(--md-sys-color-outline-variant));background:color-mix(in srgb,var(--md-sys-color-primary-container) 45%,var(--md-sys-color-surface));color:var(--md-sys-color-primary);font-family:inherit;font-size:.775rem;font-weight:600;display:inline-flex;align-items:center;gap:6px;cursor:pointer;transition:background-color .15s ease,color .15s ease}.pdx-empty-reset-btn mat-icon{width:16px;height:16px;font-size:16px}.pdx-empty-reset-btn:hover,.pdx-empty-reset-btn:focus-visible{background:var(--md-sys-color-primary);color:var(--md-sys-color-on-primary);outline:none}div[mat-dialog-actions].pdx-dialog-footer{margin:0;min-height:56px;padding:12px 24px 16px;border-top:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 70%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface-container-low) 45%,var(--md-sys-color-surface));display:flex;align-items:center;justify-content:space-between;gap:12px}.pdx-footer-hints{display:flex;align-items:center;flex-wrap:wrap;gap:6px;font-size:.72rem;color:var(--md-sys-color-on-surface-variant)}.pdx-kbd-hint{display:inline-flex;align-items:center;gap:4px}.pdx-kbd-hint kbd{display:inline-flex;align-items:center;padding:1px 5px;border-radius:5px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.68rem;font-weight:600;color:var(--md-sys-color-on-surface);background:var(--md-sys-color-surface-container-high);border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 80%,transparent)}.pdx-kbd-sep{opacity:.55}.pdx-footer-actions{display:flex;align-items:center;gap:8px;margin-left:auto}.pdx-footer-cancel-btn,.pdx-footer-commit-btn{border-radius:10px;min-height:36px;padding:0 16px;font-weight:600}@media(max-width:680px){.pdx-palette-content{min-width:300px;padding:14px 16px!important}h2[mat-dialog-title].pdx-dialog-header{padding:14px 16px}div[mat-dialog-actions].pdx-dialog-footer{padding:10px 16px 14px}.pdx-footer-hints{display:none}.pdx-palette-mode-actions{margin-left:0;width:100%}.pdx-grid{grid-template-columns:repeat(auto-fill,minmax(220px,1fr))}}\n"], dependencies: [{ kind: "ngmodule", type: A11yModule }, { 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.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatDialogModule }, { kind: "directive", type: i1.MatDialogClose, selector: "[mat-dialog-close], [matDialogClose]", inputs: ["aria-label", "type", "mat-dialog-close", "matDialogClose"], exportAs: ["matDialogClose"] }, { kind: "directive", type: i1.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i1.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i1.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3376
3619
|
}
|
|
3377
3620
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ComponentPaletteDialogComponent, decorators: [{
|
|
3378
3621
|
type: Component,
|
|
3379
|
-
args: [{ selector: 'praxis-component-palette-dialog', standalone: true, imports: [FormsModule, MatDialogModule,
|
|
3380
|
-
<h2 mat-dialog-title>
|
|
3622
|
+
args: [{ selector: 'praxis-component-palette-dialog', standalone: true, imports: [A11yModule, FormsModule, MatDialogModule, MatIconModule, MatButtonModule, PraxisIconDirective], template: `
|
|
3623
|
+
<h2 mat-dialog-title class="pdx-dialog-header">
|
|
3381
3624
|
<div class="dlg-title">
|
|
3382
|
-
<
|
|
3625
|
+
<div class="dlg-icon-wrap" aria-hidden="true">
|
|
3626
|
+
<mat-icon class="dlg-icon" [praxisIcon]="'widgets'"></mat-icon>
|
|
3627
|
+
</div>
|
|
3383
3628
|
<div class="dlg-texts">
|
|
3384
|
-
<div class="dlg-head">
|
|
3385
|
-
|
|
3629
|
+
<div class="dlg-head-row">
|
|
3630
|
+
<span class="dlg-head">{{ data?.title || tx('palette.title', 'Inserir componente') }}</span>
|
|
3631
|
+
<span class="dlg-count-pill" aria-live="polite">{{ filtered().length }} {{ filtered().length === 1 ? tx('palette.availableSingular', 'disponível') : tx('palette.available', 'disponíveis') }}</span>
|
|
3632
|
+
</div>
|
|
3633
|
+
<div class="dlg-sub">{{ tx('palette.subtitle', 'Escolha um componente para adicionar à página') }}</div>
|
|
3386
3634
|
</div>
|
|
3387
3635
|
</div>
|
|
3388
3636
|
</h2>
|
|
3389
3637
|
<div mat-dialog-content class="pdx-palette-content">
|
|
3390
|
-
<
|
|
3391
|
-
<
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3638
|
+
<div class="pdx-search-shell">
|
|
3639
|
+
<label class="visually-hidden" for="pdx-palette-search-input">
|
|
3640
|
+
{{ tx('palette.searchLabel', 'Buscar componente') }}
|
|
3641
|
+
</label>
|
|
3642
|
+
<div class="pdx-palette-search pdx-search-bar">
|
|
3643
|
+
<mat-icon class="pdx-search-icon" [praxisIcon]="'search'" aria-hidden="true"></mat-icon>
|
|
3644
|
+
<input
|
|
3645
|
+
id="pdx-palette-search-input"
|
|
3646
|
+
class="pdx-search-input"
|
|
3647
|
+
type="text"
|
|
3648
|
+
role="searchbox"
|
|
3649
|
+
cdkFocusInitial
|
|
3650
|
+
autocomplete="off"
|
|
3651
|
+
[ngModel]="query()"
|
|
3652
|
+
(ngModelChange)="query.set($event)"
|
|
3653
|
+
(keydown)="handleSearchKeydown($event)"
|
|
3654
|
+
[placeholder]="tx('palette.searchPlaceholder', 'Nome, capability ou readiness')"
|
|
3655
|
+
data-testid="component-palette-search"
|
|
3401
3656
|
/>
|
|
3402
|
-
|
|
3403
|
-
<div class="pdx-palette-toolbar" data-testid="component-palette-toolbar">
|
|
3404
|
-
<div class="pdx-palette-span-group" role="group" [attr.aria-label]="tx('palette.initialWidth', 'Largura inicial')">
|
|
3405
|
-
<span class="pdx-palette-toolbar-label">{{ tx('palette.initialWidth', 'Largura inicial') }}:</span>
|
|
3406
|
-
@for (opt of spanOptions; track opt.value) {
|
|
3657
|
+
@if (query()) {
|
|
3407
3658
|
<button
|
|
3408
|
-
mat-stroked-button
|
|
3409
3659
|
type="button"
|
|
3410
|
-
class="pdx-
|
|
3411
|
-
[
|
|
3412
|
-
|
|
3413
|
-
[attr.data-testid]="'palette-colspan-' + (opt.value ?? 'auto')"
|
|
3414
|
-
(click)="selectedColSpan.set(opt.value)"
|
|
3660
|
+
class="pdx-search-clear"
|
|
3661
|
+
[attr.aria-label]="tx('palette.clearSearchAria', 'Limpar busca')"
|
|
3662
|
+
(click)="clearSearch()"
|
|
3415
3663
|
>
|
|
3416
|
-
|
|
3664
|
+
<mat-icon [praxisIcon]="'close'" aria-hidden="true"></mat-icon>
|
|
3417
3665
|
</button>
|
|
3418
3666
|
}
|
|
3667
|
+
<kbd class="pdx-search-kbd" aria-hidden="true">↓</kbd>
|
|
3668
|
+
</div>
|
|
3669
|
+
</div>
|
|
3670
|
+
|
|
3671
|
+
@if (hasMultipleCategories()) {
|
|
3672
|
+
<div class="pdx-category-tabs" role="group" [attr.aria-label]="tx('palette.categoryAria', 'Filtrar catálogo por categoria')" data-testid="component-palette-categories">
|
|
3673
|
+
@for (cat of categoryOptions(); track cat.id) {
|
|
3674
|
+
<button
|
|
3675
|
+
type="button"
|
|
3676
|
+
class="pdx-category-pill"
|
|
3677
|
+
[class.selected]="selectedCategory() === cat.id"
|
|
3678
|
+
[attr.aria-pressed]="selectedCategory() === cat.id"
|
|
3679
|
+
[attr.data-testid]="'palette-category-' + cat.id"
|
|
3680
|
+
(click)="selectedCategory.set(cat.id)"
|
|
3681
|
+
>
|
|
3682
|
+
<span>{{ cat.label }}</span>
|
|
3683
|
+
<span class="pdx-category-count">{{ cat.count }}</span>
|
|
3684
|
+
</button>
|
|
3685
|
+
}
|
|
3686
|
+
</div>
|
|
3687
|
+
}
|
|
3688
|
+
|
|
3689
|
+
<div class="pdx-palette-toolbar" data-testid="component-palette-toolbar">
|
|
3690
|
+
<div class="pdx-palette-span-group" role="group" [attr.aria-label]="tx('palette.initialWidth', 'Largura inicial')">
|
|
3691
|
+
<span class="pdx-palette-toolbar-label">{{ tx('palette.initialWidth', 'Largura inicial') }}</span>
|
|
3692
|
+
<div class="pdx-span-segmented">
|
|
3693
|
+
@for (opt of spanOptions(); track opt.value) {
|
|
3694
|
+
<button
|
|
3695
|
+
type="button"
|
|
3696
|
+
class="pdx-span-chip"
|
|
3697
|
+
[class.selected]="selectedColSpan() === opt.value"
|
|
3698
|
+
[attr.aria-pressed]="selectedColSpan() === opt.value"
|
|
3699
|
+
[attr.data-testid]="'palette-colspan-' + (opt.value ?? 'auto')"
|
|
3700
|
+
(click)="selectedColSpan.set(opt.value)"
|
|
3701
|
+
>
|
|
3702
|
+
{{ opt.label }}
|
|
3703
|
+
</button>
|
|
3704
|
+
}
|
|
3705
|
+
</div>
|
|
3419
3706
|
</div>
|
|
3420
3707
|
<div class="pdx-palette-mode-actions">
|
|
3421
3708
|
<button
|
|
3422
|
-
mat-stroked-button
|
|
3423
3709
|
type="button"
|
|
3424
3710
|
class="pdx-batch-mode-chip"
|
|
3425
3711
|
[class.selected]="batchMode()"
|
|
@@ -3428,13 +3714,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
3428
3714
|
(click)="toggleBatchMode()"
|
|
3429
3715
|
>
|
|
3430
3716
|
<mat-icon [praxisIcon]="batchMode() ? 'playlist_add_check' : 'playlist_add'" aria-hidden="true"></mat-icon>
|
|
3431
|
-
{{ tx('palette.batchMode', 'Montar em lote') }}
|
|
3717
|
+
<span>{{ tx('palette.batchMode', 'Montar em lote') }}</span>
|
|
3432
3718
|
@if (queuedItems().length > 0) {
|
|
3433
3719
|
<span class="pdx-inserted-badge" data-testid="palette-queued-count">{{ queuedItems().length }}</span>
|
|
3434
3720
|
}
|
|
3435
3721
|
</button>
|
|
3436
3722
|
<button
|
|
3437
|
-
mat-stroked-button
|
|
3438
3723
|
type="button"
|
|
3439
3724
|
class="pdx-keep-open-chip"
|
|
3440
3725
|
[class.selected]="keepOpen()"
|
|
@@ -3443,13 +3728,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
3443
3728
|
(click)="toggleKeepOpen()"
|
|
3444
3729
|
>
|
|
3445
3730
|
<mat-icon [praxisIcon]="keepOpen() ? 'check_box' : 'check_box_outline_blank'" aria-hidden="true"></mat-icon>
|
|
3446
|
-
{{ tx('palette.keepOpen', 'Manter aberta') }}
|
|
3731
|
+
<span>{{ tx('palette.keepOpen', 'Manter aberta') }}</span>
|
|
3447
3732
|
@if (insertedCount() > 0) {
|
|
3448
3733
|
<span class="pdx-inserted-badge" data-testid="palette-inserted-count">+{{ insertedCount() }}</span>
|
|
3449
3734
|
}
|
|
3450
3735
|
</button>
|
|
3451
3736
|
</div>
|
|
3452
3737
|
</div>
|
|
3738
|
+
|
|
3453
3739
|
@if (queuedItems().length > 0) {
|
|
3454
3740
|
<div class="pdx-staging-tray" data-testid="palette-staging-tray" role="region" [attr.aria-label]="tx('palette.stagingTrayAria', 'Bandeja de composição em lote')">
|
|
3455
3741
|
<div class="pdx-staging-header">
|
|
@@ -3497,19 +3783,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
3497
3783
|
</div>
|
|
3498
3784
|
</div>
|
|
3499
3785
|
}
|
|
3786
|
+
|
|
3500
3787
|
@if (filtered().length) {
|
|
3501
|
-
<div [class]="'pdx-grid ' + density()">
|
|
3788
|
+
<div [class]="'pdx-grid ' + density() + (filtered().length === 1 ? ' single-entry' : '')">
|
|
3502
3789
|
@for (entry of filtered(); track trackById($index, entry)) {
|
|
3503
3790
|
<div class="pdx-card-item">
|
|
3504
3791
|
<button
|
|
3505
3792
|
#paletteCard
|
|
3506
|
-
class="pdx-card
|
|
3793
|
+
class="pdx-card"
|
|
3507
3794
|
type="button"
|
|
3508
3795
|
[attr.aria-label]="tx('palette.addAria', 'Adicionar') + ' ' + entry.title"
|
|
3509
3796
|
(click)="select(entry)"
|
|
3510
3797
|
(keydown)="handleCardKeydown($event, $index, entry)"
|
|
3511
|
-
|
|
3512
|
-
<div class="pdx-card-icon">
|
|
3798
|
+
>
|
|
3799
|
+
<div class="pdx-card-icon" aria-hidden="true">
|
|
3513
3800
|
<mat-icon [praxisIcon]="entry.icon"></mat-icon>
|
|
3514
3801
|
</div>
|
|
3515
3802
|
<div class="pdx-card-body">
|
|
@@ -3524,9 +3811,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
3524
3811
|
class="pdx-card-badge"
|
|
3525
3812
|
[class]="'pdx-card-badge ' + badge.tone"
|
|
3526
3813
|
[title]="badge.description"
|
|
3527
|
-
|
|
3814
|
+
>
|
|
3528
3815
|
<mat-icon [praxisIcon]="badge.icon" aria-hidden="true"></mat-icon>
|
|
3529
|
-
{{ badge.label }}
|
|
3816
|
+
<span>{{ badge.label }}</span>
|
|
3530
3817
|
</span>
|
|
3531
3818
|
}
|
|
3532
3819
|
</div>
|
|
@@ -3541,36 +3828,60 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
3541
3828
|
[title]="tx('palette.queueAddTooltip', 'Adicionar à fila de composição em lote (Shift+Enter)')"
|
|
3542
3829
|
(click)="enqueue(entry, $event)"
|
|
3543
3830
|
>
|
|
3544
|
-
|
|
3831
|
+
<span aria-hidden="true">+</span>
|
|
3832
|
+
<span>{{ tx('palette.queueAdd', 'Fila') }}</span>
|
|
3545
3833
|
</button>
|
|
3546
3834
|
</div>
|
|
3547
3835
|
}
|
|
3548
3836
|
</div>
|
|
3549
3837
|
} @else {
|
|
3550
|
-
<div class="pdx-empty">
|
|
3551
|
-
<
|
|
3552
|
-
|
|
3838
|
+
<div class="pdx-empty" role="status" aria-live="polite">
|
|
3839
|
+
<div class="pdx-empty-icon-wrap" aria-hidden="true">
|
|
3840
|
+
<mat-icon [praxisIcon]="'search_off'"></mat-icon>
|
|
3841
|
+
</div>
|
|
3842
|
+
<div class="pdx-empty-title">{{ tx('palette.empty', 'Nenhum componente disponível') }}</div>
|
|
3843
|
+
@if (query() || selectedCategory() !== 'all') {
|
|
3844
|
+
<div class="pdx-empty-sub">{{ tx('palette.emptySearchHint', 'Tente outro termo ou limpe os filtros ativos para exibir todo o catálogo.') }}</div>
|
|
3845
|
+
<button
|
|
3846
|
+
type="button"
|
|
3847
|
+
class="pdx-empty-reset-btn"
|
|
3848
|
+
data-testid="palette-empty-reset-filters"
|
|
3849
|
+
(click)="resetFilters()"
|
|
3850
|
+
>
|
|
3851
|
+
<mat-icon [praxisIcon]="'filter_alt_off'" aria-hidden="true"></mat-icon>
|
|
3852
|
+
<span>{{ tx('palette.clearFilters', 'Limpar filtros') }}</span>
|
|
3853
|
+
</button>
|
|
3854
|
+
}
|
|
3553
3855
|
</div>
|
|
3554
3856
|
}
|
|
3555
|
-
|
|
3556
3857
|
</div>
|
|
3557
|
-
<div mat-dialog-actions align="end">
|
|
3558
|
-
<
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
<
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
(
|
|
3568
|
-
>
|
|
3569
|
-
{{ tx('palette.commitBatchPrefix', 'Inserir') }} {{ queuedItems().length }} {{ queuedItems().length === 1 ? tx('palette.componentSingular', 'componente') : tx('palette.componentPlural', 'componentes') }}
|
|
3858
|
+
<div mat-dialog-actions class="pdx-dialog-footer" align="end">
|
|
3859
|
+
<div class="pdx-footer-hints" aria-hidden="true">
|
|
3860
|
+
<span class="pdx-kbd-hint"><kbd>↵</kbd> {{ tx('palette.kbdInsert', 'inserir') }}</span>
|
|
3861
|
+
<span class="pdx-kbd-sep">·</span>
|
|
3862
|
+
<span class="pdx-kbd-hint"><kbd>Shift+↵</kbd> {{ tx('palette.kbdQueue', 'fila') }}</span>
|
|
3863
|
+
<span class="pdx-kbd-sep">·</span>
|
|
3864
|
+
<span class="pdx-kbd-hint"><kbd>↑↓</kbd> {{ tx('palette.kbdNavigate', 'navegar') }}</span>
|
|
3865
|
+
</div>
|
|
3866
|
+
<div class="pdx-footer-actions">
|
|
3867
|
+
<button mat-stroked-button color="primary" class="pdx-footer-cancel-btn" mat-dialog-close data-testid="component-palette-close">
|
|
3868
|
+
{{ queuedItems().length > 0 ? tx('palette.discardQueue', 'Descartar fila') : (insertedCount() > 0 ? tx('palette.done', 'Concluir') : tx('palette.cancel', 'Cancelar')) }}
|
|
3570
3869
|
</button>
|
|
3571
|
-
|
|
3870
|
+
@if (queuedItems().length > 0) {
|
|
3871
|
+
<button
|
|
3872
|
+
mat-flat-button
|
|
3873
|
+
color="primary"
|
|
3874
|
+
type="button"
|
|
3875
|
+
class="pdx-footer-commit-btn"
|
|
3876
|
+
data-testid="component-palette-commit-batch"
|
|
3877
|
+
(click)="commitBatch()"
|
|
3878
|
+
>
|
|
3879
|
+
{{ tx('palette.commitBatchPrefix', 'Inserir') }} {{ queuedItems().length }} {{ queuedItems().length === 1 ? tx('palette.componentSingular', 'componente') : tx('palette.componentPlural', 'componentes') }}
|
|
3880
|
+
</button>
|
|
3881
|
+
}
|
|
3882
|
+
</div>
|
|
3572
3883
|
</div>
|
|
3573
|
-
`, providers: [providePraxisPageBuilderI18n()], changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block}h2[mat-dialog-title]{margin:0;padding:12px 24px 10px;border-bottom:1px solid var(--md-sys-color-outline-variant)}.dlg-title{display:flex;align-items:center;gap:10px}.dlg-icon{color:var(--md-sys-color-primary)}.dlg-head{font-size:18px;font-weight:600;line-height:1.2}.dlg-sub{font-size:12px;opacity:.85;color:var(--md-sys-color-on-surface-variant)}.pdx-palette-content{min-width:480px;max-width:920px;padding-top:8px}.pdx-palette-search{width:100%;margin-bottom:8px}.pdx-palette-toolbar{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px;margin-bottom:12px}.pdx-palette-span-group,.pdx-palette-mode-actions{display:flex;flex-wrap:wrap;align-items:center;gap:6px}.pdx-palette-toolbar-label{font-size:12px;color:var(--md-sys-color-on-surface-variant);margin-right:4px}.pdx-span-chip,.pdx-keep-open-chip,.pdx-batch-mode-chip{min-height:32px;padding:0 10px;font-size:12px;border-radius:999px}.pdx-span-chip.selected,.pdx-keep-open-chip.selected,.pdx-batch-mode-chip.selected{background:var(--md-sys-color-secondary-container);color:var(--md-sys-color-on-secondary-container);border-color:var(--md-sys-color-primary)}.pdx-inserted-badge{display:inline-flex;align-items:center;justify-content:center;margin-left:6px;padding:1px 6px;border-radius:999px;background:var(--md-sys-color-primary);color:var(--md-sys-color-on-primary);font-size:11px;font-weight:700}.pdx-staging-tray{margin-bottom:12px;padding:10px 12px;border-radius:12px;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 38%,var(--md-sys-color-outline-variant));background:color-mix(in srgb,var(--md-sys-color-primary-container) 22%,var(--md-sys-color-surface-container-low))}.pdx-staging-header{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;font-size:12px}.pdx-staging-summary{display:flex;align-items:center;gap:6px;color:var(--md-sys-color-on-surface)}.pdx-staging-summary mat-icon{width:16px;height:16px;font-size:16px;color:var(--md-sys-color-primary)}.pdx-staging-clear{min-height:26px;padding:0 8px;font-size:11px}.pdx-staging-list{display:flex;flex-wrap:wrap;gap:6px;max-height:112px;overflow-y:auto}.pdx-staging-chip{display:inline-flex;align-items:center;gap:5px;padding:3px 8px;border-radius:999px;border:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface);font-size:11px;line-height:1.2}.pdx-staging-chip mat-icon{width:14px;height:14px;font-size:14px;color:var(--md-sys-color-primary)}.pdx-staging-index{font-weight:700;color:var(--md-sys-color-primary)}.pdx-staging-title{max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600}.pdx-staging-coords{appearance:none;border:1px solid transparent;padding:1px 6px;border-radius:6px;background:var(--md-sys-color-surface-container-highest);color:var(--md-sys-color-on-surface-variant);font-family:monospace;font-size:10px;cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease}.pdx-staging-coords:hover,.pdx-staging-coords:focus-visible{background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container);border-color:var(--md-sys-color-primary)}.pdx-staging-remove{appearance:none;border:0;background:transparent;color:var(--md-sys-color-on-surface-variant);cursor:pointer;font-size:14px;line-height:1;padding:0 2px;border-radius:50%}.pdx-staging-remove:hover{color:var(--md-sys-color-error)}.pdx-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px}.pdx-grid.dense{grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px}.pdx-grid.roomy{grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px}.pdx-card-item{position:relative;display:flex;flex-direction:column}.pdx-card{appearance:none;position:relative;width:100%;flex:1;display:grid;grid-template-columns:44px 1fr;grid-template-rows:auto auto;gap:10px;padding:14px;border:1px solid transparent;border-radius:14px;cursor:pointer;outline:none;color:inherit;font:inherit;min-height:92px;text-align:left;background:var(--md-sys-color-surface);border-color:var(--md-sys-color-outline-variant);transition:box-shadow .2s ease,background .25s ease,transform .06s ease}.pdx-grid.dense .pdx-card{padding:10px;border-radius:12px;min-height:84px}.pdx-grid.roomy .pdx-card{padding:16px;border-radius:16px;min-height:100px}.pdx-card:focus,.pdx-card:hover{box-shadow:var(--mat-elevation-level4);background:var(--md-sys-color-surface-container-low);border-color:var(--md-sys-color-primary)}.pdx-card:focus-visible{outline:2px solid var(--md-sys-color-primary);outline-offset:2px}.pdx-card:active{transform:translateY(1px)}.pdx-card-icon{grid-row:span 2;display:flex;align-items:center;justify-content:center;color:var(--md-sys-color-primary)}.pdx-card-head-row{display:flex;align-items:center;justify-content:space-between;gap:6px;padding-right:48px}.pdx-card-title{font-weight:600;line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pdx-card-queue-btn{appearance:none;position:absolute;top:10px;right:10px;z-index:2;flex-shrink:0;padding:2px 7px;border-radius:999px;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 40%,var(--md-sys-color-outline-variant));background:color-mix(in srgb,var(--md-sys-color-primary-container) 45%,var(--md-sys-color-surface));color:var(--md-sys-color-primary);font-size:10px;font-weight:700;line-height:1.2;cursor:pointer;transition:background .15s ease,color .15s ease}.pdx-card-queue-btn:hover,.pdx-card-queue-btn:focus-visible{background:var(--md-sys-color-primary);color:var(--md-sys-color-on-primary)}.pdx-card-desc{color:var(--md-sys-color-on-surface-variant);display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;word-break:break-word}.pdx-card-badges{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}.pdx-card-badge{display:inline-flex;align-items:center;gap:4px;min-height:22px;padding:2px 8px;border-radius:999px;border:1px solid var(--md-sys-color-outline-variant);color:var(--md-sys-color-on-surface-variant);background:var(--md-sys-color-surface-container-highest);font-size:11px;line-height:1.2;font-weight:600;letter-spacing:.01em}.pdx-card-badge mat-icon{width:14px;height:14px;font-size:14px}.pdx-card-badge.ready{color:var(--md-sys-color-primary);border-color:color-mix(in srgb,var(--md-sys-color-primary) 45%,transparent);background:color-mix(in srgb,var(--md-sys-color-primary-container) 72%,transparent)}.pdx-card-badge.info{color:var(--md-sys-color-tertiary);border-color:color-mix(in srgb,var(--md-sys-color-tertiary) 38%,transparent);background:color-mix(in srgb,var(--md-sys-color-tertiary-container) 68%,transparent)}.pdx-card-badge.warning{color:var(--md-sys-color-error);border-color:color-mix(in srgb,var(--md-sys-color-error) 35%,transparent);background:color-mix(in srgb,var(--md-sys-color-error-container) 62%,transparent)}.pdx-empty{padding:24px;display:grid;place-items:center;color:var(--md-sys-color-on-surface-variant);gap:8px}@media(max-width:600px){.pdx-palette-content{min-width:320px}.pdx-grid{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}}\n"] }]
|
|
3884
|
+
`, providers: [providePraxisPageBuilderI18n()], changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;color:var(--md-sys-color-on-surface)}.visually-hidden:where(:not(:focus-within,:active)){position:absolute!important;clip-path:inset(50%)!important;overflow:hidden!important;width:1px!important;height:1px!important;margin:-1px!important;padding:0!important;border:0!important;white-space:nowrap!important}h2[mat-dialog-title].pdx-dialog-header{margin:0;padding:18px 24px 16px;border-bottom:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 70%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface-container-low) 55%,var(--md-sys-color-surface))}h2[mat-dialog-title].pdx-dialog-header:before{display:none;content:none}.dlg-title{display:flex;align-items:center;gap:14px}.dlg-icon-wrap{width:40px;height:40px;flex-shrink:0;border-radius:12px;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--md-sys-color-primary-container) 62%,var(--md-sys-color-surface));border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 26%,transparent);color:var(--md-sys-color-primary)}.dlg-icon{width:22px;height:22px;font-size:22px;color:var(--md-sys-color-primary)}.dlg-texts{min-width:0;flex:1;display:flex;flex-direction:column;gap:3px}.dlg-head-row{display:flex;align-items:center;flex-wrap:wrap;gap:10px}.dlg-head{font-size:1.125rem;font-weight:700;letter-spacing:-.015em;line-height:1.25;color:var(--md-sys-color-on-surface)}.dlg-count-pill{display:inline-flex;align-items:center;padding:2px 9px;border-radius:999px;font-size:.72rem;font-weight:600;line-height:1.3;color:var(--md-sys-color-on-secondary-container);background:color-mix(in srgb,var(--md-sys-color-secondary-container) 78%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 60%,transparent)}.dlg-sub{font-size:.8125rem;line-height:1.35;color:var(--md-sys-color-on-surface-variant)}.pdx-palette-content{min-width:560px;max-width:920px;padding:16px 24px 18px!important;display:flex;flex-direction:column;gap:12px}.pdx-search-shell{width:100%}.pdx-search-bar{width:100%;min-height:44px;padding:0 12px 0 14px;border-radius:12px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 90%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface-container-low) 75%,var(--md-sys-color-surface));display:flex;align-items:center;gap:10px;box-sizing:border-box;transition:border-color .16s ease,box-shadow .16s ease,background-color .16s ease}.pdx-search-bar:hover{border-color:color-mix(in srgb,var(--md-sys-color-on-surface) 32%,var(--md-sys-color-outline-variant))}.pdx-search-bar:focus-within{background:var(--md-sys-color-surface);border-color:var(--md-sys-color-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--md-sys-color-primary) 14%,transparent)}.pdx-search-icon{width:20px;height:20px;font-size:20px;flex-shrink:0;color:var(--md-sys-color-on-surface-variant);transition:color .16s ease}.pdx-search-bar:focus-within .pdx-search-icon{color:var(--md-sys-color-primary)}.pdx-search-input{flex:1;min-width:0;border:0;outline:none;background:transparent;color:var(--md-sys-color-on-surface);font:inherit;font-size:.925rem;line-height:1.4;padding:8px 0}.pdx-search-input::placeholder{color:color-mix(in srgb,var(--md-sys-color-on-surface-variant) 78%,transparent)}.pdx-search-input::-webkit-search-cancel-button{-webkit-appearance:none;appearance:none}.pdx-search-clear{appearance:none;width:26px;height:26px;border-radius:7px;border:0;background:transparent;color:var(--md-sys-color-on-surface-variant);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:0;transition:background-color .15s ease,color .15s ease}.pdx-search-clear mat-icon{width:16px;height:16px;font-size:16px}.pdx-search-clear:hover,.pdx-search-clear:focus-visible{background:var(--md-sys-color-surface-container-highest);color:var(--md-sys-color-on-surface);outline:none}.pdx-search-kbd{display:inline-flex;align-items:center;justify-content:center;min-width:22px;height:22px;padding:0 6px;border-radius:6px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.72rem;font-weight:600;color:var(--md-sys-color-on-surface-variant);background:var(--md-sys-color-surface-container-high);border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 80%,transparent);flex-shrink:0}.pdx-category-tabs{display:flex;flex-wrap:wrap;align-items:center;gap:6px}.pdx-category-pill{appearance:none;min-height:28px;padding:0 10px;border-radius:999px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 75%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface-container-low) 65%,var(--md-sys-color-surface));color:var(--md-sys-color-on-surface-variant);font-family:inherit;font-size:.75rem;font-weight:600;display:inline-flex;align-items:center;gap:6px;cursor:pointer;transition:background-color .15s ease,color .15s ease,border-color .15s ease}.pdx-category-pill:hover{color:var(--md-sys-color-on-surface);border-color:color-mix(in srgb,var(--md-sys-color-primary) 35%,var(--md-sys-color-outline-variant))}.pdx-category-pill:focus-visible{outline:2px solid var(--md-sys-color-primary);outline-offset:2px}.pdx-category-pill.selected{background:color-mix(in srgb,var(--md-sys-color-primary-container) 78%,var(--md-sys-color-surface));color:var(--md-sys-color-on-primary-container);border-color:color-mix(in srgb,var(--md-sys-color-primary) 52%,transparent);font-weight:700}.pdx-category-count{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:16px;padding:0 5px;border-radius:999px;font-size:.675rem;font-weight:700;background:color-mix(in srgb,var(--md-sys-color-surface-container-highest) 75%,transparent);color:currentColor}.pdx-category-pill.selected .pdx-category-count{background:color-mix(in srgb,var(--md-sys-color-primary) 22%,transparent)}.pdx-palette-toolbar{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:10px 14px;padding:2px 0 10px;border-bottom:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 55%,transparent)}.pdx-palette-span-group{display:flex;flex-direction:column;align-items:flex-start;gap:5px}.pdx-palette-toolbar-label{font-size:.68rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--md-sys-color-on-surface-variant);white-space:nowrap;padding-left:2px}.pdx-span-segmented{display:inline-flex;flex-wrap:wrap;align-items:center;gap:2px;padding:3px;border-radius:9px;background:color-mix(in srgb,var(--md-sys-color-surface-container-highest) 60%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 55%,transparent)}.pdx-span-chip{appearance:none;min-height:28px;padding:0 8px;font-family:inherit;font-size:.74rem;font-weight:600;line-height:1;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--md-sys-color-on-surface-variant);cursor:pointer;white-space:nowrap;transition:background-color .15s ease,color .15s ease,border-color .15s ease,box-shadow .15s ease}.pdx-span-chip:hover{color:var(--md-sys-color-on-surface);background:color-mix(in srgb,var(--md-sys-color-surface) 65%,transparent)}.pdx-span-chip:focus-visible{outline:2px solid var(--md-sys-color-primary);outline-offset:1px}.pdx-span-chip.selected{background:var(--md-sys-color-secondary-container);color:var(--md-sys-color-on-secondary-container);border-color:color-mix(in srgb,var(--md-sys-color-primary) 42%,transparent);font-weight:700;box-shadow:0 1px 2px #0000000f}.pdx-palette-mode-actions{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin-left:auto}.pdx-keep-open-chip,.pdx-batch-mode-chip{appearance:none;min-height:30px;padding:0 10px;font-family:inherit;font-size:.75rem;font-weight:600;border-radius:8px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 85%,transparent);background:var(--md-sys-color-surface);color:var(--md-sys-color-on-surface-variant);display:inline-flex;align-items:center;gap:5px;cursor:pointer;white-space:nowrap;transition:background-color .15s ease,color .15s ease,border-color .15s ease}.pdx-keep-open-chip mat-icon,.pdx-batch-mode-chip mat-icon{width:16px;height:16px;font-size:16px;color:currentColor}.pdx-keep-open-chip:hover,.pdx-batch-mode-chip:hover{color:var(--md-sys-color-on-surface);border-color:color-mix(in srgb,var(--md-sys-color-primary) 40%,var(--md-sys-color-outline-variant));background:var(--md-sys-color-surface-container-low)}.pdx-keep-open-chip:focus-visible,.pdx-batch-mode-chip:focus-visible{outline:2px solid var(--md-sys-color-primary);outline-offset:2px}.pdx-keep-open-chip.selected,.pdx-batch-mode-chip.selected{background:var(--md-sys-color-secondary-container);color:var(--md-sys-color-on-secondary-container);border-color:var(--md-sys-color-primary);font-weight:700}.pdx-inserted-badge{display:inline-flex;align-items:center;justify-content:center;margin-left:2px;padding:1px 6px;border-radius:999px;background:var(--md-sys-color-primary);color:var(--md-sys-color-on-primary);font-size:10px;font-weight:700;line-height:1.3}.pdx-staging-tray{padding:10px 12px;border-radius:12px;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 38%,var(--md-sys-color-outline-variant));background:color-mix(in srgb,var(--md-sys-color-primary-container) 22%,var(--md-sys-color-surface-container-low))}.pdx-staging-header{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;font-size:12px}.pdx-staging-summary{display:flex;align-items:center;gap:6px;color:var(--md-sys-color-on-surface)}.pdx-staging-summary mat-icon{width:16px;height:16px;font-size:16px;color:var(--md-sys-color-primary)}.pdx-staging-clear{min-height:26px;padding:0 8px;font-size:11px}.pdx-staging-list{display:flex;flex-wrap:wrap;gap:6px;max-height:112px;overflow-y:auto}.pdx-staging-chip{display:inline-flex;align-items:center;gap:5px;padding:3px 8px;border-radius:999px;border:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface);font-size:11px;line-height:1.2}.pdx-staging-chip mat-icon{width:14px;height:14px;font-size:14px;color:var(--md-sys-color-primary)}.pdx-staging-index{font-weight:700;color:var(--md-sys-color-primary)}.pdx-staging-title{max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600}.pdx-staging-coords{appearance:none;border:1px solid transparent;padding:2px 6px;border-radius:6px;background:var(--md-sys-color-surface-container-highest);color:var(--md-sys-color-on-surface-variant);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:10px;cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease}.pdx-staging-coords:hover,.pdx-staging-coords:focus-visible{background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container);border-color:var(--md-sys-color-primary)}.pdx-staging-remove{appearance:none;border:0;background:transparent;color:var(--md-sys-color-on-surface-variant);cursor:pointer;font-size:14px;line-height:1;padding:0 3px;border-radius:50%}.pdx-staging-remove:hover{color:var(--md-sys-color-error)}.pdx-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}.pdx-grid.dense{grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:10px}.pdx-grid.roomy{grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:14px}.pdx-grid.single-entry{grid-template-columns:minmax(0,1fr)}.pdx-card-item{position:relative;display:flex;flex-direction:column}.pdx-card{appearance:none;position:relative;width:100%;flex:1;display:grid;grid-template-columns:44px minmax(0,1fr);align-items:start;gap:14px;padding:16px;border-radius:14px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 85%,transparent);cursor:pointer;outline:none;color:inherit;font:inherit;min-height:96px;text-align:left;background:var(--md-sys-color-surface);box-shadow:0 1px 2px #0000000a;transition:box-shadow .18s ease,background-color .18s ease,border-color .18s ease,transform .08s ease}.pdx-grid.dense .pdx-card{padding:12px 14px;border-radius:12px;min-height:86px}.pdx-grid.roomy .pdx-card{padding:16px 18px;border-radius:14px;min-height:100px}.pdx-card:focus,.pdx-card:hover{box-shadow:0 6px 18px #00000014;background:color-mix(in srgb,var(--md-sys-color-surface-container-low) 70%,var(--md-sys-color-surface));border-color:var(--md-sys-color-primary)}.pdx-card:focus-visible{outline:2px solid var(--md-sys-color-primary);outline-offset:2px}.pdx-card:active{transform:translateY(1px)}.pdx-card-icon{width:44px;height:44px;border-radius:12px;align-self:start;display:flex;align-items:center;justify-content:center;color:var(--md-sys-color-primary);background:color-mix(in srgb,var(--md-sys-color-primary-container) 50%,var(--md-sys-color-surface-container-low));border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 22%,transparent);transition:background-color .18s ease,border-color .18s ease,transform .18s ease}.pdx-card:hover .pdx-card-icon,.pdx-card:focus-visible .pdx-card-icon{background:color-mix(in srgb,var(--md-sys-color-primary-container) 82%,var(--md-sys-color-surface));border-color:color-mix(in srgb,var(--md-sys-color-primary) 42%,transparent)}.pdx-card-icon mat-icon{width:22px;height:22px;font-size:22px}.pdx-card-body{min-width:0;display:flex;flex-direction:column;gap:4px}.pdx-card-head-row{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;padding-right:64px}.pdx-card-title{font-size:.9375rem;font-weight:650;letter-spacing:-.01em;line-height:1.3;color:var(--md-sys-color-on-surface);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pdx-card-queue-btn{appearance:none;position:absolute;top:14px;right:14px;z-index:2;flex-shrink:0;min-height:26px;min-width:52px;padding:0 10px;border-radius:999px;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 36%,var(--md-sys-color-outline-variant));background:color-mix(in srgb,var(--md-sys-color-primary-container) 42%,var(--md-sys-color-surface));color:var(--md-sys-color-primary);font-family:inherit;font-size:.6875rem;font-weight:700;line-height:1;display:inline-flex;align-items:center;justify-content:center;gap:3px;cursor:pointer;transition:background-color .15s ease,color .15s ease,border-color .15s ease}.pdx-card-queue-btn:hover,.pdx-card-queue-btn:focus-visible{background:var(--md-sys-color-primary);color:var(--md-sys-color-on-primary);border-color:var(--md-sys-color-primary);outline:none}.pdx-card-desc{font-size:.8125rem;line-height:1.48;color:var(--md-sys-color-on-surface-variant);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;word-break:break-word}.pdx-card-badges{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}.pdx-card-badge{display:inline-flex;align-items:center;gap:4px;min-height:22px;padding:2px 8px;border-radius:6px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 85%,transparent);color:var(--md-sys-color-on-surface-variant);background:var(--md-sys-color-surface-container-high);font-size:.6875rem;line-height:1.2;font-weight:600;letter-spacing:.01em}.pdx-card-badge mat-icon{width:13px;height:13px;font-size:13px;flex-shrink:0}.pdx-card-badge.ready{color:color-mix(in srgb,var(--md-sys-color-on-primary-container) 75%,var(--md-sys-color-primary));border-color:color-mix(in srgb,var(--md-sys-color-primary) 36%,transparent);background:color-mix(in srgb,var(--md-sys-color-primary-container) 68%,transparent)}.pdx-card-badge.info{color:color-mix(in srgb,var(--md-sys-color-on-tertiary-container) 75%,var(--md-sys-color-tertiary));border-color:color-mix(in srgb,var(--md-sys-color-tertiary) 34%,transparent);background:color-mix(in srgb,var(--md-sys-color-tertiary-container) 62%,transparent)}.pdx-card-badge.warning{color:color-mix(in srgb,var(--md-sys-color-on-error-container) 75%,var(--md-sys-color-error));border-color:color-mix(in srgb,var(--md-sys-color-error) 34%,transparent);background:color-mix(in srgb,var(--md-sys-color-error-container) 60%,transparent)}.pdx-empty{padding:36px 24px;border-radius:14px;border:1px dashed color-mix(in srgb,var(--md-sys-color-outline-variant) 80%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface-container-low) 50%,transparent);display:grid;place-items:center;color:var(--md-sys-color-on-surface-variant);gap:10px;text-align:center}.pdx-empty-icon-wrap{width:44px;height:44px;border-radius:12px;display:flex;align-items:center;justify-content:center;background:var(--md-sys-color-surface-container-high);color:var(--md-sys-color-on-surface-variant)}.pdx-empty-title{font-size:.875rem;font-weight:600;color:var(--md-sys-color-on-surface)}.pdx-empty-sub{font-size:.8rem;max-width:380px;line-height:1.4;color:var(--md-sys-color-on-surface-variant)}.pdx-empty-reset-btn{appearance:none;margin-top:4px;min-height:32px;padding:0 12px;border-radius:8px;border:1px solid color-mix(in srgb,var(--md-sys-color-primary) 42%,var(--md-sys-color-outline-variant));background:color-mix(in srgb,var(--md-sys-color-primary-container) 45%,var(--md-sys-color-surface));color:var(--md-sys-color-primary);font-family:inherit;font-size:.775rem;font-weight:600;display:inline-flex;align-items:center;gap:6px;cursor:pointer;transition:background-color .15s ease,color .15s ease}.pdx-empty-reset-btn mat-icon{width:16px;height:16px;font-size:16px}.pdx-empty-reset-btn:hover,.pdx-empty-reset-btn:focus-visible{background:var(--md-sys-color-primary);color:var(--md-sys-color-on-primary);outline:none}div[mat-dialog-actions].pdx-dialog-footer{margin:0;min-height:56px;padding:12px 24px 16px;border-top:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 70%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface-container-low) 45%,var(--md-sys-color-surface));display:flex;align-items:center;justify-content:space-between;gap:12px}.pdx-footer-hints{display:flex;align-items:center;flex-wrap:wrap;gap:6px;font-size:.72rem;color:var(--md-sys-color-on-surface-variant)}.pdx-kbd-hint{display:inline-flex;align-items:center;gap:4px}.pdx-kbd-hint kbd{display:inline-flex;align-items:center;padding:1px 5px;border-radius:5px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.68rem;font-weight:600;color:var(--md-sys-color-on-surface);background:var(--md-sys-color-surface-container-high);border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant) 80%,transparent)}.pdx-kbd-sep{opacity:.55}.pdx-footer-actions{display:flex;align-items:center;gap:8px;margin-left:auto}.pdx-footer-cancel-btn,.pdx-footer-commit-btn{border-radius:10px;min-height:36px;padding:0 16px;font-weight:600}@media(max-width:680px){.pdx-palette-content{min-width:300px;padding:14px 16px!important}h2[mat-dialog-title].pdx-dialog-header{padding:14px 16px}div[mat-dialog-actions].pdx-dialog-footer{padding:10px 16px 14px}.pdx-footer-hints{display:none}.pdx-palette-mode-actions{margin-left:0;width:100%}.pdx-grid{grid-template-columns:repeat(auto-fill,minmax(220px,1fr))}}\n"] }]
|
|
3574
3885
|
}], ctorParameters: () => [{ type: i1.MatDialogRef }, { type: i2$2.ComponentMetadataRegistry }, { type: i2$2.PraxisI18nService }, { type: undefined, decorators: [{
|
|
3575
3886
|
type: Inject,
|
|
3576
3887
|
args: [MAT_DIALOG_DATA]
|
|
@@ -3868,7 +4179,7 @@ class WidgetShellEditorComponent {
|
|
|
3868
4179
|
return this.i18n.t('praxis.pageBuilder.shell.stickyHeader');
|
|
3869
4180
|
}
|
|
3870
4181
|
form = new FormGroup({
|
|
3871
|
-
kind: new FormControl('
|
|
4182
|
+
kind: new FormControl('none', {
|
|
3872
4183
|
nonNullable: true,
|
|
3873
4184
|
}),
|
|
3874
4185
|
preset: new FormControl('', { nonNullable: true }),
|
|
@@ -3891,6 +4202,7 @@ class WidgetShellEditorComponent {
|
|
|
3891
4202
|
cardBorderStyle: new FormControl('', { nonNullable: true }),
|
|
3892
4203
|
cardRadius: new FormControl('', { nonNullable: true }),
|
|
3893
4204
|
cardShadow: new FormControl('', { nonNullable: true }),
|
|
4205
|
+
interactionHighlight: new FormControl('', { nonNullable: true }),
|
|
3894
4206
|
cardFilter: new FormControl('', { nonNullable: true }),
|
|
3895
4207
|
cardFallbackBg: new FormControl('', { nonNullable: true }),
|
|
3896
4208
|
headerBg: new FormControl('', { nonNullable: true }),
|
|
@@ -4092,7 +4404,7 @@ class WidgetShellEditorComponent {
|
|
|
4092
4404
|
const typography = appearance.typography || {};
|
|
4093
4405
|
this.form.setControl('actions', new FormArray((shell?.actions || []).map((a) => this.createActionGroup(a)), control => this.validateActionIds(control)));
|
|
4094
4406
|
this.form.setValue({
|
|
4095
|
-
kind: shell
|
|
4407
|
+
kind: !shell || shell.kind === 'none' ? 'none' : 'dashboard-card',
|
|
4096
4408
|
preset: shell?.preset || '',
|
|
4097
4409
|
applyToAll: false,
|
|
4098
4410
|
title: this.editableText(shell?.title),
|
|
@@ -4111,6 +4423,7 @@ class WidgetShellEditorComponent {
|
|
|
4111
4423
|
cardBorderStyle: card.borderStyle || '',
|
|
4112
4424
|
cardRadius: card.borderRadius || '',
|
|
4113
4425
|
cardShadow: card.shadow || '',
|
|
4426
|
+
interactionHighlight: card.interactionHighlight || '',
|
|
4114
4427
|
cardFilter: card.backdropFilter || '',
|
|
4115
4428
|
cardFallbackBg: card.fallbackBackground || '',
|
|
4116
4429
|
headerBg: header.background || '',
|
|
@@ -4194,7 +4507,7 @@ class WidgetShellEditorComponent {
|
|
|
4194
4507
|
buildShell() {
|
|
4195
4508
|
const raw = this.form.getRawValue();
|
|
4196
4509
|
const initial = this.initialShell.value;
|
|
4197
|
-
const shell = initial ? { ...initial } : {};
|
|
4510
|
+
const shell = initial ? { ...initial } : { kind: 'none' };
|
|
4198
4511
|
if (this.canEditShellIdentity()) {
|
|
4199
4512
|
delete shell.kind;
|
|
4200
4513
|
delete shell.preset;
|
|
@@ -4290,6 +4603,8 @@ class WidgetShellEditorComponent {
|
|
|
4290
4603
|
card.borderRadius = raw.cardRadius.trim();
|
|
4291
4604
|
if (raw.cardShadow.trim())
|
|
4292
4605
|
card.shadow = raw.cardShadow.trim();
|
|
4606
|
+
if (raw.interactionHighlight)
|
|
4607
|
+
card.interactionHighlight = raw.interactionHighlight;
|
|
4293
4608
|
if (raw.cardFilter.trim())
|
|
4294
4609
|
card.backdropFilter = raw.cardFilter.trim();
|
|
4295
4610
|
if (raw.cardFallbackBg.trim())
|
|
@@ -4350,7 +4665,7 @@ class WidgetShellEditorComponent {
|
|
|
4350
4665
|
return shellCapabilities[capability] !== false;
|
|
4351
4666
|
}
|
|
4352
4667
|
appearanceControls = [
|
|
4353
|
-
'cardBg', 'cardBorder', 'cardBorderWidth', 'cardBorderStyle', 'cardRadius', 'cardShadow', 'cardFilter', 'cardFallbackBg', 'headerBg', 'headerBorder', 'headerBorderWidth', 'headerBorderStyle',
|
|
4668
|
+
'interactionHighlight', 'cardBg', 'cardBorder', 'cardBorderWidth', 'cardBorderStyle', 'cardRadius', 'cardShadow', 'cardFilter', 'cardFallbackBg', 'headerBg', 'headerBorder', 'headerBorderWidth', 'headerBorderStyle',
|
|
4354
4669
|
'titleColor', 'subtitleColor', 'iconColor', 'bodyBg', 'bodyColor', 'bodyPadding',
|
|
4355
4670
|
'titleSize', 'titleWeight', 'subtitleSize',
|
|
4356
4671
|
];
|
|
@@ -4888,6 +5203,27 @@ class WidgetShellEditorComponent {
|
|
|
4888
5203
|
<button praxisIconButton="help_outline" size="dense" matSuffix type="button" [matTooltip]="shellText('cardFallbackHelp')" [attr.aria-label]="shellText('cardFallbackHelp')"></button>
|
|
4889
5204
|
@if (form.controls.cardFallbackBg.hasError('cssDeclaration')) { <mat-error>{{ appearanceError() }}</mat-error> }
|
|
4890
5205
|
</mat-form-field>
|
|
5206
|
+
<div class="shell-highlight-control">
|
|
5207
|
+
<span class="shell-highlight-label">{{ shellText('interactionHighlight.label') }}</span>
|
|
5208
|
+
<mat-button-toggle-group
|
|
5209
|
+
class="shell-highlight-options"
|
|
5210
|
+
data-testid="shell-interaction-highlight"
|
|
5211
|
+
[formControl]="form.controls.interactionHighlight"
|
|
5212
|
+
[attr.aria-label]="shellText('interactionHighlight.label')"
|
|
5213
|
+
>
|
|
5214
|
+
<mat-button-toggle value="">{{ shellText('interactionHighlight.inherit') }}</mat-button-toggle>
|
|
5215
|
+
<mat-button-toggle value="standard">
|
|
5216
|
+
<mat-icon aria-hidden="true" praxisIcon="crop_square"></mat-icon>
|
|
5217
|
+
{{ shellText('interactionHighlight.standard') }}
|
|
5218
|
+
</mat-button-toggle>
|
|
5219
|
+
<mat-button-toggle value="gradient">
|
|
5220
|
+
<mat-icon aria-hidden="true" praxisIcon="gradient"></mat-icon>
|
|
5221
|
+
{{ shellText('interactionHighlight.gradient') }}
|
|
5222
|
+
</mat-button-toggle>
|
|
5223
|
+
</mat-button-toggle-group>
|
|
5224
|
+
<p class="shell-action-hint">{{ shellText('interactionHighlight.help') }}</p>
|
|
5225
|
+
</div>
|
|
5226
|
+
|
|
4891
5227
|
<mat-form-field floatLabel="always" appearance="outline" subscriptSizing="dynamic">
|
|
4892
5228
|
<mat-label>{{ shellText('cardBg') }}</mat-label>
|
|
4893
5229
|
<input
|
|
@@ -5262,7 +5598,7 @@ class WidgetShellEditorComponent {
|
|
|
5262
5598
|
<div class="shell-preview-content">
|
|
5263
5599
|
<p class="shell-hint">{{ shellText('previewHint') }}</p>
|
|
5264
5600
|
<div class="shell-preview">
|
|
5265
|
-
<praxis-widget-shell [shell]="previewShell()">
|
|
5601
|
+
<praxis-widget-shell [shell]="previewShell()" [style.--pdx-widget-highlight-active]="1">
|
|
5266
5602
|
<div class="shell-sample-content">
|
|
5267
5603
|
<span>{{ shellText('sampleMetricLabel') }}</span>
|
|
5268
5604
|
<strong>842</strong>
|
|
@@ -5281,7 +5617,7 @@ class WidgetShellEditorComponent {
|
|
|
5281
5617
|
</div>
|
|
5282
5618
|
</details>
|
|
5283
5619
|
</div>
|
|
5284
|
-
`, 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}@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: MatCheckboxModule }, { kind: "component", type: i4.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: i6.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i6.MatLabel, selector: "mat-label" }, { kind: "directive", type: i6.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i6.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i6.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: i8.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 });
|
|
5620
|
+
`, 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 });
|
|
5285
5621
|
}
|
|
5286
5622
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: WidgetShellEditorComponent, decorators: [{
|
|
5287
5623
|
type: Component,
|
|
@@ -5291,6 +5627,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
5291
5627
|
FormsModule,
|
|
5292
5628
|
ReactiveFormsModule,
|
|
5293
5629
|
MatButtonModule,
|
|
5630
|
+
MatButtonToggleModule,
|
|
5294
5631
|
MatCheckboxModule,
|
|
5295
5632
|
MatTooltipModule,
|
|
5296
5633
|
MatFormFieldModule,
|
|
@@ -5552,6 +5889,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
5552
5889
|
<button praxisIconButton="help_outline" size="dense" matSuffix type="button" [matTooltip]="shellText('cardFallbackHelp')" [attr.aria-label]="shellText('cardFallbackHelp')"></button>
|
|
5553
5890
|
@if (form.controls.cardFallbackBg.hasError('cssDeclaration')) { <mat-error>{{ appearanceError() }}</mat-error> }
|
|
5554
5891
|
</mat-form-field>
|
|
5892
|
+
<div class="shell-highlight-control">
|
|
5893
|
+
<span class="shell-highlight-label">{{ shellText('interactionHighlight.label') }}</span>
|
|
5894
|
+
<mat-button-toggle-group
|
|
5895
|
+
class="shell-highlight-options"
|
|
5896
|
+
data-testid="shell-interaction-highlight"
|
|
5897
|
+
[formControl]="form.controls.interactionHighlight"
|
|
5898
|
+
[attr.aria-label]="shellText('interactionHighlight.label')"
|
|
5899
|
+
>
|
|
5900
|
+
<mat-button-toggle value="">{{ shellText('interactionHighlight.inherit') }}</mat-button-toggle>
|
|
5901
|
+
<mat-button-toggle value="standard">
|
|
5902
|
+
<mat-icon aria-hidden="true" praxisIcon="crop_square"></mat-icon>
|
|
5903
|
+
{{ shellText('interactionHighlight.standard') }}
|
|
5904
|
+
</mat-button-toggle>
|
|
5905
|
+
<mat-button-toggle value="gradient">
|
|
5906
|
+
<mat-icon aria-hidden="true" praxisIcon="gradient"></mat-icon>
|
|
5907
|
+
{{ shellText('interactionHighlight.gradient') }}
|
|
5908
|
+
</mat-button-toggle>
|
|
5909
|
+
</mat-button-toggle-group>
|
|
5910
|
+
<p class="shell-action-hint">{{ shellText('interactionHighlight.help') }}</p>
|
|
5911
|
+
</div>
|
|
5912
|
+
|
|
5555
5913
|
<mat-form-field floatLabel="always" appearance="outline" subscriptSizing="dynamic">
|
|
5556
5914
|
<mat-label>{{ shellText('cardBg') }}</mat-label>
|
|
5557
5915
|
<input
|
|
@@ -5926,7 +6284,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
5926
6284
|
<div class="shell-preview-content">
|
|
5927
6285
|
<p class="shell-hint">{{ shellText('previewHint') }}</p>
|
|
5928
6286
|
<div class="shell-preview">
|
|
5929
|
-
<praxis-widget-shell [shell]="previewShell()">
|
|
6287
|
+
<praxis-widget-shell [shell]="previewShell()" [style.--pdx-widget-highlight-active]="1">
|
|
5930
6288
|
<div class="shell-sample-content">
|
|
5931
6289
|
<span>{{ shellText('sampleMetricLabel') }}</span>
|
|
5932
6290
|
<strong>842</strong>
|
|
@@ -5945,7 +6303,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
5945
6303
|
</div>
|
|
5946
6304
|
</details>
|
|
5947
6305
|
</div>
|
|
5948
|
-
`, changeDetection: ChangeDetectionStrategy.OnPush, 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}@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"] }]
|
|
6306
|
+
`, changeDetection: ChangeDetectionStrategy.OnPush, 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"] }]
|
|
5949
6307
|
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
5950
6308
|
type: Optional
|
|
5951
6309
|
}, {
|
|
@@ -16621,11 +16979,49 @@ class DynamicPageConfigEditorComponent {
|
|
|
16621
16979
|
}
|
|
16622
16980
|
}
|
|
16623
16981
|
const normalizedPath = resourcePath.replace(/^\/+|\/+$/g, '');
|
|
16624
|
-
const
|
|
16625
|
-
|
|
16626
|
-
|
|
16627
|
-
|
|
16628
|
-
|
|
16982
|
+
const mappedMetas = mapCatalogFieldsToMetadata(entry?.responseFields);
|
|
16983
|
+
const metaByName = new Map(mappedMetas.map((meta) => [meta.name, meta]));
|
|
16984
|
+
const fallbackFields = (entry?.responseFields || [])
|
|
16985
|
+
.filter((f) => Boolean(f?.name && String(f.name).trim()))
|
|
16986
|
+
.map((f) => {
|
|
16987
|
+
const name = String(f.name).trim();
|
|
16988
|
+
const rawLabel = typeof f.label === 'string' ? f.label.trim() : '';
|
|
16989
|
+
const label = rawLabel && rawLabel !== name
|
|
16990
|
+
? rawLabel
|
|
16991
|
+
: humanizeCatalogFieldName(name);
|
|
16992
|
+
const mapped = metaByName.get(name);
|
|
16993
|
+
const description = typeof f.description === 'string' && f.description.trim()
|
|
16994
|
+
? f.description.trim()
|
|
16995
|
+
: undefined;
|
|
16996
|
+
return {
|
|
16997
|
+
name,
|
|
16998
|
+
label,
|
|
16999
|
+
type: f.type || 'string',
|
|
17000
|
+
...(description ? { description } : {}),
|
|
17001
|
+
...(typeof f.format === 'string' && f.format.trim()
|
|
17002
|
+
? { format: f.format.trim() }
|
|
17003
|
+
: {}),
|
|
17004
|
+
...(typeof f.controlType === 'string' && f.controlType.trim()
|
|
17005
|
+
? { controlType: f.controlType.trim() }
|
|
17006
|
+
: mapped?.controlType
|
|
17007
|
+
? { controlType: mapped.controlType }
|
|
17008
|
+
: {}),
|
|
17009
|
+
...(Array.isArray(f.enumValues) ? { enumValues: f.enumValues } : {}),
|
|
17010
|
+
...(mapped?.options?.length
|
|
17011
|
+
? {
|
|
17012
|
+
options: mapped.options.map((opt) => {
|
|
17013
|
+
const optLabel = String(opt.label ?? opt.text ?? opt.value);
|
|
17014
|
+
return {
|
|
17015
|
+
key: String(opt.value),
|
|
17016
|
+
value: String(opt.value),
|
|
17017
|
+
label: optLabel,
|
|
17018
|
+
text: opt.text ?? optLabel,
|
|
17019
|
+
};
|
|
17020
|
+
}),
|
|
17021
|
+
}
|
|
17022
|
+
: {}),
|
|
17023
|
+
};
|
|
17024
|
+
});
|
|
16629
17025
|
if (normalizedPath !== this.composerSchemaPath || fallbackFields.length > 0 || !entry) {
|
|
16630
17026
|
this.composerSchemaPath = '';
|
|
16631
17027
|
this.composerSchemaFields.set(fallbackFields);
|
|
@@ -16642,10 +17038,17 @@ class DynamicPageConfigEditorComponent {
|
|
|
16642
17038
|
if (current && fields.some((f) => f.name === current))
|
|
16643
17039
|
return;
|
|
16644
17040
|
const isBoolean = (f) => this.getComposerFieldTypeBadge(f).toLowerCase() === 'boolean';
|
|
16645
|
-
const
|
|
17041
|
+
const hasEnumOptions = (f) => {
|
|
17042
|
+
const raw = f;
|
|
17043
|
+
return ((Array.isArray(raw['enumValues']) && raw['enumValues'].length > 0)
|
|
17044
|
+
|| (Array.isArray(f.options) && f.options.length > 0));
|
|
17045
|
+
};
|
|
17046
|
+
const isStatusSemantic = (f) => /^(ativo|status|situacao|situação|situacaofuncional|enabled|active)/i.test(String(f.name || ''));
|
|
16646
17047
|
const candidate = fields.find((f) => isBoolean(f) && isStatusSemantic(f))
|
|
16647
17048
|
?? fields.find((f) => isBoolean(f))
|
|
16648
|
-
?? fields.find((f) =>
|
|
17049
|
+
?? fields.find((f) => hasEnumOptions(f) && isStatusSemantic(f))
|
|
17050
|
+
?? fields.find((f) => /^(ativo|status|situacao|situação|situacaofuncional|enabled|active|tipo|categoria|estado)/i.test(String(f.name || '')))
|
|
17051
|
+
?? fields.find((f) => hasEnumOptions(f))
|
|
16649
17052
|
?? fields[0];
|
|
16650
17053
|
if (candidate?.name) {
|
|
16651
17054
|
this.composerGroupFieldControl.setValue(candidate.name, { emitEvent: false });
|
|
@@ -16732,6 +17135,55 @@ class DynamicPageConfigEditorComponent {
|
|
|
16732
17135
|
},
|
|
16733
17136
|
});
|
|
16734
17137
|
}
|
|
17138
|
+
resolveGroupByMappings(groupField) {
|
|
17139
|
+
const baseMappings = {
|
|
17140
|
+
true: 'active',
|
|
17141
|
+
false: 'inactive',
|
|
17142
|
+
1: 'active',
|
|
17143
|
+
0: 'inactive',
|
|
17144
|
+
ATIVO: 'active',
|
|
17145
|
+
INATIVO: 'inactive',
|
|
17146
|
+
ACTIVE: 'active',
|
|
17147
|
+
INACTIVE: 'inactive',
|
|
17148
|
+
A: 'active',
|
|
17149
|
+
I: 'inactive',
|
|
17150
|
+
};
|
|
17151
|
+
const fieldDef = this.composerSchemaFields().find((f) => String(f?.name || '').trim() === groupField);
|
|
17152
|
+
if (!fieldDef)
|
|
17153
|
+
return baseMappings;
|
|
17154
|
+
const raw = fieldDef;
|
|
17155
|
+
const enumValues = [];
|
|
17156
|
+
if (Array.isArray(raw['enumValues'])) {
|
|
17157
|
+
for (const v of raw['enumValues']) {
|
|
17158
|
+
const s = String(v ?? '').trim();
|
|
17159
|
+
if (s)
|
|
17160
|
+
enumValues.push(s);
|
|
17161
|
+
}
|
|
17162
|
+
}
|
|
17163
|
+
else if (Array.isArray(fieldDef.options)) {
|
|
17164
|
+
for (const opt of fieldDef.options) {
|
|
17165
|
+
const s = String(opt?.value ?? opt?.key ?? '').trim();
|
|
17166
|
+
if (s)
|
|
17167
|
+
enumValues.push(s);
|
|
17168
|
+
}
|
|
17169
|
+
}
|
|
17170
|
+
if (!enumValues.length)
|
|
17171
|
+
return baseMappings;
|
|
17172
|
+
const positiveRegex = /^(true|1|ativo|active|a|sim|yes|abert[ao]|em_andamento|andamento|aprovad[ao]|concluid[ao]|regular|valid[ao]|normal|enabled)$/i;
|
|
17173
|
+
const hasPositiveMatch = enumValues.some((v) => positiveRegex.test(v));
|
|
17174
|
+
for (let i = 0; i < enumValues.length; i += 1) {
|
|
17175
|
+
const val = enumValues[i];
|
|
17176
|
+
if (baseMappings[val] !== undefined)
|
|
17177
|
+
continue;
|
|
17178
|
+
if (hasPositiveMatch) {
|
|
17179
|
+
baseMappings[val] = positiveRegex.test(val) ? 'active' : 'inactive';
|
|
17180
|
+
}
|
|
17181
|
+
else {
|
|
17182
|
+
baseMappings[val] = i === 0 ? 'active' : 'inactive';
|
|
17183
|
+
}
|
|
17184
|
+
}
|
|
17185
|
+
return baseMappings;
|
|
17186
|
+
}
|
|
16735
17187
|
composeCollectionScreen(overrides) {
|
|
16736
17188
|
const resourcePath = (overrides?.resourcePath ?? this.composerResourcePathControl.value).trim();
|
|
16737
17189
|
if (!resourcePath)
|
|
@@ -16943,18 +17395,7 @@ class DynamicPageConfigEditorComponent {
|
|
|
16943
17395
|
transform: {
|
|
16944
17396
|
kind: 'group-by-map',
|
|
16945
17397
|
totalField: 'total',
|
|
16946
|
-
mappings:
|
|
16947
|
-
true: 'active',
|
|
16948
|
-
false: 'inactive',
|
|
16949
|
-
1: 'active',
|
|
16950
|
-
0: 'inactive',
|
|
16951
|
-
ATIVO: 'active',
|
|
16952
|
-
INATIVO: 'inactive',
|
|
16953
|
-
ACTIVE: 'active',
|
|
16954
|
-
INACTIVE: 'inactive',
|
|
16955
|
-
A: 'active',
|
|
16956
|
-
I: 'inactive',
|
|
16957
|
-
},
|
|
17398
|
+
mappings: this.resolveGroupByMappings(groupField),
|
|
16958
17399
|
ratios: {
|
|
16959
17400
|
activeRatePct: {
|
|
16960
17401
|
numerator: 'active',
|
|
@@ -18129,7 +18570,7 @@ class DynamicPageConfigEditorComponent {
|
|
|
18129
18570
|
clone(value) { if (value == null || typeof value !== 'object')
|
|
18130
18571
|
return value; return JSON.parse(JSON.stringify(value)); }
|
|
18131
18572
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicPageConfigEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
18132
|
-
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: i5.MatButtonToggleGroup, selector: "mat-button-toggle-group", inputs: ["appearance", "name", "vertical", "value", "multiple", "disabled", "disabledInteractive", "hideSingleSelectionIndicator", "hideMultipleSelectionIndicator"], outputs: ["valueChange", "change"], exportAs: ["matButtonToggleGroup"] }, { kind: "component", type: i5.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: i6.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i6.MatLabel, selector: "mat-label" }, { kind: "directive", type: i6.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i6.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i6.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: i8.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.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 });
|
|
18573
|
+
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 });
|
|
18133
18574
|
}
|
|
18134
18575
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicPageConfigEditorComponent, decorators: [{
|
|
18135
18576
|
type: Component,
|
|
@@ -20067,7 +20508,7 @@ class PageConfigEditorComponent {
|
|
|
20067
20508
|
</div>
|
|
20068
20509
|
}
|
|
20069
20510
|
</div>
|
|
20070
|
-
`, 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: i6.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i6.MatLabel, selector: "mat-label" }, { kind: "directive", type: i6.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i8.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.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 });
|
|
20511
|
+
`, 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 });
|
|
20071
20512
|
}
|
|
20072
20513
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PageConfigEditorComponent, decorators: [{
|
|
20073
20514
|
type: Component,
|
|
@@ -21987,6 +22428,10 @@ const PRAXIS_PAGE_BUILDER_AUTHORING_MANIFEST = {
|
|
|
21987
22428
|
background: { type: 'string' }, borderColor: { type: 'string' }, borderWidth: { type: 'string', description: 'CSS border width; accepts per-side values.' }, borderStyle: { type: 'string', description: 'CSS border style: solid, dashed, dotted, double, none; accepts per-side values.' }, borderRadius: { type: 'string' }, shadow: { type: 'string' },
|
|
21988
22429
|
backdropFilter: { type: 'string', description: 'CSS backdrop effect for a translucent card, e.g. blur(16px) saturate(125%). none disables it.' },
|
|
21989
22430
|
fallbackBackground: { type: 'string', description: 'Opaque background for reduced transparency, unsupported blur and fullscreen/expanded modes.' },
|
|
22431
|
+
interactionHighlight: {
|
|
22432
|
+
type: 'string', enum: ['standard', 'gradient'],
|
|
22433
|
+
description: 'Editing selection and keyboard-focus highlight. Absence in the saved shell inherits the preset, then standard. This operation merges partial input: omission preserves an existing override; use the visual editor to remove it. Gradient uses theme colors; never changes child inputs.',
|
|
22434
|
+
},
|
|
21990
22435
|
} },
|
|
21991
22436
|
header: { type: 'object', properties: {
|
|
21992
22437
|
background: { type: 'string' }, borderColor: { type: 'string' },
|
|
@@ -30802,7 +31247,8 @@ class DynamicPageBuilderComponent {
|
|
|
30802
31247
|
return;
|
|
30803
31248
|
const canvas = this.currentPage()?.canvas;
|
|
30804
31249
|
const ref = this.dialog.open(ComponentPaletteDialogComponent, {
|
|
30805
|
-
width: '
|
|
31250
|
+
width: '780px',
|
|
31251
|
+
maxWidth: '94vw',
|
|
30806
31252
|
data: {
|
|
30807
31253
|
title: this.tx('connections.palette.insertComponent', 'Insert component'),
|
|
30808
31254
|
allowedWidgetIds: this.componentPaletteAllowedWidgetIds || undefined,
|
|
@@ -30914,13 +31360,14 @@ class DynamicPageBuilderComponent {
|
|
|
30914
31360
|
return metadataHints;
|
|
30915
31361
|
}
|
|
30916
31362
|
switch (componentId) {
|
|
31363
|
+
case 'praxis-crud':
|
|
31364
|
+
return { recommendedCols: 12, recommendedRows: 6 };
|
|
30917
31365
|
case 'praxis-table':
|
|
30918
31366
|
return { recommendedCols: 8, recommendedRows: 6 };
|
|
30919
31367
|
case 'praxis-filter':
|
|
30920
31368
|
return { recommendedCols: 12, recommendedRows: 3 };
|
|
30921
31369
|
case 'praxis-chart':
|
|
30922
31370
|
case 'praxis-list':
|
|
30923
|
-
case 'praxis-crud':
|
|
30924
31371
|
return { recommendedCols: 8, recommendedRows: 5 };
|
|
30925
31372
|
default:
|
|
30926
31373
|
return undefined;
|
|
@@ -31436,7 +31883,7 @@ class DynamicPageBuilderComponent {
|
|
|
31436
31883
|
emptyText: this.tx('agentic.projectKnowledgeCockpit.timelineEmpty', 'No safe timeline events published for this change-set yet.'),
|
|
31437
31884
|
});
|
|
31438
31885
|
const timelineNode = document.nodes.find((node) => node.type === 'timeline');
|
|
31439
|
-
return timelineNode?.type === 'timeline' ? timelineNode.items : [];
|
|
31886
|
+
return timelineNode?.type === 'timeline' ? (timelineNode.items ?? []) : [];
|
|
31440
31887
|
}
|
|
31441
31888
|
trackProjectKnowledgeTimelineItem(index, item) {
|
|
31442
31889
|
return item.id || `${index}:${item.title || item.badge || 'timeline'}`;
|
|
@@ -34075,7 +34522,7 @@ class DynamicPageBuilderComponent {
|
|
|
34075
34522
|
if (!metadataInputs?.length || !widgetKey)
|
|
34076
34523
|
return;
|
|
34077
34524
|
const inputNames = new Set(metadataInputs.map((input) => input.name));
|
|
34078
|
-
for (const identityInput of ['componentInstanceId', 'tabsId', 'formId']) {
|
|
34525
|
+
for (const identityInput of ['componentInstanceId', 'tabsId', 'formId', 'crudId']) {
|
|
34079
34526
|
if (inputNames.has(identityInput)
|
|
34080
34527
|
&& inputs[identityInput] === undefined
|
|
34081
34528
|
&& presetInputs?.[identityInput] === undefined) {
|
|
@@ -35668,6 +36115,196 @@ function providePraxisPageBuilderWidgets(widgets) {
|
|
|
35668
36115
|
return providers;
|
|
35669
36116
|
}
|
|
35670
36117
|
|
|
36118
|
+
/** Reusable save-as journey. Feed save() only from the builder's pageSaveRequested event. */
|
|
36119
|
+
class PraxisPageSaveAsComponent {
|
|
36120
|
+
adapter = input.required(...(ngDevMode ? [{ debugName: "adapter" }] : /* istanbul ignore next */ []));
|
|
36121
|
+
/** Monotonic host view/session identity; UI invalidation only, never an authorization grant. */
|
|
36122
|
+
contextKey = input.required(...(ngDevMode ? [{ debugName: "contextKey" }] : /* istanbul ignore next */ []));
|
|
36123
|
+
canSave = input(false, ...(ngDevMode ? [{ debugName: "canSave" }] : /* istanbul ignore next */ []));
|
|
36124
|
+
initialTitle = input('', ...(ngDevMode ? [{ debugName: "initialTitle" }] : /* istanbul ignore next */ []));
|
|
36125
|
+
initialMenuLabel = input('', ...(ngDevMode ? [{ debugName: "initialMenuLabel" }] : /* istanbul ignore next */ []));
|
|
36126
|
+
initialIcon = input('web', ...(ngDevMode ? [{ debugName: "initialIcon" }] : /* istanbul ignore next */ []));
|
|
36127
|
+
titleMaxLength = input(160, ...(ngDevMode ? [{ debugName: "titleMaxLength" }] : /* istanbul ignore next */ []));
|
|
36128
|
+
menuMaxLength = input(160, ...(ngDevMode ? [{ debugName: "menuMaxLength" }] : /* istanbul ignore next */ []));
|
|
36129
|
+
saveRequested = output();
|
|
36130
|
+
saved = output();
|
|
36131
|
+
saveFailed = output();
|
|
36132
|
+
busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
|
|
36133
|
+
choosingIcon = signal(false, ...(ngDevMode ? [{ debugName: "choosingIcon" }] : /* istanbul ignore next */ []));
|
|
36134
|
+
result = signal(null, ...(ngDevMode ? [{ debugName: "result" }] : /* istanbul ignore next */ []));
|
|
36135
|
+
error = signal(false, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
36136
|
+
iconError = signal(false, ...(ngDevMode ? [{ debugName: "iconError" }] : /* istanbul ignore next */ []));
|
|
36137
|
+
title = new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.pattern(/\S/), Validators.maxLength(160)] });
|
|
36138
|
+
menu = new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.pattern(/\S/), Validators.maxLength(160)] });
|
|
36139
|
+
icon = signal('web', ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
|
|
36140
|
+
i18n = inject(PraxisI18nService);
|
|
36141
|
+
picker = inject(IconPickerService);
|
|
36142
|
+
generation = 0;
|
|
36143
|
+
attempt;
|
|
36144
|
+
destroyed = false;
|
|
36145
|
+
constructor() {
|
|
36146
|
+
effect(() => {
|
|
36147
|
+
this.contextKey();
|
|
36148
|
+
this.adapter();
|
|
36149
|
+
const title = this.initialTitle(), menu = this.initialMenuLabel(), icon = this.initialIcon();
|
|
36150
|
+
++this.generation;
|
|
36151
|
+
this.attempt = undefined;
|
|
36152
|
+
this.busy.set(false);
|
|
36153
|
+
this.choosingIcon.set(false);
|
|
36154
|
+
this.result.set(null);
|
|
36155
|
+
this.error.set(false);
|
|
36156
|
+
this.iconError.set(false);
|
|
36157
|
+
this.title.setValidators([Validators.required, Validators.pattern(/\S/), Validators.maxLength(this.limit(this.titleMaxLength()))]);
|
|
36158
|
+
this.menu.setValidators([Validators.required, Validators.pattern(/\S/), Validators.maxLength(this.limit(this.menuMaxLength()))]);
|
|
36159
|
+
this.title.setValue(title);
|
|
36160
|
+
this.menu.setValue(menu);
|
|
36161
|
+
this.icon.set(icon);
|
|
36162
|
+
});
|
|
36163
|
+
inject(DestroyRef).onDestroy(() => { this.destroyed = true; ++this.generation; });
|
|
36164
|
+
}
|
|
36165
|
+
text(key) { return resolvePraxisPageBuilderText(this.i18n, `saveAs.${key}`, key); }
|
|
36166
|
+
destination() { return { label: this.menu.value.trim(), icon: this.icon() }; }
|
|
36167
|
+
clearResult() { this.result.set(null); }
|
|
36168
|
+
requestSave() {
|
|
36169
|
+
this.title.markAsTouched();
|
|
36170
|
+
this.menu.markAsTouched();
|
|
36171
|
+
if (this.available())
|
|
36172
|
+
this.saveRequested.emit();
|
|
36173
|
+
}
|
|
36174
|
+
async chooseIcon() {
|
|
36175
|
+
if (this.busy() || this.choosingIcon())
|
|
36176
|
+
return;
|
|
36177
|
+
const generation = this.generation;
|
|
36178
|
+
const context = this.contextKey(), adapter = this.adapter();
|
|
36179
|
+
this.choosingIcon.set(true);
|
|
36180
|
+
try {
|
|
36181
|
+
const selected = await this.picker.openDialog({ value: this.icon() });
|
|
36182
|
+
if (!this.current(generation, context, adapter) || selected === undefined)
|
|
36183
|
+
return;
|
|
36184
|
+
// The menu profile accepts Material names, not URLs or family-prefixed tokens.
|
|
36185
|
+
if (!this.validIcon(selected)) {
|
|
36186
|
+
this.iconError.set(true);
|
|
36187
|
+
return;
|
|
36188
|
+
}
|
|
36189
|
+
this.iconError.set(false);
|
|
36190
|
+
this.icon.set(selected);
|
|
36191
|
+
}
|
|
36192
|
+
catch {
|
|
36193
|
+
if (this.current(generation, context, adapter))
|
|
36194
|
+
this.iconError.set(true);
|
|
36195
|
+
}
|
|
36196
|
+
finally {
|
|
36197
|
+
if (this.current(generation, context, adapter))
|
|
36198
|
+
this.choosingIcon.set(false);
|
|
36199
|
+
}
|
|
36200
|
+
}
|
|
36201
|
+
async save(definition) {
|
|
36202
|
+
if (!this.available())
|
|
36203
|
+
return;
|
|
36204
|
+
const adapter = this.adapter(), context = this.contextKey(), generation = this.generation;
|
|
36205
|
+
this.busy.set(true);
|
|
36206
|
+
this.error.set(false);
|
|
36207
|
+
try {
|
|
36208
|
+
const details = { definition: structuredClone(definition), presentation: { title: this.title.value.trim(), icon: this.icon() }, menuLabel: this.menu.value.trim() };
|
|
36209
|
+
const signature = JSON.stringify(details);
|
|
36210
|
+
if (this.attempt?.signature !== signature) {
|
|
36211
|
+
this.attempt = { signature, request: { creationKey: crypto.randomUUID(), ...details } };
|
|
36212
|
+
}
|
|
36213
|
+
// An adapter cannot mutate the retained retry snapshot or the source document.
|
|
36214
|
+
const receipt = await adapter.save(structuredClone(this.attempt.request));
|
|
36215
|
+
if (!this.current(generation, context, adapter))
|
|
36216
|
+
return;
|
|
36217
|
+
if (!receipt || typeof receipt.pageId !== 'string' || !receipt.pageId.trim() || !this.localRoute(receipt.routePath))
|
|
36218
|
+
throw new Error('Invalid save-as receipt');
|
|
36219
|
+
const result = { pageId: receipt.pageId, routePath: receipt.routePath };
|
|
36220
|
+
this.result.set(result);
|
|
36221
|
+
this.saved.emit(result);
|
|
36222
|
+
}
|
|
36223
|
+
catch (error) {
|
|
36224
|
+
if (this.current(generation, context, adapter)) {
|
|
36225
|
+
this.error.set(true);
|
|
36226
|
+
this.saveFailed.emit(error);
|
|
36227
|
+
}
|
|
36228
|
+
}
|
|
36229
|
+
finally {
|
|
36230
|
+
if (this.current(generation, context, adapter))
|
|
36231
|
+
this.busy.set(false);
|
|
36232
|
+
}
|
|
36233
|
+
}
|
|
36234
|
+
available() {
|
|
36235
|
+
return !this.destroyed && !this.busy() && !this.choosingIcon() && !!this.contextKey().trim() && this.canSave()
|
|
36236
|
+
&& this.title.valid && this.menu.valid && !!this.title.value.trim() && !!this.menu.value.trim() && this.validIcon(this.icon());
|
|
36237
|
+
}
|
|
36238
|
+
current(generation, context, adapter) {
|
|
36239
|
+
return !this.destroyed && generation === this.generation && context === this.contextKey() && adapter === this.adapter();
|
|
36240
|
+
}
|
|
36241
|
+
limit(value) { return Number.isInteger(value) && value > 0 && value <= 160 ? value : 0; }
|
|
36242
|
+
validIcon(value) { return /^[a-z][a-z0-9_]{0,63}$/.test(value); }
|
|
36243
|
+
localRoute(value) {
|
|
36244
|
+
if (typeof value !== 'string' || !value.startsWith('/') || value.startsWith('//') || /[\\\s\u0000-\u001f\u007f]/.test(value))
|
|
36245
|
+
return false;
|
|
36246
|
+
try {
|
|
36247
|
+
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);
|
|
36248
|
+
}
|
|
36249
|
+
catch {
|
|
36250
|
+
return false;
|
|
36251
|
+
}
|
|
36252
|
+
}
|
|
36253
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisPageSaveAsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
36254
|
+
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 });
|
|
36255
|
+
}
|
|
36256
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisPageSaveAsComponent, decorators: [{
|
|
36257
|
+
type: Component,
|
|
36258
|
+
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"] }]
|
|
36259
|
+
}], 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"] }] } });
|
|
36260
|
+
|
|
36261
|
+
/** Copies the current local authoring snapshot. Allocation/provenance belongs to the destination adapter. */
|
|
36262
|
+
class PraxisPageCopyActionComponent {
|
|
36263
|
+
page = input(...(ngDevMode ? [undefined, { debugName: "page" }] : /* istanbul ignore next */ []));
|
|
36264
|
+
canCopy = input(false, ...(ngDevMode ? [{ debugName: "canCopy" }] : /* istanbul ignore next */ []));
|
|
36265
|
+
copyRequested = output();
|
|
36266
|
+
error = signal(false, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
36267
|
+
i18n = inject(PraxisI18nService);
|
|
36268
|
+
text(key) { return resolvePraxisPageBuilderText(this.i18n, `saveAs.${key}`, key); }
|
|
36269
|
+
copy() {
|
|
36270
|
+
const page = this.page();
|
|
36271
|
+
if (!this.canCopy() || !page)
|
|
36272
|
+
return;
|
|
36273
|
+
this.error.set(false);
|
|
36274
|
+
let snapshot;
|
|
36275
|
+
try {
|
|
36276
|
+
snapshot = structuredClone(page);
|
|
36277
|
+
}
|
|
36278
|
+
catch {
|
|
36279
|
+
this.error.set(true);
|
|
36280
|
+
return;
|
|
36281
|
+
}
|
|
36282
|
+
this.copyRequested.emit(snapshot);
|
|
36283
|
+
}
|
|
36284
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisPageCopyActionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
36285
|
+
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: `
|
|
36286
|
+
<button mat-flat-button type="button" data-testid="page-copy-action" [disabled]="!canCopy() || !page()" (click)="copy()">
|
|
36287
|
+
<mat-icon aria-hidden="true">content_copy</mat-icon>{{ text('copy') }}
|
|
36288
|
+
</button>
|
|
36289
|
+
@if (error()) { <p role="alert">{{ text('copyFailed') }}</p> }
|
|
36290
|
+
`, 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 });
|
|
36291
|
+
}
|
|
36292
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisPageCopyActionComponent, decorators: [{
|
|
36293
|
+
type: Component,
|
|
36294
|
+
args: [{
|
|
36295
|
+
selector: 'praxis-page-copy-action',
|
|
36296
|
+
imports: [MatButtonModule, MatIconModule],
|
|
36297
|
+
providers: [providePraxisPageBuilderI18n()],
|
|
36298
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
36299
|
+
template: `
|
|
36300
|
+
<button mat-flat-button type="button" data-testid="page-copy-action" [disabled]="!canCopy() || !page()" (click)="copy()">
|
|
36301
|
+
<mat-icon aria-hidden="true">content_copy</mat-icon>{{ text('copy') }}
|
|
36302
|
+
</button>
|
|
36303
|
+
@if (error()) { <p role="alert">{{ text('copyFailed') }}</p> }
|
|
36304
|
+
`,
|
|
36305
|
+
}]
|
|
36306
|
+
}], 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"] }] } });
|
|
36307
|
+
|
|
35671
36308
|
/*
|
|
35672
36309
|
* Public API Surface of @praxisui/page-builder
|
|
35673
36310
|
*/
|
|
@@ -35677,4 +36314,4 @@ function providePraxisPageBuilderWidgets(widgets) {
|
|
|
35677
36314
|
* Generated bundle index. Do not edit.
|
|
35678
36315
|
*/
|
|
35679
36316
|
|
|
35680
|
-
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 };
|
|
36317
|
+
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, PraxisPageCopyActionComponent, PraxisPageSaveAsComponent, TileToolbarComponent, UiCompositionResourcePreflightService, WidgetShellEditorComponent, clearWidgetAiCatalogs, compileUiCompositionPlan, getPageAiCatalog, getWidgetAiCapabilities, isComponentDocMeta, preflightUiCompositionPlan, preflightUiCompositionPlanResources, providePageBuilderWidgetAiCatalogs, providePraxisPageBuilderMetadata, providePraxisPageBuilderWidgets, registerWidgetAiCatalog, registerWidgetAiCatalogs, resolveUiCompositionPlanTemplate, validateUiCompositionPlan, verifyUiCompositionAuthoringSource };
|