@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.
@@ -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) ? true : undefined,
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.resolveToggle(contract.labels);
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
- ? { visible: labelsVisible, format: metric.format }
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
- runtimeChartDocument = signal(null, ...(ngDevMode ? [{ debugName: "runtimeChartDocument" }] : /* istanbul ignore next */ []));
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 = signal(false, ...(ngDevMode ? [{ debugName: "userEditedDocument" }] : /* istanbul ignore next */ []));
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.queryContext());
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.data();
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.data();
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.data();
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.data();
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.data();
2827
+ const explicitData = this.runtimeData();
2805
2828
  const remoteDataResolver = this.remoteDataResolver();
2806
- const queryContext = normalizePraxisDataQueryContext(this.queryContext());
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.data();
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.queryContext());
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 { PraxisChartConfigEditor } = await Promise.resolve().then(function () { return praxisChartConfigEditor; });
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: PraxisChartConfigEditor,
3021
+ component: PraxisChartWidgetConfigEditor,
2998
3022
  inputs: {
2999
- chartDocument: document,
3000
- document,
3001
- mode: 'edit',
3002
- readonly: false,
3003
- availableResources: this.resolveEditorOptions(this.availableResources(), resolvedContext['availableResources']),
3004
- availableFields: this.resolveEditorOptions(this.availableFields(), resolvedContext['availableFields']),
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 appliedSubscription = ref.applied$
3010
- .subscribe((nextDocument) => {
3011
- if (!nextDocument) {
3032
+ const consumeEditorValue = (value, phase) => {
3033
+ if (!value?.inputs?.chartDocument || !this.canOpenConfigEditor()
3034
+ || this.configEditorSessionSignature() !== sessionSignature)
3012
3035
  return;
3013
- }
3014
- const clone = structuredClone(nextDocument);
3015
- this.userEditedDocument.set(true);
3016
- this.runtimeChartDocument.set(clone);
3017
- this.chartDocumentApplied.emit(clone);
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
- this.chartDocumentPatchChange.emit({
3029
- inputPatch: {
3030
- chartDocument: structuredClone(clone),
3031
- ...(wasConfigDriven && mappedConfig ? { config: mappedConfig } : {}),
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((nextDocument) => {
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.queryContext()),
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{position:absolute;top:10px;right:10px;z-index:3;--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{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 });
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{position:absolute;top:10px;right:10px;z-index:3;--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{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"] }]
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 ? { length: 14, length2: 12, lineStyle: { color: textColor } } : undefined,
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 authoring governado de praxis.stats.',
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 authoráveis.',
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 authoring do gráfico.',
7521
- 'praxis.charts.widget.contextDiagnostics.title': 'Contexto de authoring',
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 authorável foi publicado pelo catálogo governado.',
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 authoráveis.',
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 authoring não puderam ser materializadas a partir do contexto atual da página.',
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: this.buildRows(document),
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 && !catalogIssues.some((issue) => issue.severity === 'error'),
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.catalogResolutionDiagnostics().map((diagnostic) => this.toCatalogIssue(diagnostic)),
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
- if (!this.validation().valid) {
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.refreshState(false);
8879
- this.resetChange.emit({ document: snapshot });
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.refreshState(false);
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, undefined, fallback, PRAXIS_CHARTS_I18N_NAMESPACE);
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
- toCatalogIssue(diagnostic) {
9849
- return {
9850
- severity: diagnostic.severity === 'error' ? 'error' : 'warning',
9851
- code: diagnostic.code,
9852
- field: diagnostic.path || 'catalog',
9853
- message: this.catalogDiagnosticMessage(diagnostic.code),
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, undefined, fallback, PRAXIS_CHARTS_I18N_NAMESPACE);
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 (contextDiagnostics.length) {
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
- <strong>{{ t('praxis.charts.widget.contextDiagnostics.title', 'Authoring context') }}</strong>
10276
- <ul>
10277
- @for (diagnostic of contextDiagnostics; track diagnostic.code + ':' + (diagnostic.path || '')) {
10278
- <li [class]="'chart-widget-config-editor__diagnostic--' + (diagnostic.severity || 'info')">
10279
- {{ contextDiagnosticText(diagnostic) }}
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:6px;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 ul{display:grid;gap:4px;margin:0;padding-left:18px}.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", "mode", "readonly", "availableResources", "availableFields", "availableTargets"], outputs: ["apply", "save", "resetChange", "documentChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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 (contextDiagnostics.length) {
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
- <strong>{{ t('praxis.charts.widget.contextDiagnostics.title', 'Authoring context') }}</strong>
10332
- <ul>
10333
- @for (diagnostic of contextDiagnostics; track diagnostic.code + ':' + (diagnostic.path || '')) {
10334
- <li [class]="'chart-widget-config-editor__diagnostic--' + (diagnostic.severity || 'info')">
10335
- {{ contextDiagnosticText(diagnostic) }}
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:6px;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 ul{display:grid;gap:4px;margin:0;padding-left:18px}.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"] }]
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',