@praxisui/charts 10.0.0-rc.19 → 10.0.0-rc.21
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 +7 -3
- package/ai/component-registry.json +2 -2
- package/fesm2022/praxisui-charts.mjs +901 -146
- package/package.json +3 -3
- package/types/praxisui-charts.d.ts +59 -7
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Injectable, Optional, Inject, InjectionToken, input, booleanAttribute, output, viewChild, inject, ElementRef, NgZone, DestroyRef, signal, computed, afterNextRender, effect, ChangeDetectionStrategy, Component, ViewChild, Input, Injector, ENVIRONMENT_INITIALIZER } from '@angular/core';
|
|
2
|
+
import { Injectable, Optional, Inject, InjectionToken, input, booleanAttribute, output, viewChild, inject, ElementRef, NgZone, DestroyRef, signal, linkedSignal, computed, afterNextRender, effect, ChangeDetectionStrategy, Component, ViewChild, Input, Injector, ENVIRONMENT_INITIALIZER } from '@angular/core';
|
|
3
3
|
import * as i1 from '@praxisui/core';
|
|
4
4
|
import { buildApiUrl, API_URL, PraxisI18nService, ComponentMetadataRegistry, SETTINGS_PANEL_BRIDGE, normalizePraxisDataQueryContext, resolvePraxisFilterCriteria, BUILTIN_PAGE_THEME_PRESETS, PraxisIconButtonComponent, normalizePraxisPresentationVisualization, resolvePraxisMicroVisualizationTone, AnalyticsStatsRequestBuilderService, AnalyticsPresentationResolver, providePraxisI18n, ResourceDiscoveryService, SETTINGS_PANEL_DATA, createDefaultTableConfig, PRAXIS_TABLE_DETAIL_INLINE_RENDERERS, DynamicWidgetPageComponent } from '@praxisui/core';
|
|
5
5
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
@@ -1149,7 +1149,11 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1149
1149
|
metrics: this.extractCanonicalMetrics(config),
|
|
1150
1150
|
legend: config.theme?.legend?.visible !== undefined ? config.theme.legend.visible : true,
|
|
1151
1151
|
tooltip: config.theme?.tooltip?.enabled !== undefined ? config.theme.tooltip.enabled : true,
|
|
1152
|
-
labels: config.series?.some((s) => s.labels?.visible === true)
|
|
1152
|
+
labels: config.series?.some((s) => s.labels?.visible === true)
|
|
1153
|
+
? true
|
|
1154
|
+
: config.series?.some((s) => s.labels?.visible === false)
|
|
1155
|
+
? false
|
|
1156
|
+
: undefined,
|
|
1153
1157
|
theme: this.extractCanonicalTheme(config),
|
|
1154
1158
|
motion: config.motion
|
|
1155
1159
|
? {
|
|
@@ -1245,7 +1249,7 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1245
1249
|
}
|
|
1246
1250
|
buildSeries(contract) {
|
|
1247
1251
|
const firstDimension = contract.dimensions?.[0];
|
|
1248
|
-
const labelsVisible = this.
|
|
1252
|
+
const labelsVisible = this.resolveOptionalToggle(contract.labels);
|
|
1249
1253
|
const comparison = contract.source.kind === 'praxis.stats' && contract.source.operation === 'comparison';
|
|
1250
1254
|
const metrics = contract.kind === 'scatter' && (contract.metrics?.length ?? 0) === 2
|
|
1251
1255
|
? contract.metrics.slice(1, 2)
|
|
@@ -1268,8 +1272,11 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1268
1272
|
},
|
|
1269
1273
|
color: period === 'previous' ? undefined : metric.color,
|
|
1270
1274
|
stackId: contract.kind === 'stacked-bar' || contract.kind === 'stacked-area' ? 'stack-1' : undefined,
|
|
1271
|
-
labels: labelsVisible || metric.format
|
|
1272
|
-
? {
|
|
1275
|
+
labels: labelsVisible !== undefined || metric.format
|
|
1276
|
+
? {
|
|
1277
|
+
...(labelsVisible !== undefined ? { visible: labelsVisible } : {}),
|
|
1278
|
+
...(metric.format ? { format: metric.format } : {}),
|
|
1279
|
+
}
|
|
1273
1280
|
: undefined,
|
|
1274
1281
|
smooth: this.shouldSmoothSeries(contract.kind, metric.seriesKind),
|
|
1275
1282
|
}));
|
|
@@ -1465,6 +1472,14 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1465
1472
|
return value.enabled;
|
|
1466
1473
|
return defaultValue;
|
|
1467
1474
|
}
|
|
1475
|
+
resolveOptionalToggle(value) {
|
|
1476
|
+
if (typeof value === 'boolean')
|
|
1477
|
+
return value;
|
|
1478
|
+
if (value && typeof value === 'object' && typeof value.enabled === 'boolean') {
|
|
1479
|
+
return value.enabled;
|
|
1480
|
+
}
|
|
1481
|
+
return undefined;
|
|
1482
|
+
}
|
|
1468
1483
|
mapAggregation(aggregation) {
|
|
1469
1484
|
switch (aggregation) {
|
|
1470
1485
|
case 'avg':
|
|
@@ -2450,7 +2465,35 @@ class PraxisChartComponent {
|
|
|
2450
2465
|
remoteResolvedData = signal(null, ...(ngDevMode ? [{ debugName: "remoteResolvedData" }] : /* istanbul ignore next */ []));
|
|
2451
2466
|
remoteRuntimeState = signal('idle', ...(ngDevMode ? [{ debugName: "remoteRuntimeState" }] : /* istanbul ignore next */ []));
|
|
2452
2467
|
remoteTechnicalError = signal(null, ...(ngDevMode ? [{ debugName: "remoteTechnicalError" }] : /* istanbul ignore next */ []));
|
|
2453
|
-
|
|
2468
|
+
runtimeOwner = linkedSignal({ ...(ngDevMode ? { debugName: "runtimeOwner" } : /* istanbul ignore next */ {}), source: () => ({
|
|
2469
|
+
chartDocument: this.chartDocument(), config: this.config(),
|
|
2470
|
+
data: this.data(), queryContext: this.queryContext(),
|
|
2471
|
+
}),
|
|
2472
|
+
computation: (owner, previous) => {
|
|
2473
|
+
const patch = previous?.value.editorPatch;
|
|
2474
|
+
// An owner can acknowledge the accepted patch one input at a time. A distinct
|
|
2475
|
+
// external change reclaims the complete scope, including local data/context.
|
|
2476
|
+
if (previous && patch && Object.keys(owner).every((key) => this.editorInputSignature(owner[key]) === this.editorInputSignature(previous.source[key])
|
|
2477
|
+
|| (Object.prototype.hasOwnProperty.call(patch, key)
|
|
2478
|
+
&& this.editorInputSignature(owner[key]) === this.editorInputSignature(patch[key]))))
|
|
2479
|
+
return previous.value;
|
|
2480
|
+
let document = owner.chartDocument;
|
|
2481
|
+
if (!document && owner.config?.type && Array.isArray(owner.config.series)) {
|
|
2482
|
+
try {
|
|
2483
|
+
document = this.canonicalMapper.toPraxisXUiChartContract(owner.config);
|
|
2484
|
+
}
|
|
2485
|
+
catch {
|
|
2486
|
+
document = null;
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
return {
|
|
2490
|
+
owner: structuredClone(owner),
|
|
2491
|
+
document: document ? structuredClone(document) : null,
|
|
2492
|
+
data: structuredClone(owner.data),
|
|
2493
|
+
queryContext: structuredClone(owner.queryContext),
|
|
2494
|
+
};
|
|
2495
|
+
} });
|
|
2496
|
+
runtimeChartDocument = computed(() => this.runtimeOwner().document, ...(ngDevMode ? [{ debugName: "runtimeChartDocument" }] : /* istanbul ignore next */ []));
|
|
2454
2497
|
mappedRuntimeConfig = signal(null, ...(ngDevMode ? [{ debugName: "mappedRuntimeConfig" }] : /* istanbul ignore next */ []));
|
|
2455
2498
|
chartDocumentMappingError = signal(null, ...(ngDevMode ? [{ debugName: "chartDocumentMappingError" }] : /* istanbul ignore next */ []));
|
|
2456
2499
|
activeSelectionSignature = signal(null, ...(ngDevMode ? [{ debugName: "activeSelectionSignature" }] : /* istanbul ignore next */ []));
|
|
@@ -2468,13 +2511,14 @@ class PraxisChartComponent {
|
|
|
2468
2511
|
deferredRenderAttempts = 0;
|
|
2469
2512
|
previousRemoteSignature = null;
|
|
2470
2513
|
previousRemoteDataResolver = undefined;
|
|
2471
|
-
previousDocumentSignature = null;
|
|
2472
2514
|
inheritedThemeSignature = null;
|
|
2473
2515
|
editorSessionSubscriptions = [];
|
|
2474
2516
|
remoteRequestSubscription = null;
|
|
2475
2517
|
remoteRequestSequence = 0;
|
|
2476
2518
|
previousSelectionScopeSignature = null;
|
|
2477
|
-
userEditedDocument =
|
|
2519
|
+
userEditedDocument = computed(() => !!this.runtimeOwner().editorPatch, ...(ngDevMode ? [{ debugName: "userEditedDocument" }] : /* istanbul ignore next */ []));
|
|
2520
|
+
runtimeData = computed(() => this.runtimeOwner().data, ...(ngDevMode ? [{ debugName: "runtimeData" }] : /* istanbul ignore next */ []));
|
|
2521
|
+
runtimeQueryContext = computed(() => this.runtimeOwner().queryContext, ...(ngDevMode ? [{ debugName: "runtimeQueryContext" }] : /* istanbul ignore next */ []));
|
|
2478
2522
|
effectiveConfig = computed(() => {
|
|
2479
2523
|
const directDocument = this.chartDocument();
|
|
2480
2524
|
const config = this.config();
|
|
@@ -2511,7 +2555,7 @@ class PraxisChartComponent {
|
|
|
2511
2555
|
}
|
|
2512
2556
|
}
|
|
2513
2557
|
const base = this.applyInheritedTheme(activeConfig, this.inheritedTheme(), this.inheritedHostTextColor());
|
|
2514
|
-
const runtimeQueryContext = normalizePraxisDataQueryContext(this.
|
|
2558
|
+
const runtimeQueryContext = normalizePraxisDataQueryContext(this.runtimeQueryContext());
|
|
2515
2559
|
const runtimeFilters = resolvePraxisFilterCriteria(this.filterCriteria(), runtimeQueryContext);
|
|
2516
2560
|
if (!runtimeQueryContext
|
|
2517
2561
|
&& !Object.keys(runtimeFilters).length) {
|
|
@@ -2523,7 +2567,7 @@ class PraxisChartComponent {
|
|
|
2523
2567
|
return mergeRemoteQueryContext(base, runtimeQueryContext, runtimeFilters);
|
|
2524
2568
|
}, ...(ngDevMode ? [{ debugName: "effectiveConfig" }] : /* istanbul ignore next */ []));
|
|
2525
2569
|
resolvedData = computed(() => {
|
|
2526
|
-
const inputData = this.
|
|
2570
|
+
const inputData = this.runtimeData();
|
|
2527
2571
|
if (inputData !== null && inputData !== undefined) {
|
|
2528
2572
|
return inputData;
|
|
2529
2573
|
}
|
|
@@ -2601,7 +2645,7 @@ class PraxisChartComponent {
|
|
|
2601
2645
|
: 'var(--md-sys-color-on-surface, #1a1b20)', ...(ngDevMode ? [{ debugName: "accessibleDataSurfaceTextColor" }] : /* istanbul ignore next */ []));
|
|
2602
2646
|
renderConfig = computed(() => {
|
|
2603
2647
|
const config = this.effectiveConfig();
|
|
2604
|
-
const explicitData = this.
|
|
2648
|
+
const explicitData = this.runtimeData();
|
|
2605
2649
|
const remoteState = this.remoteRuntimeState();
|
|
2606
2650
|
const remoteData = this.remoteResolvedData();
|
|
2607
2651
|
if (explicitData !== null && explicitData !== undefined) {
|
|
@@ -2674,7 +2718,7 @@ class PraxisChartComponent {
|
|
|
2674
2718
|
}, ...(ngDevMode ? [{ debugName: "sortedAccessiblePoints" }] : /* istanbul ignore next */ []));
|
|
2675
2719
|
dataDiagnostic = computed(() => {
|
|
2676
2720
|
const config = this.renderConfig();
|
|
2677
|
-
const explicitData = this.
|
|
2721
|
+
const explicitData = this.runtimeData();
|
|
2678
2722
|
if (config.dataSource?.kind === 'remote'
|
|
2679
2723
|
&& (explicitData === null || explicitData === undefined)
|
|
2680
2724
|
&& this.remoteRuntimeState() !== 'ready') {
|
|
@@ -2739,7 +2783,7 @@ class PraxisChartComponent {
|
|
|
2739
2783
|
if (config.preferredLoadState === 'error')
|
|
2740
2784
|
return 'error';
|
|
2741
2785
|
const dataSource = config.dataSource;
|
|
2742
|
-
const explicitData = this.
|
|
2786
|
+
const explicitData = this.runtimeData();
|
|
2743
2787
|
if (dataSource?.kind === 'remote' && (explicitData === null || explicitData === undefined)) {
|
|
2744
2788
|
const remoteState = this.remoteRuntimeState();
|
|
2745
2789
|
if (remoteState === 'error') {
|
|
@@ -2760,27 +2804,6 @@ class PraxisChartComponent {
|
|
|
2760
2804
|
this.observeShellSizingContext();
|
|
2761
2805
|
this.observeThemeContext();
|
|
2762
2806
|
});
|
|
2763
|
-
effect(() => {
|
|
2764
|
-
const explicitDocument = this.chartDocument();
|
|
2765
|
-
let document = explicitDocument;
|
|
2766
|
-
if (!document && !this.userEditedDocument()) {
|
|
2767
|
-
const config = this.config();
|
|
2768
|
-
if (config && config.type && Array.isArray(config.series)) {
|
|
2769
|
-
try {
|
|
2770
|
-
document = this.canonicalMapper.toPraxisXUiChartContract(config);
|
|
2771
|
-
}
|
|
2772
|
-
catch {
|
|
2773
|
-
document = null;
|
|
2774
|
-
}
|
|
2775
|
-
}
|
|
2776
|
-
}
|
|
2777
|
-
const signature = document ? JSON.stringify(document) : null;
|
|
2778
|
-
if (signature === this.previousDocumentSignature) {
|
|
2779
|
-
return;
|
|
2780
|
-
}
|
|
2781
|
-
this.previousDocumentSignature = signature;
|
|
2782
|
-
this.runtimeChartDocument.set(document ? structuredClone(document) : null);
|
|
2783
|
-
});
|
|
2784
2807
|
effect(() => {
|
|
2785
2808
|
const runtimeDocument = this.runtimeChartDocument();
|
|
2786
2809
|
if (!runtimeDocument) {
|
|
@@ -2801,9 +2824,9 @@ class PraxisChartComponent {
|
|
|
2801
2824
|
});
|
|
2802
2825
|
effect(() => {
|
|
2803
2826
|
const config = this.effectiveConfig();
|
|
2804
|
-
const explicitData = this.
|
|
2827
|
+
const explicitData = this.runtimeData();
|
|
2805
2828
|
const remoteDataResolver = this.remoteDataResolver();
|
|
2806
|
-
const queryContext = normalizePraxisDataQueryContext(this.
|
|
2829
|
+
const queryContext = normalizePraxisDataQueryContext(this.runtimeQueryContext());
|
|
2807
2830
|
const nextSignature = explicitData === null || explicitData === undefined
|
|
2808
2831
|
? this.buildRemoteSignature(config, queryContext)
|
|
2809
2832
|
: null;
|
|
@@ -2838,14 +2861,14 @@ class PraxisChartComponent {
|
|
|
2838
2861
|
});
|
|
2839
2862
|
effect(() => {
|
|
2840
2863
|
const effectiveConfig = this.effectiveConfig();
|
|
2841
|
-
const explicitData = this.
|
|
2864
|
+
const explicitData = this.runtimeData();
|
|
2842
2865
|
if (effectiveConfig.dataSource?.kind !== 'remote' || (explicitData !== null && explicitData !== undefined)) {
|
|
2843
2866
|
return;
|
|
2844
2867
|
}
|
|
2845
2868
|
if (this.remoteRuntimeState() === 'loading' || this.remoteResolvedData() !== null) {
|
|
2846
2869
|
return;
|
|
2847
2870
|
}
|
|
2848
|
-
const queryContext = normalizePraxisDataQueryContext(this.
|
|
2871
|
+
const queryContext = normalizePraxisDataQueryContext(this.runtimeQueryContext());
|
|
2849
2872
|
const event = {
|
|
2850
2873
|
chartId: effectiveConfig.id,
|
|
2851
2874
|
dataSource: effectiveConfig.dataSource,
|
|
@@ -2981,12 +3004,13 @@ class PraxisChartComponent {
|
|
|
2981
3004
|
return;
|
|
2982
3005
|
}
|
|
2983
3006
|
const focusReturnTarget = this.captureFocusReturnTarget();
|
|
3007
|
+
let sessionSignature = this.configEditorSessionSignature();
|
|
2984
3008
|
this.configEditorContextPending.set(true);
|
|
2985
3009
|
try {
|
|
2986
3010
|
const resolvedContext = await this.resolveConfigEditorContext(document);
|
|
2987
|
-
const {
|
|
3011
|
+
const { PraxisChartWidgetConfigEditor } = await Promise.resolve().then(function () { return praxisChartWidgetConfigEditor; });
|
|
2988
3012
|
// Context resolution can outlive the host's customization session.
|
|
2989
|
-
if (!this.canOpenConfigEditor())
|
|
3013
|
+
if (!this.canOpenConfigEditor() || this.configEditorSessionSignature() !== sessionSignature)
|
|
2990
3014
|
return;
|
|
2991
3015
|
this.clearEditorSessionSubscriptions();
|
|
2992
3016
|
const ref = this.settingsPanel.open({
|
|
@@ -2994,27 +3018,29 @@ class PraxisChartComponent {
|
|
|
2994
3018
|
id: `chart.${document.chartId || this.effectiveConfig().id || 'praxis-chart'}`,
|
|
2995
3019
|
title: this.editChartLabel(),
|
|
2996
3020
|
content: {
|
|
2997
|
-
component:
|
|
3021
|
+
component: PraxisChartWidgetConfigEditor,
|
|
2998
3022
|
inputs: {
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
availableTargets: this.resolveEditorOptions(this.availableTargets(), resolvedContext['availableTargets']),
|
|
3023
|
+
inputs: this.configEditorInputs(document),
|
|
3024
|
+
context: {
|
|
3025
|
+
availableResources: this.resolveEditorOptions(this.availableResources(), resolvedContext['availableResources']),
|
|
3026
|
+
availableFields: this.resolveEditorOptions(this.availableFields(), resolvedContext['availableFields']),
|
|
3027
|
+
availableTargets: this.resolveEditorOptions(this.availableTargets(), resolvedContext['availableTargets']),
|
|
3028
|
+
},
|
|
3006
3029
|
},
|
|
3007
3030
|
},
|
|
3008
3031
|
});
|
|
3009
|
-
const
|
|
3010
|
-
.
|
|
3011
|
-
|
|
3032
|
+
const consumeEditorValue = (value, phase) => {
|
|
3033
|
+
if (!value?.inputs?.chartDocument || !this.canOpenConfigEditor()
|
|
3034
|
+
|| this.configEditorSessionSignature() !== sessionSignature)
|
|
3012
3035
|
return;
|
|
3013
|
-
|
|
3014
|
-
const clone = structuredClone(
|
|
3015
|
-
this.
|
|
3016
|
-
this.
|
|
3017
|
-
|
|
3036
|
+
const nextInputs = value.inputs;
|
|
3037
|
+
const clone = structuredClone(nextInputs.chartDocument);
|
|
3038
|
+
const previousData = this.runtimeData();
|
|
3039
|
+
const previousQueryContext = this.runtimeQueryContext();
|
|
3040
|
+
const nextData = clone.source.kind !== 'praxis.stats' && Array.isArray(nextInputs.data)
|
|
3041
|
+
? structuredClone(nextInputs.data)
|
|
3042
|
+
: null;
|
|
3043
|
+
const nextQueryContext = structuredClone(nextInputs.queryContext ?? null);
|
|
3018
3044
|
const wasConfigDriven = !this.chartDocument();
|
|
3019
3045
|
let mappedConfig;
|
|
3020
3046
|
if (wasConfigDriven) {
|
|
@@ -3025,40 +3051,33 @@ class PraxisChartComponent {
|
|
|
3025
3051
|
mappedConfig = undefined;
|
|
3026
3052
|
}
|
|
3027
3053
|
}
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3054
|
+
const inputPatch = {
|
|
3055
|
+
chartDocument: structuredClone(clone),
|
|
3056
|
+
...(wasConfigDriven && mappedConfig ? { config: mappedConfig } : {}),
|
|
3057
|
+
...(nextData !== null || previousData != null ? { data: structuredClone(nextData) } : {}),
|
|
3058
|
+
...(nextQueryContext !== null || previousQueryContext != null
|
|
3059
|
+
? { queryContext: structuredClone(nextQueryContext) } : {}),
|
|
3060
|
+
};
|
|
3061
|
+
this.runtimeOwner.set({
|
|
3062
|
+
owner: this.runtimeOwner().owner,
|
|
3063
|
+
document: clone, data: nextData, queryContext: nextQueryContext,
|
|
3064
|
+
// A later patch may omit config or already-cleared data/context while
|
|
3065
|
+
// the owner is still acknowledging those fields from the prior Apply.
|
|
3066
|
+
editorPatch: { ...this.runtimeOwner().editorPatch, ...structuredClone(inputPatch) },
|
|
3033
3067
|
});
|
|
3034
|
-
|
|
3068
|
+
sessionSignature = this.configEditorSessionSignature();
|
|
3069
|
+
if (phase === 'apply')
|
|
3070
|
+
this.chartDocumentApplied.emit(structuredClone(clone));
|
|
3071
|
+
else
|
|
3072
|
+
this.chartDocumentSaved.emit(structuredClone(clone));
|
|
3073
|
+
this.chartDocumentPatchChange.emit({ inputPatch });
|
|
3074
|
+
};
|
|
3075
|
+
const appliedSubscription = ref.applied$
|
|
3076
|
+
.subscribe((value) => consumeEditorValue(value, 'apply'));
|
|
3035
3077
|
const savedSubscription = ref.saved$
|
|
3036
|
-
.subscribe((
|
|
3037
|
-
if (!nextDocument) {
|
|
3038
|
-
return;
|
|
3039
|
-
}
|
|
3040
|
-
const clone = structuredClone(nextDocument);
|
|
3041
|
-
this.userEditedDocument.set(true);
|
|
3042
|
-
this.runtimeChartDocument.set(clone);
|
|
3043
|
-
this.chartDocumentSaved.emit(clone);
|
|
3044
|
-
const wasConfigDriven = !this.chartDocument();
|
|
3045
|
-
let mappedConfig;
|
|
3046
|
-
if (wasConfigDriven) {
|
|
3047
|
-
try {
|
|
3048
|
-
mappedConfig = this.canonicalMapper.toPraxisChartConfig(clone);
|
|
3049
|
-
}
|
|
3050
|
-
catch {
|
|
3051
|
-
mappedConfig = undefined;
|
|
3052
|
-
}
|
|
3053
|
-
}
|
|
3054
|
-
this.chartDocumentPatchChange.emit({
|
|
3055
|
-
inputPatch: {
|
|
3056
|
-
chartDocument: structuredClone(clone),
|
|
3057
|
-
...(wasConfigDriven && mappedConfig ? { config: mappedConfig } : {}),
|
|
3058
|
-
},
|
|
3059
|
-
});
|
|
3060
|
-
});
|
|
3078
|
+
.subscribe((value) => consumeEditorValue(value, 'save'));
|
|
3061
3079
|
const closedSubscription = ref.closed$?.subscribe(() => {
|
|
3080
|
+
this.clearEditorSessionSubscriptions();
|
|
3062
3081
|
this.restoreFocus(focusReturnTarget);
|
|
3063
3082
|
});
|
|
3064
3083
|
this.editorSessionSubscriptions = [
|
|
@@ -3071,6 +3090,26 @@ class PraxisChartComponent {
|
|
|
3071
3090
|
this.configEditorContextPending.set(false);
|
|
3072
3091
|
}
|
|
3073
3092
|
}
|
|
3093
|
+
configEditorSessionSignature() {
|
|
3094
|
+
return this.editorInputSignature(this.runtimeOwner());
|
|
3095
|
+
}
|
|
3096
|
+
editorInputSignature(value) {
|
|
3097
|
+
return JSON.stringify(value, (_key, current) => {
|
|
3098
|
+
if (!current || typeof current !== 'object' || Array.isArray(current))
|
|
3099
|
+
return current;
|
|
3100
|
+
return Object.fromEntries(Object.keys(current).sort().map((key) => [key, current[key]]));
|
|
3101
|
+
}) ?? '';
|
|
3102
|
+
}
|
|
3103
|
+
configEditorInputs(document) {
|
|
3104
|
+
const data = this.runtimeData();
|
|
3105
|
+
const queryContext = this.runtimeQueryContext();
|
|
3106
|
+
return {
|
|
3107
|
+
chartDocument: structuredClone(document),
|
|
3108
|
+
...(!this.chartDocument() ? { config: structuredClone(this.config()) } : {}),
|
|
3109
|
+
...(data != null ? { data: structuredClone(data) } : {}),
|
|
3110
|
+
...(queryContext != null ? { queryContext: structuredClone(queryContext) } : {}),
|
|
3111
|
+
};
|
|
3112
|
+
}
|
|
3074
3113
|
async resolveConfigEditorContext(document) {
|
|
3075
3114
|
const metadata = this.componentMetadataRegistry.get('praxis-chart');
|
|
3076
3115
|
const resolver = metadata?.configEditor?.contextResolver;
|
|
@@ -3082,9 +3121,7 @@ class PraxisChartComponent {
|
|
|
3082
3121
|
componentId: metadata.id,
|
|
3083
3122
|
widgetKey: document.chartId || this.effectiveConfig().id || 'praxis-chart',
|
|
3084
3123
|
widgetType: metadata.id,
|
|
3085
|
-
persistedInputs:
|
|
3086
|
-
chartDocument: structuredClone(document),
|
|
3087
|
-
},
|
|
3124
|
+
persistedInputs: this.configEditorInputs(document),
|
|
3088
3125
|
metadata,
|
|
3089
3126
|
});
|
|
3090
3127
|
const candidate = this.isConfigEditorContextResult(resolved)
|
|
@@ -3326,7 +3363,7 @@ class PraxisChartComponent {
|
|
|
3326
3363
|
dataSource: config.dataSource,
|
|
3327
3364
|
interactions: config.interactions,
|
|
3328
3365
|
filterCriteria: this.filterCriteria(),
|
|
3329
|
-
queryContext: normalizePraxisDataQueryContext(this.
|
|
3366
|
+
queryContext: normalizePraxisDataQueryContext(this.runtimeQueryContext()),
|
|
3330
3367
|
data: this.resolvedData(),
|
|
3331
3368
|
});
|
|
3332
3369
|
}
|
|
@@ -3852,7 +3889,7 @@ class PraxisChartComponent {
|
|
|
3852
3889
|
></div>
|
|
3853
3890
|
}
|
|
3854
3891
|
</section>
|
|
3855
|
-
`, isInline: true, styles: [":host{display:block;min-height:var(--praxis-chart-runtime-height, 320px);min-width:0;color:var(--md-sys-color-on-surface, #1a1b20)}:host(.praxis-chart-host-fill-container){height:100%;min-height:0}:host-context(.pdx-shell){color:inherit}:host-context(.pdx-shell.no-shell) .praxis-chart-shell,:host-context(.pdx-shell.body-fill) .praxis-chart-shell,:host-context(.pdx-shell.expanded) .praxis-chart-shell,:host-context(.pdx-shell.fullscreen) .praxis-chart-shell{height:100%!important}.praxis-chart-shell{box-sizing:border-box;position:relative;width:100%;height:100%;min-height:240px;border-radius:var(--praxis-chart-config-surface-radius, var(--praxis-chart-surface-radius, 8px));overflow:hidden;background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) );border-color:var( --praxis-chart-config-surface-border, var( --praxis-chart-surface-border, color-mix(in srgb, var(--md-sys-color-outline, #c5c7ce) 44%, transparent) ) );border-style:solid;border-width:var(--praxis-chart-config-surface-border-width, 1px)}.praxis-chart-shell-fill-container{min-height:0}:host-context(.pdx-shell) .praxis-chart-shell:not(.praxis-chart-shell-contained),.praxis-chart-shell-embedded{border-width:var( --praxis-chart-config-surface-border-width, var(--praxis-chart-embedded-surface-border-width, 0) );border-radius:var( --praxis-chart-config-surface-radius, var(--praxis-chart-embedded-surface-radius, 0) );background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-embedded-surface-bg, transparent) )}.praxis-chart-settings-trigger
|
|
3892
|
+
`, isInline: true, styles: [":host{display:block;min-height:var(--praxis-chart-runtime-height, 320px);min-width:0;color:var(--md-sys-color-on-surface, #1a1b20)}:host(.praxis-chart-host-fill-container){height:100%;min-height:0}:host-context(.pdx-shell){color:inherit}:host-context(.pdx-shell.no-shell) .praxis-chart-shell,:host-context(.pdx-shell.body-fill) .praxis-chart-shell,:host-context(.pdx-shell.expanded) .praxis-chart-shell,:host-context(.pdx-shell.fullscreen) .praxis-chart-shell{height:100%!important}.praxis-chart-shell{box-sizing:border-box;position:relative;width:100%;height:100%;min-height:240px;border-radius:var(--praxis-chart-config-surface-radius, var(--praxis-chart-surface-radius, 8px));overflow:hidden;background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) );border-color:var( --praxis-chart-config-surface-border, var( --praxis-chart-surface-border, color-mix(in srgb, var(--md-sys-color-outline, #c5c7ce) 44%, transparent) ) );border-style:solid;border-width:var(--praxis-chart-config-surface-border-width, 1px)}.praxis-chart-shell-fill-container{min-height:0}:host-context(.pdx-shell) .praxis-chart-shell:not(.praxis-chart-shell-contained),.praxis-chart-shell-embedded{border-width:var( --praxis-chart-config-surface-border-width, var(--praxis-chart-embedded-surface-border-width, 0) );border-radius:var( --praxis-chart-config-surface-radius, var(--praxis-chart-embedded-surface-radius, 0) );background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-embedded-surface-bg, transparent) )}.praxis-chart-settings-trigger,.praxis-chart-data-trigger{position:absolute;top:10px;right:10px;z-index:5;--praxis-icon-button-filled-background: color-mix(in srgb, var(--md-sys-color-surface, #fff) 88%, rgba(18, 99, 180, .12));--praxis-icon-button-filled-foreground: var(--md-sys-color-on-surface-variant, #44474f);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-data-trigger-with-settings{right:50px}.praxis-chart-accessible-data{position:absolute;inset:0;z-index:4;display:flex;flex-direction:column;overflow:hidden;padding:52px 16px 14px;color:var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface, #1a1b20));background:var(--md-sys-color-surface-container-lowest, #fff)}.praxis-chart-accessible-data--shell-controlled{padding-top:16px}.praxis-chart-accessible-header{flex-shrink:0;margin-bottom:8px}.praxis-chart-accessible-header h3{margin:0;font-size:.95rem;font-weight:600;line-height:1.4}.praxis-chart-table-container{flex:1;min-height:0;overflow-y:auto;overflow-x:auto;overscroll-behavior:contain}.praxis-chart-accessible-data table{width:100%;border-collapse:separate;border-spacing:0;font-size:.875rem}.praxis-chart-accessible-data caption{padding:0 0 8px;text-align:left;color:inherit;opacity:.85;font-size:.8125rem}.praxis-chart-accessible-data thead{position:sticky;top:0;z-index:2}.praxis-chart-accessible-data thead th{font-weight:600;background:var(--praxis-chart-accessible-thead-bg, inherit);box-shadow:0 1px color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 44%,transparent)}.praxis-chart-accessible-data th,.praxis-chart-accessible-data td{padding:8px;border-bottom:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 44%,transparent);text-align:left}.praxis-chart-col-value{text-align:right!important;font-variant-numeric:tabular-nums}.praxis-chart-sort-header{appearance:none;display:inline-flex;align-items:center;gap:4px;padding:2px 4px;border:0;border-radius:4px;background:transparent;color:inherit;font:inherit;font-weight:600;cursor:pointer;transition:background-color .15s ease}.praxis-chart-sort-header:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.praxis-chart-sort-header:focus-visible{outline:2px solid currentColor;outline-offset:1px}.praxis-chart-sort-header--numeric{margin-left:auto}.praxis-chart-sort-indicator{font-size:.75rem;line-height:1;opacity:.85}.praxis-chart-point-action{appearance:none;padding:4px 6px;border:0;border-radius:4px;color:inherit;background:transparent;font:inherit;font-weight:600;text-align:left;text-decoration:none;cursor:pointer;transition:background-color .15s ease}.praxis-chart-point-action:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.praxis-chart-point-action:focus-visible{outline:3px solid currentColor;outline-offset:2px}.praxis-chart-point-action[aria-pressed=true]{color:var(--md-sys-color-on-primary-container, #001b3f);background:var(--md-sys-color-primary-container, #d7e3ff)}.praxis-chart-host{width:100%;height:100%;min-height:inherit}.praxis-chart-host--data-hidden{visibility:hidden;pointer-events:none}.praxis-chart-state{height:100%;min-height:min(240px,100%);display:grid;align-content:center;justify-items:center;gap:14px;padding:24px;text-align:center}.praxis-chart-state-copy{display:grid;gap:6px;justify-items:center}.praxis-chart-state-title{font-size:1rem;font-weight:600;color:var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface, #1a1b20))}.praxis-chart-state-description{font-size:.925rem;color:color-mix(in srgb,var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface-variant, #5a5d67)) 72%,transparent);max-width:36rem}.praxis-chart-loading-hero{width:min(100%,320px);display:grid;gap:14px}.praxis-chart-loading-summary{display:flex;gap:8px;justify-content:center}.praxis-chart-loading-chip,.praxis-chart-loading-bar{border-radius:999px;background:linear-gradient(90deg,#1263b414,#1263b438,#1263b414);background-size:200% 100%;animation:praxis-chart-loading-wave 1.2s ease-in-out infinite}.praxis-chart-loading-chip{display:block;width:104px;height:12px}.praxis-chart-loading-chip--short{width:64px}.praxis-chart-loading-plot{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));align-items:end;gap:10px;height:108px;padding:12px 8px 4px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface-container, #eef3f8) 72%,transparent);border:1px solid rgba(18,99,180,.08)}.praxis-chart-loading-bar{display:block;width:100%}.praxis-chart-loading-bar--1{height:32%}.praxis-chart-loading-bar--2{height:68%}.praxis-chart-loading-bar--3{height:48%}.praxis-chart-loading-bar--4{height:78%}.praxis-chart-loading-bar--5{height:56%}@keyframes praxis-chart-loading-wave{0%{background-position:100% 0}to{background-position:-100% 0}}\n"], dependencies: [{ kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i1$2.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 });
|
|
3856
3893
|
}
|
|
3857
3894
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartComponent, decorators: [{
|
|
3858
3895
|
type: Component,
|
|
@@ -4057,7 +4094,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
4057
4094
|
></div>
|
|
4058
4095
|
}
|
|
4059
4096
|
</section>
|
|
4060
|
-
`, styles: [":host{display:block;min-height:var(--praxis-chart-runtime-height, 320px);min-width:0;color:var(--md-sys-color-on-surface, #1a1b20)}:host(.praxis-chart-host-fill-container){height:100%;min-height:0}:host-context(.pdx-shell){color:inherit}:host-context(.pdx-shell.no-shell) .praxis-chart-shell,:host-context(.pdx-shell.body-fill) .praxis-chart-shell,:host-context(.pdx-shell.expanded) .praxis-chart-shell,:host-context(.pdx-shell.fullscreen) .praxis-chart-shell{height:100%!important}.praxis-chart-shell{box-sizing:border-box;position:relative;width:100%;height:100%;min-height:240px;border-radius:var(--praxis-chart-config-surface-radius, var(--praxis-chart-surface-radius, 8px));overflow:hidden;background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) );border-color:var( --praxis-chart-config-surface-border, var( --praxis-chart-surface-border, color-mix(in srgb, var(--md-sys-color-outline, #c5c7ce) 44%, transparent) ) );border-style:solid;border-width:var(--praxis-chart-config-surface-border-width, 1px)}.praxis-chart-shell-fill-container{min-height:0}:host-context(.pdx-shell) .praxis-chart-shell:not(.praxis-chart-shell-contained),.praxis-chart-shell-embedded{border-width:var( --praxis-chart-config-surface-border-width, var(--praxis-chart-embedded-surface-border-width, 0) );border-radius:var( --praxis-chart-config-surface-radius, var(--praxis-chart-embedded-surface-radius, 0) );background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-embedded-surface-bg, transparent) )}.praxis-chart-settings-trigger
|
|
4097
|
+
`, styles: [":host{display:block;min-height:var(--praxis-chart-runtime-height, 320px);min-width:0;color:var(--md-sys-color-on-surface, #1a1b20)}:host(.praxis-chart-host-fill-container){height:100%;min-height:0}:host-context(.pdx-shell){color:inherit}:host-context(.pdx-shell.no-shell) .praxis-chart-shell,:host-context(.pdx-shell.body-fill) .praxis-chart-shell,:host-context(.pdx-shell.expanded) .praxis-chart-shell,:host-context(.pdx-shell.fullscreen) .praxis-chart-shell{height:100%!important}.praxis-chart-shell{box-sizing:border-box;position:relative;width:100%;height:100%;min-height:240px;border-radius:var(--praxis-chart-config-surface-radius, var(--praxis-chart-surface-radius, 8px));overflow:hidden;background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) );border-color:var( --praxis-chart-config-surface-border, var( --praxis-chart-surface-border, color-mix(in srgb, var(--md-sys-color-outline, #c5c7ce) 44%, transparent) ) );border-style:solid;border-width:var(--praxis-chart-config-surface-border-width, 1px)}.praxis-chart-shell-fill-container{min-height:0}:host-context(.pdx-shell) .praxis-chart-shell:not(.praxis-chart-shell-contained),.praxis-chart-shell-embedded{border-width:var( --praxis-chart-config-surface-border-width, var(--praxis-chart-embedded-surface-border-width, 0) );border-radius:var( --praxis-chart-config-surface-radius, var(--praxis-chart-embedded-surface-radius, 0) );background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-embedded-surface-bg, transparent) )}.praxis-chart-settings-trigger,.praxis-chart-data-trigger{position:absolute;top:10px;right:10px;z-index:5;--praxis-icon-button-filled-background: color-mix(in srgb, var(--md-sys-color-surface, #fff) 88%, rgba(18, 99, 180, .12));--praxis-icon-button-filled-foreground: var(--md-sys-color-on-surface-variant, #44474f);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-data-trigger-with-settings{right:50px}.praxis-chart-accessible-data{position:absolute;inset:0;z-index:4;display:flex;flex-direction:column;overflow:hidden;padding:52px 16px 14px;color:var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface, #1a1b20));background:var(--md-sys-color-surface-container-lowest, #fff)}.praxis-chart-accessible-data--shell-controlled{padding-top:16px}.praxis-chart-accessible-header{flex-shrink:0;margin-bottom:8px}.praxis-chart-accessible-header h3{margin:0;font-size:.95rem;font-weight:600;line-height:1.4}.praxis-chart-table-container{flex:1;min-height:0;overflow-y:auto;overflow-x:auto;overscroll-behavior:contain}.praxis-chart-accessible-data table{width:100%;border-collapse:separate;border-spacing:0;font-size:.875rem}.praxis-chart-accessible-data caption{padding:0 0 8px;text-align:left;color:inherit;opacity:.85;font-size:.8125rem}.praxis-chart-accessible-data thead{position:sticky;top:0;z-index:2}.praxis-chart-accessible-data thead th{font-weight:600;background:var(--praxis-chart-accessible-thead-bg, inherit);box-shadow:0 1px color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 44%,transparent)}.praxis-chart-accessible-data th,.praxis-chart-accessible-data td{padding:8px;border-bottom:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 44%,transparent);text-align:left}.praxis-chart-col-value{text-align:right!important;font-variant-numeric:tabular-nums}.praxis-chart-sort-header{appearance:none;display:inline-flex;align-items:center;gap:4px;padding:2px 4px;border:0;border-radius:4px;background:transparent;color:inherit;font:inherit;font-weight:600;cursor:pointer;transition:background-color .15s ease}.praxis-chart-sort-header:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.praxis-chart-sort-header:focus-visible{outline:2px solid currentColor;outline-offset:1px}.praxis-chart-sort-header--numeric{margin-left:auto}.praxis-chart-sort-indicator{font-size:.75rem;line-height:1;opacity:.85}.praxis-chart-point-action{appearance:none;padding:4px 6px;border:0;border-radius:4px;color:inherit;background:transparent;font:inherit;font-weight:600;text-align:left;text-decoration:none;cursor:pointer;transition:background-color .15s ease}.praxis-chart-point-action:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.praxis-chart-point-action:focus-visible{outline:3px solid currentColor;outline-offset:2px}.praxis-chart-point-action[aria-pressed=true]{color:var(--md-sys-color-on-primary-container, #001b3f);background:var(--md-sys-color-primary-container, #d7e3ff)}.praxis-chart-host{width:100%;height:100%;min-height:inherit}.praxis-chart-host--data-hidden{visibility:hidden;pointer-events:none}.praxis-chart-state{height:100%;min-height:min(240px,100%);display:grid;align-content:center;justify-items:center;gap:14px;padding:24px;text-align:center}.praxis-chart-state-copy{display:grid;gap:6px;justify-items:center}.praxis-chart-state-title{font-size:1rem;font-weight:600;color:var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface, #1a1b20))}.praxis-chart-state-description{font-size:.925rem;color:color-mix(in srgb,var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface-variant, #5a5d67)) 72%,transparent);max-width:36rem}.praxis-chart-loading-hero{width:min(100%,320px);display:grid;gap:14px}.praxis-chart-loading-summary{display:flex;gap:8px;justify-content:center}.praxis-chart-loading-chip,.praxis-chart-loading-bar{border-radius:999px;background:linear-gradient(90deg,#1263b414,#1263b438,#1263b414);background-size:200% 100%;animation:praxis-chart-loading-wave 1.2s ease-in-out infinite}.praxis-chart-loading-chip{display:block;width:104px;height:12px}.praxis-chart-loading-chip--short{width:64px}.praxis-chart-loading-plot{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));align-items:end;gap:10px;height:108px;padding:12px 8px 4px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface-container, #eef3f8) 72%,transparent);border:1px solid rgba(18,99,180,.08)}.praxis-chart-loading-bar{display:block;width:100%}.praxis-chart-loading-bar--1{height:32%}.praxis-chart-loading-bar--2{height:68%}.praxis-chart-loading-bar--3{height:48%}.praxis-chart-loading-bar--4{height:78%}.praxis-chart-loading-bar--5{height:56%}@keyframes praxis-chart-loading-wave{0%{background-position:100% 0}to{background-position:-100% 0}}\n"] }]
|
|
4061
4098
|
}], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], resourceKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "resourceKey", required: false }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], chartDocument: [{ type: i0.Input, args: [{ isSignal: true, alias: "chartDocument", required: false }] }], filterCriteria: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterCriteria", required: false }] }], queryContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryContext", required: false }] }], remoteDataResolver: [{ type: i0.Input, args: [{ isSignal: true, alias: "remoteDataResolver", required: false }] }], enableCustomization: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableCustomization", required: false }] }], availableResources: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableResources", required: false }] }], availableFields: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableFields", required: false }] }], availableTargets: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableTargets", required: false }] }], pointClick: [{ type: i0.Output, args: ["pointClick"] }], pointAction: [{ type: i0.Output, args: ["pointAction"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], drillDown: [{ type: i0.Output, args: ["drillDown"] }], crossFilter: [{ type: i0.Output, args: ["crossFilter"] }], queryRequest: [{ type: i0.Output, args: ["queryRequest"] }], loadStateChange: [{ type: i0.Output, args: ["loadStateChange"] }], chartDocumentApplied: [{ type: i0.Output, args: ["chartDocumentApplied"] }], chartDocumentSaved: [{ type: i0.Output, args: ["chartDocumentSaved"] }], chartDocumentPatchChange: [{ type: i0.Output, args: ["chartDocumentPatchChange"] }], chartHost: [{ type: i0.ViewChild, args: ['chartHost', { isSignal: true }] }] } });
|
|
4062
4099
|
function mergeRemoteQueryContext(config, queryContext, runtimeFilters) {
|
|
4063
4100
|
const dataSource = config.dataSource;
|
|
@@ -6060,7 +6097,9 @@ class EChartsOptionBuilderService {
|
|
|
6060
6097
|
}
|
|
6061
6098
|
: undefined,
|
|
6062
6099
|
},
|
|
6063
|
-
labelLine: labelsVisible
|
|
6100
|
+
labelLine: labelsVisible
|
|
6101
|
+
? { show: true, length: 14, length2: 12, lineStyle: { color: textColor } }
|
|
6102
|
+
: { show: false },
|
|
6064
6103
|
data: transformed.slices.map((slice) => ({
|
|
6065
6104
|
...(slice.data || {}),
|
|
6066
6105
|
name: slice.name,
|
|
@@ -7223,7 +7262,20 @@ const PRAXIS_CHARTS_EN_US = {
|
|
|
7223
7262
|
'praxis.charts.editor.field.eventMapping': 'Mapping',
|
|
7224
7263
|
'praxis.charts.editor.preview.title': 'Chart preview',
|
|
7225
7264
|
'praxis.charts.editor.preview.caption': 'Local preview derived from the canonical contract without remote calls.',
|
|
7265
|
+
'praxis.charts.editor.preview.originLocal': 'Previewing widget local dataset (inputs.data).',
|
|
7266
|
+
'praxis.charts.editor.preview.originSynthetic': 'Previewing synthetic sample data (no local dataset records).',
|
|
7226
7267
|
'praxis.charts.editor.preview.invalid': 'Preview is unavailable while the contract has blocking errors.',
|
|
7268
|
+
'praxis.charts.editor.localData.title': 'Local dataset (inputs.data)',
|
|
7269
|
+
'praxis.charts.editor.localData.caption': 'Edit the local records rendered when the data source is derived.',
|
|
7270
|
+
'praxis.charts.editor.localData.empty': 'No local records configured yet. Add a record to replace the synthetic preview sample.',
|
|
7271
|
+
'praxis.charts.editor.localData.addRow': 'Add record',
|
|
7272
|
+
'praxis.charts.editor.localData.removeRow': 'Remove record',
|
|
7273
|
+
'praxis.charts.editor.localData.consolidateGauge': 'Consolidate to 1 bucket for gauge',
|
|
7274
|
+
'praxis.charts.editor.localData.validation.empty': 'Local dataset is empty. Add at least one record for derived runtime rendering; the preview is currently using a synthetic sample.',
|
|
7275
|
+
'praxis.charts.editor.localData.validation.gaugeCardinality': 'Gauge charts require exactly 1 consolidated record in local data ({{ count }} found). Consolidate or remove extra records before saving.',
|
|
7276
|
+
'praxis.charts.editor.localData.validation.gaugeMetricInvalid': 'Gauge metric "{{ field }}" must have a finite numeric value in the local dataset.',
|
|
7277
|
+
'praxis.charts.editor.localData.validation.gaugeOutOfRange': 'Gauge metric "{{ field }}" value ({{ value }}) is outside the configured scale ({{ min }} to {{ max }}).',
|
|
7278
|
+
'praxis.charts.editor.localData.validation.metricInvalid': 'Metric "{{ field }}" in record #{{ index }} must be a finite number.',
|
|
7227
7279
|
'praxis.charts.editor.issues.title': 'Validation issues',
|
|
7228
7280
|
'praxis.charts.editor.issues.empty': 'No issues were identified.',
|
|
7229
7281
|
'praxis.charts.editor.appearance.featuresTitle': 'Display features',
|
|
@@ -7246,14 +7298,26 @@ const PRAXIS_CHARTS_EN_US = {
|
|
|
7246
7298
|
'praxis.charts.editor.catalog.comparisonPeriodFieldStale': 'Selected period field is not time-series eligible for the governed comparison operation.',
|
|
7247
7299
|
'praxis.charts.editor.catalog.targetMissing': 'Target catalog is required for this governed event action.',
|
|
7248
7300
|
'praxis.charts.editor.catalog.targetStale': 'Selected target is not available for this event action.',
|
|
7301
|
+
'praxis.charts.editor.catalog.capabilitiesAmbiguous': 'Multiple capability endpoints were published for the selected resource.',
|
|
7249
7302
|
'praxis.charts.editor.catalog.capabilitiesDenied': 'Access to the selected resource capabilities was denied.',
|
|
7250
7303
|
'praxis.charts.editor.catalog.capabilitiesUnavailable': 'The selected resource capabilities are temporarily unavailable.',
|
|
7251
7304
|
'praxis.charts.editor.catalog.contextUnavailable': 'Governed chart authoring context could not be resolved.',
|
|
7252
7305
|
'praxis.charts.widget.contextDiagnostics.title': 'Authoring context',
|
|
7306
|
+
'praxis.charts.widget.contextDiagnostics.summarySingle': '1 authoring context diagnostic detected.',
|
|
7307
|
+
'praxis.charts.widget.contextDiagnostics.summaryMultiple': '{{ count }} authoring context diagnostics detected.',
|
|
7308
|
+
'praxis.charts.widget.contextDiagnostics.occurrences': '{{ count }} occurrences',
|
|
7309
|
+
'praxis.charts.widget.contextDiagnostics.scopeImpact': 'Affected: governed stats resource/field catalog discovery. Remains editable: visual layout, derived local data, and query context.',
|
|
7310
|
+
'praxis.charts.widget.contextDiagnostics.showDetails': 'View details',
|
|
7311
|
+
'praxis.charts.widget.contextDiagnostics.hideDetails': 'Hide details',
|
|
7312
|
+
'praxis.charts.widget.contextDiagnostics.affectedPaths': 'Affected resources/paths:',
|
|
7313
|
+
'praxis.charts.widget.contextDiagnostics.nextStepLabel': 'Next step:',
|
|
7314
|
+
'praxis.charts.widget.contextDiagnostics.technicalCode': 'Technical code:',
|
|
7315
|
+
'praxis.charts.widget.contextDiagnostics.capabilitiesAmbiguous': 'Multiple capability endpoints were published for the resource, preventing automatic discovery.',
|
|
7253
7316
|
'praxis.charts.widget.contextDiagnostics.catalogDenied': 'Access to the governed resource catalog was denied.',
|
|
7254
7317
|
'praxis.charts.widget.contextDiagnostics.catalogUnavailable': 'The governed resource catalog is temporarily unavailable.',
|
|
7255
7318
|
'praxis.charts.widget.contextDiagnostics.catalogEmpty': 'No authorable stats resources were published by the governed catalog.',
|
|
7256
7319
|
'praxis.charts.widget.contextDiagnostics.catalogPathInvalid': 'The resource catalog published an unsafe or non-relative operational path.',
|
|
7320
|
+
'praxis.charts.widget.contextDiagnostics.selectionStale': 'The selected stats resource is not available in the governed catalog.',
|
|
7257
7321
|
'praxis.charts.widget.contextDiagnostics.capabilitiesDenied': 'Access to the selected resource capabilities was denied.',
|
|
7258
7322
|
'praxis.charts.widget.contextDiagnostics.capabilitiesUnavailable': 'The selected resource capabilities are temporarily unavailable.',
|
|
7259
7323
|
'praxis.charts.widget.contextDiagnostics.operationsEmpty': 'The selected resource does not publish authorable stats operations.',
|
|
@@ -7262,6 +7326,20 @@ const PRAXIS_CHARTS_EN_US = {
|
|
|
7262
7326
|
'praxis.charts.widget.contextDiagnostics.capabilitiesPathInvalid': 'The selected capability snapshot did not publish a safe governed resource path.',
|
|
7263
7327
|
'praxis.charts.widget.contextDiagnostics.capabilitiesPathMismatch': 'The selected capability snapshot path does not match the operational path published by the catalog.',
|
|
7264
7328
|
'praxis.charts.widget.contextDiagnostics.partial': 'Some governed authoring choices could not be materialized from the current page context.',
|
|
7329
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.capabilitiesAmbiguous': 'Ensure the resource publishes a single canonical collection /capabilities endpoint in the catalog.',
|
|
7330
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.catalogEmpty': 'Publish at least one stats-enabled resource in /schemas/catalog or use a derived data source.',
|
|
7331
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.catalogDenied': 'Verify permissions for /schemas/catalog or switch the data source to derived.',
|
|
7332
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.catalogUnavailable': 'Retry after the backend catalog service responds or keep editing visual/derived settings.',
|
|
7333
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.catalogPathInvalid': 'Fix the resource path in the backend catalog so it uses a relative operational path.',
|
|
7334
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.selectionStale': 'Select an active resource from the governed catalog or publish the resource in /schemas/catalog.',
|
|
7335
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.capabilitiesDenied': 'Grant read access to the resource /capabilities endpoint or select another resource.',
|
|
7336
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.capabilitiesUnavailable': 'Check the resource /capabilities endpoint availability or select another resource.',
|
|
7337
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.operationsEmpty': 'Enable stats operations (group-by, timeseries, distribution, or comparison) on the backend resource.',
|
|
7338
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.fieldsEmpty': 'Expose eligible fields in the resource capability snapshot or filtered schema.',
|
|
7339
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.identityMismatch': 'Align resourceKey and resourcePath between /schemas/catalog and the /capabilities snapshot.',
|
|
7340
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.capabilitiesPathInvalid': 'Ensure resourcePath in the /capabilities snapshot is a safe relative path.',
|
|
7341
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.capabilitiesPathMismatch': 'Align the resource operational path in /schemas/catalog with resourcePath in /capabilities.',
|
|
7342
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.partial': 'Inspect the affected resource contract in /schemas/catalog and /capabilities.',
|
|
7265
7343
|
'praxis.charts.editor.hint.sizingMode': 'Use fill-container only when the host widget provides a defined body height.',
|
|
7266
7344
|
'praxis.charts.editor.hint.height': 'Numbers are saved as pixels; CSS lengths such as 20rem are also accepted.',
|
|
7267
7345
|
'praxis.charts.editor.hint.minHeight': 'Set a readable minimum for compact dashboard widgets.',
|
|
@@ -7492,7 +7570,20 @@ const PRAXIS_CHARTS_PT_BR = {
|
|
|
7492
7570
|
'praxis.charts.editor.field.eventMapping': 'Mapeamento',
|
|
7493
7571
|
'praxis.charts.editor.preview.title': 'Visualização do gráfico',
|
|
7494
7572
|
'praxis.charts.editor.preview.caption': 'Visualização local derivada do contrato canônico, sem chamadas remotas.',
|
|
7573
|
+
'praxis.charts.editor.preview.originLocal': 'Prévia usando os dados locais do widget (inputs.data).',
|
|
7574
|
+
'praxis.charts.editor.preview.originSynthetic': 'Prévia usando amostra sintética ilustrativa (sem registros em inputs.data).',
|
|
7495
7575
|
'praxis.charts.editor.preview.invalid': 'A visualização fica indisponível enquanto houver erro bloqueante no contrato.',
|
|
7576
|
+
'praxis.charts.editor.localData.title': 'Dados locais do widget (inputs.data)',
|
|
7577
|
+
'praxis.charts.editor.localData.caption': 'Edite os registros locais renderizados quando a fonte de dados é derivada.',
|
|
7578
|
+
'praxis.charts.editor.localData.empty': 'Nenhum registro local configurado. Adicione um registro para substituir a amostra sintética da prévia.',
|
|
7579
|
+
'praxis.charts.editor.localData.addRow': 'Adicionar registro',
|
|
7580
|
+
'praxis.charts.editor.localData.removeRow': 'Remover registro',
|
|
7581
|
+
'praxis.charts.editor.localData.consolidateGauge': 'Consolidar em 1 bucket para velocímetro',
|
|
7582
|
+
'praxis.charts.editor.localData.validation.empty': 'Os dados locais do widget (inputs.data) estão vazios. Adicione ao menos 1 registro para o runtime derivado; a prévia atual usa amostra sintética.',
|
|
7583
|
+
'praxis.charts.editor.localData.validation.gaugeCardinality': 'Gráficos de velocímetro exigem exatamente 1 registro consolidado nos dados locais ({{ count }} encontrados). Consolide ou remova os registros extras antes de salvar.',
|
|
7584
|
+
'praxis.charts.editor.localData.validation.gaugeMetricInvalid': 'A métrica do velocímetro "{{ field }}" precisa ter um valor numérico finito nos dados locais.',
|
|
7585
|
+
'praxis.charts.editor.localData.validation.gaugeOutOfRange': 'O valor da métrica "{{ field }}" ({{ value }}) está fora da escala configurada ({{ min }} a {{ max }}).',
|
|
7586
|
+
'praxis.charts.editor.localData.validation.metricInvalid': 'A métrica "{{ field }}" no registro #{{ index }} precisa ser um número finito.',
|
|
7496
7587
|
'praxis.charts.editor.issues.title': 'Problemas de validação',
|
|
7497
7588
|
'praxis.charts.editor.issues.empty': 'Nenhum problema identificado.',
|
|
7498
7589
|
'praxis.charts.editor.appearance.featuresTitle': 'Recursos visuais',
|
|
@@ -7501,9 +7592,9 @@ const PRAXIS_CHARTS_PT_BR = {
|
|
|
7501
7592
|
'praxis.charts.editor.appearance.surfaceTitle': 'Superfície',
|
|
7502
7593
|
'praxis.charts.editor.appearance.surfaceHint': 'Use embutido para widgets de dashboard e contido apenas quando o gráfico precisar ser dono da própria superfície visual.',
|
|
7503
7594
|
'praxis.charts.editor.appearance.statesTitle': 'Mensagens de estado',
|
|
7504
|
-
'praxis.charts.editor.catalog.resourceMissing': 'O catálogo de recursos é obrigatório para
|
|
7595
|
+
'praxis.charts.editor.catalog.resourceMissing': 'O catálogo de recursos é obrigatório para a autoria governada de praxis.stats.',
|
|
7505
7596
|
'praxis.charts.editor.catalog.resourceStale': 'O recurso selecionado não está disponível no catálogo governado de recursos.',
|
|
7506
|
-
'praxis.charts.editor.catalog.operationMissing': 'O recurso selecionado não publica operações de stats
|
|
7597
|
+
'praxis.charts.editor.catalog.operationMissing': 'O recurso selecionado não publica operações de stats autoráveis.',
|
|
7507
7598
|
'praxis.charts.editor.catalog.operationStale': 'A operação selecionada não é suportada pelo catálogo governado do recurso.',
|
|
7508
7599
|
'praxis.charts.editor.catalog.fieldCatalogMissing': 'O recurso selecionado não publica campos governados para a autoria do gráfico.',
|
|
7509
7600
|
'praxis.charts.editor.catalog.dimensionFieldsMissing': 'Nenhuma dimensão governada está disponível para o recurso e a operação atuais.',
|
|
@@ -7515,22 +7606,48 @@ const PRAXIS_CHARTS_PT_BR = {
|
|
|
7515
7606
|
'praxis.charts.editor.catalog.comparisonPeriodFieldStale': 'O campo de período selecionado não é elegível para série temporal na operação de comparação governada.',
|
|
7516
7607
|
'praxis.charts.editor.catalog.targetMissing': 'O catálogo de destinos é obrigatório para esta ação de evento governada.',
|
|
7517
7608
|
'praxis.charts.editor.catalog.targetStale': 'O destino selecionado não está disponível para esta ação de evento.',
|
|
7609
|
+
'praxis.charts.editor.catalog.capabilitiesAmbiguous': 'Múltiplos endpoints de capabilities de coleção foram publicados para o recurso selecionado.',
|
|
7518
7610
|
'praxis.charts.editor.catalog.capabilitiesDenied': 'O acesso às capabilities do recurso selecionado foi negado.',
|
|
7519
7611
|
'praxis.charts.editor.catalog.capabilitiesUnavailable': 'As capabilities do recurso selecionado estão temporariamente indisponíveis.',
|
|
7520
|
-
'praxis.charts.editor.catalog.contextUnavailable': 'Não foi possível resolver o contexto governado de
|
|
7521
|
-
'praxis.charts.widget.contextDiagnostics.title': 'Contexto de
|
|
7612
|
+
'praxis.charts.editor.catalog.contextUnavailable': 'Não foi possível resolver o contexto governado de autoria do gráfico.',
|
|
7613
|
+
'praxis.charts.widget.contextDiagnostics.title': 'Contexto de autoria',
|
|
7614
|
+
'praxis.charts.widget.contextDiagnostics.summarySingle': '1 diagnóstico detectado no contexto de autoria.',
|
|
7615
|
+
'praxis.charts.widget.contextDiagnostics.summaryMultiple': '{{ count }} diagnósticos detectados no contexto de autoria.',
|
|
7616
|
+
'praxis.charts.widget.contextDiagnostics.occurrences': '{{ count }} ocorrências',
|
|
7617
|
+
'praxis.charts.widget.contextDiagnostics.scopeImpact': 'Afetado: descoberta governada de recursos/campos de stats. Permanece editável: layout visual, dados locais derivados e contexto da consulta.',
|
|
7618
|
+
'praxis.charts.widget.contextDiagnostics.showDetails': 'Ver detalhes',
|
|
7619
|
+
'praxis.charts.widget.contextDiagnostics.hideDetails': 'Ocultar detalhes',
|
|
7620
|
+
'praxis.charts.widget.contextDiagnostics.affectedPaths': 'Recursos/caminhos afetados:',
|
|
7621
|
+
'praxis.charts.widget.contextDiagnostics.nextStepLabel': 'Próximo passo:',
|
|
7622
|
+
'praxis.charts.widget.contextDiagnostics.technicalCode': 'Código técnico:',
|
|
7623
|
+
'praxis.charts.widget.contextDiagnostics.capabilitiesAmbiguous': 'Múltiplos endpoints de capabilities de coleção foram publicados para o recurso, impedindo a descoberta automática.',
|
|
7522
7624
|
'praxis.charts.widget.contextDiagnostics.catalogDenied': 'O acesso ao catálogo governado de recursos foi negado.',
|
|
7523
7625
|
'praxis.charts.widget.contextDiagnostics.catalogUnavailable': 'O catálogo governado de recursos está temporariamente indisponível.',
|
|
7524
|
-
'praxis.charts.widget.contextDiagnostics.catalogEmpty': 'Nenhum recurso de stats
|
|
7626
|
+
'praxis.charts.widget.contextDiagnostics.catalogEmpty': 'Nenhum recurso de stats autorável foi publicado pelo catálogo governado.',
|
|
7525
7627
|
'praxis.charts.widget.contextDiagnostics.catalogPathInvalid': 'O catálogo de recursos publicou um path operacional inseguro ou não relativo.',
|
|
7628
|
+
'praxis.charts.widget.contextDiagnostics.selectionStale': 'O recurso de stats selecionado não está disponível no catálogo governado.',
|
|
7526
7629
|
'praxis.charts.widget.contextDiagnostics.capabilitiesDenied': 'O acesso às capabilities do recurso selecionado foi negado.',
|
|
7527
7630
|
'praxis.charts.widget.contextDiagnostics.capabilitiesUnavailable': 'As capabilities do recurso selecionado estão temporariamente indisponíveis.',
|
|
7528
|
-
'praxis.charts.widget.contextDiagnostics.operationsEmpty': 'O recurso selecionado não publica operações de stats
|
|
7631
|
+
'praxis.charts.widget.contextDiagnostics.operationsEmpty': 'O recurso selecionado não publica operações de stats autoráveis.',
|
|
7529
7632
|
'praxis.charts.widget.contextDiagnostics.fieldsEmpty': 'O recurso selecionado não publica campos governados para a autoria do gráfico.',
|
|
7530
7633
|
'praxis.charts.widget.contextDiagnostics.identityMismatch': 'A identidade do recurso selecionado não corresponde ao snapshot governado de capabilities.',
|
|
7531
7634
|
'praxis.charts.widget.contextDiagnostics.capabilitiesPathInvalid': 'O snapshot de capabilities selecionado não publicou um path de recurso governado e seguro.',
|
|
7532
7635
|
'praxis.charts.widget.contextDiagnostics.capabilitiesPathMismatch': 'O path do snapshot de capabilities não corresponde ao path operacional publicado pelo catálogo.',
|
|
7533
|
-
'praxis.charts.widget.contextDiagnostics.partial': 'Algumas opções governadas de
|
|
7636
|
+
'praxis.charts.widget.contextDiagnostics.partial': 'Algumas opções governadas de autoria não puderam ser materializadas a partir do contexto atual da página.',
|
|
7637
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.capabilitiesAmbiguous': 'Garanta que o recurso publique um único endpoint canônico de coleção /capabilities no catálogo.',
|
|
7638
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.catalogEmpty': 'Publique ao menos um recurso com GET /{recurso}/capabilities e POST /{recurso}/stats/* no catálogo, ou configure o gráfico com fonte derivada.',
|
|
7639
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.catalogDenied': 'Verifique as permissões de leitura de /schemas/catalog ou altere a fonte de dados para derivada.',
|
|
7640
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.catalogUnavailable': 'Tente novamente quando o serviço de catálogo responder ou continue editando configurações visuais/derivadas.',
|
|
7641
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.catalogPathInvalid': 'Corrija o path do recurso no catálogo do backend para usar um caminho operacional relativo.',
|
|
7642
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.selectionStale': 'Selecione um recurso ativo do catálogo governado ou publique o recurso em /schemas/catalog.',
|
|
7643
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.capabilitiesDenied': 'Conceda permissão de leitura ao endpoint /capabilities do recurso ou selecione outro recurso.',
|
|
7644
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.capabilitiesUnavailable': 'Verifique a disponibilidade do endpoint /capabilities do recurso ou selecione outro recurso.',
|
|
7645
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.operationsEmpty': 'Habilite operações de stats (group-by, timeseries, distribution ou comparison) no recurso do backend.',
|
|
7646
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.fieldsEmpty': 'Exponha campos elegíveis no snapshot de capabilities ou schema filtrado do recurso.',
|
|
7647
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.identityMismatch': 'Alinhe resourceKey e resourcePath entre /schemas/catalog e o snapshot de /capabilities.',
|
|
7648
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.capabilitiesPathInvalid': 'Garanta que resourcePath no snapshot de /capabilities seja um caminho relativo seguro.',
|
|
7649
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.capabilitiesPathMismatch': 'Alinhe o path operacional do recurso em /schemas/catalog com resourcePath em /capabilities.',
|
|
7650
|
+
'praxis.charts.widget.contextDiagnostics.nextStep.partial': 'Inspecione o contrato do recurso afetado em /schemas/catalog e /capabilities.',
|
|
7534
7651
|
'praxis.charts.editor.hint.sizingMode': 'Use preencher container apenas quando o widget host fornecer altura definida para o corpo.',
|
|
7535
7652
|
'praxis.charts.editor.hint.height': 'Números são salvos como pixels; medidas CSS como 20rem também são aceitas.',
|
|
7536
7653
|
'praxis.charts.editor.hint.minHeight': 'Defina um mínimo legível para widgets compactos de dashboard.',
|
|
@@ -7733,11 +7850,17 @@ class ChartEditorPreviewMapperService {
|
|
|
7733
7850
|
constructor(mapper) {
|
|
7734
7851
|
this.mapper = mapper;
|
|
7735
7852
|
}
|
|
7736
|
-
build(document) {
|
|
7853
|
+
build(document, localData) {
|
|
7737
7854
|
const config = this.mapper.toPraxisChartConfig(document);
|
|
7855
|
+
const hasLocalData = document.source.kind === 'derived'
|
|
7856
|
+
&& Array.isArray(localData)
|
|
7857
|
+
&& localData.length > 0;
|
|
7738
7858
|
return {
|
|
7739
7859
|
config,
|
|
7740
|
-
data:
|
|
7860
|
+
data: hasLocalData
|
|
7861
|
+
? localData.map((row) => ({ ...row }))
|
|
7862
|
+
: this.buildRows(document),
|
|
7863
|
+
dataOrigin: hasLocalData ? 'local' : 'synthetic',
|
|
7741
7864
|
};
|
|
7742
7865
|
}
|
|
7743
7866
|
buildRows(document) {
|
|
@@ -8557,7 +8680,8 @@ class PraxisChartConfigEditorContextResolverService {
|
|
|
8557
8680
|
// route user intent or choose a resource from text.
|
|
8558
8681
|
const declaredCapabilityEndpoints = endpoints.filter((endpoint) => (endpoint.method === 'GET'
|
|
8559
8682
|
&& !endpoint.path.includes('{')
|
|
8560
|
-
&& endpoint.path.endsWith('/capabilities')
|
|
8683
|
+
&& endpoint.path.endsWith('/capabilities')
|
|
8684
|
+
&& !endpoint.path.endsWith('/stats/capabilities')));
|
|
8561
8685
|
if (declaredCapabilityEndpoints.some((endpoint) => !this.isGovernedRelativePath(endpoint.path))) {
|
|
8562
8686
|
diagnostics.push({
|
|
8563
8687
|
code: 'chart-resource-catalog-path-invalid',
|
|
@@ -8693,6 +8817,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
8693
8817
|
|
|
8694
8818
|
class PraxisChartConfigEditor {
|
|
8695
8819
|
documentInput = input(null, { ...(ngDevMode ? { debugName: "documentInput" } : /* istanbul ignore next */ {}), alias: 'document' });
|
|
8820
|
+
localDataInput = input(undefined, { ...(ngDevMode ? { debugName: "localDataInput" } : /* istanbul ignore next */ {}), alias: 'localData' });
|
|
8696
8821
|
modeInput = input('edit', { ...(ngDevMode ? { debugName: "modeInput" } : /* istanbul ignore next */ {}), alias: 'mode' });
|
|
8697
8822
|
readonlyInput = input(false, { ...(ngDevMode ? { debugName: "readonlyInput" } : /* istanbul ignore next */ {}), alias: 'readonly' });
|
|
8698
8823
|
availableResourcesInput = input(null, { ...(ngDevMode ? { debugName: "availableResourcesInput" } : /* istanbul ignore next */ {}), alias: 'availableResources' });
|
|
@@ -8702,6 +8827,7 @@ class PraxisChartConfigEditor {
|
|
|
8702
8827
|
save = output();
|
|
8703
8828
|
resetChange = output();
|
|
8704
8829
|
documentChange = output();
|
|
8830
|
+
localDataChange = output();
|
|
8705
8831
|
isDirty$ = new BehaviorSubject(false);
|
|
8706
8832
|
isValid$ = new BehaviorSubject(true);
|
|
8707
8833
|
isBusy$ = new BehaviorSubject(false);
|
|
@@ -8769,39 +8895,48 @@ class PraxisChartConfigEditor {
|
|
|
8769
8895
|
i18n = inject(PraxisI18nService);
|
|
8770
8896
|
currentDocument = signal(this.defaults.create(), ...(ngDevMode ? [{ debugName: "currentDocument" }] : /* istanbul ignore next */ []));
|
|
8771
8897
|
initialDocument = signal(this.defaults.create(), ...(ngDevMode ? [{ debugName: "initialDocument" }] : /* istanbul ignore next */ []));
|
|
8898
|
+
currentLocalData = signal(undefined, ...(ngDevMode ? [{ debugName: "currentLocalData" }] : /* istanbul ignore next */ []));
|
|
8899
|
+
initialLocalData = signal(undefined, ...(ngDevMode ? [{ debugName: "initialLocalData" }] : /* istanbul ignore next */ []));
|
|
8772
8900
|
resolvedResources = signal(null, ...(ngDevMode ? [{ debugName: "resolvedResources" }] : /* istanbul ignore next */ []));
|
|
8773
8901
|
resolvedFields = signal(null, ...(ngDevMode ? [{ debugName: "resolvedFields" }] : /* istanbul ignore next */ []));
|
|
8774
8902
|
catalogResolutionDiagnostics = signal([], ...(ngDevMode ? [{ debugName: "catalogResolutionDiagnostics" }] : /* istanbul ignore next */ []));
|
|
8775
8903
|
lastExternalSignature = null;
|
|
8904
|
+
lastExternalLocalDataSignature = null;
|
|
8776
8905
|
lastCatalogInputSignature = null;
|
|
8777
8906
|
catalogResolutionRequestId = 0;
|
|
8778
8907
|
baseAvailableResources = computed(() => (this.availableResourcesInput() ?? this.injectedData?.availableResources ?? []), ...(ngDevMode ? [{ debugName: "baseAvailableResources" }] : /* istanbul ignore next */ []));
|
|
8779
8908
|
baseAvailableFields = computed(() => (this.availableFieldsInput() ?? this.injectedData?.availableFields ?? []), ...(ngDevMode ? [{ debugName: "baseAvailableFields" }] : /* istanbul ignore next */ []));
|
|
8780
8909
|
normalizedDocument = computed(() => this.normalizer.normalize(this.currentDocument()), ...(ngDevMode ? [{ debugName: "normalizedDocument" }] : /* istanbul ignore next */ []));
|
|
8910
|
+
localDataIssues = computed(() => this.validateLocalData(), ...(ngDevMode ? [{ debugName: "localDataIssues" }] : /* istanbul ignore next */ []));
|
|
8781
8911
|
validation = computed(() => {
|
|
8782
8912
|
const validation = this.validator.validate(this.normalizedDocument());
|
|
8783
8913
|
const catalogIssues = this.catalogIssues();
|
|
8914
|
+
const localDataIssues = this.localDataIssues();
|
|
8784
8915
|
return {
|
|
8785
|
-
valid: validation.valid
|
|
8916
|
+
valid: validation.valid
|
|
8917
|
+
&& !catalogIssues.some((issue) => issue.severity === 'error')
|
|
8918
|
+
&& !localDataIssues.some((issue) => issue.severity === 'error'),
|
|
8786
8919
|
issues: [
|
|
8787
8920
|
...validation.issues,
|
|
8788
8921
|
...catalogIssues,
|
|
8922
|
+
...localDataIssues,
|
|
8789
8923
|
],
|
|
8790
8924
|
};
|
|
8791
8925
|
}, ...(ngDevMode ? [{ debugName: "validation" }] : /* istanbul ignore next */ []));
|
|
8792
8926
|
issues = computed(() => this.validation().issues, ...(ngDevMode ? [{ debugName: "issues" }] : /* istanbul ignore next */ []));
|
|
8793
8927
|
catalogIssues = computed(() => [
|
|
8794
8928
|
...this.validateCatalogContext(),
|
|
8795
|
-
...this.
|
|
8929
|
+
...this.groupCatalogResolutionDiagnostics(this.catalogResolutionDiagnostics()),
|
|
8796
8930
|
], ...(ngDevMode ? [{ debugName: "catalogIssues" }] : /* istanbul ignore next */ []));
|
|
8797
8931
|
availableResources = computed(() => this.resolvedResources() ?? this.baseAvailableResources(), ...(ngDevMode ? [{ debugName: "availableResources" }] : /* istanbul ignore next */ []));
|
|
8798
8932
|
availableFields = computed(() => this.resolvedFields() ?? this.baseAvailableFields(), ...(ngDevMode ? [{ debugName: "availableFields" }] : /* istanbul ignore next */ []));
|
|
8799
8933
|
availableTargets = computed(() => (this.availableTargetsInput() ?? this.injectedData?.availableTargets ?? []), ...(ngDevMode ? [{ debugName: "availableTargets" }] : /* istanbul ignore next */ []));
|
|
8800
8934
|
preview = computed(() => {
|
|
8801
|
-
|
|
8935
|
+
const contractValidation = this.validator.validate(this.normalizedDocument());
|
|
8936
|
+
if (!contractValidation.valid || this.catalogIssues().some((issue) => issue.severity === 'error')) {
|
|
8802
8937
|
return null;
|
|
8803
8938
|
}
|
|
8804
|
-
return this.previewMapper.build(this.normalizedDocument());
|
|
8939
|
+
return this.previewMapper.build(this.normalizedDocument(), this.currentLocalData());
|
|
8805
8940
|
}, ...(ngDevMode ? [{ debugName: "preview" }] : /* istanbul ignore next */ []));
|
|
8806
8941
|
constructor() {
|
|
8807
8942
|
effect(() => {
|
|
@@ -8837,6 +8972,23 @@ class PraxisChartConfigEditor {
|
|
|
8837
8972
|
this.lastExternalSignature = signature;
|
|
8838
8973
|
this.refreshState(false);
|
|
8839
8974
|
});
|
|
8975
|
+
effect(() => {
|
|
8976
|
+
const inputVal = this.localDataInput();
|
|
8977
|
+
const externalLocalData = inputVal !== undefined ? inputVal : this.injectedData?.localData;
|
|
8978
|
+
const normalizedLocalData = Array.isArray(externalLocalData)
|
|
8979
|
+
? externalLocalData
|
|
8980
|
+
.filter((row) => !!row && typeof row === 'object' && !Array.isArray(row))
|
|
8981
|
+
.map((row) => structuredClone(row))
|
|
8982
|
+
: undefined;
|
|
8983
|
+
const signature = JSON.stringify(normalizedLocalData ?? null);
|
|
8984
|
+
if (signature === this.lastExternalLocalDataSignature || this.isDirty$.getValue()) {
|
|
8985
|
+
return;
|
|
8986
|
+
}
|
|
8987
|
+
this.currentLocalData.set(normalizedLocalData ? structuredClone(normalizedLocalData) : undefined);
|
|
8988
|
+
this.initialLocalData.set(normalizedLocalData ? structuredClone(normalizedLocalData) : undefined);
|
|
8989
|
+
this.lastExternalLocalDataSignature = signature;
|
|
8990
|
+
this.refreshState(false, false);
|
|
8991
|
+
});
|
|
8840
8992
|
effect(() => {
|
|
8841
8993
|
const document = this.currentDocument();
|
|
8842
8994
|
if (this.isReadonly() || document.source.kind !== 'praxis.stats') {
|
|
@@ -8864,6 +9016,13 @@ class PraxisChartConfigEditor {
|
|
|
8864
9016
|
getSettingsValue() {
|
|
8865
9017
|
return structuredClone(this.normalizedDocument());
|
|
8866
9018
|
}
|
|
9019
|
+
getLocalDataValue() {
|
|
9020
|
+
if (this.normalizedDocument().source.kind === 'praxis.stats') {
|
|
9021
|
+
return undefined;
|
|
9022
|
+
}
|
|
9023
|
+
const current = this.currentLocalData();
|
|
9024
|
+
return current ? structuredClone(current) : undefined;
|
|
9025
|
+
}
|
|
8867
9026
|
onSave() {
|
|
8868
9027
|
return this.saveChanges().document;
|
|
8869
9028
|
}
|
|
@@ -8874,9 +9033,16 @@ class PraxisChartConfigEditor {
|
|
|
8874
9033
|
this.catalogResolutionDiagnostics.set([]);
|
|
8875
9034
|
this.isBusy$.next(false);
|
|
8876
9035
|
const snapshot = structuredClone(this.initialDocument());
|
|
9036
|
+
const localSnapshot = this.initialLocalData()
|
|
9037
|
+
? structuredClone(this.initialLocalData())
|
|
9038
|
+
: undefined;
|
|
8877
9039
|
this.currentDocument.set(snapshot);
|
|
8878
|
-
this.
|
|
8879
|
-
this.
|
|
9040
|
+
this.currentLocalData.set(localSnapshot);
|
|
9041
|
+
this.refreshState(false, true);
|
|
9042
|
+
this.resetChange.emit({
|
|
9043
|
+
document: snapshot,
|
|
9044
|
+
localData: this.getLocalDataValue(),
|
|
9045
|
+
});
|
|
8880
9046
|
}
|
|
8881
9047
|
applyChanges() {
|
|
8882
9048
|
const payload = this.createApplyPayload();
|
|
@@ -8886,7 +9052,8 @@ class PraxisChartConfigEditor {
|
|
|
8886
9052
|
saveChanges() {
|
|
8887
9053
|
const payload = this.createSavePayload();
|
|
8888
9054
|
this.initialDocument.set(structuredClone(payload.document));
|
|
8889
|
-
this.
|
|
9055
|
+
this.initialLocalData.set(this.currentLocalData() ? structuredClone(this.currentLocalData()) : undefined);
|
|
9056
|
+
this.refreshState(false, false);
|
|
8890
9057
|
this.save.emit(payload);
|
|
8891
9058
|
return payload;
|
|
8892
9059
|
}
|
|
@@ -9648,12 +9815,240 @@ class PraxisChartConfigEditor {
|
|
|
9648
9815
|
issueTrackBy(_, issue) {
|
|
9649
9816
|
return `${issue.code}:${issue.field}`;
|
|
9650
9817
|
}
|
|
9651
|
-
t(key, fallback) {
|
|
9652
|
-
return this.i18n.t(key,
|
|
9818
|
+
t(key, fallback, params) {
|
|
9819
|
+
return this.i18n.t(key, params, fallback, PRAXIS_CHARTS_I18N_NAMESPACE);
|
|
9653
9820
|
}
|
|
9654
9821
|
doc() {
|
|
9655
9822
|
return this.currentDocument();
|
|
9656
9823
|
}
|
|
9824
|
+
localDataRows() {
|
|
9825
|
+
return this.currentLocalData() ?? [];
|
|
9826
|
+
}
|
|
9827
|
+
localDataDimensionFields() {
|
|
9828
|
+
const fields = (this.dimensions() ?? [])
|
|
9829
|
+
.map((dimension) => String(dimension.field || '').trim())
|
|
9830
|
+
.filter(Boolean);
|
|
9831
|
+
return fields.length ? Array.from(new Set(fields)) : ['category'];
|
|
9832
|
+
}
|
|
9833
|
+
localDataMetricFields() {
|
|
9834
|
+
const fields = (this.metrics() ?? [])
|
|
9835
|
+
.map((metric) => String(metric.field || '').trim())
|
|
9836
|
+
.filter(Boolean);
|
|
9837
|
+
return fields.length ? Array.from(new Set(fields)) : ['value'];
|
|
9838
|
+
}
|
|
9839
|
+
localDataCellStringValue(rowIndex, field) {
|
|
9840
|
+
const value = this.localDataRows()[rowIndex]?.[field];
|
|
9841
|
+
return value === undefined || value === null ? '' : String(value);
|
|
9842
|
+
}
|
|
9843
|
+
addLocalDataRow() {
|
|
9844
|
+
if (this.isReadonly()) {
|
|
9845
|
+
return;
|
|
9846
|
+
}
|
|
9847
|
+
const currentRows = this.localDataRows().map((row) => structuredClone(row));
|
|
9848
|
+
const nextIndex = currentRows.length + 1;
|
|
9849
|
+
const newRow = {};
|
|
9850
|
+
for (const dimField of this.localDataDimensionFields()) {
|
|
9851
|
+
newRow[dimField] = `Item ${nextIndex}`;
|
|
9852
|
+
}
|
|
9853
|
+
const document = this.normalizedDocument();
|
|
9854
|
+
const scale = document.gauge?.scale;
|
|
9855
|
+
const defaultMetricValue = document.kind === 'gauge'
|
|
9856
|
+
&& scale
|
|
9857
|
+
&& Number.isFinite(scale.min)
|
|
9858
|
+
&& Number.isFinite(scale.max)
|
|
9859
|
+
&& scale.max > scale.min
|
|
9860
|
+
? Number((scale.min + (scale.max - scale.min) * 0.5).toFixed(4))
|
|
9861
|
+
: 0;
|
|
9862
|
+
for (const metricField of this.localDataMetricFields()) {
|
|
9863
|
+
newRow[metricField] = defaultMetricValue;
|
|
9864
|
+
}
|
|
9865
|
+
this.setLocalDataRows([...currentRows, newRow]);
|
|
9866
|
+
}
|
|
9867
|
+
removeLocalDataRow(index) {
|
|
9868
|
+
if (this.isReadonly()) {
|
|
9869
|
+
return;
|
|
9870
|
+
}
|
|
9871
|
+
const currentRows = this.localDataRows();
|
|
9872
|
+
if (index < 0 || index >= currentRows.length) {
|
|
9873
|
+
return;
|
|
9874
|
+
}
|
|
9875
|
+
const nextRows = currentRows
|
|
9876
|
+
.filter((_, rowIndex) => rowIndex !== index)
|
|
9877
|
+
.map((row) => structuredClone(row));
|
|
9878
|
+
this.setLocalDataRows(nextRows);
|
|
9879
|
+
}
|
|
9880
|
+
setLocalDataCellValue(rowIndex, field, rawValue, role) {
|
|
9881
|
+
if (this.isReadonly()) {
|
|
9882
|
+
return;
|
|
9883
|
+
}
|
|
9884
|
+
const currentRows = this.localDataRows().map((row) => structuredClone(row));
|
|
9885
|
+
if (rowIndex < 0 || rowIndex >= currentRows.length) {
|
|
9886
|
+
return;
|
|
9887
|
+
}
|
|
9888
|
+
const targetRow = { ...(currentRows[rowIndex] ?? {}) };
|
|
9889
|
+
if (role === 'dimension') {
|
|
9890
|
+
targetRow[field] = String(rawValue ?? '');
|
|
9891
|
+
}
|
|
9892
|
+
else if (typeof rawValue === 'number') {
|
|
9893
|
+
targetRow[field] = rawValue;
|
|
9894
|
+
}
|
|
9895
|
+
else {
|
|
9896
|
+
const trimmed = String(rawValue ?? '').trim();
|
|
9897
|
+
if (!trimmed) {
|
|
9898
|
+
targetRow[field] = '';
|
|
9899
|
+
}
|
|
9900
|
+
else {
|
|
9901
|
+
const parsed = Number(trimmed);
|
|
9902
|
+
targetRow[field] = Number.isFinite(parsed) ? parsed : rawValue;
|
|
9903
|
+
}
|
|
9904
|
+
}
|
|
9905
|
+
currentRows[rowIndex] = targetRow;
|
|
9906
|
+
this.setLocalDataRows(currentRows);
|
|
9907
|
+
}
|
|
9908
|
+
canConsolidateGaugeLocalData() {
|
|
9909
|
+
return this.doc().source.kind === 'derived'
|
|
9910
|
+
&& this.doc().kind === 'gauge'
|
|
9911
|
+
&& this.localDataRows().length > 1;
|
|
9912
|
+
}
|
|
9913
|
+
consolidateGaugeLocalData() {
|
|
9914
|
+
if (this.isReadonly() || !this.canConsolidateGaugeLocalData()) {
|
|
9915
|
+
return;
|
|
9916
|
+
}
|
|
9917
|
+
const rows = this.localDataRows();
|
|
9918
|
+
const dimField = this.localDataDimensionFields()[0] || 'category';
|
|
9919
|
+
const primaryMetric = this.metrics()[0];
|
|
9920
|
+
const metricField = String(primaryMetric?.field || '').trim() || this.localDataMetricFields()[0] || 'value';
|
|
9921
|
+
const aggregation = primaryMetric?.aggregation || 'avg';
|
|
9922
|
+
const numericValues = rows
|
|
9923
|
+
.map((row) => {
|
|
9924
|
+
const raw = row[metricField];
|
|
9925
|
+
return typeof raw === 'number'
|
|
9926
|
+
? raw
|
|
9927
|
+
: typeof raw === 'string' && raw.trim() !== ''
|
|
9928
|
+
? Number(raw)
|
|
9929
|
+
: NaN;
|
|
9930
|
+
})
|
|
9931
|
+
.filter((value) => Number.isFinite(value));
|
|
9932
|
+
let consolidatedValue = 0;
|
|
9933
|
+
const avgValue = numericValues.length
|
|
9934
|
+
? Number((numericValues.reduce((sum, value) => sum + value, 0) / numericValues.length).toFixed(4))
|
|
9935
|
+
: 0;
|
|
9936
|
+
switch (aggregation) {
|
|
9937
|
+
case 'count':
|
|
9938
|
+
consolidatedValue = rows.length;
|
|
9939
|
+
break;
|
|
9940
|
+
case 'distinct-count':
|
|
9941
|
+
consolidatedValue = new Set(rows.map((row) => row[metricField])).size;
|
|
9942
|
+
break;
|
|
9943
|
+
case 'sum':
|
|
9944
|
+
consolidatedValue = Number(numericValues.reduce((sum, value) => sum + value, 0).toFixed(4));
|
|
9945
|
+
break;
|
|
9946
|
+
case 'min':
|
|
9947
|
+
consolidatedValue = numericValues.length ? Math.min(...numericValues) : 0;
|
|
9948
|
+
break;
|
|
9949
|
+
case 'max':
|
|
9950
|
+
consolidatedValue = numericValues.length ? Math.max(...numericValues) : 0;
|
|
9951
|
+
break;
|
|
9952
|
+
case 'avg':
|
|
9953
|
+
default:
|
|
9954
|
+
consolidatedValue = avgValue;
|
|
9955
|
+
break;
|
|
9956
|
+
}
|
|
9957
|
+
const consolidatedRow = {
|
|
9958
|
+
...(rows[0] ? structuredClone(rows[0]) : {}),
|
|
9959
|
+
[dimField]: String(rows[0]?.[dimField] ?? 'Total'),
|
|
9960
|
+
[metricField]: consolidatedValue,
|
|
9961
|
+
};
|
|
9962
|
+
this.setLocalDataRows([consolidatedRow]);
|
|
9963
|
+
}
|
|
9964
|
+
setLocalDataRows(rows) {
|
|
9965
|
+
this.currentLocalData.set(rows.map((row) => structuredClone(row)));
|
|
9966
|
+
this.refreshState(true, true);
|
|
9967
|
+
}
|
|
9968
|
+
validateLocalData() {
|
|
9969
|
+
const document = this.normalizedDocument();
|
|
9970
|
+
if (document.source.kind !== 'derived') {
|
|
9971
|
+
return [];
|
|
9972
|
+
}
|
|
9973
|
+
const rawLocalData = this.currentLocalData();
|
|
9974
|
+
const rows = rawLocalData ?? [];
|
|
9975
|
+
const issues = [];
|
|
9976
|
+
if (!rows.length) {
|
|
9977
|
+
issues.push({
|
|
9978
|
+
severity: rawLocalData !== undefined ? 'error' : 'warning',
|
|
9979
|
+
code: 'local-data-empty',
|
|
9980
|
+
field: 'inputs.data',
|
|
9981
|
+
message: this.t('praxis.charts.editor.localData.validation.empty', 'Local dataset is empty. Add at least one record for derived runtime rendering; the preview is currently using a synthetic sample.'),
|
|
9982
|
+
});
|
|
9983
|
+
return issues;
|
|
9984
|
+
}
|
|
9985
|
+
if (document.kind === 'gauge') {
|
|
9986
|
+
if (rows.length !== 1) {
|
|
9987
|
+
issues.push({
|
|
9988
|
+
severity: 'error',
|
|
9989
|
+
code: 'gauge-bucket-cardinality',
|
|
9990
|
+
field: 'inputs.data',
|
|
9991
|
+
message: this.t('praxis.charts.editor.localData.validation.gaugeCardinality', 'Gauge charts require exactly 1 consolidated record in local data ({{ count }} found). Consolidate or remove extra records before saving.', { count: rows.length }),
|
|
9992
|
+
});
|
|
9993
|
+
}
|
|
9994
|
+
const metricField = String(document.metrics?.[0]?.field || '').trim();
|
|
9995
|
+
if (metricField) {
|
|
9996
|
+
const rawValue = rows[0]?.[metricField];
|
|
9997
|
+
const numericValue = typeof rawValue === 'number'
|
|
9998
|
+
? rawValue
|
|
9999
|
+
: typeof rawValue === 'string' && rawValue.trim() !== ''
|
|
10000
|
+
? Number(rawValue)
|
|
10001
|
+
: NaN;
|
|
10002
|
+
if (!Number.isFinite(numericValue)) {
|
|
10003
|
+
issues.push({
|
|
10004
|
+
severity: 'error',
|
|
10005
|
+
code: 'gauge-value-invalid',
|
|
10006
|
+
field: `inputs.data[0].${metricField}`,
|
|
10007
|
+
message: this.t('praxis.charts.editor.localData.validation.gaugeMetricInvalid', 'Gauge metric "{{ field }}" must have a finite numeric value in the local dataset.', { field: metricField }),
|
|
10008
|
+
});
|
|
10009
|
+
}
|
|
10010
|
+
else if (document.gauge?.scale) {
|
|
10011
|
+
const { min, max } = document.gauge.scale;
|
|
10012
|
+
if (Number.isFinite(min)
|
|
10013
|
+
&& Number.isFinite(max)
|
|
10014
|
+
&& max > min
|
|
10015
|
+
&& (numericValue < min || numericValue > max)) {
|
|
10016
|
+
issues.push({
|
|
10017
|
+
severity: 'error',
|
|
10018
|
+
code: 'gauge-value-out-of-range',
|
|
10019
|
+
field: `inputs.data[0].${metricField}`,
|
|
10020
|
+
message: this.t('praxis.charts.editor.localData.validation.gaugeOutOfRange', 'Gauge metric "{{ field }}" value ({{ value }}) is outside the configured scale ({{ min }} to {{ max }}).', { field: metricField, value: numericValue, min, max }),
|
|
10021
|
+
});
|
|
10022
|
+
}
|
|
10023
|
+
}
|
|
10024
|
+
}
|
|
10025
|
+
return issues;
|
|
10026
|
+
}
|
|
10027
|
+
for (const metric of document.metrics ?? []) {
|
|
10028
|
+
const metricField = String(metric.field || '').trim();
|
|
10029
|
+
if (!metricField) {
|
|
10030
|
+
continue;
|
|
10031
|
+
}
|
|
10032
|
+
const invalidIndex = rows.findIndex((row) => {
|
|
10033
|
+
const rawValue = row[metricField];
|
|
10034
|
+
const numericValue = typeof rawValue === 'number'
|
|
10035
|
+
? rawValue
|
|
10036
|
+
: typeof rawValue === 'string' && rawValue.trim() !== ''
|
|
10037
|
+
? Number(rawValue)
|
|
10038
|
+
: NaN;
|
|
10039
|
+
return !Number.isFinite(numericValue);
|
|
10040
|
+
});
|
|
10041
|
+
if (invalidIndex >= 0) {
|
|
10042
|
+
issues.push({
|
|
10043
|
+
severity: 'error',
|
|
10044
|
+
code: 'local-data-metric-invalid',
|
|
10045
|
+
field: `inputs.data[${invalidIndex}].${metricField}`,
|
|
10046
|
+
message: this.t('praxis.charts.editor.localData.validation.metricInvalid', 'Metric "{{ field }}" in record #{{ index }} must be a finite number.', { field: metricField, index: invalidIndex + 1 }),
|
|
10047
|
+
});
|
|
10048
|
+
}
|
|
10049
|
+
}
|
|
10050
|
+
return issues;
|
|
10051
|
+
}
|
|
9657
10052
|
patchStateDescriptor(stateKey, updater) {
|
|
9658
10053
|
this.patchDocument((document) => ({
|
|
9659
10054
|
...document,
|
|
@@ -9845,16 +10240,59 @@ class PraxisChartConfigEditor {
|
|
|
9845
10240
|
normalizeResourcePath(value) {
|
|
9846
10241
|
return String(value || '').trim().replace(/^\/+|\/+$/g, '');
|
|
9847
10242
|
}
|
|
9848
|
-
|
|
9849
|
-
|
|
9850
|
-
|
|
9851
|
-
|
|
9852
|
-
|
|
9853
|
-
|
|
9854
|
-
|
|
10243
|
+
groupCatalogResolutionDiagnostics(diagnostics) {
|
|
10244
|
+
if (!diagnostics.length) {
|
|
10245
|
+
return [];
|
|
10246
|
+
}
|
|
10247
|
+
const groups = new Map();
|
|
10248
|
+
for (const diagnostic of diagnostics) {
|
|
10249
|
+
const code = String(diagnostic.code || 'chart-resource-context-partial').trim() || 'chart-resource-context-partial';
|
|
10250
|
+
const severity = diagnostic.severity === 'error' ? 'error' : 'warning';
|
|
10251
|
+
const path = String(diagnostic.path || '').trim();
|
|
10252
|
+
const existing = groups.get(code);
|
|
10253
|
+
if (!existing) {
|
|
10254
|
+
const paths = new Set();
|
|
10255
|
+
if (path) {
|
|
10256
|
+
paths.add(path);
|
|
10257
|
+
}
|
|
10258
|
+
groups.set(code, { code, severity, count: 1, paths });
|
|
10259
|
+
}
|
|
10260
|
+
else {
|
|
10261
|
+
existing.count += 1;
|
|
10262
|
+
if (severity === 'error') {
|
|
10263
|
+
existing.severity = 'error';
|
|
10264
|
+
}
|
|
10265
|
+
if (path) {
|
|
10266
|
+
existing.paths.add(path);
|
|
10267
|
+
}
|
|
10268
|
+
}
|
|
10269
|
+
}
|
|
10270
|
+
return Array.from(groups.values()).map((entry) => {
|
|
10271
|
+
const paths = Array.from(entry.paths);
|
|
10272
|
+
const baseMessage = this.catalogDiagnosticMessage(entry.code);
|
|
10273
|
+
const countSuffix = entry.count > 1
|
|
10274
|
+
? ` (${this.t('praxis.charts.widget.contextDiagnostics.occurrences', '{{ count }} occurrences', { count: entry.count })})`
|
|
10275
|
+
: '';
|
|
10276
|
+
return {
|
|
10277
|
+
severity: entry.severity,
|
|
10278
|
+
code: entry.code,
|
|
10279
|
+
field: paths.length ? paths.join(', ') : 'catalog',
|
|
10280
|
+
message: `${baseMessage}${countSuffix}`,
|
|
10281
|
+
};
|
|
10282
|
+
});
|
|
9855
10283
|
}
|
|
9856
10284
|
catalogDiagnosticMessage(code) {
|
|
9857
10285
|
switch (code) {
|
|
10286
|
+
case 'chart-resource-catalog-denied':
|
|
10287
|
+
return this.t('praxis.charts.widget.contextDiagnostics.catalogDenied', 'Access to the governed resource catalog was denied.');
|
|
10288
|
+
case 'chart-resource-catalog-unavailable':
|
|
10289
|
+
return this.t('praxis.charts.widget.contextDiagnostics.catalogUnavailable', 'The governed resource catalog is temporarily unavailable.');
|
|
10290
|
+
case 'chart-resource-catalog-empty':
|
|
10291
|
+
return this.t('praxis.charts.widget.contextDiagnostics.catalogEmpty', 'No authorable stats resources were published by the governed catalog.');
|
|
10292
|
+
case 'chart-resource-catalog-path-invalid':
|
|
10293
|
+
return this.t('praxis.charts.widget.contextDiagnostics.catalogPathInvalid', 'The resource catalog published an unsafe or non-relative operational path.');
|
|
10294
|
+
case 'chart-resource-capabilities-ambiguous':
|
|
10295
|
+
return this.t('praxis.charts.editor.catalog.capabilitiesAmbiguous', 'Multiple capability endpoints were published for the selected resource.');
|
|
9858
10296
|
case 'chart-resource-capabilities-denied':
|
|
9859
10297
|
return this.t('praxis.charts.editor.catalog.capabilitiesDenied', 'Access to the selected resource capabilities was denied.');
|
|
9860
10298
|
case 'chart-resource-capabilities-unavailable':
|
|
@@ -9865,6 +10303,12 @@ class PraxisChartConfigEditor {
|
|
|
9865
10303
|
return this.t('praxis.charts.editor.catalog.fieldCatalogMissing', 'The selected resource does not publish governed fields for chart authoring.');
|
|
9866
10304
|
case 'chart-resource-stats-operations-empty':
|
|
9867
10305
|
return this.t('praxis.charts.editor.catalog.operationMissing', 'Selected resource does not publish authorable stats operations.');
|
|
10306
|
+
case 'chart-resource-capabilities-identity-mismatch':
|
|
10307
|
+
return this.t('praxis.charts.widget.contextDiagnostics.identityMismatch', 'The selected resource identity does not match its governed capability snapshot.');
|
|
10308
|
+
case 'chart-resource-capabilities-path-invalid':
|
|
10309
|
+
return this.t('praxis.charts.widget.contextDiagnostics.capabilitiesPathInvalid', 'The selected capability snapshot did not publish a safe governed resource path.');
|
|
10310
|
+
case 'chart-resource-capabilities-path-mismatch':
|
|
10311
|
+
return this.t('praxis.charts.widget.contextDiagnostics.capabilitiesPathMismatch', 'The selected capability snapshot path does not match the operational path published by the catalog.');
|
|
9868
10312
|
default:
|
|
9869
10313
|
return this.t('praxis.charts.editor.catalog.contextUnavailable', 'Governed chart authoring context could not be resolved.');
|
|
9870
10314
|
}
|
|
@@ -9951,6 +10395,7 @@ class PraxisChartConfigEditor {
|
|
|
9951
10395
|
createApplyPayload() {
|
|
9952
10396
|
return {
|
|
9953
10397
|
document: this.getSettingsValue(),
|
|
10398
|
+
localData: this.getLocalDataValue(),
|
|
9954
10399
|
issues: [...this.issues()],
|
|
9955
10400
|
dirty: this.isDirty$.getValue(),
|
|
9956
10401
|
};
|
|
@@ -9958,6 +10403,7 @@ class PraxisChartConfigEditor {
|
|
|
9958
10403
|
createSavePayload() {
|
|
9959
10404
|
return {
|
|
9960
10405
|
document: this.getSettingsValue(),
|
|
10406
|
+
localData: this.getLocalDataValue(),
|
|
9961
10407
|
issues: [...this.issues()],
|
|
9962
10408
|
dirty: this.isDirty$.getValue(),
|
|
9963
10409
|
};
|
|
@@ -9987,13 +10433,16 @@ class PraxisChartConfigEditor {
|
|
|
9987
10433
|
colors.push(value.slice(start));
|
|
9988
10434
|
return colors;
|
|
9989
10435
|
}
|
|
9990
|
-
refreshState(markDirty) {
|
|
10436
|
+
refreshState(markDirty, emitLocalData = false) {
|
|
9991
10437
|
this.isDirty$.next(markDirty);
|
|
9992
10438
|
this.isValid$.next(this.validation().valid);
|
|
9993
10439
|
this.documentChange.emit(structuredClone(this.normalizedDocument()));
|
|
10440
|
+
if (emitLocalData) {
|
|
10441
|
+
this.localDataChange.emit(this.getLocalDataValue());
|
|
10442
|
+
}
|
|
9994
10443
|
}
|
|
9995
10444
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartConfigEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
9996
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisChartConfigEditor, isStandalone: true, selector: "praxis-chart-config-editor", inputs: { documentInput: { classPropertyName: "documentInput", publicName: "document", isSignal: true, isRequired: false, transformFunction: null }, modeInput: { classPropertyName: "modeInput", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, readonlyInput: { classPropertyName: "readonlyInput", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, availableResourcesInput: { classPropertyName: "availableResourcesInput", publicName: "availableResources", isSignal: true, isRequired: false, transformFunction: null }, availableFieldsInput: { classPropertyName: "availableFieldsInput", publicName: "availableFields", isSignal: true, isRequired: false, transformFunction: null }, availableTargetsInput: { classPropertyName: "availableTargetsInput", publicName: "availableTargets", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { apply: "apply", save: "save", resetChange: "resetChange", documentChange: "documentChange" }, providers: [providePraxisChartsI18n()], ngImport: i0, template: "<div class=\"editor-shell\">\n <div class=\"editor-nav\">\n @for (section of sections; track section.id) {\n <button\n mat-stroked-button\n type=\"button\"\n [attr.data-testid]=\"'chart-editor-section-' + section.id\"\n [class.active]=\"activeSection() === section.id\"\n (click)=\"setSection(section.id)\"\n >\n {{ t(section.labelKey, section.fallback) }}\n </button>\n }\n </div>\n\n <div class=\"editor-layout\">\n <div class=\"editor-form\">\n <mat-card class=\"editor-card\">\n <mat-card-content>\n @switch (activeSection()) {\n @case ('general') {\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.chartId', 'Chart ID') }}</mat-label>\n <input matInput [ngModel]=\"doc().chartId || ''\" (ngModelChange)=\"setChartId($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.kind', 'Kind') }}</mat-label>\n <mat-select [ngModel]=\"doc().kind\" (ngModelChange)=\"setKind($event)\" [disabled]=\"isReadonly()\">\n @for (kind of chartKinds; track kind) {\n <mat-option [value]=\"kind\">\n {{ t('praxis.charts.editor.kind.' + kind, kind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.title', 'Title') }}</mat-label>\n <input matInput data-testid=\"chart-editor-title-input\" [ngModel]=\"titleValue()\" (ngModelChange)=\"setTitle($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.subtitle', 'Subtitle') }}</mat-label>\n <input matInput [ngModel]=\"subtitleValue()\" (ngModelChange)=\"setSubtitle($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.sizingMode', 'Sizing') }}</mat-label>\n <mat-select [ngModel]=\"sizingModeValue()\" (ngModelChange)=\"setSizingMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of sizingModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.sizing.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>{{ t('praxis.charts.editor.hint.sizingMode', 'Use fill-container only when the host widget provides a defined body height.') }}</mat-hint>\n </mat-form-field>\n\n @if (sizingModeValue() === 'fixed') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.height', 'Height') }}</mat-label>\n <input matInput [ngModel]=\"heightValue()\" (ngModelChange)=\"setSizingHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.height', 'Numbers are saved as pixels; CSS lengths such as 20rem are also accepted.') }}</mat-hint>\n </mat-form-field>\n }\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.minHeight', 'Minimum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMinHeightValue()\" (ngModelChange)=\"setSizingMinHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.minHeight', 'Set a readable minimum for compact dashboard widgets.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.maxHeight', 'Maximum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMaxHeightValue()\" (ngModelChange)=\"setSizingMaxHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.maxHeight', 'Leave empty unless the chart must stop growing inside a flexible layout.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.aspectRatio', 'Aspect ratio') }}</mat-label>\n <input matInput [ngModel]=\"sizingAspectRatioValue()\" (ngModelChange)=\"setSizingAspectRatio($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.aspectRatio', 'Optional. Use values such as 1.777 or 16 / 9.') }}</mat-hint>\n </mat-form-field>\n </div>\n }\n\n @case ('data') {\n <div class=\"editor-stack\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.sourceKind', 'Source') }}</mat-label>\n <mat-select data-testid=\"chart-editor-source-kind\" [ngModel]=\"doc().source.kind\" (ngModelChange)=\"setSourceKind($event)\" [disabled]=\"isReadonly()\">\n @for (sourceKind of sourceKinds; track sourceKind) {\n <mat-option [value]=\"sourceKind\">\n {{ t('praxis.charts.editor.sourceKind.' + (sourceKind === 'praxis.stats' ? 'praxisStats' : 'derived'), sourceKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (doc().source.kind === 'praxis.stats') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.resource', 'Resource') }}</mat-label>\n <mat-select data-testid=\"chart-editor-resource\" [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly() || resourceCatalogUnavailable()\">\n @for (resource of resourceOptions(); track resource.id) {\n <mat-option [value]=\"resource.path\">{{ resource.label }}</mat-option>\n }\n </mat-select>\n @if (resourceCatalogUnavailable()) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.resourceMissing', 'Resource catalog is required for governed praxis.stats authoring.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.operation', 'Operation') }}</mat-label>\n <mat-select\n data-testid=\"chart-editor-operation\"\n [ngModel]=\"doc().source.operation || 'group-by'\"\n (ngModelChange)=\"setOperation($event)\"\n [disabled]=\"isReadonly() || operationCatalogUnavailable()\"\n >\n @for (operation of operationOptions(); track operation) {\n <mat-option [value]=\"operation\">\n {{ t('praxis.charts.editor.operation.' + (operation === 'group-by' ? 'groupBy' : operation), operation) }}\n </mat-option>\n }\n </mat-select>\n @if (operationCatalogUnavailable()) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.operationMissing', 'Selected resource does not publish authorable stats operations.') }}</mat-hint>\n }\n </mat-form-field>\n }\n </div>\n\n @if (showTimeseriesControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.timeseriesTitle', 'Timeseries options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.granularity', 'Granularity') }}</mat-label>\n <mat-select [ngModel]=\"granularityValue()\" (ngModelChange)=\"setGranularity($event)\" [disabled]=\"isReadonly()\">\n @for (granularity of timeGranularities; track granularity) {\n <mat-option [value]=\"granularity\">\n {{ t('praxis.charts.editor.granularity.' + granularity, granularity) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-slide-toggle\n [ngModel]=\"fillGapsValue()\"\n (ngModelChange)=\"setFillGaps($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.fillGaps', 'Fill missing intervals') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showDistributionControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.distributionTitle', 'Distribution options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.distributionMode', 'Distribution mode') }}</mat-label>\n <mat-select data-testid=\"chart-editor-distribution-mode\" [ngModel]=\"distributionModeValue()\" (ngModelChange)=\"setDistributionMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of distributionModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.distributionMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (distributionModeValue() === 'histogram') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketSize', 'Bucket size') }}</mat-label>\n <input matInput data-testid=\"chart-editor-bucket-size\" [ngModel]=\"bucketSizeValue()\" (ngModelChange)=\"setBucketSize($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketCount', 'Bucket count') }}</mat-label>\n <input matInput data-testid=\"chart-editor-bucket-count\" [ngModel]=\"bucketCountValue()\" (ngModelChange)=\"setBucketCount($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n }\n </mat-card-content>\n </mat-card>\n }\n\n @if (showComparisonControls()) {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-comparison-options\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comparisonTitle', 'Comparison period') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonPeriodField', 'Period field') }}</mat-label>\n @if (fieldOptions('time').length) {\n <mat-select\n [ngModel]=\"comparisonPeriodFieldValue()\"\n (ngModelChange)=\"setComparisonPeriodField($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (field of fieldOptions('time'); track field.field) {\n <mat-option [value]=\"field.field\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"comparisonPeriodFieldValue()\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.comparisonPeriodFieldsMissing', 'No governed time-series field is available for comparison periods.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonTimezone', 'Timezone') }}</mat-label>\n <input\n matInput\n [ngModel]=\"comparisonPeriodTimezoneValue()\"\n (ngModelChange)=\"setComparisonPeriodTimezone($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonPreset', 'Period preset') }}</mat-label>\n <mat-select\n [ngModel]=\"comparisonPeriodPresetValue()\"\n (ngModelChange)=\"setComparisonPeriodPreset($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (preset of comparisonPeriodPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.comparisonPreset.' + preset, preset) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonMode', 'Comparison mode') }}</mat-label>\n <mat-select\n [ngModel]=\"comparisonPeriodModeValue()\"\n (ngModelChange)=\"setComparisonPeriodMode($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (mode of comparisonPeriodModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.comparisonMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n }\n </div>\n }\n\n @case ('motion') {\n <div class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"normalizedDocument().motion?.enabled !== false\"\n (ngModelChange)=\"setMotionEnabled($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.motionEnabled', 'Enable animations') }}\n </mat-slide-toggle>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.motionPreset', 'Motion preset') }}</mat-label>\n <mat-select\n [ngModel]=\"normalizedDocument().motion?.preset || 'standard'\"\n (ngModelChange)=\"setMotionPreset($event)\"\n [disabled]=\"isReadonly() || normalizedDocument().motion?.enabled === false\"\n >\n @for (preset of motionPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.motionPreset.' + preset, preset) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n\n @case ('appearance') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.featuresTitle', 'Display features') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('legend')\"\n (ngModelChange)=\"setFeatureEnabled('legend', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.legendEnabled', 'Show legend') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('labels')\"\n (ngModelChange)=\"setFeatureEnabled('labels', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.labelsEnabled', 'Show labels') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('tooltip')\"\n (ngModelChange)=\"setFeatureEnabled('tooltip', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.tooltipEnabled', 'Show tooltip') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.paletteTitle', 'Palette') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.themeVariant', 'Theme variant') }}</mat-label>\n <mat-select [ngModel]=\"themeVariantValue()\" (ngModelChange)=\"setThemeVariant($event)\" [disabled]=\"isReadonly()\">\n <mat-option [value]=\"''\">\n {{ t('praxis.charts.editor.themeVariant.none', 'No variant') }}\n </mat-option>\n @for (variant of themeVariants; track variant) {\n <mat-option [value]=\"variant\">\n {{ t('praxis.charts.editor.themeVariant.' + variant, variant) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteMode', 'Palette mode') }}</mat-label>\n <mat-select [ngModel]=\"paletteModeValue()\" (ngModelChange)=\"setPaletteMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of paletteModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.paletteMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (paletteModeValue() === 'token') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteToken', 'Palette token') }}</mat-label>\n <mat-select [ngModel]=\"paletteTokenValue()\" (ngModelChange)=\"setPaletteToken($event)\" [disabled]=\"isReadonly()\">\n @for (token of paletteTokens; track token) {\n <mat-option [value]=\"token\">\n {{ t('praxis.charts.editor.paletteToken.' + token, token) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.palette', 'Palette colors') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"paletteValue()\"\n (ngModelChange)=\"setPalette($event)\"\n [disabled]=\"isReadonly()\"\n ></textarea>\n </mat-form-field>\n }\n\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.paletteHint', 'Use a registered token or comma-separated colors to persist theme.palette in the canonical contract.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.surfaceTitle', 'Surface') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.surfaceMode', 'Surface mode') }}</mat-label>\n <mat-select [ngModel]=\"surfaceModeValue()\" (ngModelChange)=\"setSurfaceMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of surfaceModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.surface.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.surfaceHint', 'Use embedded for dashboard widgets and contained only when the chart must own its visual surface.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.statesTitle', 'State messages') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyTitle', 'Empty title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('empty')\" (ngModelChange)=\"setStateTitle('empty', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyDescription', 'Empty description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('empty')\" (ngModelChange)=\"setStateDescription('empty', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingTitle', 'Loading title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('loading')\" (ngModelChange)=\"setStateTitle('loading', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingDescription', 'Loading description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('loading')\" (ngModelChange)=\"setStateDescription('loading', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorTitle', 'Error title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('error')\" (ngModelChange)=\"setStateTitle('error', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorDescription', 'Error description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('error')\" (ngModelChange)=\"setStateDescription('error', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('analytics') {\n <div class=\"editor-stack\">\n @if (showGaugePanel()) {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-gauge-scale\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.gaugeTitle', 'Gauge scale') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.gaugeHint', 'Gauge charts render exactly one bucket and one metric. Remote stats use group-by with limit 1; values outside the scale fail instead of being clamped.') }}\n </p>\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.gaugeMin', 'Scale minimum') }}</mat-label>\n <input\n matInput\n type=\"number\"\n data-testid=\"chart-editor-gauge-min\"\n [ngModel]=\"gaugeMinValue()\"\n (ngModelChange)=\"setGaugeMin($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.gaugeMax', 'Scale maximum') }}</mat-label>\n <input\n matInput\n type=\"number\"\n data-testid=\"chart-editor-gauge-max\"\n [ngModel]=\"gaugeMaxValue()\"\n (ngModelChange)=\"setGaugeMax($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n </div>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showComboPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comboTitle', 'Combo guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.comboHint', 'Combo charts require at least two metrics and allow per-metric axis and series kind mapping.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showPieDonutPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.pieDonutTitle', 'Composition guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.pieDonutHint', 'Pie, donut, funnel, pyramid and treemap charts keep one metric and use the first dimension as the category segment.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showScatterPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.scatterTitle', 'Scatter guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.scatterHint', 'With one metric, scatter uses the first dimension as X and the metric as Y. With two metrics, the dimension identifies the grouped bucket and the metrics map to X and Y.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.dimensionsTitle', 'Dimensions') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (dimension of dimensions(); track $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-editor-dimension-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-field-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.dimension', 'Dimension') }}</mat-label>\n @if (fieldOptions('dimension').length) {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('dimension'); track field.field) {\n <mat-option [value]=\"field.field\">{{ fieldOptionLabel(field) }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.dimensionFieldsMissing', 'No governed dimensions are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-role-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.dimensionRole', 'Dimension role') }}</mat-label>\n <mat-select [ngModel]=\"dimension.role || 'category'\" (ngModelChange)=\"setDimensionRole($index, $event)\" [disabled]=\"isReadonly()\">\n @for (role of dimensionRoles; track role) {\n <mat-option [value]=\"role\">\n {{ t('praxis.charts.editor.dimensionRole.' + role, role) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" (click)=\"removeDimension($index)\" [disabled]=\"isReadonly() || dimensions().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeDimension', 'Remove dimension') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-dimension\" (click)=\"addDimension()\" [disabled]=\"isReadonly()\">\n {{ t('praxis.charts.editor.analytics.addDimension', 'Add dimension') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.metricsTitle', 'Metrics') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (metric of metrics(); track $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-editor-metric-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-field-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metric', 'Metric') }}</mat-label>\n @if (fieldOptions('metric').length) {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('metric'); track field.field) {\n <mat-option [value]=\"field.field\">{{ fieldOptionLabel(field) }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.metricFieldsMissing', 'No governed metrics are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-label-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricLabel', 'Metric label') }}</mat-label>\n <input matInput [ngModel]=\"metric.label || ''\" (ngModelChange)=\"setMetricLabel($index, $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-aggregation-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAggregation', 'Aggregation') }}</mat-label>\n <mat-select [ngModel]=\"metric.aggregation || 'sum'\" (ngModelChange)=\"setMetricAggregation($index, $event)\" [disabled]=\"isReadonly()\">\n @for (aggregation of metricAggregationOptions(metric.field); track aggregation) {\n <mat-option [value]=\"aggregation\">\n {{ t('praxis.charts.editor.metricAggregation.' + aggregation, aggregation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (showMetricAxisControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-axis-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAxis', 'Axis') }}</mat-label>\n <mat-select [ngModel]=\"metric.axis || 'primary'\" (ngModelChange)=\"setMetricAxis($index, $event)\" [disabled]=\"isReadonly()\">\n @for (axis of metricAxes; track axis) {\n <mat-option [value]=\"axis\">\n {{ t('praxis.charts.editor.metricAxis.' + axis, axis) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @if (showMetricSeriesKindControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-series-kind-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricSeriesKind', 'Series kind') }}</mat-label>\n <mat-select [ngModel]=\"metric.seriesKind || 'bar'\" (ngModelChange)=\"setMetricSeriesKind($index, $event)\" [disabled]=\"isReadonly()\">\n @for (seriesKind of metricSeriesKinds; track seriesKind) {\n <mat-option [value]=\"seriesKind\">\n {{ t('praxis.charts.editor.metricSeriesKind.' + seriesKind, seriesKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" [attr.data-testid]=\"'chart-editor-remove-metric-' + $index\" (click)=\"removeMetric($index)\" [disabled]=\"isReadonly() || metrics().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeMetric', 'Remove metric') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-metric\" (click)=\"addMetric()\" [disabled]=\"isReadonly() || !canAddMetric()\">\n {{ t('praxis.charts.editor.analytics.addMetric', 'Add metric') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('events') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-pointClick\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.pointClickTitle', 'Point click') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-action\" [ngModel]=\"eventAction('pointClick')\" (ngModelChange)=\"setEventAction('pointClick', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick') || targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')\">\n @for (target of targetOptions(eventAction('pointClick'), 'pointClick'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-pointClick-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('pointClick')\"\n (ngModelChange)=\"setEventMapping('pointClick', $event)\"\n [disabled]=\"isReadonly() || !eventAction('pointClick')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-selectionChange\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.selectionChangeTitle', 'Selection change') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-action\" [ngModel]=\"eventAction('selectionChange')\" (ngModelChange)=\"setEventAction('selectionChange', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange') || targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')\">\n @for (target of targetOptions(eventAction('selectionChange'), 'selectionChange'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-selectionChange-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('selectionChange')\"\n (ngModelChange)=\"setEventMapping('selectionChange', $event)\"\n [disabled]=\"isReadonly() || !eventAction('selectionChange')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-drillDown\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.drillDownTitle', 'Drill down') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-action\" [ngModel]=\"eventAction('drillDown')\" (ngModelChange)=\"setEventAction('drillDown', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown') || targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')\">\n @for (target of targetOptions(eventAction('drillDown'), 'drillDown'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-drillDown-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('drillDown')\"\n (ngModelChange)=\"setEventMapping('drillDown', $event)\"\n [disabled]=\"isReadonly() || !eventAction('drillDown')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-crossFilter\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.crossFilterTitle', 'Cross filter') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-action\" [ngModel]=\"eventAction('crossFilter')\" (ngModelChange)=\"setEventAction('crossFilter', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter') || targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')\">\n @for (target of targetOptions(eventAction('crossFilter'), 'crossFilter'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-crossFilter-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('crossFilter')\"\n (ngModelChange)=\"setEventMapping('crossFilter', $event)\"\n [disabled]=\"isReadonly() || !eventAction('crossFilter')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('preview') {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n }\n }\n </mat-card-content>\n </mat-card>\n </div>\n\n <div class=\"editor-side\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.issues.title', 'Validation issues') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (issues().length) {\n <ul class=\"editor-issues\">\n @for (issue of issues(); track issueTrackBy($index, issue)) {\n <li class=\"editor-issue\">\n <strong>{{ issue.field }}</strong>\n <span>{{ issue.message }}</span>\n </li>\n }\n </ul>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.issues.empty', 'No issues were identified.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.preview.title', 'Chart preview') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (preview(); as chartPreview) {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n <praxis-chart [config]=\"chartPreview.config\" [data]=\"chartPreview.data\"></praxis-chart>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.preview.invalid', 'Preview is unavailable while the contract has blocking errors.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n</div>\n", styles: [":host{display:block;min-width:0;container:praxis-chart-editor / inline-size;color:var(--md-sys-color-on-surface, #1a1b20)}.editor-shell{display:grid;gap:18px;min-width:0}.editor-nav{display:flex;gap:8px;flex-wrap:wrap}.editor-nav button.active{background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 18%,transparent);color:var(--md-sys-color-primary, #1263b4)}.editor-layout{display:grid;gap:18px;grid-template-columns:minmax(0,1.35fr) minmax(320px,.9fr);align-items:start}.editor-form,.editor-side{display:grid;gap:16px;min-width:0}.editor-card{box-sizing:border-box;width:100%;min-width:0;border-radius:20px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#1263b408,#1263b400)}.editor-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-stack{display:grid;gap:14px}.editor-row-card{padding:14px;border-radius:16px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 54%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 92%,rgba(18,99,180,.04))}.editor-row-actions{display:flex;justify-content:flex-end}.editor-field{width:100%}.editor-issues{display:grid;gap:10px;margin:0;padding:0;list-style:none}.editor-issue{padding:12px 14px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-error, #b3261e) 8%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-error, #b3261e) 18%,transparent)}.editor-issue strong{display:block;margin-bottom:4px}.editor-caption{margin:0 0 12px;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.92rem}.editor-empty{padding:18px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-variant, #eceff4) 78%,transparent)}.editor-side praxis-chart{display:block;width:100%;min-width:0;max-width:100%}@container praxis-chart-editor (max-width: 840px){.editor-layout{grid-template-columns:minmax(0,1fr)}}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.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$3.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i3.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i3.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i3.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i3.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i7.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "resourceKey", "data", "chartDocument", "filterCriteria", "queryContext", "remoteDataResolver", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "pointAction", "selectionChange", "drillDown", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved", "chartDocumentPatchChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
10445
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisChartConfigEditor, isStandalone: true, selector: "praxis-chart-config-editor", inputs: { documentInput: { classPropertyName: "documentInput", publicName: "document", isSignal: true, isRequired: false, transformFunction: null }, localDataInput: { classPropertyName: "localDataInput", publicName: "localData", isSignal: true, isRequired: false, transformFunction: null }, modeInput: { classPropertyName: "modeInput", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, readonlyInput: { classPropertyName: "readonlyInput", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, availableResourcesInput: { classPropertyName: "availableResourcesInput", publicName: "availableResources", isSignal: true, isRequired: false, transformFunction: null }, availableFieldsInput: { classPropertyName: "availableFieldsInput", publicName: "availableFields", isSignal: true, isRequired: false, transformFunction: null }, availableTargetsInput: { classPropertyName: "availableTargetsInput", publicName: "availableTargets", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { apply: "apply", save: "save", resetChange: "resetChange", documentChange: "documentChange", localDataChange: "localDataChange" }, providers: [providePraxisChartsI18n()], ngImport: i0, template: "<div class=\"editor-shell\">\n <div class=\"editor-nav\">\n @for (section of sections; track section.id) {\n <button\n mat-stroked-button\n type=\"button\"\n [attr.data-testid]=\"'chart-editor-section-' + section.id\"\n [class.active]=\"activeSection() === section.id\"\n (click)=\"setSection(section.id)\"\n >\n {{ t(section.labelKey, section.fallback) }}\n </button>\n }\n </div>\n\n <div class=\"editor-layout\">\n <div class=\"editor-form\">\n <mat-card class=\"editor-card\">\n <mat-card-content>\n @switch (activeSection()) {\n @case ('general') {\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.chartId', 'Chart ID') }}</mat-label>\n <input matInput [ngModel]=\"doc().chartId || ''\" (ngModelChange)=\"setChartId($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.kind', 'Kind') }}</mat-label>\n <mat-select [ngModel]=\"doc().kind\" (ngModelChange)=\"setKind($event)\" [disabled]=\"isReadonly()\">\n @for (kind of chartKinds; track kind) {\n <mat-option [value]=\"kind\">\n {{ t('praxis.charts.editor.kind.' + kind, kind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.title', 'Title') }}</mat-label>\n <input matInput data-testid=\"chart-editor-title-input\" [ngModel]=\"titleValue()\" (ngModelChange)=\"setTitle($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.subtitle', 'Subtitle') }}</mat-label>\n <input matInput [ngModel]=\"subtitleValue()\" (ngModelChange)=\"setSubtitle($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.sizingMode', 'Sizing') }}</mat-label>\n <mat-select [ngModel]=\"sizingModeValue()\" (ngModelChange)=\"setSizingMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of sizingModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.sizing.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>{{ t('praxis.charts.editor.hint.sizingMode', 'Use fill-container only when the host widget provides a defined body height.') }}</mat-hint>\n </mat-form-field>\n\n @if (sizingModeValue() === 'fixed') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.height', 'Height') }}</mat-label>\n <input matInput [ngModel]=\"heightValue()\" (ngModelChange)=\"setSizingHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.height', 'Numbers are saved as pixels; CSS lengths such as 20rem are also accepted.') }}</mat-hint>\n </mat-form-field>\n }\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.minHeight', 'Minimum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMinHeightValue()\" (ngModelChange)=\"setSizingMinHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.minHeight', 'Set a readable minimum for compact dashboard widgets.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.maxHeight', 'Maximum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMaxHeightValue()\" (ngModelChange)=\"setSizingMaxHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.maxHeight', 'Leave empty unless the chart must stop growing inside a flexible layout.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.aspectRatio', 'Aspect ratio') }}</mat-label>\n <input matInput [ngModel]=\"sizingAspectRatioValue()\" (ngModelChange)=\"setSizingAspectRatio($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.aspectRatio', 'Optional. Use values such as 1.777 or 16 / 9.') }}</mat-hint>\n </mat-form-field>\n </div>\n }\n\n @case ('data') {\n <div class=\"editor-stack\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.sourceKind', 'Source') }}</mat-label>\n <mat-select data-testid=\"chart-editor-source-kind\" [ngModel]=\"doc().source.kind\" (ngModelChange)=\"setSourceKind($event)\" [disabled]=\"isReadonly()\">\n @for (sourceKind of sourceKinds; track sourceKind) {\n <mat-option [value]=\"sourceKind\">\n {{ t('praxis.charts.editor.sourceKind.' + (sourceKind === 'praxis.stats' ? 'praxisStats' : 'derived'), sourceKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (doc().source.kind === 'praxis.stats') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.resource', 'Resource') }}</mat-label>\n <mat-select data-testid=\"chart-editor-resource\" [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly() || resourceCatalogUnavailable()\">\n @for (resource of resourceOptions(); track resource.id) {\n <mat-option [value]=\"resource.path\">{{ resource.label }}</mat-option>\n }\n </mat-select>\n @if (resourceCatalogUnavailable()) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.resourceMissing', 'Resource catalog is required for governed praxis.stats authoring.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.operation', 'Operation') }}</mat-label>\n <mat-select\n data-testid=\"chart-editor-operation\"\n [ngModel]=\"doc().source.operation || 'group-by'\"\n (ngModelChange)=\"setOperation($event)\"\n [disabled]=\"isReadonly() || operationCatalogUnavailable()\"\n >\n @for (operation of operationOptions(); track operation) {\n <mat-option [value]=\"operation\">\n {{ t('praxis.charts.editor.operation.' + (operation === 'group-by' ? 'groupBy' : operation), operation) }}\n </mat-option>\n }\n </mat-select>\n @if (operationCatalogUnavailable()) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.operationMissing', 'Selected resource does not publish authorable stats operations.') }}</mat-hint>\n }\n </mat-form-field>\n }\n </div>\n\n @if (showTimeseriesControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.timeseriesTitle', 'Timeseries options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.granularity', 'Granularity') }}</mat-label>\n <mat-select [ngModel]=\"granularityValue()\" (ngModelChange)=\"setGranularity($event)\" [disabled]=\"isReadonly()\">\n @for (granularity of timeGranularities; track granularity) {\n <mat-option [value]=\"granularity\">\n {{ t('praxis.charts.editor.granularity.' + granularity, granularity) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-slide-toggle\n [ngModel]=\"fillGapsValue()\"\n (ngModelChange)=\"setFillGaps($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.fillGaps', 'Fill missing intervals') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showDistributionControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.distributionTitle', 'Distribution options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.distributionMode', 'Distribution mode') }}</mat-label>\n <mat-select data-testid=\"chart-editor-distribution-mode\" [ngModel]=\"distributionModeValue()\" (ngModelChange)=\"setDistributionMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of distributionModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.distributionMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (distributionModeValue() === 'histogram') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketSize', 'Bucket size') }}</mat-label>\n <input matInput data-testid=\"chart-editor-bucket-size\" [ngModel]=\"bucketSizeValue()\" (ngModelChange)=\"setBucketSize($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketCount', 'Bucket count') }}</mat-label>\n <input matInput data-testid=\"chart-editor-bucket-count\" [ngModel]=\"bucketCountValue()\" (ngModelChange)=\"setBucketCount($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n }\n </mat-card-content>\n </mat-card>\n }\n\n @if (showComparisonControls()) {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-comparison-options\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comparisonTitle', 'Comparison period') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonPeriodField', 'Period field') }}</mat-label>\n @if (fieldOptions('time').length) {\n <mat-select\n [ngModel]=\"comparisonPeriodFieldValue()\"\n (ngModelChange)=\"setComparisonPeriodField($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (field of fieldOptions('time'); track field.field) {\n <mat-option [value]=\"field.field\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"comparisonPeriodFieldValue()\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.comparisonPeriodFieldsMissing', 'No governed time-series field is available for comparison periods.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonTimezone', 'Timezone') }}</mat-label>\n <input\n matInput\n [ngModel]=\"comparisonPeriodTimezoneValue()\"\n (ngModelChange)=\"setComparisonPeriodTimezone($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonPreset', 'Period preset') }}</mat-label>\n <mat-select\n [ngModel]=\"comparisonPeriodPresetValue()\"\n (ngModelChange)=\"setComparisonPeriodPreset($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (preset of comparisonPeriodPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.comparisonPreset.' + preset, preset) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonMode', 'Comparison mode') }}</mat-label>\n <mat-select\n [ngModel]=\"comparisonPeriodModeValue()\"\n (ngModelChange)=\"setComparisonPeriodMode($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (mode of comparisonPeriodModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.comparisonMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n }\n\n @if (doc().source.kind === 'derived') {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-local-data\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.localData.title', 'Local dataset (inputs.data)') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.localData.caption', 'Edit the local records rendered when the data source is derived.') }}\n </p>\n\n @if (canConsolidateGaugeLocalData()) {\n <div class=\"editor-row-actions\">\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-local-data-consolidate-gauge\"\n (click)=\"consolidateGaugeLocalData()\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.localData.consolidateGauge', 'Consolidate to 1 bucket for gauge') }}\n </button>\n </div>\n }\n\n @if (localDataRows().length) {\n @for (row of localDataRows(); track $index; let rowIndex = $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-local-data-row-' + rowIndex\">\n <div class=\"editor-grid\">\n @for (dimField of localDataDimensionFields(); track dimField) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ dimField }}</mat-label>\n <input\n matInput\n [attr.data-testid]=\"'chart-local-data-dim-' + rowIndex + '-' + dimField\"\n [ngModel]=\"localDataCellStringValue(rowIndex, dimField)\"\n (ngModelChange)=\"setLocalDataCellValue(rowIndex, dimField, $event, 'dimension')\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n }\n @for (metricField of localDataMetricFields(); track metricField) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ metricField }}</mat-label>\n <input\n matInput\n type=\"number\"\n [attr.data-testid]=\"'chart-local-data-metric-' + rowIndex + '-' + metricField\"\n [ngModel]=\"localDataCellStringValue(rowIndex, metricField)\"\n (ngModelChange)=\"setLocalDataCellValue(rowIndex, metricField, $event, 'metric')\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n }\n </div>\n <div class=\"editor-row-actions\">\n <button\n mat-button\n type=\"button\"\n [attr.data-testid]=\"'chart-local-data-remove-row-' + rowIndex\"\n (click)=\"removeLocalDataRow(rowIndex)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.localData.removeRow', 'Remove record') }}\n </button>\n </div>\n </div>\n }\n } @else {\n <div class=\"editor-empty\" data-testid=\"chart-local-data-empty\">\n {{ t('praxis.charts.editor.localData.empty', 'No local records configured yet. Add a record to replace the synthetic preview sample.') }}\n </div>\n }\n\n <div>\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-local-data-add-row\"\n (click)=\"addLocalDataRow()\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.localData.addRow', 'Add record') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n }\n </div>\n }\n\n @case ('motion') {\n <div class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"normalizedDocument().motion?.enabled !== false\"\n (ngModelChange)=\"setMotionEnabled($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.motionEnabled', 'Enable animations') }}\n </mat-slide-toggle>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.motionPreset', 'Motion preset') }}</mat-label>\n <mat-select\n [ngModel]=\"normalizedDocument().motion?.preset || 'standard'\"\n (ngModelChange)=\"setMotionPreset($event)\"\n [disabled]=\"isReadonly() || normalizedDocument().motion?.enabled === false\"\n >\n @for (preset of motionPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.motionPreset.' + preset, preset) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n\n @case ('appearance') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.featuresTitle', 'Display features') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('legend')\"\n (ngModelChange)=\"setFeatureEnabled('legend', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.legendEnabled', 'Show legend') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('labels')\"\n (ngModelChange)=\"setFeatureEnabled('labels', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.labelsEnabled', 'Show labels') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('tooltip')\"\n (ngModelChange)=\"setFeatureEnabled('tooltip', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.tooltipEnabled', 'Show tooltip') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.paletteTitle', 'Palette') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.themeVariant', 'Theme variant') }}</mat-label>\n <mat-select [ngModel]=\"themeVariantValue()\" (ngModelChange)=\"setThemeVariant($event)\" [disabled]=\"isReadonly()\">\n <mat-option [value]=\"''\">\n {{ t('praxis.charts.editor.themeVariant.none', 'No variant') }}\n </mat-option>\n @for (variant of themeVariants; track variant) {\n <mat-option [value]=\"variant\">\n {{ t('praxis.charts.editor.themeVariant.' + variant, variant) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteMode', 'Palette mode') }}</mat-label>\n <mat-select [ngModel]=\"paletteModeValue()\" (ngModelChange)=\"setPaletteMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of paletteModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.paletteMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (paletteModeValue() === 'token') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteToken', 'Palette token') }}</mat-label>\n <mat-select [ngModel]=\"paletteTokenValue()\" (ngModelChange)=\"setPaletteToken($event)\" [disabled]=\"isReadonly()\">\n @for (token of paletteTokens; track token) {\n <mat-option [value]=\"token\">\n {{ t('praxis.charts.editor.paletteToken.' + token, token) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.palette', 'Palette colors') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"paletteValue()\"\n (ngModelChange)=\"setPalette($event)\"\n [disabled]=\"isReadonly()\"\n ></textarea>\n </mat-form-field>\n }\n\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.paletteHint', 'Use a registered token or comma-separated colors to persist theme.palette in the canonical contract.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.surfaceTitle', 'Surface') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.surfaceMode', 'Surface mode') }}</mat-label>\n <mat-select [ngModel]=\"surfaceModeValue()\" (ngModelChange)=\"setSurfaceMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of surfaceModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.surface.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.surfaceHint', 'Use embedded for dashboard widgets and contained only when the chart must own its visual surface.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.statesTitle', 'State messages') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyTitle', 'Empty title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('empty')\" (ngModelChange)=\"setStateTitle('empty', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyDescription', 'Empty description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('empty')\" (ngModelChange)=\"setStateDescription('empty', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingTitle', 'Loading title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('loading')\" (ngModelChange)=\"setStateTitle('loading', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingDescription', 'Loading description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('loading')\" (ngModelChange)=\"setStateDescription('loading', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorTitle', 'Error title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('error')\" (ngModelChange)=\"setStateTitle('error', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorDescription', 'Error description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('error')\" (ngModelChange)=\"setStateDescription('error', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('analytics') {\n <div class=\"editor-stack\">\n @if (showGaugePanel()) {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-gauge-scale\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.gaugeTitle', 'Gauge scale') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.gaugeHint', 'Gauge charts render exactly one bucket and one metric. Remote stats use group-by with limit 1; values outside the scale fail instead of being clamped.') }}\n </p>\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.gaugeMin', 'Scale minimum') }}</mat-label>\n <input\n matInput\n type=\"number\"\n data-testid=\"chart-editor-gauge-min\"\n [ngModel]=\"gaugeMinValue()\"\n (ngModelChange)=\"setGaugeMin($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.gaugeMax', 'Scale maximum') }}</mat-label>\n <input\n matInput\n type=\"number\"\n data-testid=\"chart-editor-gauge-max\"\n [ngModel]=\"gaugeMaxValue()\"\n (ngModelChange)=\"setGaugeMax($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n </div>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showComboPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comboTitle', 'Combo guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.comboHint', 'Combo charts require at least two metrics and allow per-metric axis and series kind mapping.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showPieDonutPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.pieDonutTitle', 'Composition guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.pieDonutHint', 'Pie, donut, funnel, pyramid and treemap charts keep one metric and use the first dimension as the category segment.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showScatterPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.scatterTitle', 'Scatter guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.scatterHint', 'With one metric, scatter uses the first dimension as X and the metric as Y. With two metrics, the dimension identifies the grouped bucket and the metrics map to X and Y.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.dimensionsTitle', 'Dimensions') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (dimension of dimensions(); track $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-editor-dimension-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-field-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.dimension', 'Dimension') }}</mat-label>\n @if (fieldOptions('dimension').length) {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('dimension'); track field.field) {\n <mat-option [value]=\"field.field\">{{ fieldOptionLabel(field) }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.dimensionFieldsMissing', 'No governed dimensions are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-role-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.dimensionRole', 'Dimension role') }}</mat-label>\n <mat-select [ngModel]=\"dimension.role || 'category'\" (ngModelChange)=\"setDimensionRole($index, $event)\" [disabled]=\"isReadonly()\">\n @for (role of dimensionRoles; track role) {\n <mat-option [value]=\"role\">\n {{ t('praxis.charts.editor.dimensionRole.' + role, role) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" (click)=\"removeDimension($index)\" [disabled]=\"isReadonly() || dimensions().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeDimension', 'Remove dimension') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-dimension\" (click)=\"addDimension()\" [disabled]=\"isReadonly()\">\n {{ t('praxis.charts.editor.analytics.addDimension', 'Add dimension') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.metricsTitle', 'Metrics') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (metric of metrics(); track $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-editor-metric-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-field-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metric', 'Metric') }}</mat-label>\n @if (fieldOptions('metric').length) {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('metric'); track field.field) {\n <mat-option [value]=\"field.field\">{{ fieldOptionLabel(field) }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.metricFieldsMissing', 'No governed metrics are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-label-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricLabel', 'Metric label') }}</mat-label>\n <input matInput [ngModel]=\"metric.label || ''\" (ngModelChange)=\"setMetricLabel($index, $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-aggregation-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAggregation', 'Aggregation') }}</mat-label>\n <mat-select [ngModel]=\"metric.aggregation || 'sum'\" (ngModelChange)=\"setMetricAggregation($index, $event)\" [disabled]=\"isReadonly()\">\n @for (aggregation of metricAggregationOptions(metric.field); track aggregation) {\n <mat-option [value]=\"aggregation\">\n {{ t('praxis.charts.editor.metricAggregation.' + aggregation, aggregation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (showMetricAxisControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-axis-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAxis', 'Axis') }}</mat-label>\n <mat-select [ngModel]=\"metric.axis || 'primary'\" (ngModelChange)=\"setMetricAxis($index, $event)\" [disabled]=\"isReadonly()\">\n @for (axis of metricAxes; track axis) {\n <mat-option [value]=\"axis\">\n {{ t('praxis.charts.editor.metricAxis.' + axis, axis) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @if (showMetricSeriesKindControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-series-kind-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricSeriesKind', 'Series kind') }}</mat-label>\n <mat-select [ngModel]=\"metric.seriesKind || 'bar'\" (ngModelChange)=\"setMetricSeriesKind($index, $event)\" [disabled]=\"isReadonly()\">\n @for (seriesKind of metricSeriesKinds; track seriesKind) {\n <mat-option [value]=\"seriesKind\">\n {{ t('praxis.charts.editor.metricSeriesKind.' + seriesKind, seriesKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" [attr.data-testid]=\"'chart-editor-remove-metric-' + $index\" (click)=\"removeMetric($index)\" [disabled]=\"isReadonly() || metrics().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeMetric', 'Remove metric') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-metric\" (click)=\"addMetric()\" [disabled]=\"isReadonly() || !canAddMetric()\">\n {{ t('praxis.charts.editor.analytics.addMetric', 'Add metric') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('events') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-pointClick\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.pointClickTitle', 'Point click') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-action\" [ngModel]=\"eventAction('pointClick')\" (ngModelChange)=\"setEventAction('pointClick', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick') || targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')\">\n @for (target of targetOptions(eventAction('pointClick'), 'pointClick'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-pointClick-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('pointClick')\"\n (ngModelChange)=\"setEventMapping('pointClick', $event)\"\n [disabled]=\"isReadonly() || !eventAction('pointClick')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-selectionChange\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.selectionChangeTitle', 'Selection change') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-action\" [ngModel]=\"eventAction('selectionChange')\" (ngModelChange)=\"setEventAction('selectionChange', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange') || targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')\">\n @for (target of targetOptions(eventAction('selectionChange'), 'selectionChange'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-selectionChange-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('selectionChange')\"\n (ngModelChange)=\"setEventMapping('selectionChange', $event)\"\n [disabled]=\"isReadonly() || !eventAction('selectionChange')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-drillDown\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.drillDownTitle', 'Drill down') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-action\" [ngModel]=\"eventAction('drillDown')\" (ngModelChange)=\"setEventAction('drillDown', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown') || targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')\">\n @for (target of targetOptions(eventAction('drillDown'), 'drillDown'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-drillDown-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('drillDown')\"\n (ngModelChange)=\"setEventMapping('drillDown', $event)\"\n [disabled]=\"isReadonly() || !eventAction('drillDown')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-crossFilter\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.crossFilterTitle', 'Cross filter') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-action\" [ngModel]=\"eventAction('crossFilter')\" (ngModelChange)=\"setEventAction('crossFilter', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter') || targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')\">\n @for (target of targetOptions(eventAction('crossFilter'), 'crossFilter'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-crossFilter-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('crossFilter')\"\n (ngModelChange)=\"setEventMapping('crossFilter', $event)\"\n [disabled]=\"isReadonly() || !eventAction('crossFilter')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('preview') {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n }\n }\n </mat-card-content>\n </mat-card>\n </div>\n\n <div class=\"editor-side\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.issues.title', 'Validation issues') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (issues().length) {\n <ul class=\"editor-issues\">\n @for (issue of issues(); track issueTrackBy($index, issue)) {\n <li class=\"editor-issue\">\n <strong>{{ issue.field }}</strong>\n <span>{{ issue.message }}</span>\n </li>\n }\n </ul>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.issues.empty', 'No issues were identified.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.preview.title', 'Chart preview') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (preview(); as chartPreview) {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n @if (doc().source.kind === 'derived') {\n <p class=\"editor-caption\" data-testid=\"chart-editor-preview-origin\">\n {{\n chartPreview.dataOrigin === 'local'\n ? t('praxis.charts.editor.preview.originLocal', 'Previewing widget local dataset (inputs.data).')\n : t('praxis.charts.editor.preview.originSynthetic', 'Previewing synthetic sample data (no local dataset records).')\n }}\n </p>\n }\n <praxis-chart [config]=\"chartPreview.config\" [data]=\"chartPreview.data\"></praxis-chart>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.preview.invalid', 'Preview is unavailable while the contract has blocking errors.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n</div>\n", styles: [":host{display:block;min-width:0;container:praxis-chart-editor / inline-size;color:var(--md-sys-color-on-surface, #1a1b20)}.editor-shell{display:grid;gap:18px;min-width:0}.editor-nav{display:flex;gap:8px;flex-wrap:wrap}.editor-nav button.active{background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 18%,transparent);color:var(--md-sys-color-primary, #1263b4)}.editor-layout{display:grid;gap:18px;grid-template-columns:minmax(0,1.35fr) minmax(320px,.9fr);align-items:start}.editor-form,.editor-side{display:grid;gap:16px;min-width:0}.editor-card{box-sizing:border-box;width:100%;min-width:0;border-radius:20px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#1263b408,#1263b400)}.editor-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-stack{display:grid;gap:14px}.editor-row-card{padding:14px;border-radius:16px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 54%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 92%,rgba(18,99,180,.04))}.editor-row-actions{display:flex;justify-content:flex-end}.editor-field{width:100%}.editor-issues{display:grid;gap:10px;margin:0;padding:0;list-style:none}.editor-issue{padding:12px 14px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-error, #b3261e) 8%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-error, #b3261e) 18%,transparent)}.editor-issue strong{display:block;margin-bottom:4px}.editor-caption{margin:0 0 12px;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.92rem}.editor-empty{padding:18px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-variant, #eceff4) 78%,transparent)}.editor-side praxis-chart{display:block;width:100%;min-width:0;max-width:100%}@container praxis-chart-editor (max-width: 840px){.editor-layout{grid-template-columns:minmax(0,1fr)}}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.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$3.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i3.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i3.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i3.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i3.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i7.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "resourceKey", "data", "chartDocument", "filterCriteria", "queryContext", "remoteDataResolver", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "pointAction", "selectionChange", "drillDown", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved", "chartDocumentPatchChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
9997
10446
|
}
|
|
9998
10447
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartConfigEditor, decorators: [{
|
|
9999
10448
|
type: Component,
|
|
@@ -10006,13 +10455,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
10006
10455
|
MatSelectModule,
|
|
10007
10456
|
MatSlideToggleModule,
|
|
10008
10457
|
PraxisChartComponent
|
|
10009
|
-
], providers: [providePraxisChartsI18n()], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"editor-shell\">\n <div class=\"editor-nav\">\n @for (section of sections; track section.id) {\n <button\n mat-stroked-button\n type=\"button\"\n [attr.data-testid]=\"'chart-editor-section-' + section.id\"\n [class.active]=\"activeSection() === section.id\"\n (click)=\"setSection(section.id)\"\n >\n {{ t(section.labelKey, section.fallback) }}\n </button>\n }\n </div>\n\n <div class=\"editor-layout\">\n <div class=\"editor-form\">\n <mat-card class=\"editor-card\">\n <mat-card-content>\n @switch (activeSection()) {\n @case ('general') {\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.chartId', 'Chart ID') }}</mat-label>\n <input matInput [ngModel]=\"doc().chartId || ''\" (ngModelChange)=\"setChartId($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.kind', 'Kind') }}</mat-label>\n <mat-select [ngModel]=\"doc().kind\" (ngModelChange)=\"setKind($event)\" [disabled]=\"isReadonly()\">\n @for (kind of chartKinds; track kind) {\n <mat-option [value]=\"kind\">\n {{ t('praxis.charts.editor.kind.' + kind, kind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.title', 'Title') }}</mat-label>\n <input matInput data-testid=\"chart-editor-title-input\" [ngModel]=\"titleValue()\" (ngModelChange)=\"setTitle($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.subtitle', 'Subtitle') }}</mat-label>\n <input matInput [ngModel]=\"subtitleValue()\" (ngModelChange)=\"setSubtitle($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.sizingMode', 'Sizing') }}</mat-label>\n <mat-select [ngModel]=\"sizingModeValue()\" (ngModelChange)=\"setSizingMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of sizingModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.sizing.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>{{ t('praxis.charts.editor.hint.sizingMode', 'Use fill-container only when the host widget provides a defined body height.') }}</mat-hint>\n </mat-form-field>\n\n @if (sizingModeValue() === 'fixed') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.height', 'Height') }}</mat-label>\n <input matInput [ngModel]=\"heightValue()\" (ngModelChange)=\"setSizingHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.height', 'Numbers are saved as pixels; CSS lengths such as 20rem are also accepted.') }}</mat-hint>\n </mat-form-field>\n }\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.minHeight', 'Minimum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMinHeightValue()\" (ngModelChange)=\"setSizingMinHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.minHeight', 'Set a readable minimum for compact dashboard widgets.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.maxHeight', 'Maximum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMaxHeightValue()\" (ngModelChange)=\"setSizingMaxHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.maxHeight', 'Leave empty unless the chart must stop growing inside a flexible layout.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.aspectRatio', 'Aspect ratio') }}</mat-label>\n <input matInput [ngModel]=\"sizingAspectRatioValue()\" (ngModelChange)=\"setSizingAspectRatio($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.aspectRatio', 'Optional. Use values such as 1.777 or 16 / 9.') }}</mat-hint>\n </mat-form-field>\n </div>\n }\n\n @case ('data') {\n <div class=\"editor-stack\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.sourceKind', 'Source') }}</mat-label>\n <mat-select data-testid=\"chart-editor-source-kind\" [ngModel]=\"doc().source.kind\" (ngModelChange)=\"setSourceKind($event)\" [disabled]=\"isReadonly()\">\n @for (sourceKind of sourceKinds; track sourceKind) {\n <mat-option [value]=\"sourceKind\">\n {{ t('praxis.charts.editor.sourceKind.' + (sourceKind === 'praxis.stats' ? 'praxisStats' : 'derived'), sourceKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (doc().source.kind === 'praxis.stats') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.resource', 'Resource') }}</mat-label>\n <mat-select data-testid=\"chart-editor-resource\" [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly() || resourceCatalogUnavailable()\">\n @for (resource of resourceOptions(); track resource.id) {\n <mat-option [value]=\"resource.path\">{{ resource.label }}</mat-option>\n }\n </mat-select>\n @if (resourceCatalogUnavailable()) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.resourceMissing', 'Resource catalog is required for governed praxis.stats authoring.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.operation', 'Operation') }}</mat-label>\n <mat-select\n data-testid=\"chart-editor-operation\"\n [ngModel]=\"doc().source.operation || 'group-by'\"\n (ngModelChange)=\"setOperation($event)\"\n [disabled]=\"isReadonly() || operationCatalogUnavailable()\"\n >\n @for (operation of operationOptions(); track operation) {\n <mat-option [value]=\"operation\">\n {{ t('praxis.charts.editor.operation.' + (operation === 'group-by' ? 'groupBy' : operation), operation) }}\n </mat-option>\n }\n </mat-select>\n @if (operationCatalogUnavailable()) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.operationMissing', 'Selected resource does not publish authorable stats operations.') }}</mat-hint>\n }\n </mat-form-field>\n }\n </div>\n\n @if (showTimeseriesControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.timeseriesTitle', 'Timeseries options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.granularity', 'Granularity') }}</mat-label>\n <mat-select [ngModel]=\"granularityValue()\" (ngModelChange)=\"setGranularity($event)\" [disabled]=\"isReadonly()\">\n @for (granularity of timeGranularities; track granularity) {\n <mat-option [value]=\"granularity\">\n {{ t('praxis.charts.editor.granularity.' + granularity, granularity) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-slide-toggle\n [ngModel]=\"fillGapsValue()\"\n (ngModelChange)=\"setFillGaps($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.fillGaps', 'Fill missing intervals') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showDistributionControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.distributionTitle', 'Distribution options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.distributionMode', 'Distribution mode') }}</mat-label>\n <mat-select data-testid=\"chart-editor-distribution-mode\" [ngModel]=\"distributionModeValue()\" (ngModelChange)=\"setDistributionMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of distributionModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.distributionMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (distributionModeValue() === 'histogram') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketSize', 'Bucket size') }}</mat-label>\n <input matInput data-testid=\"chart-editor-bucket-size\" [ngModel]=\"bucketSizeValue()\" (ngModelChange)=\"setBucketSize($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketCount', 'Bucket count') }}</mat-label>\n <input matInput data-testid=\"chart-editor-bucket-count\" [ngModel]=\"bucketCountValue()\" (ngModelChange)=\"setBucketCount($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n }\n </mat-card-content>\n </mat-card>\n }\n\n @if (showComparisonControls()) {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-comparison-options\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comparisonTitle', 'Comparison period') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonPeriodField', 'Period field') }}</mat-label>\n @if (fieldOptions('time').length) {\n <mat-select\n [ngModel]=\"comparisonPeriodFieldValue()\"\n (ngModelChange)=\"setComparisonPeriodField($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (field of fieldOptions('time'); track field.field) {\n <mat-option [value]=\"field.field\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"comparisonPeriodFieldValue()\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.comparisonPeriodFieldsMissing', 'No governed time-series field is available for comparison periods.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonTimezone', 'Timezone') }}</mat-label>\n <input\n matInput\n [ngModel]=\"comparisonPeriodTimezoneValue()\"\n (ngModelChange)=\"setComparisonPeriodTimezone($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonPreset', 'Period preset') }}</mat-label>\n <mat-select\n [ngModel]=\"comparisonPeriodPresetValue()\"\n (ngModelChange)=\"setComparisonPeriodPreset($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (preset of comparisonPeriodPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.comparisonPreset.' + preset, preset) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonMode', 'Comparison mode') }}</mat-label>\n <mat-select\n [ngModel]=\"comparisonPeriodModeValue()\"\n (ngModelChange)=\"setComparisonPeriodMode($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (mode of comparisonPeriodModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.comparisonMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n }\n </div>\n }\n\n @case ('motion') {\n <div class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"normalizedDocument().motion?.enabled !== false\"\n (ngModelChange)=\"setMotionEnabled($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.motionEnabled', 'Enable animations') }}\n </mat-slide-toggle>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.motionPreset', 'Motion preset') }}</mat-label>\n <mat-select\n [ngModel]=\"normalizedDocument().motion?.preset || 'standard'\"\n (ngModelChange)=\"setMotionPreset($event)\"\n [disabled]=\"isReadonly() || normalizedDocument().motion?.enabled === false\"\n >\n @for (preset of motionPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.motionPreset.' + preset, preset) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n\n @case ('appearance') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.featuresTitle', 'Display features') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('legend')\"\n (ngModelChange)=\"setFeatureEnabled('legend', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.legendEnabled', 'Show legend') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('labels')\"\n (ngModelChange)=\"setFeatureEnabled('labels', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.labelsEnabled', 'Show labels') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('tooltip')\"\n (ngModelChange)=\"setFeatureEnabled('tooltip', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.tooltipEnabled', 'Show tooltip') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.paletteTitle', 'Palette') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.themeVariant', 'Theme variant') }}</mat-label>\n <mat-select [ngModel]=\"themeVariantValue()\" (ngModelChange)=\"setThemeVariant($event)\" [disabled]=\"isReadonly()\">\n <mat-option [value]=\"''\">\n {{ t('praxis.charts.editor.themeVariant.none', 'No variant') }}\n </mat-option>\n @for (variant of themeVariants; track variant) {\n <mat-option [value]=\"variant\">\n {{ t('praxis.charts.editor.themeVariant.' + variant, variant) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteMode', 'Palette mode') }}</mat-label>\n <mat-select [ngModel]=\"paletteModeValue()\" (ngModelChange)=\"setPaletteMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of paletteModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.paletteMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (paletteModeValue() === 'token') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteToken', 'Palette token') }}</mat-label>\n <mat-select [ngModel]=\"paletteTokenValue()\" (ngModelChange)=\"setPaletteToken($event)\" [disabled]=\"isReadonly()\">\n @for (token of paletteTokens; track token) {\n <mat-option [value]=\"token\">\n {{ t('praxis.charts.editor.paletteToken.' + token, token) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.palette', 'Palette colors') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"paletteValue()\"\n (ngModelChange)=\"setPalette($event)\"\n [disabled]=\"isReadonly()\"\n ></textarea>\n </mat-form-field>\n }\n\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.paletteHint', 'Use a registered token or comma-separated colors to persist theme.palette in the canonical contract.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.surfaceTitle', 'Surface') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.surfaceMode', 'Surface mode') }}</mat-label>\n <mat-select [ngModel]=\"surfaceModeValue()\" (ngModelChange)=\"setSurfaceMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of surfaceModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.surface.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.surfaceHint', 'Use embedded for dashboard widgets and contained only when the chart must own its visual surface.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.statesTitle', 'State messages') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyTitle', 'Empty title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('empty')\" (ngModelChange)=\"setStateTitle('empty', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyDescription', 'Empty description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('empty')\" (ngModelChange)=\"setStateDescription('empty', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingTitle', 'Loading title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('loading')\" (ngModelChange)=\"setStateTitle('loading', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingDescription', 'Loading description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('loading')\" (ngModelChange)=\"setStateDescription('loading', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorTitle', 'Error title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('error')\" (ngModelChange)=\"setStateTitle('error', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorDescription', 'Error description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('error')\" (ngModelChange)=\"setStateDescription('error', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('analytics') {\n <div class=\"editor-stack\">\n @if (showGaugePanel()) {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-gauge-scale\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.gaugeTitle', 'Gauge scale') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.gaugeHint', 'Gauge charts render exactly one bucket and one metric. Remote stats use group-by with limit 1; values outside the scale fail instead of being clamped.') }}\n </p>\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.gaugeMin', 'Scale minimum') }}</mat-label>\n <input\n matInput\n type=\"number\"\n data-testid=\"chart-editor-gauge-min\"\n [ngModel]=\"gaugeMinValue()\"\n (ngModelChange)=\"setGaugeMin($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.gaugeMax', 'Scale maximum') }}</mat-label>\n <input\n matInput\n type=\"number\"\n data-testid=\"chart-editor-gauge-max\"\n [ngModel]=\"gaugeMaxValue()\"\n (ngModelChange)=\"setGaugeMax($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n </div>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showComboPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comboTitle', 'Combo guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.comboHint', 'Combo charts require at least two metrics and allow per-metric axis and series kind mapping.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showPieDonutPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.pieDonutTitle', 'Composition guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.pieDonutHint', 'Pie, donut, funnel, pyramid and treemap charts keep one metric and use the first dimension as the category segment.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showScatterPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.scatterTitle', 'Scatter guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.scatterHint', 'With one metric, scatter uses the first dimension as X and the metric as Y. With two metrics, the dimension identifies the grouped bucket and the metrics map to X and Y.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.dimensionsTitle', 'Dimensions') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (dimension of dimensions(); track $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-editor-dimension-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-field-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.dimension', 'Dimension') }}</mat-label>\n @if (fieldOptions('dimension').length) {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('dimension'); track field.field) {\n <mat-option [value]=\"field.field\">{{ fieldOptionLabel(field) }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.dimensionFieldsMissing', 'No governed dimensions are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-role-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.dimensionRole', 'Dimension role') }}</mat-label>\n <mat-select [ngModel]=\"dimension.role || 'category'\" (ngModelChange)=\"setDimensionRole($index, $event)\" [disabled]=\"isReadonly()\">\n @for (role of dimensionRoles; track role) {\n <mat-option [value]=\"role\">\n {{ t('praxis.charts.editor.dimensionRole.' + role, role) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" (click)=\"removeDimension($index)\" [disabled]=\"isReadonly() || dimensions().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeDimension', 'Remove dimension') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-dimension\" (click)=\"addDimension()\" [disabled]=\"isReadonly()\">\n {{ t('praxis.charts.editor.analytics.addDimension', 'Add dimension') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.metricsTitle', 'Metrics') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (metric of metrics(); track $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-editor-metric-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-field-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metric', 'Metric') }}</mat-label>\n @if (fieldOptions('metric').length) {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('metric'); track field.field) {\n <mat-option [value]=\"field.field\">{{ fieldOptionLabel(field) }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.metricFieldsMissing', 'No governed metrics are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-label-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricLabel', 'Metric label') }}</mat-label>\n <input matInput [ngModel]=\"metric.label || ''\" (ngModelChange)=\"setMetricLabel($index, $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-aggregation-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAggregation', 'Aggregation') }}</mat-label>\n <mat-select [ngModel]=\"metric.aggregation || 'sum'\" (ngModelChange)=\"setMetricAggregation($index, $event)\" [disabled]=\"isReadonly()\">\n @for (aggregation of metricAggregationOptions(metric.field); track aggregation) {\n <mat-option [value]=\"aggregation\">\n {{ t('praxis.charts.editor.metricAggregation.' + aggregation, aggregation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (showMetricAxisControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-axis-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAxis', 'Axis') }}</mat-label>\n <mat-select [ngModel]=\"metric.axis || 'primary'\" (ngModelChange)=\"setMetricAxis($index, $event)\" [disabled]=\"isReadonly()\">\n @for (axis of metricAxes; track axis) {\n <mat-option [value]=\"axis\">\n {{ t('praxis.charts.editor.metricAxis.' + axis, axis) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @if (showMetricSeriesKindControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-series-kind-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricSeriesKind', 'Series kind') }}</mat-label>\n <mat-select [ngModel]=\"metric.seriesKind || 'bar'\" (ngModelChange)=\"setMetricSeriesKind($index, $event)\" [disabled]=\"isReadonly()\">\n @for (seriesKind of metricSeriesKinds; track seriesKind) {\n <mat-option [value]=\"seriesKind\">\n {{ t('praxis.charts.editor.metricSeriesKind.' + seriesKind, seriesKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" [attr.data-testid]=\"'chart-editor-remove-metric-' + $index\" (click)=\"removeMetric($index)\" [disabled]=\"isReadonly() || metrics().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeMetric', 'Remove metric') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-metric\" (click)=\"addMetric()\" [disabled]=\"isReadonly() || !canAddMetric()\">\n {{ t('praxis.charts.editor.analytics.addMetric', 'Add metric') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('events') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-pointClick\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.pointClickTitle', 'Point click') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-action\" [ngModel]=\"eventAction('pointClick')\" (ngModelChange)=\"setEventAction('pointClick', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick') || targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')\">\n @for (target of targetOptions(eventAction('pointClick'), 'pointClick'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-pointClick-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('pointClick')\"\n (ngModelChange)=\"setEventMapping('pointClick', $event)\"\n [disabled]=\"isReadonly() || !eventAction('pointClick')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-selectionChange\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.selectionChangeTitle', 'Selection change') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-action\" [ngModel]=\"eventAction('selectionChange')\" (ngModelChange)=\"setEventAction('selectionChange', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange') || targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')\">\n @for (target of targetOptions(eventAction('selectionChange'), 'selectionChange'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-selectionChange-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('selectionChange')\"\n (ngModelChange)=\"setEventMapping('selectionChange', $event)\"\n [disabled]=\"isReadonly() || !eventAction('selectionChange')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-drillDown\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.drillDownTitle', 'Drill down') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-action\" [ngModel]=\"eventAction('drillDown')\" (ngModelChange)=\"setEventAction('drillDown', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown') || targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')\">\n @for (target of targetOptions(eventAction('drillDown'), 'drillDown'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-drillDown-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('drillDown')\"\n (ngModelChange)=\"setEventMapping('drillDown', $event)\"\n [disabled]=\"isReadonly() || !eventAction('drillDown')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-crossFilter\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.crossFilterTitle', 'Cross filter') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-action\" [ngModel]=\"eventAction('crossFilter')\" (ngModelChange)=\"setEventAction('crossFilter', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter') || targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')\">\n @for (target of targetOptions(eventAction('crossFilter'), 'crossFilter'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-crossFilter-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('crossFilter')\"\n (ngModelChange)=\"setEventMapping('crossFilter', $event)\"\n [disabled]=\"isReadonly() || !eventAction('crossFilter')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('preview') {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n }\n }\n </mat-card-content>\n </mat-card>\n </div>\n\n <div class=\"editor-side\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.issues.title', 'Validation issues') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (issues().length) {\n <ul class=\"editor-issues\">\n @for (issue of issues(); track issueTrackBy($index, issue)) {\n <li class=\"editor-issue\">\n <strong>{{ issue.field }}</strong>\n <span>{{ issue.message }}</span>\n </li>\n }\n </ul>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.issues.empty', 'No issues were identified.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.preview.title', 'Chart preview') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (preview(); as chartPreview) {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n <praxis-chart [config]=\"chartPreview.config\" [data]=\"chartPreview.data\"></praxis-chart>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.preview.invalid', 'Preview is unavailable while the contract has blocking errors.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n</div>\n", styles: [":host{display:block;min-width:0;container:praxis-chart-editor / inline-size;color:var(--md-sys-color-on-surface, #1a1b20)}.editor-shell{display:grid;gap:18px;min-width:0}.editor-nav{display:flex;gap:8px;flex-wrap:wrap}.editor-nav button.active{background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 18%,transparent);color:var(--md-sys-color-primary, #1263b4)}.editor-layout{display:grid;gap:18px;grid-template-columns:minmax(0,1.35fr) minmax(320px,.9fr);align-items:start}.editor-form,.editor-side{display:grid;gap:16px;min-width:0}.editor-card{box-sizing:border-box;width:100%;min-width:0;border-radius:20px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#1263b408,#1263b400)}.editor-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-stack{display:grid;gap:14px}.editor-row-card{padding:14px;border-radius:16px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 54%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 92%,rgba(18,99,180,.04))}.editor-row-actions{display:flex;justify-content:flex-end}.editor-field{width:100%}.editor-issues{display:grid;gap:10px;margin:0;padding:0;list-style:none}.editor-issue{padding:12px 14px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-error, #b3261e) 8%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-error, #b3261e) 18%,transparent)}.editor-issue strong{display:block;margin-bottom:4px}.editor-caption{margin:0 0 12px;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.92rem}.editor-empty{padding:18px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-variant, #eceff4) 78%,transparent)}.editor-side praxis-chart{display:block;width:100%;min-width:0;max-width:100%}@container praxis-chart-editor (max-width: 840px){.editor-layout{grid-template-columns:minmax(0,1fr)}}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"] }]
|
|
10010
|
-
}], ctorParameters: () => [], propDecorators: { documentInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "document", required: false }] }], modeInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], readonlyInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], availableResourcesInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableResources", required: false }] }], availableFieldsInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableFields", required: false }] }], availableTargetsInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableTargets", required: false }] }], apply: [{ type: i0.Output, args: ["apply"] }], save: [{ type: i0.Output, args: ["save"] }], resetChange: [{ type: i0.Output, args: ["resetChange"] }], documentChange: [{ type: i0.Output, args: ["documentChange"] }] } });
|
|
10011
|
-
|
|
10012
|
-
var praxisChartConfigEditor = /*#__PURE__*/Object.freeze({
|
|
10013
|
-
__proto__: null,
|
|
10014
|
-
PraxisChartConfigEditor: PraxisChartConfigEditor
|
|
10015
|
-
});
|
|
10458
|
+
], providers: [providePraxisChartsI18n()], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"editor-shell\">\n <div class=\"editor-nav\">\n @for (section of sections; track section.id) {\n <button\n mat-stroked-button\n type=\"button\"\n [attr.data-testid]=\"'chart-editor-section-' + section.id\"\n [class.active]=\"activeSection() === section.id\"\n (click)=\"setSection(section.id)\"\n >\n {{ t(section.labelKey, section.fallback) }}\n </button>\n }\n </div>\n\n <div class=\"editor-layout\">\n <div class=\"editor-form\">\n <mat-card class=\"editor-card\">\n <mat-card-content>\n @switch (activeSection()) {\n @case ('general') {\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.chartId', 'Chart ID') }}</mat-label>\n <input matInput [ngModel]=\"doc().chartId || ''\" (ngModelChange)=\"setChartId($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.kind', 'Kind') }}</mat-label>\n <mat-select [ngModel]=\"doc().kind\" (ngModelChange)=\"setKind($event)\" [disabled]=\"isReadonly()\">\n @for (kind of chartKinds; track kind) {\n <mat-option [value]=\"kind\">\n {{ t('praxis.charts.editor.kind.' + kind, kind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.title', 'Title') }}</mat-label>\n <input matInput data-testid=\"chart-editor-title-input\" [ngModel]=\"titleValue()\" (ngModelChange)=\"setTitle($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.subtitle', 'Subtitle') }}</mat-label>\n <input matInput [ngModel]=\"subtitleValue()\" (ngModelChange)=\"setSubtitle($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.sizingMode', 'Sizing') }}</mat-label>\n <mat-select [ngModel]=\"sizingModeValue()\" (ngModelChange)=\"setSizingMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of sizingModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.sizing.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>{{ t('praxis.charts.editor.hint.sizingMode', 'Use fill-container only when the host widget provides a defined body height.') }}</mat-hint>\n </mat-form-field>\n\n @if (sizingModeValue() === 'fixed') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.height', 'Height') }}</mat-label>\n <input matInput [ngModel]=\"heightValue()\" (ngModelChange)=\"setSizingHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.height', 'Numbers are saved as pixels; CSS lengths such as 20rem are also accepted.') }}</mat-hint>\n </mat-form-field>\n }\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.minHeight', 'Minimum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMinHeightValue()\" (ngModelChange)=\"setSizingMinHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.minHeight', 'Set a readable minimum for compact dashboard widgets.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.maxHeight', 'Maximum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMaxHeightValue()\" (ngModelChange)=\"setSizingMaxHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.maxHeight', 'Leave empty unless the chart must stop growing inside a flexible layout.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.aspectRatio', 'Aspect ratio') }}</mat-label>\n <input matInput [ngModel]=\"sizingAspectRatioValue()\" (ngModelChange)=\"setSizingAspectRatio($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.aspectRatio', 'Optional. Use values such as 1.777 or 16 / 9.') }}</mat-hint>\n </mat-form-field>\n </div>\n }\n\n @case ('data') {\n <div class=\"editor-stack\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.sourceKind', 'Source') }}</mat-label>\n <mat-select data-testid=\"chart-editor-source-kind\" [ngModel]=\"doc().source.kind\" (ngModelChange)=\"setSourceKind($event)\" [disabled]=\"isReadonly()\">\n @for (sourceKind of sourceKinds; track sourceKind) {\n <mat-option [value]=\"sourceKind\">\n {{ t('praxis.charts.editor.sourceKind.' + (sourceKind === 'praxis.stats' ? 'praxisStats' : 'derived'), sourceKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (doc().source.kind === 'praxis.stats') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.resource', 'Resource') }}</mat-label>\n <mat-select data-testid=\"chart-editor-resource\" [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly() || resourceCatalogUnavailable()\">\n @for (resource of resourceOptions(); track resource.id) {\n <mat-option [value]=\"resource.path\">{{ resource.label }}</mat-option>\n }\n </mat-select>\n @if (resourceCatalogUnavailable()) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.resourceMissing', 'Resource catalog is required for governed praxis.stats authoring.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.operation', 'Operation') }}</mat-label>\n <mat-select\n data-testid=\"chart-editor-operation\"\n [ngModel]=\"doc().source.operation || 'group-by'\"\n (ngModelChange)=\"setOperation($event)\"\n [disabled]=\"isReadonly() || operationCatalogUnavailable()\"\n >\n @for (operation of operationOptions(); track operation) {\n <mat-option [value]=\"operation\">\n {{ t('praxis.charts.editor.operation.' + (operation === 'group-by' ? 'groupBy' : operation), operation) }}\n </mat-option>\n }\n </mat-select>\n @if (operationCatalogUnavailable()) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.operationMissing', 'Selected resource does not publish authorable stats operations.') }}</mat-hint>\n }\n </mat-form-field>\n }\n </div>\n\n @if (showTimeseriesControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.timeseriesTitle', 'Timeseries options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.granularity', 'Granularity') }}</mat-label>\n <mat-select [ngModel]=\"granularityValue()\" (ngModelChange)=\"setGranularity($event)\" [disabled]=\"isReadonly()\">\n @for (granularity of timeGranularities; track granularity) {\n <mat-option [value]=\"granularity\">\n {{ t('praxis.charts.editor.granularity.' + granularity, granularity) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-slide-toggle\n [ngModel]=\"fillGapsValue()\"\n (ngModelChange)=\"setFillGaps($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.fillGaps', 'Fill missing intervals') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showDistributionControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.distributionTitle', 'Distribution options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.distributionMode', 'Distribution mode') }}</mat-label>\n <mat-select data-testid=\"chart-editor-distribution-mode\" [ngModel]=\"distributionModeValue()\" (ngModelChange)=\"setDistributionMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of distributionModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.distributionMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (distributionModeValue() === 'histogram') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketSize', 'Bucket size') }}</mat-label>\n <input matInput data-testid=\"chart-editor-bucket-size\" [ngModel]=\"bucketSizeValue()\" (ngModelChange)=\"setBucketSize($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketCount', 'Bucket count') }}</mat-label>\n <input matInput data-testid=\"chart-editor-bucket-count\" [ngModel]=\"bucketCountValue()\" (ngModelChange)=\"setBucketCount($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n }\n </mat-card-content>\n </mat-card>\n }\n\n @if (showComparisonControls()) {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-comparison-options\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comparisonTitle', 'Comparison period') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonPeriodField', 'Period field') }}</mat-label>\n @if (fieldOptions('time').length) {\n <mat-select\n [ngModel]=\"comparisonPeriodFieldValue()\"\n (ngModelChange)=\"setComparisonPeriodField($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (field of fieldOptions('time'); track field.field) {\n <mat-option [value]=\"field.field\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"comparisonPeriodFieldValue()\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.comparisonPeriodFieldsMissing', 'No governed time-series field is available for comparison periods.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonTimezone', 'Timezone') }}</mat-label>\n <input\n matInput\n [ngModel]=\"comparisonPeriodTimezoneValue()\"\n (ngModelChange)=\"setComparisonPeriodTimezone($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonPreset', 'Period preset') }}</mat-label>\n <mat-select\n [ngModel]=\"comparisonPeriodPresetValue()\"\n (ngModelChange)=\"setComparisonPeriodPreset($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (preset of comparisonPeriodPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.comparisonPreset.' + preset, preset) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonMode', 'Comparison mode') }}</mat-label>\n <mat-select\n [ngModel]=\"comparisonPeriodModeValue()\"\n (ngModelChange)=\"setComparisonPeriodMode($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (mode of comparisonPeriodModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.comparisonMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n }\n\n @if (doc().source.kind === 'derived') {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-local-data\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.localData.title', 'Local dataset (inputs.data)') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.localData.caption', 'Edit the local records rendered when the data source is derived.') }}\n </p>\n\n @if (canConsolidateGaugeLocalData()) {\n <div class=\"editor-row-actions\">\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-local-data-consolidate-gauge\"\n (click)=\"consolidateGaugeLocalData()\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.localData.consolidateGauge', 'Consolidate to 1 bucket for gauge') }}\n </button>\n </div>\n }\n\n @if (localDataRows().length) {\n @for (row of localDataRows(); track $index; let rowIndex = $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-local-data-row-' + rowIndex\">\n <div class=\"editor-grid\">\n @for (dimField of localDataDimensionFields(); track dimField) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ dimField }}</mat-label>\n <input\n matInput\n [attr.data-testid]=\"'chart-local-data-dim-' + rowIndex + '-' + dimField\"\n [ngModel]=\"localDataCellStringValue(rowIndex, dimField)\"\n (ngModelChange)=\"setLocalDataCellValue(rowIndex, dimField, $event, 'dimension')\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n }\n @for (metricField of localDataMetricFields(); track metricField) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ metricField }}</mat-label>\n <input\n matInput\n type=\"number\"\n [attr.data-testid]=\"'chart-local-data-metric-' + rowIndex + '-' + metricField\"\n [ngModel]=\"localDataCellStringValue(rowIndex, metricField)\"\n (ngModelChange)=\"setLocalDataCellValue(rowIndex, metricField, $event, 'metric')\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n }\n </div>\n <div class=\"editor-row-actions\">\n <button\n mat-button\n type=\"button\"\n [attr.data-testid]=\"'chart-local-data-remove-row-' + rowIndex\"\n (click)=\"removeLocalDataRow(rowIndex)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.localData.removeRow', 'Remove record') }}\n </button>\n </div>\n </div>\n }\n } @else {\n <div class=\"editor-empty\" data-testid=\"chart-local-data-empty\">\n {{ t('praxis.charts.editor.localData.empty', 'No local records configured yet. Add a record to replace the synthetic preview sample.') }}\n </div>\n }\n\n <div>\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-local-data-add-row\"\n (click)=\"addLocalDataRow()\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.localData.addRow', 'Add record') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n }\n </div>\n }\n\n @case ('motion') {\n <div class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"normalizedDocument().motion?.enabled !== false\"\n (ngModelChange)=\"setMotionEnabled($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.motionEnabled', 'Enable animations') }}\n </mat-slide-toggle>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.motionPreset', 'Motion preset') }}</mat-label>\n <mat-select\n [ngModel]=\"normalizedDocument().motion?.preset || 'standard'\"\n (ngModelChange)=\"setMotionPreset($event)\"\n [disabled]=\"isReadonly() || normalizedDocument().motion?.enabled === false\"\n >\n @for (preset of motionPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.motionPreset.' + preset, preset) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n\n @case ('appearance') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.featuresTitle', 'Display features') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('legend')\"\n (ngModelChange)=\"setFeatureEnabled('legend', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.legendEnabled', 'Show legend') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('labels')\"\n (ngModelChange)=\"setFeatureEnabled('labels', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.labelsEnabled', 'Show labels') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('tooltip')\"\n (ngModelChange)=\"setFeatureEnabled('tooltip', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.tooltipEnabled', 'Show tooltip') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.paletteTitle', 'Palette') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.themeVariant', 'Theme variant') }}</mat-label>\n <mat-select [ngModel]=\"themeVariantValue()\" (ngModelChange)=\"setThemeVariant($event)\" [disabled]=\"isReadonly()\">\n <mat-option [value]=\"''\">\n {{ t('praxis.charts.editor.themeVariant.none', 'No variant') }}\n </mat-option>\n @for (variant of themeVariants; track variant) {\n <mat-option [value]=\"variant\">\n {{ t('praxis.charts.editor.themeVariant.' + variant, variant) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteMode', 'Palette mode') }}</mat-label>\n <mat-select [ngModel]=\"paletteModeValue()\" (ngModelChange)=\"setPaletteMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of paletteModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.paletteMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (paletteModeValue() === 'token') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteToken', 'Palette token') }}</mat-label>\n <mat-select [ngModel]=\"paletteTokenValue()\" (ngModelChange)=\"setPaletteToken($event)\" [disabled]=\"isReadonly()\">\n @for (token of paletteTokens; track token) {\n <mat-option [value]=\"token\">\n {{ t('praxis.charts.editor.paletteToken.' + token, token) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.palette', 'Palette colors') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"paletteValue()\"\n (ngModelChange)=\"setPalette($event)\"\n [disabled]=\"isReadonly()\"\n ></textarea>\n </mat-form-field>\n }\n\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.paletteHint', 'Use a registered token or comma-separated colors to persist theme.palette in the canonical contract.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.surfaceTitle', 'Surface') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.surfaceMode', 'Surface mode') }}</mat-label>\n <mat-select [ngModel]=\"surfaceModeValue()\" (ngModelChange)=\"setSurfaceMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of surfaceModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.surface.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.surfaceHint', 'Use embedded for dashboard widgets and contained only when the chart must own its visual surface.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.statesTitle', 'State messages') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyTitle', 'Empty title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('empty')\" (ngModelChange)=\"setStateTitle('empty', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyDescription', 'Empty description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('empty')\" (ngModelChange)=\"setStateDescription('empty', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingTitle', 'Loading title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('loading')\" (ngModelChange)=\"setStateTitle('loading', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingDescription', 'Loading description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('loading')\" (ngModelChange)=\"setStateDescription('loading', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorTitle', 'Error title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('error')\" (ngModelChange)=\"setStateTitle('error', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorDescription', 'Error description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('error')\" (ngModelChange)=\"setStateDescription('error', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('analytics') {\n <div class=\"editor-stack\">\n @if (showGaugePanel()) {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-gauge-scale\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.gaugeTitle', 'Gauge scale') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.gaugeHint', 'Gauge charts render exactly one bucket and one metric. Remote stats use group-by with limit 1; values outside the scale fail instead of being clamped.') }}\n </p>\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.gaugeMin', 'Scale minimum') }}</mat-label>\n <input\n matInput\n type=\"number\"\n data-testid=\"chart-editor-gauge-min\"\n [ngModel]=\"gaugeMinValue()\"\n (ngModelChange)=\"setGaugeMin($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.gaugeMax', 'Scale maximum') }}</mat-label>\n <input\n matInput\n type=\"number\"\n data-testid=\"chart-editor-gauge-max\"\n [ngModel]=\"gaugeMaxValue()\"\n (ngModelChange)=\"setGaugeMax($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n </div>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showComboPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comboTitle', 'Combo guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.comboHint', 'Combo charts require at least two metrics and allow per-metric axis and series kind mapping.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showPieDonutPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.pieDonutTitle', 'Composition guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.pieDonutHint', 'Pie, donut, funnel, pyramid and treemap charts keep one metric and use the first dimension as the category segment.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showScatterPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.scatterTitle', 'Scatter guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.scatterHint', 'With one metric, scatter uses the first dimension as X and the metric as Y. With two metrics, the dimension identifies the grouped bucket and the metrics map to X and Y.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.dimensionsTitle', 'Dimensions') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (dimension of dimensions(); track $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-editor-dimension-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-field-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.dimension', 'Dimension') }}</mat-label>\n @if (fieldOptions('dimension').length) {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('dimension'); track field.field) {\n <mat-option [value]=\"field.field\">{{ fieldOptionLabel(field) }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.dimensionFieldsMissing', 'No governed dimensions are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-role-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.dimensionRole', 'Dimension role') }}</mat-label>\n <mat-select [ngModel]=\"dimension.role || 'category'\" (ngModelChange)=\"setDimensionRole($index, $event)\" [disabled]=\"isReadonly()\">\n @for (role of dimensionRoles; track role) {\n <mat-option [value]=\"role\">\n {{ t('praxis.charts.editor.dimensionRole.' + role, role) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" (click)=\"removeDimension($index)\" [disabled]=\"isReadonly() || dimensions().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeDimension', 'Remove dimension') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-dimension\" (click)=\"addDimension()\" [disabled]=\"isReadonly()\">\n {{ t('praxis.charts.editor.analytics.addDimension', 'Add dimension') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.metricsTitle', 'Metrics') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (metric of metrics(); track $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-editor-metric-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-field-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metric', 'Metric') }}</mat-label>\n @if (fieldOptions('metric').length) {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('metric'); track field.field) {\n <mat-option [value]=\"field.field\">{{ fieldOptionLabel(field) }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.metricFieldsMissing', 'No governed metrics are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-label-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricLabel', 'Metric label') }}</mat-label>\n <input matInput [ngModel]=\"metric.label || ''\" (ngModelChange)=\"setMetricLabel($index, $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-aggregation-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAggregation', 'Aggregation') }}</mat-label>\n <mat-select [ngModel]=\"metric.aggregation || 'sum'\" (ngModelChange)=\"setMetricAggregation($index, $event)\" [disabled]=\"isReadonly()\">\n @for (aggregation of metricAggregationOptions(metric.field); track aggregation) {\n <mat-option [value]=\"aggregation\">\n {{ t('praxis.charts.editor.metricAggregation.' + aggregation, aggregation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (showMetricAxisControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-axis-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAxis', 'Axis') }}</mat-label>\n <mat-select [ngModel]=\"metric.axis || 'primary'\" (ngModelChange)=\"setMetricAxis($index, $event)\" [disabled]=\"isReadonly()\">\n @for (axis of metricAxes; track axis) {\n <mat-option [value]=\"axis\">\n {{ t('praxis.charts.editor.metricAxis.' + axis, axis) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @if (showMetricSeriesKindControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-series-kind-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricSeriesKind', 'Series kind') }}</mat-label>\n <mat-select [ngModel]=\"metric.seriesKind || 'bar'\" (ngModelChange)=\"setMetricSeriesKind($index, $event)\" [disabled]=\"isReadonly()\">\n @for (seriesKind of metricSeriesKinds; track seriesKind) {\n <mat-option [value]=\"seriesKind\">\n {{ t('praxis.charts.editor.metricSeriesKind.' + seriesKind, seriesKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" [attr.data-testid]=\"'chart-editor-remove-metric-' + $index\" (click)=\"removeMetric($index)\" [disabled]=\"isReadonly() || metrics().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeMetric', 'Remove metric') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-metric\" (click)=\"addMetric()\" [disabled]=\"isReadonly() || !canAddMetric()\">\n {{ t('praxis.charts.editor.analytics.addMetric', 'Add metric') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('events') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-pointClick\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.pointClickTitle', 'Point click') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-action\" [ngModel]=\"eventAction('pointClick')\" (ngModelChange)=\"setEventAction('pointClick', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick') || targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')\">\n @for (target of targetOptions(eventAction('pointClick'), 'pointClick'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-pointClick-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('pointClick')\"\n (ngModelChange)=\"setEventMapping('pointClick', $event)\"\n [disabled]=\"isReadonly() || !eventAction('pointClick')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-selectionChange\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.selectionChangeTitle', 'Selection change') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-action\" [ngModel]=\"eventAction('selectionChange')\" (ngModelChange)=\"setEventAction('selectionChange', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange') || targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')\">\n @for (target of targetOptions(eventAction('selectionChange'), 'selectionChange'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-selectionChange-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('selectionChange')\"\n (ngModelChange)=\"setEventMapping('selectionChange', $event)\"\n [disabled]=\"isReadonly() || !eventAction('selectionChange')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-drillDown\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.drillDownTitle', 'Drill down') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-action\" [ngModel]=\"eventAction('drillDown')\" (ngModelChange)=\"setEventAction('drillDown', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown') || targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')\">\n @for (target of targetOptions(eventAction('drillDown'), 'drillDown'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-drillDown-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('drillDown')\"\n (ngModelChange)=\"setEventMapping('drillDown', $event)\"\n [disabled]=\"isReadonly() || !eventAction('drillDown')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-crossFilter\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.crossFilterTitle', 'Cross filter') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-action\" [ngModel]=\"eventAction('crossFilter')\" (ngModelChange)=\"setEventAction('crossFilter', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter') || targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')\">\n @for (target of targetOptions(eventAction('crossFilter'), 'crossFilter'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-crossFilter-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('crossFilter')\"\n (ngModelChange)=\"setEventMapping('crossFilter', $event)\"\n [disabled]=\"isReadonly() || !eventAction('crossFilter')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('preview') {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n }\n }\n </mat-card-content>\n </mat-card>\n </div>\n\n <div class=\"editor-side\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.issues.title', 'Validation issues') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (issues().length) {\n <ul class=\"editor-issues\">\n @for (issue of issues(); track issueTrackBy($index, issue)) {\n <li class=\"editor-issue\">\n <strong>{{ issue.field }}</strong>\n <span>{{ issue.message }}</span>\n </li>\n }\n </ul>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.issues.empty', 'No issues were identified.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.preview.title', 'Chart preview') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (preview(); as chartPreview) {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n @if (doc().source.kind === 'derived') {\n <p class=\"editor-caption\" data-testid=\"chart-editor-preview-origin\">\n {{\n chartPreview.dataOrigin === 'local'\n ? t('praxis.charts.editor.preview.originLocal', 'Previewing widget local dataset (inputs.data).')\n : t('praxis.charts.editor.preview.originSynthetic', 'Previewing synthetic sample data (no local dataset records).')\n }}\n </p>\n }\n <praxis-chart [config]=\"chartPreview.config\" [data]=\"chartPreview.data\"></praxis-chart>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.preview.invalid', 'Preview is unavailable while the contract has blocking errors.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n</div>\n", styles: [":host{display:block;min-width:0;container:praxis-chart-editor / inline-size;color:var(--md-sys-color-on-surface, #1a1b20)}.editor-shell{display:grid;gap:18px;min-width:0}.editor-nav{display:flex;gap:8px;flex-wrap:wrap}.editor-nav button.active{background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 18%,transparent);color:var(--md-sys-color-primary, #1263b4)}.editor-layout{display:grid;gap:18px;grid-template-columns:minmax(0,1.35fr) minmax(320px,.9fr);align-items:start}.editor-form,.editor-side{display:grid;gap:16px;min-width:0}.editor-card{box-sizing:border-box;width:100%;min-width:0;border-radius:20px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#1263b408,#1263b400)}.editor-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-stack{display:grid;gap:14px}.editor-row-card{padding:14px;border-radius:16px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 54%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 92%,rgba(18,99,180,.04))}.editor-row-actions{display:flex;justify-content:flex-end}.editor-field{width:100%}.editor-issues{display:grid;gap:10px;margin:0;padding:0;list-style:none}.editor-issue{padding:12px 14px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-error, #b3261e) 8%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-error, #b3261e) 18%,transparent)}.editor-issue strong{display:block;margin-bottom:4px}.editor-caption{margin:0 0 12px;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.92rem}.editor-empty{padding:18px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-variant, #eceff4) 78%,transparent)}.editor-side praxis-chart{display:block;width:100%;min-width:0;max-width:100%}@container praxis-chart-editor (max-width: 840px){.editor-layout{grid-template-columns:minmax(0,1fr)}}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"] }]
|
|
10459
|
+
}], ctorParameters: () => [], propDecorators: { documentInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "document", required: false }] }], localDataInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "localData", required: false }] }], modeInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], readonlyInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], availableResourcesInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableResources", required: false }] }], availableFieldsInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableFields", required: false }] }], availableTargetsInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableTargets", required: false }] }], apply: [{ type: i0.Output, args: ["apply"] }], save: [{ type: i0.Output, args: ["save"] }], resetChange: [{ type: i0.Output, args: ["resetChange"] }], documentChange: [{ type: i0.Output, args: ["documentChange"] }], localDataChange: [{ type: i0.Output, args: ["localDataChange"] }] } });
|
|
10016
10460
|
|
|
10017
10461
|
class PraxisChartWidgetConfigEditor {
|
|
10018
10462
|
i18n = inject(PraxisI18nService);
|
|
@@ -10020,10 +10464,12 @@ class PraxisChartWidgetConfigEditor {
|
|
|
10020
10464
|
inputs = null;
|
|
10021
10465
|
context = null;
|
|
10022
10466
|
contextDiagnostics = [];
|
|
10467
|
+
diagnosticsExpanded = signal(false, ...(ngDevMode ? [{ debugName: "diagnosticsExpanded" }] : /* istanbul ignore next */ []));
|
|
10023
10468
|
activeChartEditor;
|
|
10024
10469
|
chartEditorSubscription = new Subscription();
|
|
10025
10470
|
derivedChartDocument = null;
|
|
10026
10471
|
lastDerivedFromConfig = null;
|
|
10472
|
+
lastSyncedInputs = null;
|
|
10027
10473
|
set chartEditor(value) {
|
|
10028
10474
|
if (this.activeChartEditor === value) {
|
|
10029
10475
|
return;
|
|
@@ -10049,9 +10495,13 @@ class PraxisChartWidgetConfigEditor {
|
|
|
10049
10495
|
chartEditorValid = true;
|
|
10050
10496
|
queryContextDirty = false;
|
|
10051
10497
|
baselineQueryContextText = '';
|
|
10498
|
+
localDataDirty = false;
|
|
10499
|
+
baselineLocalData = undefined;
|
|
10500
|
+
currentLocalDataState = undefined;
|
|
10052
10501
|
queryContextText = '';
|
|
10053
10502
|
queryContextError = '';
|
|
10054
10503
|
get chartDocument() {
|
|
10504
|
+
this.ensureInputsSynced();
|
|
10055
10505
|
if (this.inputs?.chartDocument) {
|
|
10056
10506
|
return this.inputs.chartDocument;
|
|
10057
10507
|
}
|
|
@@ -10070,6 +10520,10 @@ class PraxisChartWidgetConfigEditor {
|
|
|
10070
10520
|
}
|
|
10071
10521
|
return null;
|
|
10072
10522
|
}
|
|
10523
|
+
get localData() {
|
|
10524
|
+
this.ensureInputsSynced();
|
|
10525
|
+
return this.currentLocalDataState;
|
|
10526
|
+
}
|
|
10073
10527
|
get availableResources() {
|
|
10074
10528
|
return this.context?.availableResources ?? this.inputs?.availableResources ?? [];
|
|
10075
10529
|
}
|
|
@@ -10079,9 +10533,56 @@ class PraxisChartWidgetConfigEditor {
|
|
|
10079
10533
|
get availableTargets() {
|
|
10080
10534
|
return this.context?.availableTargets ?? this.inputs?.availableTargets ?? [];
|
|
10081
10535
|
}
|
|
10536
|
+
get groupedContextDiagnostics() {
|
|
10537
|
+
if (!this.contextDiagnostics?.length) {
|
|
10538
|
+
return [];
|
|
10539
|
+
}
|
|
10540
|
+
const groups = new Map();
|
|
10541
|
+
for (const diagnostic of this.contextDiagnostics) {
|
|
10542
|
+
const code = String(diagnostic.code || 'chart-resource-context-partial').trim() || 'chart-resource-context-partial';
|
|
10543
|
+
const severity = diagnostic.severity === 'error'
|
|
10544
|
+
? 'error'
|
|
10545
|
+
: diagnostic.severity === 'warning'
|
|
10546
|
+
? 'warning'
|
|
10547
|
+
: 'info';
|
|
10548
|
+
const path = String(diagnostic.path || '').trim();
|
|
10549
|
+
const existing = groups.get(code);
|
|
10550
|
+
if (!existing) {
|
|
10551
|
+
const paths = new Set();
|
|
10552
|
+
if (path) {
|
|
10553
|
+
paths.add(path);
|
|
10554
|
+
}
|
|
10555
|
+
groups.set(code, {
|
|
10556
|
+
code,
|
|
10557
|
+
severity,
|
|
10558
|
+
count: 1,
|
|
10559
|
+
paths,
|
|
10560
|
+
});
|
|
10561
|
+
}
|
|
10562
|
+
else {
|
|
10563
|
+
existing.count += 1;
|
|
10564
|
+
if (severity === 'error' || (severity === 'warning' && existing.severity === 'info')) {
|
|
10565
|
+
existing.severity = severity;
|
|
10566
|
+
}
|
|
10567
|
+
if (path) {
|
|
10568
|
+
existing.paths.add(path);
|
|
10569
|
+
}
|
|
10570
|
+
}
|
|
10571
|
+
}
|
|
10572
|
+
return Array.from(groups.values()).map((entry) => ({
|
|
10573
|
+
code: entry.code,
|
|
10574
|
+
severity: entry.severity,
|
|
10575
|
+
count: entry.count,
|
|
10576
|
+
cause: this.contextDiagnosticText({ code: entry.code, severity: entry.severity }),
|
|
10577
|
+
nextStep: this.contextDiagnosticNextStep(entry.code),
|
|
10578
|
+
paths: Array.from(entry.paths),
|
|
10579
|
+
}));
|
|
10580
|
+
}
|
|
10082
10581
|
ngOnChanges(changes) {
|
|
10083
10582
|
if (changes['inputs'] && (changes['inputs'].firstChange || !this.isDirty$.value)) {
|
|
10583
|
+
this.lastSyncedInputs = this.inputs;
|
|
10084
10584
|
this.rebaseQueryContext();
|
|
10585
|
+
this.rebaseLocalData();
|
|
10085
10586
|
if (this.inputs?.config !== this.lastDerivedFromConfig) {
|
|
10086
10587
|
this.derivedChartDocument = null;
|
|
10087
10588
|
this.lastDerivedFromConfig = null;
|
|
@@ -10091,7 +10592,25 @@ class PraxisChartWidgetConfigEditor {
|
|
|
10091
10592
|
ngOnDestroy() {
|
|
10092
10593
|
this.chartEditorSubscription.unsubscribe();
|
|
10093
10594
|
}
|
|
10595
|
+
toggleDiagnostics() {
|
|
10596
|
+
this.diagnosticsExpanded.update((expanded) => !expanded);
|
|
10597
|
+
}
|
|
10598
|
+
contextDiagnosticsSummaryText() {
|
|
10599
|
+
const count = this.contextDiagnostics.length;
|
|
10600
|
+
if (count === 1) {
|
|
10601
|
+
return this.t('praxis.charts.widget.contextDiagnostics.summarySingle', '1 authoring context diagnostic detected.');
|
|
10602
|
+
}
|
|
10603
|
+
return this.t('praxis.charts.widget.contextDiagnostics.summaryMultiple', '{{ count }} authoring context diagnostics detected.', { count });
|
|
10604
|
+
}
|
|
10605
|
+
onLocalDataChange(nextData) {
|
|
10606
|
+
this.ensureInputsSynced();
|
|
10607
|
+
this.currentLocalDataState = nextData ? structuredClone(nextData) : undefined;
|
|
10608
|
+
this.localDataDirty = JSON.stringify(this.currentLocalDataState) !== JSON.stringify(this.baselineLocalData);
|
|
10609
|
+
this.updateDirty();
|
|
10610
|
+
this.updateValidity();
|
|
10611
|
+
}
|
|
10094
10612
|
setQueryContextText(value) {
|
|
10613
|
+
this.ensureInputsSynced();
|
|
10095
10614
|
this.queryContextText = value;
|
|
10096
10615
|
this.queryContextDirty = value !== this.baselineQueryContextText;
|
|
10097
10616
|
this.queryContextError = this.validateQueryContextText(value);
|
|
@@ -10099,20 +10618,31 @@ class PraxisChartWidgetConfigEditor {
|
|
|
10099
10618
|
this.updateValidity();
|
|
10100
10619
|
}
|
|
10101
10620
|
getSettingsValue() {
|
|
10621
|
+
this.ensureInputsSynced();
|
|
10102
10622
|
return this.buildValue(this.getChartDocumentForApply());
|
|
10103
10623
|
}
|
|
10104
10624
|
onSave() {
|
|
10625
|
+
this.ensureInputsSynced();
|
|
10105
10626
|
const value = this.buildValue(this.isValid$.value
|
|
10106
10627
|
? this.getChartDocumentForSave()
|
|
10107
10628
|
: this.getChartDocumentForApply());
|
|
10108
10629
|
if (this.isValid$.value) {
|
|
10109
10630
|
this.baselineQueryContextText = this.queryContextText;
|
|
10110
10631
|
this.queryContextDirty = false;
|
|
10632
|
+
this.baselineLocalData = this.currentLocalDataState
|
|
10633
|
+
? structuredClone(this.currentLocalDataState)
|
|
10634
|
+
: undefined;
|
|
10635
|
+
this.localDataDirty = false;
|
|
10111
10636
|
this.updateDirty();
|
|
10112
10637
|
}
|
|
10113
10638
|
return value;
|
|
10114
10639
|
}
|
|
10115
10640
|
reset() {
|
|
10641
|
+
this.ensureInputsSynced();
|
|
10642
|
+
this.currentLocalDataState = this.baselineLocalData
|
|
10643
|
+
? structuredClone(this.baselineLocalData)
|
|
10644
|
+
: undefined;
|
|
10645
|
+
this.localDataDirty = false;
|
|
10116
10646
|
this.chartEditor?.reset();
|
|
10117
10647
|
this.queryContextText = this.baselineQueryContextText;
|
|
10118
10648
|
this.queryContextError = '';
|
|
@@ -10122,8 +10652,8 @@ class PraxisChartWidgetConfigEditor {
|
|
|
10122
10652
|
this.updateDirty();
|
|
10123
10653
|
this.updateValidity();
|
|
10124
10654
|
}
|
|
10125
|
-
t(key, fallback) {
|
|
10126
|
-
return this.i18n.t(key,
|
|
10655
|
+
t(key, fallback, params) {
|
|
10656
|
+
return this.i18n.t(key, params, fallback, PRAXIS_CHARTS_I18N_NAMESPACE);
|
|
10127
10657
|
}
|
|
10128
10658
|
contextDiagnosticText(diagnostic) {
|
|
10129
10659
|
switch (diagnostic.code) {
|
|
@@ -10135,6 +10665,10 @@ class PraxisChartWidgetConfigEditor {
|
|
|
10135
10665
|
return this.t('praxis.charts.widget.contextDiagnostics.catalogEmpty', 'No authorable stats resources were published by the governed catalog.');
|
|
10136
10666
|
case 'chart-resource-catalog-path-invalid':
|
|
10137
10667
|
return this.t('praxis.charts.widget.contextDiagnostics.catalogPathInvalid', 'The resource catalog published an unsafe or non-relative operational path.');
|
|
10668
|
+
case 'chart-resource-catalog-selection-stale':
|
|
10669
|
+
return this.t('praxis.charts.widget.contextDiagnostics.selectionStale', 'The selected stats resource is not available in the governed catalog.');
|
|
10670
|
+
case 'chart-resource-capabilities-ambiguous':
|
|
10671
|
+
return this.t('praxis.charts.widget.contextDiagnostics.capabilitiesAmbiguous', 'Multiple capability endpoints were published for the resource, preventing automatic discovery.');
|
|
10138
10672
|
case 'chart-resource-capabilities-denied':
|
|
10139
10673
|
return this.t('praxis.charts.widget.contextDiagnostics.capabilitiesDenied', 'Access to the selected resource capabilities was denied.');
|
|
10140
10674
|
case 'chart-resource-capabilities-unavailable':
|
|
@@ -10153,6 +10687,38 @@ class PraxisChartWidgetConfigEditor {
|
|
|
10153
10687
|
return this.t('praxis.charts.widget.contextDiagnostics.partial', 'Some governed authoring choices could not be materialized from the current page context.');
|
|
10154
10688
|
}
|
|
10155
10689
|
}
|
|
10690
|
+
contextDiagnosticNextStep(code) {
|
|
10691
|
+
switch (code) {
|
|
10692
|
+
case 'chart-resource-catalog-denied':
|
|
10693
|
+
return this.t('praxis.charts.widget.contextDiagnostics.nextStep.catalogDenied', 'Verify permissions for /schemas/catalog or switch the data source to derived.');
|
|
10694
|
+
case 'chart-resource-catalog-unavailable':
|
|
10695
|
+
return this.t('praxis.charts.widget.contextDiagnostics.nextStep.catalogUnavailable', 'Retry after the backend catalog service responds or keep editing visual/derived settings.');
|
|
10696
|
+
case 'chart-resource-catalog-empty':
|
|
10697
|
+
return this.t('praxis.charts.widget.contextDiagnostics.nextStep.catalogEmpty', 'Publish at least one stats-enabled resource in /schemas/catalog or use a derived data source.');
|
|
10698
|
+
case 'chart-resource-catalog-path-invalid':
|
|
10699
|
+
return this.t('praxis.charts.widget.contextDiagnostics.nextStep.catalogPathInvalid', 'Fix the resource path in the backend catalog so it uses a relative operational path.');
|
|
10700
|
+
case 'chart-resource-catalog-selection-stale':
|
|
10701
|
+
return this.t('praxis.charts.widget.contextDiagnostics.nextStep.selectionStale', 'Select an active resource from the governed catalog or publish the resource in /schemas/catalog.');
|
|
10702
|
+
case 'chart-resource-capabilities-ambiguous':
|
|
10703
|
+
return this.t('praxis.charts.widget.contextDiagnostics.nextStep.capabilitiesAmbiguous', 'Ensure the resource publishes a single canonical collection /capabilities endpoint in the catalog.');
|
|
10704
|
+
case 'chart-resource-capabilities-denied':
|
|
10705
|
+
return this.t('praxis.charts.widget.contextDiagnostics.nextStep.capabilitiesDenied', 'Grant read access to the resource /capabilities endpoint or select another resource.');
|
|
10706
|
+
case 'chart-resource-capabilities-unavailable':
|
|
10707
|
+
return this.t('praxis.charts.widget.contextDiagnostics.nextStep.capabilitiesUnavailable', 'Check the resource /capabilities endpoint availability or select another resource.');
|
|
10708
|
+
case 'chart-resource-stats-operations-empty':
|
|
10709
|
+
return this.t('praxis.charts.widget.contextDiagnostics.nextStep.operationsEmpty', 'Enable stats operations (group-by, timeseries, distribution, or comparison) on the backend resource.');
|
|
10710
|
+
case 'chart-resource-stats-fields-empty':
|
|
10711
|
+
return this.t('praxis.charts.widget.contextDiagnostics.nextStep.fieldsEmpty', 'Expose eligible fields in the resource capability snapshot or filtered schema.');
|
|
10712
|
+
case 'chart-resource-capabilities-identity-mismatch':
|
|
10713
|
+
return this.t('praxis.charts.widget.contextDiagnostics.nextStep.identityMismatch', 'Align resourceKey and resourcePath between /schemas/catalog and the /capabilities snapshot.');
|
|
10714
|
+
case 'chart-resource-capabilities-path-invalid':
|
|
10715
|
+
return this.t('praxis.charts.widget.contextDiagnostics.nextStep.capabilitiesPathInvalid', 'Ensure resourcePath in the /capabilities snapshot is a safe relative path.');
|
|
10716
|
+
case 'chart-resource-capabilities-path-mismatch':
|
|
10717
|
+
return this.t('praxis.charts.widget.contextDiagnostics.nextStep.capabilitiesPathMismatch', 'Align the resource operational path in /schemas/catalog with resourcePath in /capabilities.');
|
|
10718
|
+
default:
|
|
10719
|
+
return this.t('praxis.charts.widget.contextDiagnostics.nextStep.partial', 'Inspect the affected resource contract in /schemas/catalog and /capabilities.');
|
|
10720
|
+
}
|
|
10721
|
+
}
|
|
10156
10722
|
buildValue(document) {
|
|
10157
10723
|
const inputs = {
|
|
10158
10724
|
...(this.inputs ?? {}),
|
|
@@ -10178,9 +10744,15 @@ class PraxisChartWidgetConfigEditor {
|
|
|
10178
10744
|
else if (queryContext.valid) {
|
|
10179
10745
|
inputs.queryContext = queryContext.value;
|
|
10180
10746
|
}
|
|
10747
|
+
const effectiveLocalData = this.chartEditor
|
|
10748
|
+
? this.chartEditor.getLocalDataValue()
|
|
10749
|
+
: this.currentLocalDataState;
|
|
10181
10750
|
if (document?.source?.kind === 'praxis.stats') {
|
|
10182
10751
|
delete inputs.data;
|
|
10183
10752
|
}
|
|
10753
|
+
else if (effectiveLocalData !== undefined) {
|
|
10754
|
+
inputs.data = structuredClone(effectiveLocalData);
|
|
10755
|
+
}
|
|
10184
10756
|
delete inputs.availableResources;
|
|
10185
10757
|
delete inputs.availableFields;
|
|
10186
10758
|
delete inputs.availableTargets;
|
|
@@ -10212,6 +10784,17 @@ class PraxisChartWidgetConfigEditor {
|
|
|
10212
10784
|
}
|
|
10213
10785
|
return this.chartEditor?.onSave?.() ?? this.chartEditor?.getSettingsValue();
|
|
10214
10786
|
}
|
|
10787
|
+
ensureInputsSynced() {
|
|
10788
|
+
if (this.inputs === this.lastSyncedInputs) {
|
|
10789
|
+
return;
|
|
10790
|
+
}
|
|
10791
|
+
const isInitialSync = this.lastSyncedInputs === null;
|
|
10792
|
+
this.lastSyncedInputs = this.inputs;
|
|
10793
|
+
if (isInitialSync || !this.isDirty$.value) {
|
|
10794
|
+
this.rebaseQueryContext();
|
|
10795
|
+
this.rebaseLocalData();
|
|
10796
|
+
}
|
|
10797
|
+
}
|
|
10215
10798
|
rebaseQueryContext() {
|
|
10216
10799
|
this.baselineQueryContextText = this.formatQueryContext(this.inputs?.queryContext);
|
|
10217
10800
|
this.queryContextText = this.baselineQueryContextText;
|
|
@@ -10220,8 +10803,25 @@ class PraxisChartWidgetConfigEditor {
|
|
|
10220
10803
|
this.updateDirty();
|
|
10221
10804
|
this.updateValidity();
|
|
10222
10805
|
}
|
|
10806
|
+
rebaseLocalData() {
|
|
10807
|
+
const rawData = this.inputs?.['data'];
|
|
10808
|
+
if (Array.isArray(rawData)) {
|
|
10809
|
+
const normalizedRows = rawData
|
|
10810
|
+
.filter((item) => !!item && typeof item === 'object' && !Array.isArray(item))
|
|
10811
|
+
.map((item) => structuredClone(item));
|
|
10812
|
+
this.baselineLocalData = normalizedRows;
|
|
10813
|
+
this.currentLocalDataState = structuredClone(normalizedRows);
|
|
10814
|
+
}
|
|
10815
|
+
else {
|
|
10816
|
+
this.baselineLocalData = undefined;
|
|
10817
|
+
this.currentLocalDataState = undefined;
|
|
10818
|
+
}
|
|
10819
|
+
this.localDataDirty = false;
|
|
10820
|
+
this.updateDirty();
|
|
10821
|
+
this.updateValidity();
|
|
10822
|
+
}
|
|
10223
10823
|
updateDirty() {
|
|
10224
|
-
this.isDirty$.next(this.chartEditorDirty || this.queryContextDirty);
|
|
10824
|
+
this.isDirty$.next(this.chartEditorDirty || this.queryContextDirty || this.localDataDirty);
|
|
10225
10825
|
}
|
|
10226
10826
|
updateValidity() {
|
|
10227
10827
|
this.isValid$.next(this.chartEditorValid && !this.queryContextError);
|
|
@@ -10266,29 +10866,104 @@ class PraxisChartWidgetConfigEditor {
|
|
|
10266
10866
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartWidgetConfigEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10267
10867
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisChartWidgetConfigEditor, isStandalone: true, selector: "praxis-chart-widget-config-editor", inputs: { inputs: "inputs", context: "context", contextDiagnostics: "contextDiagnostics" }, providers: [providePraxisChartsI18n()], viewQueries: [{ propertyName: "chartEditor", first: true, predicate: ["chartEditor"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
|
|
10268
10868
|
<section data-testid="chart-widget-config-editor">
|
|
10269
|
-
@if (
|
|
10869
|
+
@if (groupedContextDiagnostics.length) {
|
|
10270
10870
|
<section
|
|
10271
10871
|
class="chart-widget-config-editor__diagnostics"
|
|
10272
10872
|
data-testid="chart-widget-context-diagnostics"
|
|
10273
10873
|
role="status"
|
|
10274
10874
|
>
|
|
10275
|
-
<
|
|
10276
|
-
|
|
10277
|
-
|
|
10278
|
-
<
|
|
10279
|
-
{{
|
|
10875
|
+
<div class="chart-widget-config-editor__diagnostics-header">
|
|
10876
|
+
<div class="chart-widget-config-editor__diagnostics-heading">
|
|
10877
|
+
<strong>{{ t('praxis.charts.widget.contextDiagnostics.title', 'Authoring context') }}</strong>
|
|
10878
|
+
<span class="chart-widget-config-editor__diagnostics-summary" data-testid="chart-widget-context-diagnostics-summary">
|
|
10879
|
+
{{ contextDiagnosticsSummaryText() }}
|
|
10880
|
+
</span>
|
|
10881
|
+
</div>
|
|
10882
|
+
<button
|
|
10883
|
+
type="button"
|
|
10884
|
+
class="chart-widget-config-editor__diagnostics-toggle"
|
|
10885
|
+
data-testid="chart-widget-context-diagnostics-toggle"
|
|
10886
|
+
[attr.aria-expanded]="diagnosticsExpanded()"
|
|
10887
|
+
(click)="toggleDiagnostics()"
|
|
10888
|
+
>
|
|
10889
|
+
{{
|
|
10890
|
+
diagnosticsExpanded()
|
|
10891
|
+
? t('praxis.charts.widget.contextDiagnostics.hideDetails', 'Hide details')
|
|
10892
|
+
: t('praxis.charts.widget.contextDiagnostics.showDetails', 'View details')
|
|
10893
|
+
}}
|
|
10894
|
+
</button>
|
|
10895
|
+
</div>
|
|
10896
|
+
<p class="chart-widget-config-editor__diagnostics-scope">
|
|
10897
|
+
{{
|
|
10898
|
+
t(
|
|
10899
|
+
'praxis.charts.widget.contextDiagnostics.scopeImpact',
|
|
10900
|
+
'Affected: governed stats resource/field catalog discovery. Remains editable: visual layout, derived local data, and query context.'
|
|
10901
|
+
)
|
|
10902
|
+
}}
|
|
10903
|
+
</p>
|
|
10904
|
+
<ul class="chart-widget-config-editor__diagnostics-groups">
|
|
10905
|
+
@for (group of groupedContextDiagnostics; track group.code) {
|
|
10906
|
+
<li
|
|
10907
|
+
[class]="'chart-widget-config-editor__diagnostic--' + group.severity"
|
|
10908
|
+
data-testid="chart-widget-context-diagnostic-group"
|
|
10909
|
+
>
|
|
10910
|
+
<span>{{ group.cause }}</span>
|
|
10911
|
+
@if (group.count > 1) {
|
|
10912
|
+
<span class="chart-widget-config-editor__diagnostic-count" data-testid="chart-widget-context-diagnostic-count">
|
|
10913
|
+
({{
|
|
10914
|
+
t(
|
|
10915
|
+
'praxis.charts.widget.contextDiagnostics.occurrences',
|
|
10916
|
+
'{{ count }} occurrences',
|
|
10917
|
+
{ count: group.count }
|
|
10918
|
+
)
|
|
10919
|
+
}})
|
|
10920
|
+
</span>
|
|
10921
|
+
}
|
|
10280
10922
|
</li>
|
|
10281
10923
|
}
|
|
10282
10924
|
</ul>
|
|
10925
|
+
@if (diagnosticsExpanded()) {
|
|
10926
|
+
<div
|
|
10927
|
+
class="chart-widget-config-editor__diagnostics-details"
|
|
10928
|
+
data-testid="chart-widget-context-diagnostics-details"
|
|
10929
|
+
>
|
|
10930
|
+
@for (group of groupedContextDiagnostics; track group.code) {
|
|
10931
|
+
<div
|
|
10932
|
+
class="chart-widget-config-editor__diagnostic-detail"
|
|
10933
|
+
data-testid="chart-widget-context-diagnostic-detail"
|
|
10934
|
+
>
|
|
10935
|
+
<div class="chart-widget-config-editor__diagnostic-detail-row">
|
|
10936
|
+
<strong>{{ group.cause }}</strong>
|
|
10937
|
+
</div>
|
|
10938
|
+
@if (group.paths.length) {
|
|
10939
|
+
<div class="chart-widget-config-editor__diagnostic-detail-row" data-testid="chart-widget-diagnostic-paths">
|
|
10940
|
+
<span>{{ t('praxis.charts.widget.contextDiagnostics.affectedPaths', 'Affected resources/paths:') }}</span>
|
|
10941
|
+
<code>{{ group.paths.join(', ') }}</code>
|
|
10942
|
+
</div>
|
|
10943
|
+
}
|
|
10944
|
+
<div class="chart-widget-config-editor__diagnostic-detail-row" data-testid="chart-widget-diagnostic-next-step">
|
|
10945
|
+
<span>{{ t('praxis.charts.widget.contextDiagnostics.nextStepLabel', 'Next step:') }}</span>
|
|
10946
|
+
<span>{{ group.nextStep }}</span>
|
|
10947
|
+
</div>
|
|
10948
|
+
<div class="chart-widget-config-editor__diagnostic-detail-row">
|
|
10949
|
+
<span>{{ t('praxis.charts.widget.contextDiagnostics.technicalCode', 'Technical code:') }}</span>
|
|
10950
|
+
<code data-testid="chart-widget-diagnostic-code">{{ group.code }}</code>
|
|
10951
|
+
</div>
|
|
10952
|
+
</div>
|
|
10953
|
+
}
|
|
10954
|
+
</div>
|
|
10955
|
+
}
|
|
10283
10956
|
</section>
|
|
10284
10957
|
}
|
|
10285
10958
|
@if (chartDocument) {
|
|
10286
10959
|
<praxis-chart-config-editor
|
|
10287
10960
|
#chartEditor
|
|
10288
10961
|
[document]="chartDocument"
|
|
10962
|
+
[localData]="localData"
|
|
10289
10963
|
[availableResources]="availableResources"
|
|
10290
10964
|
[availableFields]="availableFields"
|
|
10291
10965
|
[availableTargets]="availableTargets"
|
|
10966
|
+
(localDataChange)="onLocalDataChange($event)"
|
|
10292
10967
|
/>
|
|
10293
10968
|
} @else {
|
|
10294
10969
|
<section class="chart-widget-config-editor__notice" data-testid="chart-widget-missing-document">
|
|
@@ -10316,35 +10991,110 @@ class PraxisChartWidgetConfigEditor {
|
|
|
10316
10991
|
}
|
|
10317
10992
|
</section>
|
|
10318
10993
|
</section>
|
|
10319
|
-
`, isInline: true, styles: [".chart-widget-config-editor__runtime{display:grid;gap:8px;margin-top:16px}.chart-widget-config-editor__diagnostics{display:grid;gap:
|
|
10994
|
+
`, isInline: true, styles: [".chart-widget-config-editor__runtime{display:grid;gap:8px;margin-top:16px}.chart-widget-config-editor__diagnostics{display:grid;gap:8px;margin-bottom:16px;padding:12px 14px;border:1px solid color-mix(in srgb,var(--md-sys-color-tertiary, #765b00) 28%,transparent);border-radius:10px;background:color-mix(in srgb,var(--md-sys-color-tertiary-container, #ffdf92) 42%,var(--md-sys-color-surface, #fff));color:var(--md-sys-color-on-tertiary-container, #271900);font-size:12px;line-height:1.4}.chart-widget-config-editor__diagnostics-header{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.chart-widget-config-editor__diagnostics-heading{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}.chart-widget-config-editor__diagnostics-summary{opacity:.88}.chart-widget-config-editor__diagnostics-scope{margin:0;opacity:.86}.chart-widget-config-editor__diagnostics-toggle{padding:4px 10px;border:1px solid color-mix(in srgb,currentColor 28%,transparent);border-radius:999px;background:transparent;color:inherit;font:inherit;font-weight:600;cursor:pointer}.chart-widget-config-editor__diagnostics ul{display:grid;gap:4px;margin:0;padding-left:18px}.chart-widget-config-editor__diagnostic-count{margin-left:6px;font-weight:600}.chart-widget-config-editor__diagnostics-details{display:grid;gap:8px;margin-top:4px;padding-top:8px;border-top:1px solid color-mix(in srgb,currentColor 18%,transparent)}.chart-widget-config-editor__diagnostic-detail{display:grid;gap:4px;padding:8px 10px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 65%,transparent)}.chart-widget-config-editor__diagnostic-detail-row{display:flex;gap:6px;flex-wrap:wrap;align-items:baseline}.chart-widget-config-editor__diagnostic--error{color:var(--md-sys-color-error, #b3261e)}.chart-widget-config-editor__label{font-size:12px;font-weight:600;color:#000000b8}.chart-widget-config-editor__textarea{box-sizing:border-box;width:100%;min-height:112px;padding:10px 12px;border:1px solid rgba(0,0,0,.2);border-radius:6px;font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,monospace;resize:vertical}.chart-widget-config-editor__error{color:#b3261e;font-size:12px}.chart-widget-config-editor__notice{padding:12px;border:1px solid rgba(0,0,0,.12);border-radius:6px;color:#000000b8;font-size:12px;line-height:1.4;background:#00000008}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.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$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: PraxisChartConfigEditor, selector: "praxis-chart-config-editor", inputs: ["document", "localData", "mode", "readonly", "availableResources", "availableFields", "availableTargets"], outputs: ["apply", "save", "resetChange", "documentChange", "localDataChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
10320
10995
|
}
|
|
10321
10996
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartWidgetConfigEditor, decorators: [{
|
|
10322
10997
|
type: Component,
|
|
10323
10998
|
args: [{ selector: 'praxis-chart-widget-config-editor', standalone: true, imports: [FormsModule, PraxisChartConfigEditor], template: `
|
|
10324
10999
|
<section data-testid="chart-widget-config-editor">
|
|
10325
|
-
@if (
|
|
11000
|
+
@if (groupedContextDiagnostics.length) {
|
|
10326
11001
|
<section
|
|
10327
11002
|
class="chart-widget-config-editor__diagnostics"
|
|
10328
11003
|
data-testid="chart-widget-context-diagnostics"
|
|
10329
11004
|
role="status"
|
|
10330
11005
|
>
|
|
10331
|
-
<
|
|
10332
|
-
|
|
10333
|
-
|
|
10334
|
-
<
|
|
10335
|
-
{{
|
|
11006
|
+
<div class="chart-widget-config-editor__diagnostics-header">
|
|
11007
|
+
<div class="chart-widget-config-editor__diagnostics-heading">
|
|
11008
|
+
<strong>{{ t('praxis.charts.widget.contextDiagnostics.title', 'Authoring context') }}</strong>
|
|
11009
|
+
<span class="chart-widget-config-editor__diagnostics-summary" data-testid="chart-widget-context-diagnostics-summary">
|
|
11010
|
+
{{ contextDiagnosticsSummaryText() }}
|
|
11011
|
+
</span>
|
|
11012
|
+
</div>
|
|
11013
|
+
<button
|
|
11014
|
+
type="button"
|
|
11015
|
+
class="chart-widget-config-editor__diagnostics-toggle"
|
|
11016
|
+
data-testid="chart-widget-context-diagnostics-toggle"
|
|
11017
|
+
[attr.aria-expanded]="diagnosticsExpanded()"
|
|
11018
|
+
(click)="toggleDiagnostics()"
|
|
11019
|
+
>
|
|
11020
|
+
{{
|
|
11021
|
+
diagnosticsExpanded()
|
|
11022
|
+
? t('praxis.charts.widget.contextDiagnostics.hideDetails', 'Hide details')
|
|
11023
|
+
: t('praxis.charts.widget.contextDiagnostics.showDetails', 'View details')
|
|
11024
|
+
}}
|
|
11025
|
+
</button>
|
|
11026
|
+
</div>
|
|
11027
|
+
<p class="chart-widget-config-editor__diagnostics-scope">
|
|
11028
|
+
{{
|
|
11029
|
+
t(
|
|
11030
|
+
'praxis.charts.widget.contextDiagnostics.scopeImpact',
|
|
11031
|
+
'Affected: governed stats resource/field catalog discovery. Remains editable: visual layout, derived local data, and query context.'
|
|
11032
|
+
)
|
|
11033
|
+
}}
|
|
11034
|
+
</p>
|
|
11035
|
+
<ul class="chart-widget-config-editor__diagnostics-groups">
|
|
11036
|
+
@for (group of groupedContextDiagnostics; track group.code) {
|
|
11037
|
+
<li
|
|
11038
|
+
[class]="'chart-widget-config-editor__diagnostic--' + group.severity"
|
|
11039
|
+
data-testid="chart-widget-context-diagnostic-group"
|
|
11040
|
+
>
|
|
11041
|
+
<span>{{ group.cause }}</span>
|
|
11042
|
+
@if (group.count > 1) {
|
|
11043
|
+
<span class="chart-widget-config-editor__diagnostic-count" data-testid="chart-widget-context-diagnostic-count">
|
|
11044
|
+
({{
|
|
11045
|
+
t(
|
|
11046
|
+
'praxis.charts.widget.contextDiagnostics.occurrences',
|
|
11047
|
+
'{{ count }} occurrences',
|
|
11048
|
+
{ count: group.count }
|
|
11049
|
+
)
|
|
11050
|
+
}})
|
|
11051
|
+
</span>
|
|
11052
|
+
}
|
|
10336
11053
|
</li>
|
|
10337
11054
|
}
|
|
10338
11055
|
</ul>
|
|
11056
|
+
@if (diagnosticsExpanded()) {
|
|
11057
|
+
<div
|
|
11058
|
+
class="chart-widget-config-editor__diagnostics-details"
|
|
11059
|
+
data-testid="chart-widget-context-diagnostics-details"
|
|
11060
|
+
>
|
|
11061
|
+
@for (group of groupedContextDiagnostics; track group.code) {
|
|
11062
|
+
<div
|
|
11063
|
+
class="chart-widget-config-editor__diagnostic-detail"
|
|
11064
|
+
data-testid="chart-widget-context-diagnostic-detail"
|
|
11065
|
+
>
|
|
11066
|
+
<div class="chart-widget-config-editor__diagnostic-detail-row">
|
|
11067
|
+
<strong>{{ group.cause }}</strong>
|
|
11068
|
+
</div>
|
|
11069
|
+
@if (group.paths.length) {
|
|
11070
|
+
<div class="chart-widget-config-editor__diagnostic-detail-row" data-testid="chart-widget-diagnostic-paths">
|
|
11071
|
+
<span>{{ t('praxis.charts.widget.contextDiagnostics.affectedPaths', 'Affected resources/paths:') }}</span>
|
|
11072
|
+
<code>{{ group.paths.join(', ') }}</code>
|
|
11073
|
+
</div>
|
|
11074
|
+
}
|
|
11075
|
+
<div class="chart-widget-config-editor__diagnostic-detail-row" data-testid="chart-widget-diagnostic-next-step">
|
|
11076
|
+
<span>{{ t('praxis.charts.widget.contextDiagnostics.nextStepLabel', 'Next step:') }}</span>
|
|
11077
|
+
<span>{{ group.nextStep }}</span>
|
|
11078
|
+
</div>
|
|
11079
|
+
<div class="chart-widget-config-editor__diagnostic-detail-row">
|
|
11080
|
+
<span>{{ t('praxis.charts.widget.contextDiagnostics.technicalCode', 'Technical code:') }}</span>
|
|
11081
|
+
<code data-testid="chart-widget-diagnostic-code">{{ group.code }}</code>
|
|
11082
|
+
</div>
|
|
11083
|
+
</div>
|
|
11084
|
+
}
|
|
11085
|
+
</div>
|
|
11086
|
+
}
|
|
10339
11087
|
</section>
|
|
10340
11088
|
}
|
|
10341
11089
|
@if (chartDocument) {
|
|
10342
11090
|
<praxis-chart-config-editor
|
|
10343
11091
|
#chartEditor
|
|
10344
11092
|
[document]="chartDocument"
|
|
11093
|
+
[localData]="localData"
|
|
10345
11094
|
[availableResources]="availableResources"
|
|
10346
11095
|
[availableFields]="availableFields"
|
|
10347
11096
|
[availableTargets]="availableTargets"
|
|
11097
|
+
(localDataChange)="onLocalDataChange($event)"
|
|
10348
11098
|
/>
|
|
10349
11099
|
} @else {
|
|
10350
11100
|
<section class="chart-widget-config-editor__notice" data-testid="chart-widget-missing-document">
|
|
@@ -10372,7 +11122,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
10372
11122
|
}
|
|
10373
11123
|
</section>
|
|
10374
11124
|
</section>
|
|
10375
|
-
`, changeDetection: ChangeDetectionStrategy.OnPush, providers: [providePraxisChartsI18n()], styles: [".chart-widget-config-editor__runtime{display:grid;gap:8px;margin-top:16px}.chart-widget-config-editor__diagnostics{display:grid;gap:
|
|
11125
|
+
`, changeDetection: ChangeDetectionStrategy.OnPush, providers: [providePraxisChartsI18n()], styles: [".chart-widget-config-editor__runtime{display:grid;gap:8px;margin-top:16px}.chart-widget-config-editor__diagnostics{display:grid;gap:8px;margin-bottom:16px;padding:12px 14px;border:1px solid color-mix(in srgb,var(--md-sys-color-tertiary, #765b00) 28%,transparent);border-radius:10px;background:color-mix(in srgb,var(--md-sys-color-tertiary-container, #ffdf92) 42%,var(--md-sys-color-surface, #fff));color:var(--md-sys-color-on-tertiary-container, #271900);font-size:12px;line-height:1.4}.chart-widget-config-editor__diagnostics-header{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.chart-widget-config-editor__diagnostics-heading{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}.chart-widget-config-editor__diagnostics-summary{opacity:.88}.chart-widget-config-editor__diagnostics-scope{margin:0;opacity:.86}.chart-widget-config-editor__diagnostics-toggle{padding:4px 10px;border:1px solid color-mix(in srgb,currentColor 28%,transparent);border-radius:999px;background:transparent;color:inherit;font:inherit;font-weight:600;cursor:pointer}.chart-widget-config-editor__diagnostics ul{display:grid;gap:4px;margin:0;padding-left:18px}.chart-widget-config-editor__diagnostic-count{margin-left:6px;font-weight:600}.chart-widget-config-editor__diagnostics-details{display:grid;gap:8px;margin-top:4px;padding-top:8px;border-top:1px solid color-mix(in srgb,currentColor 18%,transparent)}.chart-widget-config-editor__diagnostic-detail{display:grid;gap:4px;padding:8px 10px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 65%,transparent)}.chart-widget-config-editor__diagnostic-detail-row{display:flex;gap:6px;flex-wrap:wrap;align-items:baseline}.chart-widget-config-editor__diagnostic--error{color:var(--md-sys-color-error, #b3261e)}.chart-widget-config-editor__label{font-size:12px;font-weight:600;color:#000000b8}.chart-widget-config-editor__textarea{box-sizing:border-box;width:100%;min-height:112px;padding:10px 12px;border:1px solid rgba(0,0,0,.2);border-radius:6px;font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,monospace;resize:vertical}.chart-widget-config-editor__error{color:#b3261e;font-size:12px}.chart-widget-config-editor__notice{padding:12px;border:1px solid rgba(0,0,0,.12);border-radius:6px;color:#000000b8;font-size:12px;line-height:1.4;background:#00000008}\n"] }]
|
|
10376
11126
|
}], propDecorators: { inputs: [{
|
|
10377
11127
|
type: Input
|
|
10378
11128
|
}], context: [{
|
|
@@ -10384,6 +11134,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
10384
11134
|
args: ['chartEditor']
|
|
10385
11135
|
}] } });
|
|
10386
11136
|
|
|
11137
|
+
var praxisChartWidgetConfigEditor = /*#__PURE__*/Object.freeze({
|
|
11138
|
+
__proto__: null,
|
|
11139
|
+
PraxisChartWidgetConfigEditor: PraxisChartWidgetConfigEditor
|
|
11140
|
+
});
|
|
11141
|
+
|
|
10387
11142
|
const PRAXIS_CHART_PALETTE_DEFAULT_CONFIG = {
|
|
10388
11143
|
id: 'palette-chart',
|
|
10389
11144
|
type: 'bar',
|