@praxisui/charts 9.0.0-beta.73 → 9.0.0-beta.74

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,9 +1,9 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, Inject, InjectionToken, input, booleanAttribute, output, viewChild, inject, ElementRef, NgZone, DestroyRef, signal, computed, afterNextRender, effect, ChangeDetectionStrategy, Component, ViewChild, Input, ENVIRONMENT_INITIALIZER } from '@angular/core';
2
+ import { Injectable, Inject, InjectionToken, input, booleanAttribute, output, viewChild, inject, ElementRef, NgZone, DestroyRef, signal, computed, afterNextRender, effect, ChangeDetectionStrategy, Component, ViewChild, Input, Injector, ENVIRONMENT_INITIALIZER, Optional } from '@angular/core';
3
3
  import * as i1$1 from '@praxisui/core';
4
- import { buildApiUrl, API_URL, PraxisI18nService, SETTINGS_PANEL_BRIDGE, normalizePraxisPresentationVisualization, providePraxisI18n, SETTINGS_PANEL_DATA, ComponentMetadataRegistry, createDefaultTableConfig, PRAXIS_TABLE_DETAIL_INLINE_RENDERERS, AnalyticsStatsRequestBuilderService, DynamicWidgetPageComponent } from '@praxisui/core';
4
+ import { buildApiUrl, API_URL, PraxisI18nService, SETTINGS_PANEL_BRIDGE, normalizePraxisDataQueryContext, resolvePraxisFilterCriteria, BUILTIN_PAGE_THEME_PRESETS, normalizePraxisPresentationVisualization, providePraxisI18n, ComponentMetadataRegistry, ResourceDiscoveryService, SETTINGS_PANEL_DATA, createDefaultTableConfig, PRAXIS_TABLE_DETAIL_INLINE_RENDERERS, AnalyticsStatsRequestBuilderService, DynamicWidgetPageComponent } from '@praxisui/core';
5
5
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
6
- import { throwError, map, of, isObservable, from, BehaviorSubject, Subscription } from 'rxjs';
6
+ import { throwError, map, of, isObservable, from, firstValueFrom, timeout, BehaviorSubject, Subscription } from 'rxjs';
7
7
  import * as i1$2 from '@angular/material/button';
8
8
  import { MatButtonModule } from '@angular/material/button';
9
9
  import * as i2 from '@angular/material/icon';
@@ -31,6 +31,53 @@ import * as i7 from '@angular/material/slide-toggle';
31
31
  import { MatSlideToggleModule } from '@angular/material/slide-toggle';
32
32
  import { PraxisTable } from '@praxisui/table';
33
33
 
34
+ const PRAXIS_CHART_PALETTE_TOKENS = {
35
+ 'brand-primary': ['#1263b4', '#0f766e', '#f08c00', '#c92a2a', '#7b61ff'],
36
+ 'brand-balanced': ['#1263b4', '#15803d', '#7c3aed', '#c2410c', '#be123c'],
37
+ status: ['#15803d', '#ca8a04', '#dc2626', '#2563eb', '#7c3aed'],
38
+ executive: ['#14b8a6', '#f59e0b', '#e11d48', '#8b5cf6', '#38bdf8'],
39
+ };
40
+ function isPraxisChartPaletteToken(value) {
41
+ return Object.prototype.hasOwnProperty.call(PRAXIS_CHART_PALETTE_TOKENS, value);
42
+ }
43
+ function resolvePraxisChartPaletteToken(value) {
44
+ return isPraxisChartPaletteToken(value)
45
+ ? [...PRAXIS_CHART_PALETTE_TOKENS[value]]
46
+ : undefined;
47
+ }
48
+ const PRAXIS_CHART_THEME_VARIANTS = {
49
+ default: {
50
+ palette: [...PRAXIS_CHART_PALETTE_TOKENS['brand-primary']],
51
+ legend: { position: 'bottom' },
52
+ surface: { mode: 'auto' },
53
+ },
54
+ compact: {
55
+ palette: [...PRAXIS_CHART_PALETTE_TOKENS['brand-balanced']],
56
+ borderRadius: 6,
57
+ legend: { position: 'bottom' },
58
+ surface: {
59
+ mode: 'embedded',
60
+ background: 'transparent',
61
+ borderWidth: 0,
62
+ borderRadius: 0,
63
+ },
64
+ },
65
+ executive: {
66
+ palette: [...PRAXIS_CHART_PALETTE_TOKENS.executive],
67
+ backgroundColor: '#111827',
68
+ textColor: '#f9fafb',
69
+ borderRadius: 8,
70
+ legend: { position: 'bottom' },
71
+ surface: {
72
+ mode: 'contained',
73
+ background: '#111827',
74
+ borderColor: '#374151',
75
+ borderWidth: 1,
76
+ borderRadius: 8,
77
+ },
78
+ },
79
+ };
80
+
34
81
  class PraxisChartDataTransformerService {
35
82
  transform(config, rows) {
36
83
  if (config.type === 'pie' || config.type === 'donut') {
@@ -52,9 +99,10 @@ class PraxisChartDataTransformerService {
52
99
  hasData: false,
53
100
  };
54
101
  }
55
- const categories = Array.from(new Set(rows.map((row) => this.normalizeCategory(row[categoryField]))));
56
- const timeAxis = config.axes?.x?.type === 'time';
57
- const series = config.series.map((seriesConfig) => this.buildCartesianSeries(config, seriesConfig, rows, categories, timeAxis));
102
+ const categoryBuckets = this.buildCategoryBuckets(config, rows, categoryField);
103
+ const categories = categoryBuckets.map((bucket) => bucket.label);
104
+ const timeAxis = this.usesCanonicalTimeCoordinates(config);
105
+ const series = config.series.map((seriesConfig) => this.buildCartesianSeries(config, seriesConfig, categoryBuckets, timeAxis));
58
106
  return {
59
107
  mode: 'cartesian',
60
108
  categories,
@@ -75,23 +123,17 @@ class PraxisChartDataTransformerService {
75
123
  hasData: false,
76
124
  };
77
125
  }
78
- const bucket = new Map();
79
- for (const row of rows) {
80
- const key = this.normalizeCategory(row[categoryField]);
81
- const nextValue = this.extractMetricValue(row, seriesConfig);
82
- const current = bucket.get(key);
83
- bucket.set(key, {
84
- value: (current?.value ?? 0) + nextValue,
85
- source: current?.source ?? row,
86
- });
87
- }
88
- const slices = Array.from(bucket.entries()).map(([name, item]) => ({
89
- id: `${seriesConfig.id}:${name}`,
90
- name,
91
- value: item.value,
92
- color: seriesConfig.color,
93
- data: this.buildPointData(item.source, item.value),
94
- }));
126
+ const slices = this.buildCategoryBuckets(config, rows, categoryField)
127
+ .map((bucket) => {
128
+ const value = bucket.rows.reduce((sum, row) => sum + this.extractMetricValue(row, seriesConfig), 0);
129
+ return {
130
+ id: `${seriesConfig.id}:${bucket.identity}`,
131
+ name: bucket.label,
132
+ value,
133
+ color: seriesConfig.color,
134
+ data: this.buildPointData(bucket.rows[0], value),
135
+ };
136
+ });
95
137
  return {
96
138
  mode: 'pie',
97
139
  categories: slices.map((slice) => slice.name),
@@ -100,15 +142,7 @@ class PraxisChartDataTransformerService {
100
142
  hasData: slices.some((slice) => slice.value > 0),
101
143
  };
102
144
  }
103
- buildCartesianSeries(config, seriesConfig, rows, categories, timeAxis) {
104
- const categoryField = config.axes?.x?.field;
105
- const byCategory = new Map();
106
- for (const row of rows) {
107
- const key = this.normalizeCategory(row[categoryField]);
108
- const bucket = byCategory.get(key) ?? [];
109
- bucket.push(row);
110
- byCategory.set(key, bucket);
111
- }
145
+ buildCartesianSeries(config, seriesConfig, categoryBuckets, timeAxis) {
112
146
  return {
113
147
  id: seriesConfig.id,
114
148
  name: seriesConfig.name ?? seriesConfig.metric?.label ?? seriesConfig.id,
@@ -119,16 +153,94 @@ class PraxisChartDataTransformerService {
119
153
  area: this.isAreaLikeSeries(config, seriesConfig),
120
154
  color: seriesConfig.color,
121
155
  labelsVisible: seriesConfig.labels?.visible ?? false,
122
- points: categories.map((category) => {
123
- const bucket = byCategory.get(category) ?? [];
124
- const value = bucket.length ? this.aggregate(bucket, seriesConfig) : null;
125
- const source = bucket[0];
156
+ points: categoryBuckets.map((bucket) => {
157
+ const value = this.aggregate(bucket.rows, seriesConfig);
158
+ const source = bucket.rows[0];
126
159
  return source
127
- ? this.buildPointData(source, timeAxis ? [category, value] : value)
128
- : (timeAxis ? [category, value] : value);
160
+ ? this.buildPointData(source, timeAxis ? [bucket.coordinate, value] : value)
161
+ : (timeAxis ? [bucket.coordinate, value] : value);
129
162
  }),
130
163
  };
131
164
  }
165
+ buildCategoryBuckets(config, rows, categoryField) {
166
+ const buckets = new Map();
167
+ const usesCanonicalStatsIdentity = config.dataSource?.kind === 'remote'
168
+ && config.dataSource.query?.sourceKind === 'praxis.stats';
169
+ const usesCanonicalTimeCoordinate = usesCanonicalStatsIdentity
170
+ && this.usesCanonicalTimeCoordinates(config);
171
+ for (const row of rows) {
172
+ const category = this.normalizeCategory(row[categoryField]);
173
+ const coordinate = usesCanonicalTimeCoordinate
174
+ ? this.resolveCanonicalTimeCoordinate(row, categoryField, category)
175
+ : category;
176
+ const label = usesCanonicalTimeCoordinate
177
+ ? this.resolveTimePresentationLabel(row, category)
178
+ : category;
179
+ const identity = this.resolveCategoryIdentity(row, label, coordinate, usesCanonicalStatsIdentity, usesCanonicalTimeCoordinate);
180
+ const bucket = buckets.get(identity);
181
+ if (bucket) {
182
+ bucket.rows.push(row);
183
+ continue;
184
+ }
185
+ buckets.set(identity, {
186
+ identity,
187
+ label,
188
+ coordinate,
189
+ rows: [row],
190
+ });
191
+ }
192
+ return Array.from(buckets.values());
193
+ }
194
+ resolveCategoryIdentity(row, label, coordinate, usesCanonicalStatsIdentity, usesCanonicalTimeCoordinate) {
195
+ if (!usesCanonicalStatsIdentity) {
196
+ return label;
197
+ }
198
+ if (usesCanonicalTimeCoordinate) {
199
+ return `time:${this.typedIdentity(coordinate)}`;
200
+ }
201
+ if (row['key'] !== null && row['key'] !== undefined) {
202
+ return `key:${this.typedIdentity(row['key'])}`;
203
+ }
204
+ return `label:${label}`;
205
+ }
206
+ resolveCanonicalTimeCoordinate(row, categoryField, fallback) {
207
+ return this.extractTimeCoordinate(row['start'])
208
+ ?? this.extractTimeCoordinate(row['key'])
209
+ ?? this.extractTimeCoordinate(row[categoryField])
210
+ ?? fallback;
211
+ }
212
+ usesCanonicalTimeCoordinates(config) {
213
+ if (config.axes?.x?.type === 'time') {
214
+ return true;
215
+ }
216
+ return config.dataSource?.kind === 'remote'
217
+ && config.dataSource.query?.sourceKind === 'praxis.stats'
218
+ && config.dataSource.query.statsOperation === 'timeseries';
219
+ }
220
+ extractTimeCoordinate(value) {
221
+ const candidate = this.extractScatterXValue(value, 'time');
222
+ if (candidate === null) {
223
+ return null;
224
+ }
225
+ if (typeof candidate === 'string'
226
+ && !/^\d{4}-\d{2}-\d{2}(?:[T\s]\d{2}:\d{2}.*)?$/.test(candidate)) {
227
+ return null;
228
+ }
229
+ const timestamp = typeof candidate === 'number' ? new Date(candidate).getTime() : Date.parse(candidate);
230
+ return Number.isFinite(timestamp) ? candidate : null;
231
+ }
232
+ resolveTimePresentationLabel(row, fallback) {
233
+ const label = row['label'];
234
+ return label === null || label === undefined || label === ''
235
+ ? fallback
236
+ : this.normalizeCategory(label);
237
+ }
238
+ typedIdentity(value) {
239
+ if (value instanceof Date) {
240
+ return `date:${value.toISOString()}`;
241
+ }
242
+ return `${typeof value}:${String(value)}`;
243
+ }
132
244
  buildPointData(row, value) {
133
245
  return {
134
246
  ...row,
@@ -295,54 +407,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
295
407
  args: [{ providedIn: 'root' }]
296
408
  }] });
297
409
 
298
- const PRAXIS_CHART_PALETTE_TOKENS = {
299
- 'brand-primary': ['#1263b4', '#0f766e', '#f08c00', '#c92a2a', '#7b61ff'],
300
- 'brand-balanced': ['#1263b4', '#15803d', '#7c3aed', '#c2410c', '#be123c'],
301
- status: ['#15803d', '#ca8a04', '#dc2626', '#2563eb', '#7c3aed'],
302
- executive: ['#14b8a6', '#f59e0b', '#e11d48', '#8b5cf6', '#38bdf8'],
303
- };
304
- function isPraxisChartPaletteToken(value) {
305
- return Object.prototype.hasOwnProperty.call(PRAXIS_CHART_PALETTE_TOKENS, value);
306
- }
307
- function resolvePraxisChartPaletteToken(value) {
308
- return isPraxisChartPaletteToken(value)
309
- ? [...PRAXIS_CHART_PALETTE_TOKENS[value]]
310
- : undefined;
311
- }
312
- const PRAXIS_CHART_THEME_VARIANTS = {
313
- default: {
314
- palette: [...PRAXIS_CHART_PALETTE_TOKENS['brand-primary']],
315
- legend: { position: 'bottom' },
316
- surface: { mode: 'auto' },
317
- },
318
- compact: {
319
- palette: [...PRAXIS_CHART_PALETTE_TOKENS['brand-balanced']],
320
- textColor: '#374151',
321
- borderRadius: 6,
322
- legend: { position: 'bottom' },
323
- surface: {
324
- mode: 'embedded',
325
- background: 'transparent',
326
- borderWidth: 0,
327
- borderRadius: 0,
328
- },
329
- },
330
- executive: {
331
- palette: [...PRAXIS_CHART_PALETTE_TOKENS.executive],
332
- backgroundColor: '#111827',
333
- textColor: '#f9fafb',
334
- borderRadius: 8,
335
- legend: { position: 'bottom' },
336
- surface: {
337
- mode: 'contained',
338
- background: '#111827',
339
- borderColor: '#374151',
340
- borderWidth: 1,
341
- borderRadius: 8,
342
- },
343
- },
344
- };
345
-
346
410
  class ChartContractNormalizerService {
347
411
  normalize(input) {
348
412
  let document = structuredClone(input);
@@ -504,9 +568,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
504
568
  args: [{ providedIn: 'root' }]
505
569
  }] });
506
570
 
571
+ const PRAXIS_X_UI_CHART_AUTHORABLE_VERSION = '0.1.0';
572
+
507
573
  class ChartContractValidationService {
508
574
  validate(document) {
509
575
  const issues = [];
576
+ this.validateVersion(document, issues);
510
577
  this.validateSource(document, issues);
511
578
  this.validateTheme(document, issues);
512
579
  this.validateSizing(document, issues);
@@ -518,6 +585,11 @@ class ChartContractValidationService {
518
585
  issues,
519
586
  };
520
587
  }
588
+ validateVersion(document, issues) {
589
+ if (document.version !== PRAXIS_X_UI_CHART_AUTHORABLE_VERSION) {
590
+ issues.push(this.error('unsupported-version', 'version', `x-ui.chart version="${document.version}" is not authorable in @praxisui/charts; expected ${PRAXIS_X_UI_CHART_AUTHORABLE_VERSION}.`));
591
+ }
592
+ }
521
593
  validateSource(document, issues) {
522
594
  if (document.source.kind !== 'praxis.stats' && document.source.kind !== 'derived') {
523
595
  issues.push(this.error('unsupported-source-kind', 'source.kind', `x-ui.chart source.kind="${document.source.kind}" is not supported in @praxisui/charts.`));
@@ -525,6 +597,17 @@ class ChartContractValidationService {
525
597
  if (document.source.kind === 'praxis.stats' && !document.source.resource?.trim()) {
526
598
  issues.push(this.error('missing-resource', 'source.resource', 'x-ui.chart source.resource is required for source.kind="praxis.stats".'));
527
599
  }
600
+ const rawStatsResource = document.source.kind === 'praxis.stats'
601
+ ? document.source.resource
602
+ : undefined;
603
+ const statsResource = rawStatsResource?.trim();
604
+ if (statsResource
605
+ && (rawStatsResource !== statsResource
606
+ || !statsResource.startsWith('/')
607
+ || statsResource.startsWith('//')
608
+ || /^[a-z][a-z\d+.-]*:/i.test(statsResource))) {
609
+ issues.push(this.error('invalid-resource-path', 'source.resource', 'x-ui.chart source.resource must be a governed root-relative path starting with exactly one "/".'));
610
+ }
528
611
  if (document.source.kind === 'praxis.stats' && !document.source.operation) {
529
612
  issues.push(this.error('missing-operation', 'source.operation', 'x-ui.chart source.operation is required for source.kind="praxis.stats".'));
530
613
  }
@@ -546,6 +629,11 @@ class ChartContractValidationService {
546
629
  if (!document.metrics?.length) {
547
630
  issues.push(this.error('missing-metric', 'metrics', 'x-ui.chart requires at least one metric.'));
548
631
  }
632
+ document.metrics?.forEach((metric, index) => {
633
+ if (!metric.field?.trim()) {
634
+ issues.push(this.error('metric-field-required', `metrics[${index}].field`, 'x-ui.chart metric.field is required.'));
635
+ }
636
+ });
549
637
  document.metrics
550
638
  ?.filter((metric) => metric.aggregation === 'distinct-count' && !metric.field?.trim())
551
639
  .forEach((_metric, index) => {
@@ -580,6 +668,11 @@ class ChartContractValidationService {
580
668
  }
581
669
  validateKinds(document, issues) {
582
670
  const metricCount = document.metrics?.length ?? 0;
671
+ document.dimensions?.forEach((dimension, index) => {
672
+ if (!dimension.field?.trim()) {
673
+ issues.push(this.error('dimension-field-required', `dimensions[${index}].field`, 'x-ui.chart dimension.field is required.'));
674
+ }
675
+ });
583
676
  if (document.kind !== 'pie' && document.kind !== 'donut' && !document.dimensions?.length) {
584
677
  issues.push(this.error('missing-dimension', 'dimensions', 'x-ui.chart cartesian charts require at least one dimension.'));
585
678
  }
@@ -595,17 +688,7 @@ class ChartContractValidationService {
595
688
  if (document.kind !== 'combo' && document.metrics?.some((metric) => metric.axis === 'secondary')) {
596
689
  issues.push(this.error('secondary-axis-non-combo', 'metrics', 'x-ui.chart axis="secondary" is supported only for combo charts in @praxisui/charts.'));
597
690
  }
598
- if (document.source.kind === 'praxis.stats'
599
- && document.source.operation === 'distribution'
600
- && metricCount > 1) {
601
- issues.push(this.error('distribution-single-metric', 'metrics', 'x-ui.chart praxis.stats distribution supports only a single metric in @praxisui/charts.'));
602
- }
603
- if (document.source.kind === 'praxis.stats'
604
- && document.source.operation === 'distribution'
605
- && document.source.options?.mode === 'histogram'
606
- && !(Number(document.source.options.bucketSize) > 0)) {
607
- issues.push(this.error('distribution-histogram-missing-bucket-size', 'source.options.bucketSize', 'x-ui.chart praxis.stats histogram distribution requires source.options.bucketSize to match the backend contract.'));
608
- }
691
+ this.validateDistribution(document, issues);
609
692
  if (document.kind === 'horizontal-bar' && document.orientation && document.orientation !== 'horizontal') {
610
693
  issues.push(this.error('horizontal-bar-orientation', 'orientation', 'x-ui.chart kind="horizontal-bar" requires orientation="horizontal" when orientation is provided.'));
611
694
  }
@@ -622,6 +705,48 @@ class ChartContractValidationService {
622
705
  issues.push(this.error('combo-operation-unsupported', 'source.operation', 'x-ui.chart combo charts over praxis.stats support only group-by or timeseries operations in @praxisui/charts.'));
623
706
  }
624
707
  }
708
+ validateDistribution(document, issues) {
709
+ if (document.source.kind !== 'praxis.stats'
710
+ || document.source.operation !== 'distribution') {
711
+ return;
712
+ }
713
+ const dimensions = document.dimensions ?? [];
714
+ const metrics = document.metrics ?? [];
715
+ if (dimensions.length !== 1) {
716
+ issues.push(this.error('distribution-single-dimension', 'dimensions', 'x-ui.chart praxis.stats distribution requires exactly one dimension.'));
717
+ }
718
+ if (metrics.length !== 1) {
719
+ issues.push(this.error('distribution-single-metric', 'metrics', 'x-ui.chart praxis.stats distribution requires exactly one metric.'));
720
+ }
721
+ const options = document.source.options;
722
+ const mode = options?.mode ?? 'terms';
723
+ if (mode !== 'terms' && mode !== 'histogram') {
724
+ issues.push(this.error('distribution-mode-unsupported', 'source.options.mode', `x-ui.chart praxis.stats distribution mode="${String(mode)}" is not supported; expected terms or histogram.`));
725
+ return;
726
+ }
727
+ if (mode === 'terms') {
728
+ if (options?.bucketSize !== undefined || options?.bucketCount !== undefined) {
729
+ issues.push(this.error('distribution-terms-histogram-options', 'source.options', 'x-ui.chart praxis.stats terms distribution does not support source.options.bucketSize or source.options.bucketCount.'));
730
+ }
731
+ return;
732
+ }
733
+ const bucketSize = options?.bucketSize;
734
+ if (typeof bucketSize !== 'number' || !Number.isFinite(bucketSize) || bucketSize <= 0) {
735
+ issues.push(this.error('distribution-histogram-missing-bucket-size', 'source.options.bucketSize', 'x-ui.chart praxis.stats histogram distribution requires a finite source.options.bucketSize greater than zero.'));
736
+ }
737
+ const bucketCount = options?.bucketCount;
738
+ if (bucketCount !== undefined
739
+ && (typeof bucketCount !== 'number'
740
+ || !Number.isFinite(bucketCount)
741
+ || !Number.isInteger(bucketCount)
742
+ || bucketCount <= 0)) {
743
+ issues.push(this.error('distribution-histogram-bucket-count-invalid', 'source.options.bucketCount', 'x-ui.chart praxis.stats histogram source.options.bucketCount must be a positive integer when provided.'));
744
+ }
745
+ const metric = metrics[0];
746
+ if (metrics.length === 1 && (metric.aggregation ?? 'count') !== 'count') {
747
+ issues.push(this.error('distribution-histogram-count-only', 'metrics[0].aggregation', 'x-ui.chart praxis.stats histogram distribution requires COUNT; the canonical mapper materializes this backend metric without a field.'));
748
+ }
749
+ }
625
750
  validateEvents(document, issues) {
626
751
  this.validateEventAction('pointClick', document.events?.pointClick, issues);
627
752
  this.validateEventAction('drillDown', document.events?.drillDown, issues);
@@ -856,7 +981,7 @@ class PraxisChartCanonicalContractMapperService {
856
981
  }
857
982
  buildInteractions(contract) {
858
983
  return {
859
- pointClick: Boolean(contract.events?.pointClick || contract.events?.drillDown),
984
+ pointClick: Boolean(contract.events?.pointClick),
860
985
  selection: Boolean(contract.events?.selectionChange),
861
986
  drillDown: Boolean(contract.events?.drillDown),
862
987
  crossFilter: Boolean(contract.events?.crossFilter),
@@ -1201,13 +1326,24 @@ class PraxisChartStatsApiService {
1201
1326
  return [];
1202
1327
  }
1203
1328
  const metricBindings = this.resolveMetricBindings(config, request, response);
1329
+ if ('periodField' in response) {
1330
+ return response.buckets.map((bucket) => {
1331
+ const category = this.resolveBucketCategory(bucket, categoryField, config);
1332
+ return {
1333
+ [categoryField]: category,
1334
+ ...this.projectComparisonMetricValues(metricBindings, bucket.values),
1335
+ key: bucket.key ?? null,
1336
+ label: bucket.label ?? category,
1337
+ };
1338
+ });
1339
+ }
1204
1340
  if ('points' in response) {
1205
1341
  return response.points.map((point) => {
1206
- const category = point.label ?? point.start ?? point.end ?? '';
1342
+ const category = point.start ?? point.end ?? point.label ?? '';
1207
1343
  return {
1208
1344
  [categoryField]: category,
1209
1345
  ...this.projectMetricValues(metricBindings, point.values, point.value, point.count),
1210
- key: point.start ?? point.label ?? point.end ?? category,
1346
+ key: point.start ?? point.end ?? point.label ?? category,
1211
1347
  label: point.label ?? category,
1212
1348
  value: point.value ?? null,
1213
1349
  count: point.count ?? null,
@@ -1358,6 +1494,16 @@ class PraxisChartStatsApiService {
1358
1494
  return acc;
1359
1495
  }, {});
1360
1496
  }
1497
+ projectComparisonMetricValues(bindings, values) {
1498
+ return bindings.reduce((acc, binding) => {
1499
+ const match = /^__praxisComparison_(.+)_(current|previous)$/.exec(binding.field);
1500
+ const value = match
1501
+ ? values?.[match[1]]?.[match[2]]
1502
+ : undefined;
1503
+ acc[binding.field] = this.resolveMetricValue(value, null);
1504
+ return acc;
1505
+ }, {});
1506
+ }
1361
1507
  buildStatsUrl(statsPath) {
1362
1508
  const base = this.buildDefaultApiBase();
1363
1509
  const normalizedStatsPath = this.normalizePath(statsPath);
@@ -1422,7 +1568,9 @@ class PraxisChartComponent {
1422
1568
  availableFields = input([], ...(ngDevMode ? [{ debugName: "availableFields" }] : /* istanbul ignore next */ []));
1423
1569
  availableTargets = input([], ...(ngDevMode ? [{ debugName: "availableTargets" }] : /* istanbul ignore next */ []));
1424
1570
  pointClick = output();
1571
+ pointAction = output();
1425
1572
  selectionChange = output();
1573
+ drillDown = output();
1426
1574
  crossFilter = output();
1427
1575
  queryRequest = output();
1428
1576
  loadStateChange = output();
@@ -1441,6 +1589,8 @@ class PraxisChartComponent {
1441
1589
  resizeObserver = signal(null, ...(ngDevMode ? [{ debugName: "resizeObserver" }] : /* istanbul ignore next */ []));
1442
1590
  resizeObserverHost = null;
1443
1591
  shellObserver = signal(null, ...(ngDevMode ? [{ debugName: "shellObserver" }] : /* istanbul ignore next */ []));
1592
+ themeObserver = signal(null, ...(ngDevMode ? [{ debugName: "themeObserver" }] : /* istanbul ignore next */ []));
1593
+ inheritedTheme = signal(null, ...(ngDevMode ? [{ debugName: "inheritedTheme" }] : /* istanbul ignore next */ []));
1444
1594
  currentLoadState = signal('idle', ...(ngDevMode ? [{ debugName: "currentLoadState" }] : /* istanbul ignore next */ []));
1445
1595
  remoteResolvedData = signal(null, ...(ngDevMode ? [{ debugName: "remoteResolvedData" }] : /* istanbul ignore next */ []));
1446
1596
  remoteRuntimeState = signal('idle', ...(ngDevMode ? [{ debugName: "remoteRuntimeState" }] : /* istanbul ignore next */ []));
@@ -1456,11 +1606,14 @@ class PraxisChartComponent {
1456
1606
  previousRemoteSignature = null;
1457
1607
  previousRemoteDataResolver = undefined;
1458
1608
  previousDocumentSignature = null;
1609
+ inheritedThemeSignature = null;
1459
1610
  editorSessionSubscriptions = [];
1611
+ remoteRequestSubscription = null;
1612
+ remoteRequestSequence = 0;
1460
1613
  effectiveConfig = computed(() => {
1461
- const base = this.mappedRuntimeConfig() ?? this.config();
1462
- const runtimeQueryContext = normalizePraxisDataQueryContextBridge(this.queryContext());
1463
- const runtimeFilters = resolvePraxisFilterCriteriaBridge(this.filterCriteria(), runtimeQueryContext);
1614
+ const base = this.applyInheritedTheme(this.mappedRuntimeConfig() ?? this.config(), this.inheritedTheme());
1615
+ const runtimeQueryContext = normalizePraxisDataQueryContext(this.queryContext());
1616
+ const runtimeFilters = resolvePraxisFilterCriteria(this.filterCriteria(), runtimeQueryContext);
1464
1617
  if (!runtimeQueryContext
1465
1618
  && !Object.keys(runtimeFilters).length) {
1466
1619
  return base;
@@ -1535,18 +1688,17 @@ class PraxisChartComponent {
1535
1688
  };
1536
1689
  }
1537
1690
  if (remoteState === 'error') {
1691
+ const technicalDetails = this.remoteTechnicalError() ?? config.state?.error?.technicalDetails;
1538
1692
  return {
1539
1693
  ...config,
1540
1694
  preferredLoadState: 'error',
1541
- state: config.state?.error
1542
- ? {
1543
- ...config.state,
1544
- error: {
1545
- ...config.state.error,
1546
- technicalDetails: this.remoteTechnicalError() ?? config.state.error.technicalDetails,
1547
- },
1548
- }
1549
- : config.state,
1695
+ state: {
1696
+ ...config.state,
1697
+ error: {
1698
+ ...config.state?.error,
1699
+ technicalDetails,
1700
+ },
1701
+ },
1550
1702
  };
1551
1703
  }
1552
1704
  if (remoteData !== null) {
@@ -1615,6 +1767,7 @@ class PraxisChartComponent {
1615
1767
  constructor() {
1616
1768
  afterNextRender(() => {
1617
1769
  this.observeShellSizingContext();
1770
+ this.observeThemeContext();
1618
1771
  });
1619
1772
  effect(() => {
1620
1773
  const document = this.chartDocument();
@@ -1647,8 +1800,9 @@ class PraxisChartComponent {
1647
1800
  const config = this.effectiveConfig();
1648
1801
  const explicitData = this.data();
1649
1802
  const remoteDataResolver = this.remoteDataResolver();
1803
+ const queryContext = normalizePraxisDataQueryContext(this.queryContext());
1650
1804
  const nextSignature = explicitData === null || explicitData === undefined
1651
- ? this.buildRemoteSignature(config)
1805
+ ? this.buildRemoteSignature(config, queryContext)
1652
1806
  : null;
1653
1807
  if (nextSignature === this.previousRemoteSignature
1654
1808
  && remoteDataResolver === this.previousRemoteDataResolver) {
@@ -1656,6 +1810,7 @@ class PraxisChartComponent {
1656
1810
  }
1657
1811
  this.previousRemoteSignature = nextSignature;
1658
1812
  this.previousRemoteDataResolver = remoteDataResolver;
1813
+ this.cancelRemoteDataRequest();
1659
1814
  this.remoteResolvedData.set(null);
1660
1815
  this.remoteRuntimeState.set('idle');
1661
1816
  this.remoteTechnicalError.set(null);
@@ -1676,25 +1831,42 @@ class PraxisChartComponent {
1676
1831
  if (this.remoteRuntimeState() === 'loading' || this.remoteResolvedData() !== null) {
1677
1832
  return;
1678
1833
  }
1834
+ const queryContext = normalizePraxisDataQueryContext(this.queryContext());
1679
1835
  const event = {
1680
1836
  chartId: effectiveConfig.id,
1681
1837
  dataSource: effectiveConfig.dataSource,
1682
1838
  query: effectiveConfig.dataSource.query,
1683
- queryContext: normalizePraxisDataQueryContextBridge(this.queryContext()),
1839
+ queryContext,
1684
1840
  };
1685
1841
  this.queryRequest.emit(event);
1842
+ const unsupportedExpression = resolveUnsupportedStatsFilterExpressionDiagnostic(queryContext);
1843
+ if (unsupportedExpression) {
1844
+ this.cancelRemoteDataRequest();
1845
+ this.remoteRuntimeState.set('error');
1846
+ this.remoteResolvedData.set([]);
1847
+ this.remoteTechnicalError.set(unsupportedExpression);
1848
+ return;
1849
+ }
1686
1850
  this.remoteRuntimeState.set('loading');
1687
1851
  this.remoteResolvedData.set([]);
1688
1852
  this.remoteTechnicalError.set(null);
1689
- this.executeRemoteDataRequest(event, effectiveConfig)
1853
+ this.cancelRemoteDataRequest();
1854
+ const requestId = ++this.remoteRequestSequence;
1855
+ this.remoteRequestSubscription = this.executeRemoteDataRequest(event, effectiveConfig)
1690
1856
  .pipe(takeUntilDestroyed(this.destroyRef))
1691
1857
  .subscribe({
1692
1858
  next: (rows) => {
1859
+ if (requestId !== this.remoteRequestSequence) {
1860
+ return;
1861
+ }
1693
1862
  this.remoteResolvedData.set(rows);
1694
1863
  this.remoteRuntimeState.set('ready');
1695
1864
  this.remoteTechnicalError.set(null);
1696
1865
  },
1697
1866
  error: (error) => {
1867
+ if (requestId !== this.remoteRequestSequence) {
1868
+ return;
1869
+ }
1698
1870
  this.remoteResolvedData.set([]);
1699
1871
  this.remoteRuntimeState.set('error');
1700
1872
  this.remoteTechnicalError.set(error instanceof Error && error.message.trim()
@@ -1733,8 +1905,10 @@ class PraxisChartComponent {
1733
1905
  });
1734
1906
  this.destroyRef.onDestroy(() => {
1735
1907
  this.clearEditorSessionSubscriptions();
1908
+ this.cancelRemoteDataRequest();
1736
1909
  this.destroyResizeObserver();
1737
1910
  this.shellObserver()?.disconnect();
1911
+ this.themeObserver()?.disconnect();
1738
1912
  this.cancelScheduledResize();
1739
1913
  this.cancelScheduledRender();
1740
1914
  this.engine.destroy();
@@ -1793,13 +1967,45 @@ class PraxisChartComponent {
1793
1967
  this.pointClick.emit(event);
1794
1968
  const config = this.renderConfig();
1795
1969
  const interactions = config.interactions;
1796
- if (!interactions?.selection && !interactions?.crossFilter) {
1970
+ if (!interactions?.pointClick && !interactions?.selection && !interactions?.drillDown && !interactions?.crossFilter) {
1797
1971
  return;
1798
1972
  }
1799
1973
  const selectionEvent = this.buildSelectionEvent(config, event);
1974
+ if (interactions.pointClick) {
1975
+ const pointAction = interactions.eventActions?.pointClick;
1976
+ if (pointAction) {
1977
+ this.pointAction.emit({
1978
+ chartId: config.id ?? event.chartId,
1979
+ event: 'pointClick',
1980
+ point: {
1981
+ ...event,
1982
+ chartId: config.id ?? event.chartId,
1983
+ },
1984
+ filters: this.buildEventFilters(config, event, pointAction.mapping),
1985
+ target: pointAction.target,
1986
+ action: pointAction,
1987
+ });
1988
+ }
1989
+ }
1800
1990
  if (interactions.selection) {
1801
1991
  this.selectionChange.emit(selectionEvent);
1802
1992
  }
1993
+ if (interactions.drillDown) {
1994
+ const drillDownAction = interactions.eventActions?.drillDown;
1995
+ if (drillDownAction) {
1996
+ this.drillDown.emit({
1997
+ chartId: config.id ?? event.chartId,
1998
+ filters: this.buildEventFilters(config, event, drillDownAction.mapping),
1999
+ target: drillDownAction.target,
2000
+ action: drillDownAction,
2001
+ source: selectionEvent,
2002
+ point: {
2003
+ ...event,
2004
+ chartId: config.id ?? event.chartId,
2005
+ },
2006
+ });
2007
+ }
2008
+ }
1803
2009
  if (interactions.crossFilter) {
1804
2010
  const crossFilterAction = interactions.eventActions?.crossFilter;
1805
2011
  this.crossFilter.emit({
@@ -1818,6 +2024,7 @@ class PraxisChartComponent {
1818
2024
  ...event,
1819
2025
  chartId: config.id ?? event.chartId,
1820
2026
  selected: true,
2027
+ mode: 'single',
1821
2028
  filters,
1822
2029
  action,
1823
2030
  };
@@ -1913,7 +2120,7 @@ class PraxisChartComponent {
1913
2120
  const trimmed = value?.trim();
1914
2121
  return trimmed || null;
1915
2122
  }
1916
- buildRemoteSignature(config) {
2123
+ buildRemoteSignature(config, queryContext) {
1917
2124
  if (config.dataSource?.kind !== 'remote') {
1918
2125
  return null;
1919
2126
  }
@@ -1921,8 +2128,14 @@ class PraxisChartComponent {
1921
2128
  id: config.id,
1922
2129
  resourcePath: config.dataSource.resourcePath,
1923
2130
  query: config.dataSource.query,
2131
+ queryContext,
1924
2132
  });
1925
2133
  }
2134
+ cancelRemoteDataRequest() {
2135
+ this.remoteRequestSequence += 1;
2136
+ this.remoteRequestSubscription?.unsubscribe();
2137
+ this.remoteRequestSubscription = null;
2138
+ }
1926
2139
  clearEditorSessionSubscriptions() {
1927
2140
  for (const subscription of this.editorSessionSubscriptions) {
1928
2141
  subscription.unsubscribe();
@@ -1950,6 +2163,100 @@ class PraxisChartComponent {
1950
2163
  });
1951
2164
  this.shellObserver.set(observer);
1952
2165
  }
2166
+ observeThemeContext() {
2167
+ const host = this.hostElement.nativeElement;
2168
+ this.syncInheritedTheme(host);
2169
+ if (typeof MutationObserver === 'undefined') {
2170
+ return;
2171
+ }
2172
+ this.themeObserver()?.disconnect();
2173
+ const observer = new MutationObserver(() => {
2174
+ this.syncInheritedTheme(host);
2175
+ });
2176
+ let current = host;
2177
+ while (current) {
2178
+ observer.observe(current, {
2179
+ attributes: true,
2180
+ attributeFilter: [
2181
+ 'class',
2182
+ 'style',
2183
+ 'data-theme',
2184
+ 'data-theme-preset',
2185
+ 'data-color-scheme',
2186
+ ],
2187
+ });
2188
+ current = current.parentElement;
2189
+ }
2190
+ this.themeObserver.set(observer);
2191
+ }
2192
+ syncInheritedTheme(host) {
2193
+ if (typeof getComputedStyle !== 'function') {
2194
+ return;
2195
+ }
2196
+ const pageThemePresetId = host
2197
+ .closest('.pdx-page-wrapper')
2198
+ ?.getAttribute('data-theme-preset')
2199
+ ?.trim();
2200
+ const chartThemePresetId = pageThemePresetId
2201
+ ? BUILTIN_PAGE_THEME_PRESETS[pageThemePresetId]?.chartThemePreset?.trim()
2202
+ : undefined;
2203
+ const preset = chartThemePresetId && this.isThemeVariant(chartThemePresetId)
2204
+ ? this.cloneTheme(PRAXIS_CHART_THEME_VARIANTS[chartThemePresetId])
2205
+ : {};
2206
+ const hostTextColor = getComputedStyle(host).color.trim();
2207
+ const theme = {
2208
+ ...preset,
2209
+ textColor: preset.textColor || hostTextColor || undefined,
2210
+ };
2211
+ const normalizedTheme = Object.values(theme).some((value) => value !== undefined)
2212
+ ? theme
2213
+ : null;
2214
+ const signature = normalizedTheme ? JSON.stringify(normalizedTheme) : null;
2215
+ if (signature === this.inheritedThemeSignature) {
2216
+ return;
2217
+ }
2218
+ this.inheritedThemeSignature = signature;
2219
+ this.inheritedTheme.set(normalizedTheme);
2220
+ }
2221
+ isThemeVariant(value) {
2222
+ return Object.prototype.hasOwnProperty.call(PRAXIS_CHART_THEME_VARIANTS, value);
2223
+ }
2224
+ applyInheritedTheme(config, inherited) {
2225
+ if (!inherited) {
2226
+ return config;
2227
+ }
2228
+ const explicit = config.theme;
2229
+ return {
2230
+ ...config,
2231
+ theme: {
2232
+ palette: explicit?.palette ?? inherited.palette,
2233
+ backgroundColor: explicit?.backgroundColor ?? inherited.backgroundColor,
2234
+ textColor: explicit?.textColor ?? inherited.textColor,
2235
+ borderRadius: explicit?.borderRadius ?? inherited.borderRadius,
2236
+ legend: this.mergeOptionalThemeObject(inherited.legend, explicit?.legend),
2237
+ tooltip: this.mergeOptionalThemeObject(inherited.tooltip, explicit?.tooltip),
2238
+ surface: this.mergeOptionalThemeObject(inherited.surface, explicit?.surface),
2239
+ },
2240
+ };
2241
+ }
2242
+ cloneTheme(theme) {
2243
+ return {
2244
+ ...theme,
2245
+ palette: theme.palette ? [...theme.palette] : undefined,
2246
+ legend: this.mergeOptionalThemeObject(undefined, theme.legend),
2247
+ tooltip: this.mergeOptionalThemeObject(undefined, theme.tooltip),
2248
+ surface: this.mergeOptionalThemeObject(undefined, theme.surface),
2249
+ };
2250
+ }
2251
+ mergeOptionalThemeObject(defaults, explicit) {
2252
+ if (!defaults && !explicit) {
2253
+ return undefined;
2254
+ }
2255
+ return {
2256
+ ...(defaults ?? {}),
2257
+ ...(explicit ?? {}),
2258
+ };
2259
+ }
1953
2260
  shouldFillShellContainer(shell) {
1954
2261
  const rect = shell.getBoundingClientRect();
1955
2262
  const hasControlledHeight = (rect.height || shell.clientHeight || shell.offsetHeight) > 0;
@@ -2018,7 +2325,7 @@ class PraxisChartComponent {
2018
2325
  this.resizeFrameId = null;
2019
2326
  }
2020
2327
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2021
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisChartComponent, isStandalone: true, selector: "praxis-chart", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, chartDocument: { classPropertyName: "chartDocument", publicName: "chartDocument", isSignal: true, isRequired: false, transformFunction: null }, filterCriteria: { classPropertyName: "filterCriteria", publicName: "filterCriteria", isSignal: true, isRequired: false, transformFunction: null }, queryContext: { classPropertyName: "queryContext", publicName: "queryContext", isSignal: true, isRequired: false, transformFunction: null }, remoteDataResolver: { classPropertyName: "remoteDataResolver", publicName: "remoteDataResolver", isSignal: true, isRequired: false, transformFunction: null }, enableCustomization: { classPropertyName: "enableCustomization", publicName: "enableCustomization", isSignal: true, isRequired: false, transformFunction: null }, availableResources: { classPropertyName: "availableResources", publicName: "availableResources", isSignal: true, isRequired: false, transformFunction: null }, availableFields: { classPropertyName: "availableFields", publicName: "availableFields", isSignal: true, isRequired: false, transformFunction: null }, availableTargets: { classPropertyName: "availableTargets", publicName: "availableTargets", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pointClick: "pointClick", selectionChange: "selectionChange", crossFilter: "crossFilter", queryRequest: "queryRequest", loadStateChange: "loadStateChange", chartDocumentApplied: "chartDocumentApplied", chartDocumentSaved: "chartDocumentSaved" }, host: { properties: { "class.praxis-chart-host-fill-container": "isFillContainerMode()" } }, viewQueries: [{ propertyName: "chartHost", first: true, predicate: ["chartHost"], descendants: true, isSignal: true }], ngImport: i0, template: `
2328
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisChartComponent, isStandalone: true, selector: "praxis-chart", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, chartDocument: { classPropertyName: "chartDocument", publicName: "chartDocument", isSignal: true, isRequired: false, transformFunction: null }, filterCriteria: { classPropertyName: "filterCriteria", publicName: "filterCriteria", isSignal: true, isRequired: false, transformFunction: null }, queryContext: { classPropertyName: "queryContext", publicName: "queryContext", isSignal: true, isRequired: false, transformFunction: null }, remoteDataResolver: { classPropertyName: "remoteDataResolver", publicName: "remoteDataResolver", isSignal: true, isRequired: false, transformFunction: null }, enableCustomization: { classPropertyName: "enableCustomization", publicName: "enableCustomization", isSignal: true, isRequired: false, transformFunction: null }, availableResources: { classPropertyName: "availableResources", publicName: "availableResources", isSignal: true, isRequired: false, transformFunction: null }, availableFields: { classPropertyName: "availableFields", publicName: "availableFields", isSignal: true, isRequired: false, transformFunction: null }, availableTargets: { classPropertyName: "availableTargets", publicName: "availableTargets", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pointClick: "pointClick", pointAction: "pointAction", selectionChange: "selectionChange", drillDown: "drillDown", crossFilter: "crossFilter", queryRequest: "queryRequest", loadStateChange: "loadStateChange", chartDocumentApplied: "chartDocumentApplied", chartDocumentSaved: "chartDocumentSaved" }, host: { properties: { "class.praxis-chart-host-fill-container": "isFillContainerMode()" } }, viewQueries: [{ propertyName: "chartHost", first: true, predicate: ["chartHost"], descendants: true, isSignal: true }], ngImport: i0, template: `
2022
2329
  <section
2023
2330
  class="praxis-chart-shell"
2024
2331
  [class.praxis-chart-shell-fill-container]="isFillContainerMode()"
@@ -2092,7 +2399,7 @@ class PraxisChartComponent {
2092
2399
  <div #chartHost class="praxis-chart-host"></div>
2093
2400
  }
2094
2401
  </section>
2095
- `, isInline: true, styles: [":host{display:block;min-height:var(--praxis-chart-runtime-height, 320px);min-width:0}:host(.praxis-chart-host-fill-container){height:100%;min-height:0}: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{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;background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 88%,rgba(18,99,180,.12));-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-host{width:100%;height:100%;min-height:inherit}.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(--md-sys-color-on-surface, #1a1b20)}.praxis-chart-state-description{font-size:.925rem;color:var(--md-sys-color-on-surface-variant, #5a5d67);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: MatButtonModule }, { kind: "component", type: i1$2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2402
+ `, 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.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{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;background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 88%,rgba(18,99,180,.12));-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-host{width:100%;height:100%;min-height:inherit}.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(--md-sys-color-on-surface, #1a1b20)}.praxis-chart-state-description{font-size:.925rem;color:var(--md-sys-color-on-surface-variant, #5a5d67);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: MatButtonModule }, { kind: "component", type: i1$2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2096
2403
  }
2097
2404
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartComponent, decorators: [{
2098
2405
  type: Component,
@@ -2172,65 +2479,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
2172
2479
  <div #chartHost class="praxis-chart-host"></div>
2173
2480
  }
2174
2481
  </section>
2175
- `, styles: [":host{display:block;min-height:var(--praxis-chart-runtime-height, 320px);min-width:0}:host(.praxis-chart-host-fill-container){height:100%;min-height:0}: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{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;background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 88%,rgba(18,99,180,.12));-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-host{width:100%;height:100%;min-height:inherit}.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(--md-sys-color-on-surface, #1a1b20)}.praxis-chart-state-description{font-size:.925rem;color:var(--md-sys-color-on-surface-variant, #5a5d67);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"] }]
2176
- }], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", 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"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], 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"] }], chartHost: [{ type: i0.ViewChild, args: ['chartHost', { isSignal: true }] }] } });
2177
- function normalizeFilterCriteria(criteria) {
2178
- if (!criteria)
2179
- return null;
2180
- const next = Object.entries(criteria).reduce((acc, [key, value]) => {
2181
- if (value === null || value === undefined)
2182
- return acc;
2183
- if (Array.isArray(value) && value.length === 0)
2184
- return acc;
2185
- if (typeof value === 'string' && value.trim() === '')
2186
- return acc;
2187
- acc[key] = value;
2188
- return acc;
2189
- }, {});
2190
- return Object.keys(next).length ? next : null;
2191
- }
2192
- function normalizePraxisDataQueryContextBridge(context) {
2193
- if (!context || typeof context !== 'object')
2194
- return null;
2195
- const filters = normalizeFilterCriteria(context.filters || null);
2196
- const meta = normalizeFilterCriteria(context.meta || null);
2197
- const sort = Array.isArray(context.sort)
2198
- ? context.sort
2199
- .map((item) => (typeof item === 'string' ? item.trim() : ''))
2200
- .filter((item) => item.length > 0)
2201
- : [];
2202
- const limit = typeof context.limit === 'number' && Number.isFinite(context.limit)
2203
- ? Math.max(0, Math.trunc(context.limit))
2204
- : null;
2205
- const page = context.page && typeof context.page === 'object'
2206
- ? {
2207
- index: typeof context.page.index === 'number' && Number.isFinite(context.page.index)
2208
- ? Math.max(0, Math.trunc(context.page.index))
2209
- : null,
2210
- size: typeof context.page.size === 'number' && Number.isFinite(context.page.size)
2211
- ? Math.max(0, Math.trunc(context.page.size))
2212
- : null,
2213
- }
2214
- : null;
2215
- const next = {};
2216
- if (filters)
2217
- next.filters = filters;
2218
- if (sort.length)
2219
- next.sort = sort;
2220
- if (limit !== null)
2221
- next.limit = limit;
2222
- if (page && (page.index !== null || page.size !== null))
2223
- next.page = page;
2224
- if (meta)
2225
- next.meta = meta;
2226
- return Object.keys(next).length ? next : null;
2227
- }
2228
- function resolvePraxisFilterCriteriaBridge(filterCriteria, queryContext) {
2229
- return {
2230
- ...(normalizeFilterCriteria(filterCriteria) || {}),
2231
- ...(normalizePraxisDataQueryContextBridge(queryContext)?.filters || {}),
2232
- };
2233
- }
2482
+ `, 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.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{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;background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 88%,rgba(18,99,180,.12));-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-host{width:100%;height:100%;min-height:inherit}.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(--md-sys-color-on-surface, #1a1b20)}.praxis-chart-state-description{font-size:.925rem;color:var(--md-sys-color-on-surface-variant, #5a5d67);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"] }]
2483
+ }], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", 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"] }], chartHost: [{ type: i0.ViewChild, args: ['chartHost', { isSignal: true }] }] } });
2234
2484
  function mergeRemoteQueryContext(config, queryContext, runtimeFilters) {
2235
2485
  const dataSource = config.dataSource;
2236
2486
  if (dataSource?.kind !== 'remote' || !dataSource.query)
@@ -2263,6 +2513,17 @@ function mergeRemoteQueryContext(config, queryContext, runtimeFilters) {
2263
2513
  },
2264
2514
  };
2265
2515
  }
2516
+ function resolveUnsupportedStatsFilterExpressionDiagnostic(queryContext) {
2517
+ if (!queryContext?.filterExpression) {
2518
+ return null;
2519
+ }
2520
+ const governance = queryContext.filterExpression.governance;
2521
+ const decision = governance?.decisionId ? ` Decision: ${governance.decisionId}.` : '';
2522
+ const reason = queryContext.filterExpression.projection?.reason
2523
+ ? ` Projection reason: ${queryContext.filterExpression.projection.reason}.`
2524
+ : '';
2525
+ return `Praxis Charts received a governed queryContext.filterExpression, but praxis.stats execution currently accepts only flat statsRequest.filter criteria. The expression was rejected instead of being flattened into an unsafe AND filter.${decision}${reason}`;
2526
+ }
2266
2527
  function normalizeRemoteDataResolverResult$1(result) {
2267
2528
  if (Array.isArray(result)) {
2268
2529
  return of(result);
@@ -3264,7 +3525,7 @@ class PraxisChartDrilldownPanelComponent {
3264
3525
  [data]="detailData()"
3265
3526
  ></praxis-chart>
3266
3527
  </section>
3267
- `, isInline: true, styles: [":host{display:block}.drilldown-shell{display:grid;gap:14px}.drilldown-header h3,.drilldown-eyebrow,.drilldown-description{margin:0}.drilldown-eyebrow{font-size:.75rem;letter-spacing:.14em;text-transform:uppercase;color:var(--md-sys-color-on-surface-variant, #5a5d67)}.drilldown-header h3{font-size:1.2rem;color:var(--md-sys-color-on-surface, #1a1b20)}.drilldown-description{color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"], dependencies: [{ kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "data", "chartDocument", "filterCriteria", "queryContext", "remoteDataResolver", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "selectionChange", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3528
+ `, isInline: true, styles: [":host{display:block}.drilldown-shell{display:grid;gap:14px}.drilldown-header h3,.drilldown-eyebrow,.drilldown-description{margin:0}.drilldown-eyebrow{font-size:.75rem;letter-spacing:.14em;text-transform:uppercase;color:var(--md-sys-color-on-surface-variant, #5a5d67)}.drilldown-header h3{font-size:1.2rem;color:var(--md-sys-color-on-surface, #1a1b20)}.drilldown-description{color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"], dependencies: [{ kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "data", "chartDocument", "filterCriteria", "queryContext", "remoteDataResolver", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "pointAction", "selectionChange", "drillDown", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3268
3529
  }
3269
3530
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartDrilldownPanelComponent, decorators: [{
3270
3531
  type: Component,
@@ -3447,19 +3708,14 @@ class EChartsOptionBuilderService {
3447
3708
  };
3448
3709
  }
3449
3710
  const horizontal = config.orientation === 'horizontal' || config.type === 'horizontal-bar';
3450
- const xAxisType = config.axes?.x?.type ?? 'category';
3711
+ const xAxisType = this.usesCanonicalStatsTime(config)
3712
+ ? 'time'
3713
+ : config.axes?.x?.type ?? 'category';
3451
3714
  return {
3452
3715
  backgroundColor: config.theme?.backgroundColor,
3453
3716
  color: palette,
3454
3717
  title: this.buildTitle(config, 'left', textColor),
3455
- tooltip: tooltipEnabled
3456
- ? {
3457
- trigger: config.theme?.tooltip?.trigger ?? 'axis',
3458
- confine: true,
3459
- appendToBody: false,
3460
- valueFormatter: (value) => this.formatValue(value, this.primaryValueFormat(config)),
3461
- }
3462
- : undefined,
3718
+ tooltip: tooltipEnabled ? this.buildCartesianTooltip(config) : undefined,
3463
3719
  legend: this.buildLegend(config, legendVisible, 'bottom', textColor),
3464
3720
  grid: this.buildGrid(config, { horizontal, legendVisible }),
3465
3721
  xAxis: horizontal
@@ -3679,6 +3935,87 @@ class EChartsOptionBuilderService {
3679
3935
  ?? config.axes?.y?.labels?.format
3680
3936
  ?? config.series[0]?.labels?.format;
3681
3937
  }
3938
+ buildCartesianTooltip(config) {
3939
+ const tooltip = {
3940
+ trigger: config.theme?.tooltip?.trigger ?? 'axis',
3941
+ confine: true,
3942
+ appendToBody: false,
3943
+ valueFormatter: (value) => this.formatValue(value, this.primaryValueFormat(config)),
3944
+ };
3945
+ return this.usesCanonicalStatsTime(config)
3946
+ ? {
3947
+ ...tooltip,
3948
+ formatter: (params) => this.formatCanonicalTimeTooltip(params, config),
3949
+ }
3950
+ : tooltip;
3951
+ }
3952
+ usesCanonicalStatsTime(config) {
3953
+ if (config.dataSource?.kind !== 'remote'
3954
+ || config.dataSource.query?.sourceKind !== 'praxis.stats') {
3955
+ return false;
3956
+ }
3957
+ return config.axes?.x?.type === 'time'
3958
+ || config.dataSource.query.statsOperation === 'timeseries';
3959
+ }
3960
+ formatCanonicalTimeTooltip(params, config) {
3961
+ const items = (Array.isArray(params) ? params : [params])
3962
+ .filter((item) => !!item && typeof item === 'object');
3963
+ if (!items.length) {
3964
+ return '';
3965
+ }
3966
+ const first = items[0];
3967
+ const data = this.tooltipPointData(first['data']);
3968
+ const header = this.formatCanonicalTimeTooltipHeader(first, data, config);
3969
+ const lines = items.map((item) => {
3970
+ const itemData = this.tooltipPointData(item['data']);
3971
+ const seriesId = item['seriesId'] === null || item['seriesId'] === undefined
3972
+ ? undefined
3973
+ : String(item['seriesId']);
3974
+ const format = seriesId
3975
+ ? this.seriesLabelFormat(config, seriesId) ?? this.primaryValueFormat(config)
3976
+ : this.primaryValueFormat(config);
3977
+ const value = this.formatValue(item['value'] ?? itemData['value'], format);
3978
+ const seriesName = item['seriesName'] === null || item['seriesName'] === undefined
3979
+ ? ''
3980
+ : String(item['seriesName']);
3981
+ return seriesName
3982
+ ? `${escapeTooltipHtml(seriesName)}: ${escapeTooltipHtml(value)}`
3983
+ : escapeTooltipHtml(value);
3984
+ });
3985
+ return [escapeTooltipHtml(header), ...lines]
3986
+ .filter((part) => part.length > 0)
3987
+ .join('<br/>');
3988
+ }
3989
+ formatCanonicalTimeTooltipHeader(params, data, config) {
3990
+ const dateFormat = config.axes?.x?.labels?.format;
3991
+ const label = this.nonEmptyText(data['label']);
3992
+ const start = this.nonEmptyText(data['start']);
3993
+ const end = this.nonEmptyText(data['end']);
3994
+ const formattedStart = start ? this.formatValue(start, dateFormat) : '';
3995
+ const formattedEnd = end ? this.formatValue(end, dateFormat) : '';
3996
+ const interval = formattedStart && formattedEnd && formattedStart !== formattedEnd
3997
+ ? `${formattedStart} – ${formattedEnd}`
3998
+ : formattedStart || formattedEnd;
3999
+ const fallback = this.nonEmptyText(params['axisValueLabel'])
4000
+ ?? this.nonEmptyText(params['name'])
4001
+ ?? '';
4002
+ if (label && interval && label !== formattedStart && label !== interval) {
4003
+ return `${label} · ${interval}`;
4004
+ }
4005
+ return interval || label || fallback;
4006
+ }
4007
+ tooltipPointData(value) {
4008
+ return value && typeof value === 'object' && !Array.isArray(value)
4009
+ ? value
4010
+ : {};
4011
+ }
4012
+ nonEmptyText(value) {
4013
+ if (value === null || value === undefined) {
4014
+ return undefined;
4015
+ }
4016
+ const text = String(value).trim();
4017
+ return text || undefined;
4018
+ }
3682
4019
  seriesLabelFormat(config, seriesId) {
3683
4020
  return config.series.find((series) => series.id === seriesId)?.labels?.format;
3684
4021
  }
@@ -3841,6 +4178,14 @@ function truncateLegendText(name) {
3841
4178
  }
3842
4179
  return `${value.slice(0, 25)}...`;
3843
4180
  }
4181
+ function escapeTooltipHtml(value) {
4182
+ return String(value ?? '')
4183
+ .replace(/&/g, '&amp;')
4184
+ .replace(/</g, '&lt;')
4185
+ .replace(/>/g, '&gt;')
4186
+ .replace(/"/g, '&quot;')
4187
+ .replace(/'/g, '&#39;');
4188
+ }
3844
4189
 
3845
4190
  use([
3846
4191
  AriaComponent,
@@ -3884,13 +4229,14 @@ class EChartsEngineAdapter {
3884
4229
  chart.on('click', (params) => {
3885
4230
  this.lastSeriesClickAt = Date.now();
3886
4231
  this.lastGridClick = undefined;
4232
+ const data = pointData(params?.name, params?.data ?? params?.value);
3887
4233
  payload.onPointClick?.({
3888
4234
  chartId: payload.config.id,
3889
4235
  seriesId: params?.seriesId,
3890
4236
  seriesName: params?.seriesName,
3891
- category: params?.name,
4237
+ category: this.resolvePointCategory(payload, data, params?.name),
3892
4238
  value: pointValue(params?.value ?? params?.data),
3893
- data: pointData(params?.name, params?.data ?? params?.value),
4239
+ data,
3894
4240
  });
3895
4241
  });
3896
4242
  const zrender = chart?.getZr?.();
@@ -3950,6 +4296,26 @@ class EChartsEngineAdapter {
3950
4296
  this.domClickHandler = undefined;
3951
4297
  this.zrenderClickHandler = undefined;
3952
4298
  }
4299
+ resolvePointCategory(payload, data, fallback) {
4300
+ const categoryField = payload.config.axes?.x?.field
4301
+ ?? payload.config.series.find((series) => series.categoryField)?.categoryField;
4302
+ const dataSource = payload.config.dataSource;
4303
+ const usesCanonicalStatsTime = dataSource?.kind === 'remote'
4304
+ && dataSource.query?.sourceKind === 'praxis.stats';
4305
+ const usesCanonicalStatsTimePoint = usesCanonicalStatsTime
4306
+ && (payload.config.axes?.x?.type === 'time'
4307
+ || (dataSource?.kind === 'remote' && dataSource.query?.statsOperation === 'timeseries'));
4308
+ const candidate = usesCanonicalStatsTimePoint && data['label'] !== null
4309
+ && data['label'] !== undefined
4310
+ && data['label'] !== ''
4311
+ ? data['label']
4312
+ : categoryField && data[categoryField] !== null && data[categoryField] !== undefined
4313
+ ? data[categoryField]
4314
+ : fallback;
4315
+ return candidate === null || candidate === undefined
4316
+ ? undefined
4317
+ : String(candidate);
4318
+ }
3953
4319
  emitCategoryClickFromGrid(event, payload) {
3954
4320
  const chart = this.chart;
3955
4321
  if (!chart || typeof chart.containPixel !== 'function' || typeof chart.convertFromPixel !== 'function') {
@@ -3977,10 +4343,8 @@ class EChartsEngineAdapter {
3977
4343
  }
3978
4344
  const category = categories[categoryIndex];
3979
4345
  const categoryValue = category == null ? undefined : String(category);
3980
- const categoryField = horizontal
3981
- ? payload.config.axes?.y?.field
3982
- : payload.config.axes?.x?.field
3983
- ?? payload.config.series.find((series) => series.categoryField)?.categoryField;
4346
+ const categoryField = payload.config.axes?.x?.field
4347
+ ?? payload.config.series.find((series) => series.categoryField)?.categoryField;
3984
4348
  const signature = JSON.stringify({
3985
4349
  chartId: payload.config.id,
3986
4350
  category: categoryValue,
@@ -3990,7 +4354,20 @@ class EChartsEngineAdapter {
3990
4354
  if (this.isDuplicateGridClick(signature)) {
3991
4355
  return;
3992
4356
  }
3993
- const data = { category: categoryValue };
4357
+ const usesCanonicalStatsRows = payload.config.dataSource?.kind === 'remote'
4358
+ && payload.config.dataSource.query?.sourceKind === 'praxis.stats';
4359
+ const fallbackSourceRow = !usesCanonicalStatsRows
4360
+ && categoryField
4361
+ && categoryValue !== undefined
4362
+ ? payload.data.find((row) => String(row[categoryField] ?? '') === categoryValue)
4363
+ : undefined;
4364
+ const sourceRow = usesCanonicalStatsRows
4365
+ ? payload.data[categoryIndex]
4366
+ : fallbackSourceRow;
4367
+ const data = {
4368
+ ...(sourceRow ?? {}),
4369
+ category: categoryValue,
4370
+ };
3994
4371
  if (categoryField && categoryValue !== undefined) {
3995
4372
  data[categoryField] = categoryValue;
3996
4373
  }
@@ -4042,6 +4419,13 @@ const PRAXIS_CHARTS_EN_US = {
4042
4419
  'praxis.charts.runtime.remoteErrorTitle': 'Chart data could not be loaded',
4043
4420
  'praxis.charts.runtime.remoteErrorDescription': 'Review the chart data source and analytics request before continuing.',
4044
4421
  'praxis.charts.runtime.booleanFalseLabel': 'Not {{ label }}',
4422
+ 'praxis.charts.runtime.comparisonCurrent': 'Current',
4423
+ 'praxis.charts.runtime.comparisonPrevious': 'Previous',
4424
+ 'praxis.charts.widget.missingDocument': 'This widget does not have a canonical chart document yet. Runtime inputs are preserved until a canonical chartDocument is provided by the host.',
4425
+ 'praxis.charts.widget.queryContext.label': 'Query context',
4426
+ 'praxis.charts.widget.queryContext.ariaLabel': 'Chart query context',
4427
+ 'praxis.charts.widget.queryContext.invalidJson': 'Query context must be valid JSON.',
4428
+ 'praxis.charts.widget.queryContext.mustBeObject': 'Query context must be a JSON object.',
4045
4429
  'praxis.charts.editor.section.general': 'General',
4046
4430
  'praxis.charts.editor.section.data': 'Data',
4047
4431
  'praxis.charts.editor.section.analytics': 'Analytics',
@@ -4103,6 +4487,34 @@ const PRAXIS_CHARTS_EN_US = {
4103
4487
  'praxis.charts.editor.appearance.surfaceTitle': 'Surface',
4104
4488
  'praxis.charts.editor.appearance.surfaceHint': 'Use embedded for dashboard widgets and contained only when the chart must own its visual surface.',
4105
4489
  'praxis.charts.editor.appearance.statesTitle': 'State messages',
4490
+ 'praxis.charts.editor.catalog.resourceMissing': 'Resource catalog is required for governed praxis.stats authoring.',
4491
+ 'praxis.charts.editor.catalog.resourceStale': 'Selected resource is not available in the governed resource catalog.',
4492
+ 'praxis.charts.editor.catalog.operationMissing': 'Selected resource does not publish authorable stats operations.',
4493
+ 'praxis.charts.editor.catalog.operationStale': 'Selected operation is not supported by the governed resource catalog.',
4494
+ 'praxis.charts.editor.catalog.fieldCatalogMissing': 'The selected resource does not publish governed fields for chart authoring.',
4495
+ 'praxis.charts.editor.catalog.dimensionFieldsMissing': 'No governed dimensions are available for the current resource and operation.',
4496
+ 'praxis.charts.editor.catalog.metricFieldsMissing': 'No governed metrics are available for the current resource and operation.',
4497
+ 'praxis.charts.editor.catalog.dimensionFieldStale': 'Selected dimension is not eligible for the current resource and operation.',
4498
+ 'praxis.charts.editor.catalog.metricFieldStale': 'Selected metric is not eligible for the current resource and operation.',
4499
+ 'praxis.charts.editor.catalog.metricAggregationStale': 'Selected aggregation is not allowed for the selected metric field.',
4500
+ 'praxis.charts.editor.catalog.targetMissing': 'Target catalog is required for this governed event action.',
4501
+ 'praxis.charts.editor.catalog.targetStale': 'Selected target is not available for this event action.',
4502
+ 'praxis.charts.editor.catalog.capabilitiesDenied': 'Access to the selected resource capabilities was denied.',
4503
+ 'praxis.charts.editor.catalog.capabilitiesUnavailable': 'The selected resource capabilities are temporarily unavailable.',
4504
+ 'praxis.charts.editor.catalog.contextUnavailable': 'Governed chart authoring context could not be resolved.',
4505
+ 'praxis.charts.widget.contextDiagnostics.title': 'Authoring context',
4506
+ 'praxis.charts.widget.contextDiagnostics.catalogDenied': 'Access to the governed resource catalog was denied.',
4507
+ 'praxis.charts.widget.contextDiagnostics.catalogUnavailable': 'The governed resource catalog is temporarily unavailable.',
4508
+ 'praxis.charts.widget.contextDiagnostics.catalogEmpty': 'No authorable stats resources were published by the governed catalog.',
4509
+ 'praxis.charts.widget.contextDiagnostics.catalogPathInvalid': 'The resource catalog published an unsafe or non-relative operational path.',
4510
+ 'praxis.charts.widget.contextDiagnostics.capabilitiesDenied': 'Access to the selected resource capabilities was denied.',
4511
+ 'praxis.charts.widget.contextDiagnostics.capabilitiesUnavailable': 'The selected resource capabilities are temporarily unavailable.',
4512
+ 'praxis.charts.widget.contextDiagnostics.operationsEmpty': 'The selected resource does not publish authorable stats operations.',
4513
+ 'praxis.charts.widget.contextDiagnostics.fieldsEmpty': 'The selected resource does not publish governed fields for chart authoring.',
4514
+ 'praxis.charts.widget.contextDiagnostics.identityMismatch': 'The selected resource identity does not match its governed capability snapshot.',
4515
+ 'praxis.charts.widget.contextDiagnostics.capabilitiesPathInvalid': 'The selected capability snapshot did not publish a safe governed resource path.',
4516
+ 'praxis.charts.widget.contextDiagnostics.capabilitiesPathMismatch': 'The selected capability snapshot path does not match the operational path published by the catalog.',
4517
+ 'praxis.charts.widget.contextDiagnostics.partial': 'Some governed authoring choices could not be materialized from the current page context.',
4106
4518
  'praxis.charts.editor.hint.sizingMode': 'Use fill-container only when the host widget provides a defined body height.',
4107
4519
  'praxis.charts.editor.hint.height': 'Numbers are saved as pixels; CSS lengths such as 20rem are also accepted.',
4108
4520
  'praxis.charts.editor.hint.minHeight': 'Set a readable minimum for compact dashboard widgets.',
@@ -4195,6 +4607,13 @@ const PRAXIS_CHARTS_PT_BR = {
4195
4607
  'praxis.charts.runtime.remoteErrorTitle': 'Não foi possível carregar os dados do gráfico',
4196
4608
  'praxis.charts.runtime.remoteErrorDescription': 'Revise a fonte de dados e os filtros antes de continuar.',
4197
4609
  'praxis.charts.runtime.booleanFalseLabel': 'Não {{ label }}',
4610
+ 'praxis.charts.runtime.comparisonCurrent': 'Atual',
4611
+ 'praxis.charts.runtime.comparisonPrevious': 'Anterior',
4612
+ 'praxis.charts.widget.missingDocument': 'Este widget ainda não possui um documento canônico de gráfico. Os inputs runtime são preservados até o host fornecer um chartDocument canônico.',
4613
+ 'praxis.charts.widget.queryContext.label': 'Contexto da consulta',
4614
+ 'praxis.charts.widget.queryContext.ariaLabel': 'Contexto da consulta do gráfico',
4615
+ 'praxis.charts.widget.queryContext.invalidJson': 'O contexto da consulta deve ser um JSON válido.',
4616
+ 'praxis.charts.widget.queryContext.mustBeObject': 'O contexto da consulta deve ser um objeto JSON.',
4198
4617
  'praxis.charts.editor.section.general': 'Geral',
4199
4618
  'praxis.charts.editor.section.data': 'Dados',
4200
4619
  'praxis.charts.editor.section.analytics': 'Estrutura',
@@ -4256,6 +4675,34 @@ const PRAXIS_CHARTS_PT_BR = {
4256
4675
  'praxis.charts.editor.appearance.surfaceTitle': 'Superfície',
4257
4676
  '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.',
4258
4677
  'praxis.charts.editor.appearance.statesTitle': 'Mensagens de estado',
4678
+ 'praxis.charts.editor.catalog.resourceMissing': 'O catálogo de recursos é obrigatório para authoring governado de praxis.stats.',
4679
+ 'praxis.charts.editor.catalog.resourceStale': 'O recurso selecionado não está disponível no catálogo governado de recursos.',
4680
+ 'praxis.charts.editor.catalog.operationMissing': 'O recurso selecionado não publica operações de stats authoráveis.',
4681
+ 'praxis.charts.editor.catalog.operationStale': 'A operação selecionada não é suportada pelo catálogo governado do recurso.',
4682
+ 'praxis.charts.editor.catalog.fieldCatalogMissing': 'O recurso selecionado não publica campos governados para a autoria do gráfico.',
4683
+ 'praxis.charts.editor.catalog.dimensionFieldsMissing': 'Nenhuma dimensão governada está disponível para o recurso e a operação atuais.',
4684
+ 'praxis.charts.editor.catalog.metricFieldsMissing': 'Nenhuma métrica governada está disponível para o recurso e a operação atuais.',
4685
+ 'praxis.charts.editor.catalog.dimensionFieldStale': 'A dimensão selecionada não é elegível para o recurso e a operação atuais.',
4686
+ 'praxis.charts.editor.catalog.metricFieldStale': 'A métrica selecionada não é elegível para o recurso e a operação atuais.',
4687
+ 'praxis.charts.editor.catalog.metricAggregationStale': 'A agregação selecionada não é permitida para a métrica escolhida.',
4688
+ 'praxis.charts.editor.catalog.targetMissing': 'O catálogo de destinos é obrigatório para esta ação de evento governada.',
4689
+ 'praxis.charts.editor.catalog.targetStale': 'O destino selecionado não está disponível para esta ação de evento.',
4690
+ 'praxis.charts.editor.catalog.capabilitiesDenied': 'O acesso às capabilities do recurso selecionado foi negado.',
4691
+ 'praxis.charts.editor.catalog.capabilitiesUnavailable': 'As capabilities do recurso selecionado estão temporariamente indisponíveis.',
4692
+ 'praxis.charts.editor.catalog.contextUnavailable': 'Não foi possível resolver o contexto governado de authoring do gráfico.',
4693
+ 'praxis.charts.widget.contextDiagnostics.title': 'Contexto de authoring',
4694
+ 'praxis.charts.widget.contextDiagnostics.catalogDenied': 'O acesso ao catálogo governado de recursos foi negado.',
4695
+ 'praxis.charts.widget.contextDiagnostics.catalogUnavailable': 'O catálogo governado de recursos está temporariamente indisponível.',
4696
+ 'praxis.charts.widget.contextDiagnostics.catalogEmpty': 'Nenhum recurso de stats authorável foi publicado pelo catálogo governado.',
4697
+ 'praxis.charts.widget.contextDiagnostics.catalogPathInvalid': 'O catálogo de recursos publicou um path operacional inseguro ou não relativo.',
4698
+ 'praxis.charts.widget.contextDiagnostics.capabilitiesDenied': 'O acesso às capabilities do recurso selecionado foi negado.',
4699
+ 'praxis.charts.widget.contextDiagnostics.capabilitiesUnavailable': 'As capabilities do recurso selecionado estão temporariamente indisponíveis.',
4700
+ 'praxis.charts.widget.contextDiagnostics.operationsEmpty': 'O recurso selecionado não publica operações de stats authoráveis.',
4701
+ 'praxis.charts.widget.contextDiagnostics.fieldsEmpty': 'O recurso selecionado não publica campos governados para a autoria do gráfico.',
4702
+ 'praxis.charts.widget.contextDiagnostics.identityMismatch': 'A identidade do recurso selecionado não corresponde ao snapshot governado de capabilities.',
4703
+ 'praxis.charts.widget.contextDiagnostics.capabilitiesPathInvalid': 'O snapshot de capabilities selecionado não publicou um path de recurso governado e seguro.',
4704
+ 'praxis.charts.widget.contextDiagnostics.capabilitiesPathMismatch': 'O path do snapshot de capabilities não corresponde ao path operacional publicado pelo catálogo.',
4705
+ 'praxis.charts.widget.contextDiagnostics.partial': 'Algumas opções governadas de authoring não puderam ser materializadas a partir do contexto atual da página.',
4259
4706
  'praxis.charts.editor.hint.sizingMode': 'Use preencher container apenas quando o widget host fornecer altura definida para o corpo.',
4260
4707
  'praxis.charts.editor.hint.height': 'Números são salvos como pixels; medidas CSS como 20rem também são aceitas.',
4261
4708
  'praxis.charts.editor.hint.minHeight': 'Defina um mínimo legível para widgets compactos de dashboard.',
@@ -4394,7 +4841,7 @@ function resolvePraxisChartsText(value, fallback) {
4394
4841
  class ChartEditorDefaultsService {
4395
4842
  create() {
4396
4843
  return {
4397
- version: '1.0.0',
4844
+ version: PRAXIS_X_UI_CHART_AUTHORABLE_VERSION,
4398
4845
  kind: 'bar',
4399
4846
  title: 'Untitled chart',
4400
4847
  source: {
@@ -4470,13 +4917,844 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
4470
4917
  args: [{ providedIn: 'root' }]
4471
4918
  }], ctorParameters: () => [{ type: PraxisChartCanonicalContractMapperService }] });
4472
4919
 
4473
- class PraxisChartConfigEditor {
4474
- documentInput = input(null, { ...(ngDevMode ? { debugName: "documentInput" } : /* istanbul ignore next */ {}), alias: 'document' });
4920
+ /**
4921
+ * Internal projection of already-governed Page Builder composition evidence.
4922
+ *
4923
+ * It deliberately does not discover unconnected widgets, nested widgets,
4924
+ * routes, surfaces, or global actions. A target enters this catalog only when
4925
+ * an existing top-level link from the current chart proves the destination.
4926
+ */
4927
+ class ChartCompositionTargetCatalogAdapter {
4928
+ registry = inject(ComponentMetadataRegistry);
4929
+ resolve(request) {
4930
+ const targets = new Map();
4931
+ const diagnostics = [];
4932
+ const diagnosticKeys = new Set();
4933
+ const addDiagnostic = (code, severity, path) => {
4934
+ const key = `${code}:${path}`;
4935
+ if (diagnosticKeys.has(key)) {
4936
+ return;
4937
+ }
4938
+ diagnosticKeys.add(key);
4939
+ diagnostics.push({ code, severity, path });
4940
+ };
4941
+ if (!this.isPageEvidence(request.page)) {
4942
+ addDiagnostic('chart-target-page-invalid', 'error', 'page');
4943
+ return { targets: [], diagnostics };
4944
+ }
4945
+ const page = request.page;
4946
+ const currentWidget = page.widgets.find((candidate) => this.isWidgetEvidence(candidate) && candidate.key === request.widgetKey);
4947
+ if (!currentWidget) {
4948
+ addDiagnostic('chart-target-source-widget-missing', 'error', 'page.widgets');
4949
+ return { targets: [], diagnostics };
4950
+ }
4951
+ for (const [index, candidate] of (page.composition?.links ?? []).entries()) {
4952
+ if (this.sourceWidgetKey(candidate) !== request.widgetKey) {
4953
+ continue;
4954
+ }
4955
+ const linkPath = `page.composition.links[${index}]`;
4956
+ if (!this.isRecord(candidate)) {
4957
+ addDiagnostic('chart-target-link-invalid', 'warning', linkPath);
4958
+ continue;
4959
+ }
4960
+ if (this.isDeprecatedLink(candidate)) {
4961
+ addDiagnostic('chart-target-link-deprecated', 'warning', linkPath);
4962
+ continue;
4963
+ }
4964
+ const source = this.readComponentPortEndpoint(candidate['from']);
4965
+ if (!source || source.ref.direction !== 'output') {
4966
+ addDiagnostic('chart-target-source-endpoint-invalid', 'warning', `${linkPath}.from`);
4967
+ continue;
4968
+ }
4969
+ if (source.ref.nestedPath?.length) {
4970
+ addDiagnostic('chart-target-source-nested-unsupported', 'info', `${linkPath}.from`);
4971
+ continue;
4972
+ }
4973
+ if (!this.endpointComponentTypeMatches(source.ref.componentType, currentWidget.definition.id, request.widgetType, request.componentId, request.metadata.id, request.metadata.componentType)) {
4974
+ addDiagnostic('chart-target-source-component-type-mismatch', 'warning', `${linkPath}.from`);
4975
+ continue;
4976
+ }
4977
+ const sourcePort = request.metadata.ports?.find((port) => port.id === source.ref.port);
4978
+ if (!this.isAuthorablePort(sourcePort, 'output')) {
4979
+ addDiagnostic('chart-target-source-port-not-authorable', 'warning', `${linkPath}.from`);
4980
+ continue;
4981
+ }
4982
+ const sourceEvent = this.sourceEvent(sourcePort);
4983
+ if (!sourceEvent) {
4984
+ addDiagnostic('chart-target-source-event-unsupported', 'info', `${linkPath}.from.ref.port`);
4985
+ continue;
4986
+ }
4987
+ const target = candidate['to'];
4988
+ if (this.hasEndpointKind(target, 'component-port')) {
4989
+ this.resolveWidgetTarget(target, sourceEvent, sourcePort, candidate['transform'], page, request.widgetKey, linkPath, targets, addDiagnostic);
4990
+ continue;
4991
+ }
4992
+ if (this.hasEndpointKind(target, 'state')) {
4993
+ this.resolveStateTarget(target, sourceEvent, candidate['intent'], page, linkPath, targets, addDiagnostic);
4994
+ continue;
4995
+ }
4996
+ if (this.hasEndpointKind(target, 'global-action')) {
4997
+ addDiagnostic('chart-target-global-action-unsupported', 'info', `${linkPath}.to`);
4998
+ continue;
4999
+ }
5000
+ addDiagnostic('chart-target-endpoint-unsupported', 'warning', `${linkPath}.to`);
5001
+ }
5002
+ return {
5003
+ targets: Array.from(targets.values()),
5004
+ diagnostics,
5005
+ };
5006
+ }
5007
+ resolveWidgetTarget(value, sourceEvent, sourcePort, transform, page, sourceWidgetKey, linkPath, targets, addDiagnostic) {
5008
+ const endpoint = this.readComponentPortEndpoint(value);
5009
+ if (!endpoint || endpoint.ref.direction !== 'input') {
5010
+ addDiagnostic('chart-target-widget-endpoint-invalid', 'warning', `${linkPath}.to`);
5011
+ return;
5012
+ }
5013
+ if (endpoint.ref.nestedPath?.length) {
5014
+ addDiagnostic('chart-target-widget-nested-unsupported', 'info', `${linkPath}.to`);
5015
+ return;
5016
+ }
5017
+ const widget = page.widgets.find((candidate) => this.isWidgetEvidence(candidate) && candidate.key === endpoint.ref.widget);
5018
+ if (!widget) {
5019
+ addDiagnostic('chart-target-widget-missing', 'warning', `${linkPath}.to`);
5020
+ return;
5021
+ }
5022
+ if (widget.key === sourceWidgetKey) {
5023
+ addDiagnostic('chart-target-widget-self-unsupported', 'warning', `${linkPath}.to`);
5024
+ return;
5025
+ }
5026
+ const metadata = this.registry.get(widget.definition.id);
5027
+ if (!metadata) {
5028
+ addDiagnostic('chart-target-widget-metadata-missing', 'warning', `${linkPath}.to`);
5029
+ return;
5030
+ }
5031
+ if (!this.endpointComponentTypeMatches(endpoint.ref.componentType, widget.definition.id, metadata.id, metadata.componentType)) {
5032
+ addDiagnostic('chart-target-widget-component-type-mismatch', 'warning', `${linkPath}.to`);
5033
+ return;
5034
+ }
5035
+ const port = metadata.ports?.find((candidate) => candidate.id === endpoint.ref.port);
5036
+ if (!this.isAuthorablePort(port, 'input')
5037
+ || port?.semanticKind !== 'query-context') {
5038
+ addDiagnostic('chart-target-widget-port-not-authorable', 'warning', `${linkPath}.to`);
5039
+ return;
5040
+ }
5041
+ if (!this.isWidgetLinkSemanticallyCompatible(sourcePort, port, transform)) {
5042
+ addDiagnostic('chart-target-widget-link-incompatible', 'warning', linkPath);
5043
+ return;
5044
+ }
5045
+ const id = widget.key;
5046
+ this.upsertTarget(targets, `filter-widget:${id}`, {
5047
+ id,
5048
+ label: this.widgetLabel(metadata.friendlyName, id),
5049
+ kind: 'widget',
5050
+ actions: ['filter-widget'],
5051
+ events: [sourceEvent],
5052
+ }, sourceEvent);
5053
+ }
5054
+ resolveStateTarget(value, sourceEvent, intent, page, linkPath, targets, addDiagnostic) {
5055
+ const endpoint = this.readStateEndpoint(value);
5056
+ if (!endpoint || !endpoint.ref.path || endpoint.ref.path.trim() !== endpoint.ref.path) {
5057
+ addDiagnostic('chart-target-state-endpoint-invalid', 'warning', `${linkPath}.to`);
5058
+ return;
5059
+ }
5060
+ if (endpoint.ref.layer !== undefined && endpoint.ref.layer !== 'values') {
5061
+ addDiagnostic('chart-target-state-layer-unsupported', 'warning', `${linkPath}.to`);
5062
+ return;
5063
+ }
5064
+ if (endpoint.ref.writable !== true) {
5065
+ addDiagnostic('chart-target-state-not-explicitly-writable', 'warning', `${linkPath}.to`);
5066
+ return;
5067
+ }
5068
+ if (intent !== 'state-write') {
5069
+ addDiagnostic('chart-target-state-intent-invalid', 'warning', `${linkPath}.intent`);
5070
+ return;
5071
+ }
5072
+ const schema = this.readDeclaredStateSchema(page.state);
5073
+ if (!schema || !Object.prototype.hasOwnProperty.call(schema, endpoint.ref.path)) {
5074
+ addDiagnostic('chart-target-state-path-undeclared', 'warning', `${linkPath}.to.ref.path`);
5075
+ return;
5076
+ }
5077
+ const id = endpoint.ref.path;
5078
+ this.upsertTarget(targets, `update-context:${id}`, {
5079
+ id,
5080
+ label: this.stateLabel(schema[id], id),
5081
+ actions: ['update-context'],
5082
+ events: [sourceEvent],
5083
+ }, sourceEvent);
5084
+ }
5085
+ sourceEvent(port) {
5086
+ switch (port.id) {
5087
+ case 'pointAction':
5088
+ return 'pointClick';
5089
+ case 'selectionChange':
5090
+ case 'drillDown':
5091
+ case 'crossFilter':
5092
+ return port.id;
5093
+ default:
5094
+ return null;
5095
+ }
5096
+ }
5097
+ isWidgetLinkSemanticallyCompatible(sourcePort, targetPort, transform) {
5098
+ let effectiveSourceKind = sourcePort.semanticKind;
5099
+ if (transform !== undefined) {
5100
+ if (!this.isRecord(transform) || !Array.isArray(transform['steps'])) {
5101
+ return false;
5102
+ }
5103
+ if (transform['steps'].some((step) => (!this.isRecord(step)
5104
+ || typeof step['kind'] !== 'string'
5105
+ || typeof step['phase'] !== 'string'))) {
5106
+ return false;
5107
+ }
5108
+ const output = transform['output'];
5109
+ if (output !== undefined) {
5110
+ if (!this.isRecord(output)) {
5111
+ return false;
5112
+ }
5113
+ if (output['semanticKind'] !== undefined
5114
+ && typeof output['semanticKind'] !== 'string') {
5115
+ return false;
5116
+ }
5117
+ effectiveSourceKind = typeof output['semanticKind'] === 'string'
5118
+ ? output['semanticKind']
5119
+ : effectiveSourceKind;
5120
+ }
5121
+ }
5122
+ return targetPort.semanticKind === 'query-context'
5123
+ && (effectiveSourceKind === 'query-context' || effectiveSourceKind === 'value');
5124
+ }
5125
+ upsertTarget(targets, key, target, sourceEvent) {
5126
+ const current = targets.get(key);
5127
+ if (!current) {
5128
+ targets.set(key, target);
5129
+ return;
5130
+ }
5131
+ targets.set(key, {
5132
+ ...current,
5133
+ events: current.events?.includes(sourceEvent)
5134
+ ? current.events
5135
+ : [...(current.events ?? []), sourceEvent],
5136
+ });
5137
+ }
5138
+ isPageEvidence(value) {
5139
+ if (!this.isRecord(value) || !Array.isArray(value['widgets'])) {
5140
+ return false;
5141
+ }
5142
+ const composition = value['composition'];
5143
+ if (composition === undefined) {
5144
+ return true;
5145
+ }
5146
+ if (!this.isRecord(composition)) {
5147
+ return false;
5148
+ }
5149
+ return composition['links'] === undefined || Array.isArray(composition['links']);
5150
+ }
5151
+ isWidgetEvidence(value) {
5152
+ if (!this.isRecord(value) || typeof value['key'] !== 'string') {
5153
+ return false;
5154
+ }
5155
+ const definition = value['definition'];
5156
+ return this.isRecord(definition) && typeof definition['id'] === 'string';
5157
+ }
5158
+ sourceWidgetKey(value) {
5159
+ if (!this.isRecord(value)) {
5160
+ return undefined;
5161
+ }
5162
+ const source = value['from'];
5163
+ if (!this.hasEndpointKind(source, 'component-port') || !this.isRecord(source['ref'])) {
5164
+ return undefined;
5165
+ }
5166
+ return typeof source['ref']['widget'] === 'string'
5167
+ ? source['ref']['widget']
5168
+ : undefined;
5169
+ }
5170
+ readComponentPortEndpoint(value) {
5171
+ if (!this.hasEndpointKind(value, 'component-port')) {
5172
+ return null;
5173
+ }
5174
+ const ref = value['ref'];
5175
+ if (!this.isRecord(ref)
5176
+ || typeof ref['widget'] !== 'string'
5177
+ || typeof ref['port'] !== 'string'
5178
+ || typeof ref['direction'] !== 'string'
5179
+ || (ref['componentType'] !== undefined && typeof ref['componentType'] !== 'string')
5180
+ || (ref['nestedPath'] !== undefined && !Array.isArray(ref['nestedPath']))) {
5181
+ return null;
5182
+ }
5183
+ return {
5184
+ kind: 'component-port',
5185
+ ref: {
5186
+ widget: ref['widget'],
5187
+ port: ref['port'],
5188
+ direction: ref['direction'],
5189
+ ...(typeof ref['componentType'] === 'string'
5190
+ ? { componentType: ref['componentType'] }
5191
+ : {}),
5192
+ ...(Array.isArray(ref['nestedPath'])
5193
+ ? { nestedPath: ref['nestedPath'] }
5194
+ : {}),
5195
+ },
5196
+ };
5197
+ }
5198
+ readStateEndpoint(value) {
5199
+ if (!this.hasEndpointKind(value, 'state')) {
5200
+ return null;
5201
+ }
5202
+ const ref = value['ref'];
5203
+ if (!this.isRecord(ref)
5204
+ || typeof ref['path'] !== 'string'
5205
+ || (ref['layer'] !== undefined && typeof ref['layer'] !== 'string')
5206
+ || (ref['writable'] !== undefined && typeof ref['writable'] !== 'boolean')) {
5207
+ return null;
5208
+ }
5209
+ return {
5210
+ kind: 'state',
5211
+ ref: {
5212
+ path: ref['path'],
5213
+ ...(typeof ref['layer'] === 'string' ? { layer: ref['layer'] } : {}),
5214
+ ...(typeof ref['writable'] === 'boolean' ? { writable: ref['writable'] } : {}),
5215
+ },
5216
+ };
5217
+ }
5218
+ readDeclaredStateSchema(value) {
5219
+ if (!this.isRecord(value) || !this.isRecord(value['schema'])) {
5220
+ return null;
5221
+ }
5222
+ return value['schema'];
5223
+ }
5224
+ isAuthorablePort(port, direction) {
5225
+ return !!port
5226
+ && port.direction === direction
5227
+ && port.exposure?.public === true
5228
+ && port.exposure.deprecated !== true;
5229
+ }
5230
+ endpointComponentTypeMatches(endpointComponentType, ...canonicalIds) {
5231
+ if (endpointComponentType === undefined) {
5232
+ return true;
5233
+ }
5234
+ return canonicalIds.some((candidate) => candidate === endpointComponentType);
5235
+ }
5236
+ widgetLabel(friendlyName, widgetKey) {
5237
+ const label = friendlyName.trim();
5238
+ return label && label !== widgetKey
5239
+ ? `${label} (${widgetKey})`
5240
+ : widgetKey;
5241
+ }
5242
+ stateLabel(descriptor, path) {
5243
+ const description = this.isRecord(descriptor) && typeof descriptor['description'] === 'string'
5244
+ ? descriptor['description'].trim()
5245
+ : '';
5246
+ return description && description !== path
5247
+ ? `${description} (${path})`
5248
+ : path;
5249
+ }
5250
+ isDeprecatedLink(value) {
5251
+ const metadata = value['metadata'];
5252
+ return this.isRecord(metadata) && metadata['deprecated'] === true;
5253
+ }
5254
+ hasEndpointKind(value, kind) {
5255
+ return this.isRecord(value) && value['kind'] === kind;
5256
+ }
5257
+ isRecord(value) {
5258
+ return !!value && typeof value === 'object' && !Array.isArray(value);
5259
+ }
5260
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ChartCompositionTargetCatalogAdapter, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
5261
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ChartCompositionTargetCatalogAdapter, providedIn: 'root' });
5262
+ }
5263
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ChartCompositionTargetCatalogAdapter, decorators: [{
5264
+ type: Injectable,
5265
+ args: [{ providedIn: 'root' }]
5266
+ }] });
5267
+
5268
+ class ChartResourceCapabilityCatalogAdapter {
5269
+ toResourceOperations(capabilities) {
5270
+ if (!capabilities) {
5271
+ return [];
5272
+ }
5273
+ const operations = [];
5274
+ if (capabilities.canonicalOperations?.['statsGroupBy'] === true) {
5275
+ operations.push('group-by');
5276
+ }
5277
+ if (capabilities.canonicalOperations?.['statsTimeSeries'] === true) {
5278
+ operations.push('timeseries');
5279
+ }
5280
+ if (capabilities.canonicalOperations?.['statsDistribution'] === true) {
5281
+ operations.push('distribution');
5282
+ }
5283
+ return operations;
5284
+ }
5285
+ toFieldOptions(capabilities, operation) {
5286
+ if (!capabilities) {
5287
+ return [];
5288
+ }
5289
+ const fields = capabilities?.stats?.fields ?? [];
5290
+ const resourceOperations = this.toResourceOperations(capabilities);
5291
+ const fieldOptions = fields
5292
+ .filter((field) => !!field?.field)
5293
+ .flatMap((field) => this.toFieldOptionsForRoles(field, capabilities, resourceOperations));
5294
+ const countOption = this.toCountOption(fields, capabilities, resourceOperations);
5295
+ return [...fieldOptions, ...(countOption ? [countOption] : [])]
5296
+ .filter((option) => !operation || option.operations?.includes(operation));
5297
+ }
5298
+ toFieldOptionsForRoles(field, capabilities, resourceOperations) {
5299
+ const aggregations = this.resolveAggregations(field);
5300
+ const shared = {
5301
+ field: field.field,
5302
+ ...(field.label ? { label: field.label } : {}),
5303
+ ...(capabilities.resourceKey ? { resourceIds: [capabilities.resourceKey] } : {}),
5304
+ ...(capabilities.resourcePath ? { resourcePaths: [capabilities.resourcePath] } : {}),
5305
+ ...(aggregations.length ? { aggregations } : {}),
5306
+ };
5307
+ const options = [];
5308
+ const dimensionOperations = this.resolveDimensionOperations(field)
5309
+ .filter((candidate) => resourceOperations.includes(candidate));
5310
+ const distributionModes = this.resolveDistributionModes(field);
5311
+ if (dimensionOperations.length) {
5312
+ options.push({
5313
+ ...shared,
5314
+ roles: field.timeSeriesEligible === true
5315
+ ? ['dimension', 'time']
5316
+ : ['dimension'],
5317
+ operations: dimensionOperations,
5318
+ ...(dimensionOperations.includes('distribution')
5319
+ ? { distributionModes }
5320
+ : {}),
5321
+ });
5322
+ }
5323
+ if (field.metricFieldEligible === true && aggregations.length) {
5324
+ options.push({
5325
+ ...shared,
5326
+ aggregable: true,
5327
+ roles: ['metric'],
5328
+ operations: resourceOperations,
5329
+ ...(resourceOperations.includes('distribution')
5330
+ ? { distributionModes: ['terms'] }
5331
+ : {}),
5332
+ });
5333
+ }
5334
+ return options;
5335
+ }
5336
+ resolveDimensionOperations(field) {
5337
+ const operations = new Set();
5338
+ const modes = field.modes ?? [];
5339
+ if (field.groupByEligible === true || modes.includes('GROUP_BY')) {
5340
+ operations.add('group-by');
5341
+ }
5342
+ if (field.timeSeriesEligible === true || modes.includes('TIME_SERIES')) {
5343
+ operations.add('timeseries');
5344
+ }
5345
+ if (field.distributionTermsEligible === true
5346
+ || field.distributionHistogramEligible === true
5347
+ || modes.includes('DISTRIBUTION_TERMS')
5348
+ || modes.includes('DISTRIBUTION_HISTOGRAM')) {
5349
+ operations.add('distribution');
5350
+ }
5351
+ return Array.from(operations);
5352
+ }
5353
+ resolveAggregations(field) {
5354
+ return (field.metrics ?? [])
5355
+ .map((metric) => this.mapMetric(metric))
5356
+ .filter((metric) => !!metric && metric !== 'count');
5357
+ }
5358
+ resolveDistributionModes(field) {
5359
+ const modes = [];
5360
+ if (field.distributionTermsEligible === true || field.modes?.includes('DISTRIBUTION_TERMS')) {
5361
+ modes.push('terms');
5362
+ }
5363
+ if (field.distributionHistogramEligible === true
5364
+ || field.modes?.includes('DISTRIBUTION_HISTOGRAM')) {
5365
+ modes.push('histogram');
5366
+ }
5367
+ return modes;
5368
+ }
5369
+ mapMetric(metric) {
5370
+ switch (String(metric || '').trim().toUpperCase()) {
5371
+ case 'COUNT':
5372
+ return 'count';
5373
+ case 'DISTINCT_COUNT':
5374
+ return 'distinct-count';
5375
+ case 'SUM':
5376
+ return 'sum';
5377
+ case 'AVG':
5378
+ case 'AVERAGE':
5379
+ return 'avg';
5380
+ case 'MIN':
5381
+ return 'min';
5382
+ case 'MAX':
5383
+ return 'max';
5384
+ default:
5385
+ return null;
5386
+ }
5387
+ }
5388
+ toCountOption(fields, capabilities, resourceOperations) {
5389
+ if (!resourceOperations.length
5390
+ || !fields.some((field) => (field.metrics ?? []).some((metric) => String(metric).toUpperCase() === 'COUNT'))) {
5391
+ return null;
5392
+ }
5393
+ return {
5394
+ field: 'count',
5395
+ label: 'Count',
5396
+ aggregable: true,
5397
+ roles: ['metric'],
5398
+ ...(capabilities.resourceKey ? { resourceIds: [capabilities.resourceKey] } : {}),
5399
+ ...(capabilities.resourcePath ? { resourcePaths: [capabilities.resourcePath] } : {}),
5400
+ operations: resourceOperations,
5401
+ ...(resourceOperations.includes('distribution')
5402
+ ? { distributionModes: ['terms', 'histogram'] }
5403
+ : {}),
5404
+ aggregations: ['count'],
5405
+ };
5406
+ }
5407
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ChartResourceCapabilityCatalogAdapter, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
5408
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ChartResourceCapabilityCatalogAdapter, providedIn: 'root' });
5409
+ }
5410
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ChartResourceCapabilityCatalogAdapter, decorators: [{
5411
+ type: Injectable,
5412
+ args: [{ providedIn: 'root' }]
5413
+ }] });
5414
+
5415
+ class PraxisChartConfigEditorContextResolverService {
5416
+ static REQUEST_TIMEOUT_MS = 10_000;
5417
+ discovery = inject(ResourceDiscoveryService);
5418
+ capabilityAdapter = inject(ChartResourceCapabilityCatalogAdapter);
5419
+ targetAdapter = inject(ChartCompositionTargetCatalogAdapter);
5420
+ /**
5421
+ * ComponentConfigEditorContextRequest currently exposes no principal, tenant, or
5422
+ * profile identity. Do not infer an authorization scope from widget/page data.
5423
+ * Successful reads are not cached across requests because capabilities may be
5424
+ * permission-sensitive. Only concurrent reads are deduplicated, and every pending
5425
+ * read has a finite timeout; server-side authorization remains canonical.
5426
+ */
5427
+ catalogProjectionRequest;
5428
+ capabilityRequests = new Map();
5429
+ async resolve(request) {
5430
+ const targetProjection = this.targetAdapter.resolve(request);
5431
+ let projection;
5432
+ try {
5433
+ projection = await this.getCatalogProjection();
5434
+ }
5435
+ catch (error) {
5436
+ return {
5437
+ context: {
5438
+ availableResources: [],
5439
+ availableFields: [],
5440
+ availableTargets: targetProjection.targets,
5441
+ },
5442
+ diagnostics: [
5443
+ ...(targetProjection.diagnostics ?? []),
5444
+ this.httpDiagnostic(error, 'catalog'),
5445
+ ],
5446
+ };
5447
+ }
5448
+ const resources = projection.candidates.map((candidate) => candidate.option);
5449
+ const selectedResource = this.selectedResource(request.persistedInputs);
5450
+ if (!selectedResource) {
5451
+ return {
5452
+ context: {
5453
+ availableResources: resources,
5454
+ availableFields: [],
5455
+ availableTargets: targetProjection.targets,
5456
+ },
5457
+ diagnostics: [
5458
+ ...projection.diagnostics,
5459
+ ...(targetProjection.diagnostics ?? []),
5460
+ ],
5461
+ };
5462
+ }
5463
+ const selected = await this.resolveResourceFromProjection(selectedResource, projection);
5464
+ const resolvedResources = selected.resource
5465
+ ? resources.map((resource) => resource.id === selected.resource?.id ? selected.resource : resource)
5466
+ : resources;
5467
+ return {
5468
+ context: {
5469
+ availableResources: resolvedResources,
5470
+ availableFields: selected.fields,
5471
+ availableTargets: targetProjection.targets,
5472
+ },
5473
+ diagnostics: [
5474
+ ...projection.diagnostics,
5475
+ ...(targetProjection.diagnostics ?? []),
5476
+ ...selected.diagnostics,
5477
+ ],
5478
+ };
5479
+ }
5480
+ async resolveResource(resourceIdentity) {
5481
+ let projection;
5482
+ try {
5483
+ projection = await this.getCatalogProjection();
5484
+ }
5485
+ catch (error) {
5486
+ return {
5487
+ fields: [],
5488
+ diagnostics: [this.httpDiagnostic(error, 'catalog')],
5489
+ };
5490
+ }
5491
+ const selected = await this.resolveResourceFromProjection(resourceIdentity, projection);
5492
+ return {
5493
+ ...selected,
5494
+ diagnostics: [...projection.diagnostics, ...selected.diagnostics],
5495
+ };
5496
+ }
5497
+ async resolveResourceFromProjection(resourceIdentity, projection) {
5498
+ const candidate = this.findCandidate(projection.candidates, resourceIdentity);
5499
+ if (!candidate) {
5500
+ return {
5501
+ fields: [],
5502
+ diagnostics: [{
5503
+ code: 'chart-resource-catalog-selection-stale',
5504
+ severity: 'error',
5505
+ path: 'chartDocument.source.resource',
5506
+ }],
5507
+ };
5508
+ }
5509
+ let capabilities;
5510
+ try {
5511
+ capabilities = await this.getCapabilities(candidate);
5512
+ }
5513
+ catch (error) {
5514
+ return {
5515
+ fields: [],
5516
+ diagnostics: [this.httpDiagnostic(error, 'capabilities')],
5517
+ };
5518
+ }
5519
+ if (String(capabilities.resourceKey || '').trim() !== candidate.option.id) {
5520
+ return {
5521
+ fields: [],
5522
+ diagnostics: [{
5523
+ code: 'chart-resource-capabilities-identity-mismatch',
5524
+ severity: 'error',
5525
+ path: 'chartDocument.source.resource',
5526
+ }],
5527
+ };
5528
+ }
5529
+ const capabilityResourcePath = String(capabilities.resourcePath || '').trim();
5530
+ if (!this.isGovernedRelativePath(capabilityResourcePath)) {
5531
+ return {
5532
+ fields: [],
5533
+ diagnostics: [{
5534
+ code: 'chart-resource-capabilities-path-invalid',
5535
+ severity: 'error',
5536
+ path: 'chartDocument.source.resource',
5537
+ }],
5538
+ };
5539
+ }
5540
+ const catalogResourcePath = this.normalizeOperationalPath(candidate.option.path);
5541
+ if (this.normalizeOperationalPath(capabilityResourcePath) !== catalogResourcePath) {
5542
+ return {
5543
+ fields: [],
5544
+ diagnostics: [{
5545
+ code: 'chart-resource-capabilities-path-mismatch',
5546
+ severity: 'error',
5547
+ path: 'chartDocument.source.resource',
5548
+ }],
5549
+ };
5550
+ }
5551
+ const canonicalCapabilities = {
5552
+ ...capabilities,
5553
+ resourcePath: catalogResourcePath,
5554
+ };
5555
+ const operations = this.capabilityAdapter.toResourceOperations(canonicalCapabilities);
5556
+ const fields = this.capabilityAdapter.toFieldOptions(canonicalCapabilities);
5557
+ const diagnostics = [];
5558
+ if (!operations.length) {
5559
+ diagnostics.push({
5560
+ code: 'chart-resource-stats-operations-empty',
5561
+ severity: 'warning',
5562
+ path: 'chartDocument.source.operation',
5563
+ });
5564
+ }
5565
+ if (!fields.length) {
5566
+ diagnostics.push({
5567
+ code: 'chart-resource-stats-fields-empty',
5568
+ severity: 'error',
5569
+ path: 'chartDocument.dimensions',
5570
+ });
5571
+ }
5572
+ return {
5573
+ resource: {
5574
+ ...candidate.option,
5575
+ id: canonicalCapabilities.resourceKey,
5576
+ path: catalogResourcePath,
5577
+ operations,
5578
+ },
5579
+ fields,
5580
+ diagnostics,
5581
+ };
5582
+ }
5583
+ async getCatalogProjection() {
5584
+ const cached = this.catalogProjectionRequest;
5585
+ if (cached) {
5586
+ return cached.promise;
5587
+ }
5588
+ let request;
5589
+ const promise = firstValueFrom(this.discovery.getSchemaCatalog().pipe(timeout(PraxisChartConfigEditorContextResolverService.REQUEST_TIMEOUT_MS)))
5590
+ .then((catalog) => this.projectCatalog(catalog))
5591
+ .finally(() => {
5592
+ if (this.catalogProjectionRequest === request) {
5593
+ this.catalogProjectionRequest = undefined;
5594
+ }
5595
+ });
5596
+ request = { promise };
5597
+ this.catalogProjectionRequest = request;
5598
+ return request.promise;
5599
+ }
5600
+ projectCatalog(catalog) {
5601
+ const byResourceKey = new Map();
5602
+ for (const endpoint of Array.isArray(catalog?.endpoints) ? catalog.endpoints : []) {
5603
+ const resourceKey = String(endpoint?.resourceKey || '').trim();
5604
+ if (!resourceKey) {
5605
+ continue;
5606
+ }
5607
+ byResourceKey.set(resourceKey, [...(byResourceKey.get(resourceKey) ?? []), endpoint]);
5608
+ }
5609
+ const candidates = [];
5610
+ const diagnostics = [];
5611
+ for (const [resourceKey, endpoints] of byResourceKey.entries()) {
5612
+ // The semantic resourceKey scopes the resource first. Path suffixes below only
5613
+ // identify canonical HTTP projections inside that governed scope; they never
5614
+ // route user intent or choose a resource from text.
5615
+ const declaredCapabilityEndpoints = endpoints.filter((endpoint) => (endpoint.method === 'GET'
5616
+ && !endpoint.path.includes('{')
5617
+ && endpoint.path.endsWith('/capabilities')));
5618
+ if (declaredCapabilityEndpoints.some((endpoint) => !this.isGovernedRelativePath(endpoint.path))) {
5619
+ diagnostics.push({
5620
+ code: 'chart-resource-catalog-path-invalid',
5621
+ severity: 'error',
5622
+ path: resourceKey,
5623
+ });
5624
+ continue;
5625
+ }
5626
+ const capabilityEndpoints = declaredCapabilityEndpoints.filter((endpoint) => (this.isGovernedRelativePath(endpoint.path.slice(0, -'/capabilities'.length))));
5627
+ if (declaredCapabilityEndpoints.length && !capabilityEndpoints.length) {
5628
+ diagnostics.push({
5629
+ code: 'chart-resource-catalog-path-invalid',
5630
+ severity: 'error',
5631
+ path: resourceKey,
5632
+ });
5633
+ continue;
5634
+ }
5635
+ if (capabilityEndpoints.length !== 1) {
5636
+ if (capabilityEndpoints.length > 1) {
5637
+ diagnostics.push({
5638
+ code: 'chart-resource-capabilities-endpoint-ambiguous',
5639
+ severity: 'warning',
5640
+ path: resourceKey,
5641
+ });
5642
+ }
5643
+ continue;
5644
+ }
5645
+ const capabilitiesEndpoint = capabilityEndpoints[0];
5646
+ const resourcePath = this.normalizeOperationalPath(capabilitiesEndpoint.path.slice(0, -'/capabilities'.length));
5647
+ const statsPaths = new Set([
5648
+ `${resourcePath}/stats/group-by`,
5649
+ `${resourcePath}/stats/timeseries`,
5650
+ `${resourcePath}/stats/distribution`,
5651
+ ]);
5652
+ const publishesStats = endpoints.some((endpoint) => (endpoint.method === 'POST'
5653
+ && statsPaths.has(endpoint.path)));
5654
+ if (!publishesStats) {
5655
+ continue;
5656
+ }
5657
+ const visual = capabilitiesEndpoint.resourceVisual
5658
+ ?? endpoints.find((endpoint) => endpoint.resourceVisual)?.resourceVisual
5659
+ ?? null;
5660
+ candidates.push({
5661
+ capabilitiesHref: capabilitiesEndpoint.path,
5662
+ option: {
5663
+ id: resourceKey,
5664
+ label: String(visual?.title || resourceKey).trim(),
5665
+ path: resourcePath,
5666
+ ...(visual?.description ? { description: visual.description } : {}),
5667
+ },
5668
+ });
5669
+ }
5670
+ candidates.sort((left, right) => (left.option.label.localeCompare(right.option.label)
5671
+ || left.option.id.localeCompare(right.option.id)));
5672
+ if (!candidates.length) {
5673
+ diagnostics.push({
5674
+ code: 'chart-resource-catalog-empty',
5675
+ severity: 'warning',
5676
+ path: 'availableResources',
5677
+ });
5678
+ }
5679
+ return { candidates, diagnostics };
5680
+ }
5681
+ async getCapabilities(candidate) {
5682
+ const key = `${candidate.option.id}\u0000${candidate.capabilitiesHref}`;
5683
+ const cached = this.capabilityRequests.get(key);
5684
+ if (cached) {
5685
+ return cached.promise;
5686
+ }
5687
+ let request;
5688
+ const promise = firstValueFrom(this.discovery.getCapabilitiesByUrl(candidate.capabilitiesHref).pipe(timeout(PraxisChartConfigEditorContextResolverService.REQUEST_TIMEOUT_MS)))
5689
+ .finally(() => {
5690
+ if (this.capabilityRequests.get(key) === request) {
5691
+ this.capabilityRequests.delete(key);
5692
+ }
5693
+ });
5694
+ request = { promise };
5695
+ this.capabilityRequests.set(key, request);
5696
+ return request.promise;
5697
+ }
5698
+ findCandidate(candidates, resourceIdentity) {
5699
+ const identity = String(resourceIdentity || '').trim();
5700
+ const normalizedPath = this.normalizeOperationalPath(identity);
5701
+ return candidates.find((candidate) => (candidate.option.id === identity
5702
+ || this.normalizeOperationalPath(candidate.option.path) === normalizedPath));
5703
+ }
5704
+ selectedResource(inputs) {
5705
+ const chartDocument = this.asRecord(inputs?.['chartDocument']);
5706
+ const source = this.asRecord(chartDocument?.source);
5707
+ if (source?.['kind'] !== 'praxis.stats') {
5708
+ return null;
5709
+ }
5710
+ const resource = String(source?.['resource'] || '').trim();
5711
+ return resource || null;
5712
+ }
5713
+ normalizeOperationalPath(value) {
5714
+ const path = String(value || '').trim();
5715
+ let end = path.length;
5716
+ while (end > 1 && path.charAt(end - 1) === '/') {
5717
+ end -= 1;
5718
+ }
5719
+ return path.slice(0, end);
5720
+ }
5721
+ isGovernedRelativePath(value) {
5722
+ const path = String(value || '').trim();
5723
+ return path.startsWith('/') && !path.startsWith('//');
5724
+ }
5725
+ asRecord(value) {
5726
+ return value && typeof value === 'object' && !Array.isArray(value)
5727
+ ? value
5728
+ : null;
5729
+ }
5730
+ httpDiagnostic(error, phase) {
5731
+ const denied = error instanceof HttpErrorResponse
5732
+ && (error.status === 401 || error.status === 403);
5733
+ return {
5734
+ code: denied
5735
+ ? `chart-resource-${phase}-denied`
5736
+ : `chart-resource-${phase}-unavailable`,
5737
+ severity: 'error',
5738
+ path: phase === 'catalog'
5739
+ ? 'availableResources'
5740
+ : 'chartDocument.source.resource',
5741
+ };
5742
+ }
5743
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartConfigEditorContextResolverService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
5744
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartConfigEditorContextResolverService, providedIn: 'root' });
5745
+ }
5746
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartConfigEditorContextResolverService, decorators: [{
5747
+ type: Injectable,
5748
+ args: [{ providedIn: 'root' }]
5749
+ }] });
5750
+
5751
+ class PraxisChartConfigEditor {
5752
+ documentInput = input(null, { ...(ngDevMode ? { debugName: "documentInput" } : /* istanbul ignore next */ {}), alias: 'document' });
4475
5753
  modeInput = input('edit', { ...(ngDevMode ? { debugName: "modeInput" } : /* istanbul ignore next */ {}), alias: 'mode' });
4476
5754
  readonlyInput = input(false, { ...(ngDevMode ? { debugName: "readonlyInput" } : /* istanbul ignore next */ {}), alias: 'readonly' });
4477
- availableResourcesInput = input([], { ...(ngDevMode ? { debugName: "availableResourcesInput" } : /* istanbul ignore next */ {}), alias: 'availableResources' });
4478
- availableFieldsInput = input([], { ...(ngDevMode ? { debugName: "availableFieldsInput" } : /* istanbul ignore next */ {}), alias: 'availableFields' });
4479
- availableTargetsInput = input([], { ...(ngDevMode ? { debugName: "availableTargetsInput" } : /* istanbul ignore next */ {}), alias: 'availableTargets' });
5755
+ availableResourcesInput = input(null, { ...(ngDevMode ? { debugName: "availableResourcesInput" } : /* istanbul ignore next */ {}), alias: 'availableResources' });
5756
+ availableFieldsInput = input(null, { ...(ngDevMode ? { debugName: "availableFieldsInput" } : /* istanbul ignore next */ {}), alias: 'availableFields' });
5757
+ availableTargetsInput = input(null, { ...(ngDevMode ? { debugName: "availableTargetsInput" } : /* istanbul ignore next */ {}), alias: 'availableTargets' });
4480
5758
  apply = output();
4481
5759
  save = output();
4482
5760
  resetChange = output();
@@ -4526,22 +5804,38 @@ class PraxisChartConfigEditor {
4526
5804
  normalizer = inject(ChartContractNormalizerService);
4527
5805
  validator = inject(ChartContractValidationService);
4528
5806
  previewMapper = inject(ChartEditorPreviewMapperService);
5807
+ contextResolver = inject(PraxisChartConfigEditorContextResolverService);
4529
5808
  i18n = inject(PraxisI18nService);
4530
5809
  currentDocument = signal(this.defaults.create(), ...(ngDevMode ? [{ debugName: "currentDocument" }] : /* istanbul ignore next */ []));
4531
5810
  initialDocument = signal(this.defaults.create(), ...(ngDevMode ? [{ debugName: "initialDocument" }] : /* istanbul ignore next */ []));
5811
+ resolvedResources = signal(null, ...(ngDevMode ? [{ debugName: "resolvedResources" }] : /* istanbul ignore next */ []));
5812
+ resolvedFields = signal(null, ...(ngDevMode ? [{ debugName: "resolvedFields" }] : /* istanbul ignore next */ []));
5813
+ catalogResolutionDiagnostics = signal([], ...(ngDevMode ? [{ debugName: "catalogResolutionDiagnostics" }] : /* istanbul ignore next */ []));
4532
5814
  lastExternalSignature = null;
5815
+ lastCatalogInputSignature = null;
5816
+ catalogResolutionRequestId = 0;
5817
+ baseAvailableResources = computed(() => (this.availableResourcesInput() ?? this.injectedData?.availableResources ?? []), ...(ngDevMode ? [{ debugName: "baseAvailableResources" }] : /* istanbul ignore next */ []));
5818
+ baseAvailableFields = computed(() => (this.availableFieldsInput() ?? this.injectedData?.availableFields ?? []), ...(ngDevMode ? [{ debugName: "baseAvailableFields" }] : /* istanbul ignore next */ []));
4533
5819
  normalizedDocument = computed(() => this.normalizer.normalize(this.currentDocument()), ...(ngDevMode ? [{ debugName: "normalizedDocument" }] : /* istanbul ignore next */ []));
4534
- validation = computed(() => this.validator.validate(this.normalizedDocument()), ...(ngDevMode ? [{ debugName: "validation" }] : /* istanbul ignore next */ []));
5820
+ validation = computed(() => {
5821
+ const validation = this.validator.validate(this.normalizedDocument());
5822
+ const catalogIssues = this.catalogIssues();
5823
+ return {
5824
+ valid: validation.valid && !catalogIssues.some((issue) => issue.severity === 'error'),
5825
+ issues: [
5826
+ ...validation.issues,
5827
+ ...catalogIssues,
5828
+ ],
5829
+ };
5830
+ }, ...(ngDevMode ? [{ debugName: "validation" }] : /* istanbul ignore next */ []));
4535
5831
  issues = computed(() => this.validation().issues, ...(ngDevMode ? [{ debugName: "issues" }] : /* istanbul ignore next */ []));
4536
- availableResources = computed(() => this.availableResourcesInput().length
4537
- ? this.availableResourcesInput()
4538
- : (this.injectedData?.availableResources ?? []), ...(ngDevMode ? [{ debugName: "availableResources" }] : /* istanbul ignore next */ []));
4539
- availableFields = computed(() => this.availableFieldsInput().length
4540
- ? this.availableFieldsInput()
4541
- : (this.injectedData?.availableFields ?? []), ...(ngDevMode ? [{ debugName: "availableFields" }] : /* istanbul ignore next */ []));
4542
- availableTargets = computed(() => this.availableTargetsInput().length
4543
- ? this.availableTargetsInput()
4544
- : (this.injectedData?.availableTargets ?? []), ...(ngDevMode ? [{ debugName: "availableTargets" }] : /* istanbul ignore next */ []));
5832
+ catalogIssues = computed(() => [
5833
+ ...this.validateCatalogContext(),
5834
+ ...this.catalogResolutionDiagnostics().map((diagnostic) => this.toCatalogIssue(diagnostic)),
5835
+ ], ...(ngDevMode ? [{ debugName: "catalogIssues" }] : /* istanbul ignore next */ []));
5836
+ availableResources = computed(() => this.resolvedResources() ?? this.baseAvailableResources(), ...(ngDevMode ? [{ debugName: "availableResources" }] : /* istanbul ignore next */ []));
5837
+ availableFields = computed(() => this.resolvedFields() ?? this.baseAvailableFields(), ...(ngDevMode ? [{ debugName: "availableFields" }] : /* istanbul ignore next */ []));
5838
+ availableTargets = computed(() => (this.availableTargetsInput() ?? this.injectedData?.availableTargets ?? []), ...(ngDevMode ? [{ debugName: "availableTargets" }] : /* istanbul ignore next */ []));
4545
5839
  preview = computed(() => {
4546
5840
  if (!this.validation().valid) {
4547
5841
  return null;
@@ -4549,6 +5843,21 @@ class PraxisChartConfigEditor {
4549
5843
  return this.previewMapper.build(this.normalizedDocument());
4550
5844
  }, ...(ngDevMode ? [{ debugName: "preview" }] : /* istanbul ignore next */ []));
4551
5845
  constructor() {
5846
+ effect(() => {
5847
+ const signature = JSON.stringify({
5848
+ resources: this.baseAvailableResources(),
5849
+ fields: this.baseAvailableFields(),
5850
+ });
5851
+ if (signature === this.lastCatalogInputSignature) {
5852
+ return;
5853
+ }
5854
+ this.lastCatalogInputSignature = signature;
5855
+ this.catalogResolutionRequestId += 1;
5856
+ this.resolvedResources.set(null);
5857
+ this.resolvedFields.set(null);
5858
+ this.catalogResolutionDiagnostics.set([]);
5859
+ this.isBusy$.next(false);
5860
+ });
4552
5861
  effect(() => {
4553
5862
  const externalDocument = this.documentInput()
4554
5863
  ?? this.injectedData?.chartDocument
@@ -4567,6 +5876,29 @@ class PraxisChartConfigEditor {
4567
5876
  this.lastExternalSignature = signature;
4568
5877
  this.refreshState(false);
4569
5878
  });
5879
+ effect(() => {
5880
+ const document = this.currentDocument();
5881
+ if (this.isReadonly() || document.source.kind !== 'praxis.stats') {
5882
+ return;
5883
+ }
5884
+ const resource = String(document.source.resource || '').trim();
5885
+ const option = resource
5886
+ ? this.availableResources().find((candidate) => this.resourceIdentityMatches(candidate, resource))
5887
+ : undefined;
5888
+ const canonicalPath = String(option?.path || '').trim();
5889
+ if (!canonicalPath || canonicalPath === resource) {
5890
+ return;
5891
+ }
5892
+ this.patchDocument((current) => current.source.kind === 'praxis.stats'
5893
+ ? {
5894
+ ...current,
5895
+ source: {
5896
+ ...current.source,
5897
+ resource: canonicalPath,
5898
+ },
5899
+ }
5900
+ : current);
5901
+ });
4570
5902
  }
4571
5903
  getSettingsValue() {
4572
5904
  return structuredClone(this.normalizedDocument());
@@ -4575,6 +5907,11 @@ class PraxisChartConfigEditor {
4575
5907
  return this.saveChanges().document;
4576
5908
  }
4577
5909
  reset() {
5910
+ this.catalogResolutionRequestId += 1;
5911
+ this.resolvedResources.set(null);
5912
+ this.resolvedFields.set(null);
5913
+ this.catalogResolutionDiagnostics.set([]);
5914
+ this.isBusy$.next(false);
4578
5915
  const snapshot = structuredClone(this.initialDocument());
4579
5916
  this.currentDocument.set(snapshot);
4580
5917
  this.refreshState(false);
@@ -4692,15 +6029,29 @@ class PraxisChartConfigEditor {
4692
6029
  }));
4693
6030
  }
4694
6031
  setResource(value) {
6032
+ const identity = value.trim();
6033
+ const option = this.resourceOptions().find((candidate) => (this.resourceIdentityMatches(candidate, identity)));
6034
+ const resource = String(option?.path || identity).trim();
4695
6035
  this.patchDocument((document) => ({
4696
6036
  ...document,
4697
6037
  source: document.source.kind === 'praxis.stats'
4698
6038
  ? {
4699
6039
  ...document.source,
4700
- resource: value.trim() || undefined,
6040
+ resource: resource || undefined,
4701
6041
  }
4702
6042
  : document.source,
4703
6043
  }));
6044
+ const baseCatalogAlreadyCoversSelection = !!option?.operations?.length
6045
+ && this.baseAvailableFields().some((field) => this.fieldMatchesResourceOption(field, option));
6046
+ if (!resource || !option || baseCatalogAlreadyCoversSelection) {
6047
+ this.catalogResolutionRequestId += 1;
6048
+ this.isBusy$.next(false);
6049
+ this.catalogResolutionDiagnostics.set([]);
6050
+ this.resolvedFields.set(null);
6051
+ this.refreshState(this.isDirty$.value);
6052
+ return;
6053
+ }
6054
+ void this.resolveSelectedResource(resource);
4704
6055
  }
4705
6056
  setOperation(value) {
4706
6057
  this.patchDocument((document) => ({
@@ -4747,10 +6098,18 @@ class PraxisChartConfigEditor {
4747
6098
  source: document.source.kind === 'praxis.stats'
4748
6099
  ? {
4749
6100
  ...document.source,
4750
- options: {
4751
- ...(document.source.options ?? {}),
4752
- mode: value,
4753
- },
6101
+ options: value === 'terms'
6102
+ ? {
6103
+ granularity: document.source.options?.granularity,
6104
+ fillGaps: document.source.options?.fillGaps,
6105
+ mode: value,
6106
+ orderBy: document.source.options?.orderBy,
6107
+ limit: document.source.options?.limit,
6108
+ }
6109
+ : {
6110
+ ...(document.source.options ?? {}),
6111
+ mode: value,
6112
+ },
4754
6113
  }
4755
6114
  : document.source,
4756
6115
  }));
@@ -5019,6 +6378,29 @@ class PraxisChartConfigEditor {
5019
6378
  resourceOptions() {
5020
6379
  return this.availableResources();
5021
6380
  }
6381
+ selectedResourceOption() {
6382
+ const resource = this.resourceValue();
6383
+ if (!resource) {
6384
+ return undefined;
6385
+ }
6386
+ return this.resourceOptions().find((option) => this.resourceIdentityMatches(option, resource));
6387
+ }
6388
+ operationOptions() {
6389
+ const selectedResource = this.selectedResourceOption();
6390
+ if (!selectedResource) {
6391
+ return this.resourceOptions().length ? this.operations : [];
6392
+ }
6393
+ return selectedResource.operations?.length ? selectedResource.operations : [];
6394
+ }
6395
+ resourceCatalogUnavailable() {
6396
+ return this.doc().source.kind === 'praxis.stats' && !this.resourceOptions().length;
6397
+ }
6398
+ operationCatalogUnavailable() {
6399
+ return this.doc().source.kind === 'praxis.stats'
6400
+ && !!this.resourceValue()
6401
+ && !!this.selectedResourceOption()
6402
+ && !this.operationOptions().length;
6403
+ }
5022
6404
  granularityValue() {
5023
6405
  return this.doc().source.kind === 'praxis.stats'
5024
6406
  ? (this.doc().source.options?.granularity ?? 'day')
@@ -5112,8 +6494,15 @@ class PraxisChartConfigEditor {
5112
6494
  eventTarget(eventKey) {
5113
6495
  return this.doc().events?.[eventKey]?.target ?? '';
5114
6496
  }
5115
- targetOptions() {
5116
- return this.availableTargets();
6497
+ targetOptions(action, eventKey) {
6498
+ const targets = this.availableTargets();
6499
+ return targets.filter((target) => ((!action || !target.actions?.length || target.actions.includes(action))
6500
+ && (!eventKey
6501
+ || target.events === undefined
6502
+ || target.events.includes(eventKey))));
6503
+ }
6504
+ targetCatalogUnavailable(action, eventKey) {
6505
+ return !!action && action !== 'emit' && !this.targetOptions(action, eventKey).length;
5117
6506
  }
5118
6507
  eventMappingText(eventKey) {
5119
6508
  const mapping = this.doc().events?.[eventKey]?.mapping;
@@ -5135,12 +6524,23 @@ class PraxisChartConfigEditor {
5135
6524
  if (!fields.length) {
5136
6525
  return [];
5137
6526
  }
5138
- return fields.filter((field) => {
5139
- if (!field.roles?.length) {
5140
- return role === 'dimension' ? field.aggregable !== true : true;
5141
- }
5142
- return field.roles.includes(role);
5143
- });
6527
+ return fields.filter((field) => this.fieldMatchesSelectedResource(field)
6528
+ && this.fieldMatchesOperation(field)
6529
+ && this.fieldMatchesDistributionMode(field)
6530
+ && this.fieldMatchesRole(field, role));
6531
+ }
6532
+ metricAggregationOptions(fieldName) {
6533
+ const field = this.fieldOptions('metric').find((option) => option.field === fieldName);
6534
+ if (!field?.aggregations?.length) {
6535
+ return this.metricAggregations;
6536
+ }
6537
+ return this.metricAggregations.filter((aggregation) => field.aggregations?.includes(aggregation));
6538
+ }
6539
+ fieldOptionLabel(field) {
6540
+ if (field.field === 'count') {
6541
+ return this.t('praxis.charts.editor.metricAggregation.count', 'count');
6542
+ }
6543
+ return field.label || field.field;
5144
6544
  }
5145
6545
  showMetricAxisControls() {
5146
6546
  return this.doc().kind === 'combo';
@@ -5215,6 +6615,184 @@ class PraxisChartConfigEditor {
5215
6615
  return accumulator;
5216
6616
  }, {});
5217
6617
  }
6618
+ validateCatalogContext() {
6619
+ const document = this.normalizedDocument();
6620
+ const issues = [];
6621
+ if (document.source.kind === 'praxis.stats') {
6622
+ if (!this.resourceOptions().length) {
6623
+ issues.push(this.catalogError('resource-catalog-missing', 'source.resource', this.t('praxis.charts.editor.catalog.resourceMissing', 'Resource catalog is required for governed praxis.stats authoring.')));
6624
+ }
6625
+ else if (document.source.resource && !this.selectedResourceOption()) {
6626
+ issues.push(this.catalogError('resource-stale', 'source.resource', this.t('praxis.charts.editor.catalog.resourceStale', 'Selected resource is not available in the governed resource catalog.')));
6627
+ }
6628
+ if (document.source.resource && this.selectedResourceOption() && !this.operationOptions().length) {
6629
+ issues.push(this.catalogError('operation-catalog-missing', 'source.operation', this.t('praxis.charts.editor.catalog.operationMissing', 'Selected resource does not publish authorable stats operations.')));
6630
+ }
6631
+ else if (document.source.operation
6632
+ && this.operationOptions().length
6633
+ && !this.operationOptions().includes(document.source.operation)) {
6634
+ issues.push(this.catalogError('operation-stale', 'source.operation', this.t('praxis.charts.editor.catalog.operationStale', 'Selected operation is not supported by the governed resource catalog.')));
6635
+ }
6636
+ if (document.source.resource
6637
+ && this.selectedResourceOption()
6638
+ && this.operationOptions().length
6639
+ && !this.availableFields().length) {
6640
+ issues.push(this.catalogError('field-catalog-missing', 'dimensions', this.t('praxis.charts.editor.catalog.fieldCatalogMissing', 'The selected resource does not publish governed fields for chart authoring.')));
6641
+ }
6642
+ }
6643
+ if (this.availableFields().length) {
6644
+ document.dimensions?.forEach((dimension, index) => {
6645
+ if (!dimension.field || this.fieldOptions('dimension').some((field) => field.field === dimension.field)) {
6646
+ return;
6647
+ }
6648
+ issues.push(this.catalogError('dimension-field-stale', `dimensions[${index}].field`, this.t('praxis.charts.editor.catalog.dimensionFieldStale', 'Selected dimension is not eligible for the current resource and operation.')));
6649
+ });
6650
+ document.metrics?.forEach((metric, index) => {
6651
+ const metricField = metric.field;
6652
+ if (metricField && !this.fieldOptions('metric').some((field) => field.field === metricField)) {
6653
+ issues.push(this.catalogError('metric-field-stale', `metrics[${index}].field`, this.t('praxis.charts.editor.catalog.metricFieldStale', 'Selected metric is not eligible for the current resource and operation.')));
6654
+ return;
6655
+ }
6656
+ const aggregationOptions = this.metricAggregationOptions(metricField);
6657
+ if (metric.aggregation && !aggregationOptions.includes(metric.aggregation)) {
6658
+ issues.push(this.catalogError('metric-aggregation-stale', `metrics[${index}].aggregation`, this.t('praxis.charts.editor.catalog.metricAggregationStale', 'Selected aggregation is not allowed for the selected metric field.')));
6659
+ }
6660
+ });
6661
+ }
6662
+ for (const eventKey of ['pointClick', 'selectionChange', 'drillDown', 'crossFilter']) {
6663
+ const eventAction = document.events?.[eventKey];
6664
+ if (!eventAction?.action || eventAction.action === 'emit') {
6665
+ continue;
6666
+ }
6667
+ const targetOptions = this.targetOptions(eventAction.action, eventKey);
6668
+ if (!targetOptions.length) {
6669
+ issues.push(this.catalogError('target-catalog-missing', `events.${eventKey}.target`, this.t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.')));
6670
+ }
6671
+ else if (eventAction.target && !targetOptions.some((target) => target.id === eventAction.target)) {
6672
+ issues.push(this.catalogError('target-stale', `events.${eventKey}.target`, this.t('praxis.charts.editor.catalog.targetStale', 'Selected target is not available for this event action.')));
6673
+ }
6674
+ }
6675
+ return issues;
6676
+ }
6677
+ fieldMatchesSelectedResource(field) {
6678
+ const resource = this.selectedResourceOption();
6679
+ if (!resource) {
6680
+ return true;
6681
+ }
6682
+ return this.fieldMatchesResourceOption(field, resource);
6683
+ }
6684
+ fieldMatchesResourceOption(field, resource) {
6685
+ const resourceIds = field.resourceIds ?? [];
6686
+ const resourcePaths = field.resourcePaths ?? [];
6687
+ if (!resourceIds.length && !resourcePaths.length) {
6688
+ return true;
6689
+ }
6690
+ return resourceIds.includes(resource.id)
6691
+ || resourcePaths.some((path) => this.normalizeResourcePath(path) === this.normalizeResourcePath(resource.path));
6692
+ }
6693
+ async resolveSelectedResource(resource) {
6694
+ const requestId = ++this.catalogResolutionRequestId;
6695
+ this.resolvedFields.set([]);
6696
+ this.catalogResolutionDiagnostics.set([]);
6697
+ this.isBusy$.next(true);
6698
+ this.refreshState(this.isDirty$.value);
6699
+ try {
6700
+ const resolution = await this.contextResolver.resolveResource(resource);
6701
+ if (requestId !== this.catalogResolutionRequestId) {
6702
+ return;
6703
+ }
6704
+ if (resolution.resource) {
6705
+ const currentResources = this.resolvedResources() ?? this.baseAvailableResources();
6706
+ const matchIndex = currentResources.findIndex((candidate) => (candidate.id === resolution.resource?.id
6707
+ || this.resourceIdentityMatches(candidate, resource)));
6708
+ this.resolvedResources.set(matchIndex >= 0
6709
+ ? currentResources.map((candidate, index) => index === matchIndex ? resolution.resource : candidate)
6710
+ : [...currentResources, resolution.resource]);
6711
+ }
6712
+ this.resolvedFields.set(resolution.fields);
6713
+ this.catalogResolutionDiagnostics.set(resolution.diagnostics);
6714
+ }
6715
+ catch {
6716
+ if (requestId !== this.catalogResolutionRequestId) {
6717
+ return;
6718
+ }
6719
+ this.resolvedFields.set([]);
6720
+ this.catalogResolutionDiagnostics.set([{
6721
+ code: 'chart-resource-capabilities-unavailable',
6722
+ severity: 'error',
6723
+ path: 'chartDocument.source.resource',
6724
+ }]);
6725
+ }
6726
+ finally {
6727
+ if (requestId === this.catalogResolutionRequestId) {
6728
+ this.isBusy$.next(false);
6729
+ this.refreshState(this.isDirty$.value);
6730
+ }
6731
+ }
6732
+ }
6733
+ resourceIdentityMatches(option, identity) {
6734
+ const normalizedIdentity = this.normalizeResourcePath(identity);
6735
+ return option.id === identity
6736
+ || this.normalizeResourcePath(option.path) === normalizedIdentity;
6737
+ }
6738
+ normalizeResourcePath(value) {
6739
+ return String(value || '').trim().replace(/^\/+|\/+$/g, '');
6740
+ }
6741
+ toCatalogIssue(diagnostic) {
6742
+ return {
6743
+ severity: diagnostic.severity === 'error' ? 'error' : 'warning',
6744
+ code: diagnostic.code,
6745
+ field: diagnostic.path || 'catalog',
6746
+ message: this.catalogDiagnosticMessage(diagnostic.code),
6747
+ };
6748
+ }
6749
+ catalogDiagnosticMessage(code) {
6750
+ switch (code) {
6751
+ case 'chart-resource-capabilities-denied':
6752
+ return this.t('praxis.charts.editor.catalog.capabilitiesDenied', 'Access to the selected resource capabilities was denied.');
6753
+ case 'chart-resource-capabilities-unavailable':
6754
+ return this.t('praxis.charts.editor.catalog.capabilitiesUnavailable', 'The selected resource capabilities are temporarily unavailable.');
6755
+ case 'chart-resource-catalog-selection-stale':
6756
+ return this.t('praxis.charts.editor.catalog.resourceStale', 'Selected resource is not available in the governed resource catalog.');
6757
+ case 'chart-resource-stats-fields-empty':
6758
+ return this.t('praxis.charts.editor.catalog.fieldCatalogMissing', 'The selected resource does not publish governed fields for chart authoring.');
6759
+ case 'chart-resource-stats-operations-empty':
6760
+ return this.t('praxis.charts.editor.catalog.operationMissing', 'Selected resource does not publish authorable stats operations.');
6761
+ default:
6762
+ return this.t('praxis.charts.editor.catalog.contextUnavailable', 'Governed chart authoring context could not be resolved.');
6763
+ }
6764
+ }
6765
+ fieldMatchesOperation(field) {
6766
+ const operation = this.doc().source.kind === 'praxis.stats' ? this.doc().source.operation : undefined;
6767
+ if (!operation || !field.operations?.length) {
6768
+ return true;
6769
+ }
6770
+ return field.operations.includes(operation);
6771
+ }
6772
+ fieldMatchesDistributionMode(field) {
6773
+ const source = this.doc().source;
6774
+ if (source.kind !== 'praxis.stats' || source.operation !== 'distribution') {
6775
+ return true;
6776
+ }
6777
+ if (!field.distributionModes?.length) {
6778
+ return true;
6779
+ }
6780
+ return field.distributionModes.includes(source.options?.mode ?? 'terms');
6781
+ }
6782
+ fieldMatchesRole(field, role) {
6783
+ if (!field.roles?.length) {
6784
+ return role === 'dimension' ? field.aggregable !== true : true;
6785
+ }
6786
+ return field.roles.includes(role);
6787
+ }
6788
+ catalogError(code, field, message) {
6789
+ return {
6790
+ severity: 'error',
6791
+ code,
6792
+ field,
6793
+ message,
6794
+ };
6795
+ }
5218
6796
  normalizeSizingSizeInput(value) {
5219
6797
  const trimmed = value.trim();
5220
6798
  if (!trimmed) {
@@ -5258,7 +6836,7 @@ class PraxisChartConfigEditor {
5258
6836
  this.documentChange.emit(structuredClone(this.normalizedDocument()));
5259
6837
  }
5260
6838
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartConfigEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
5261
- 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 [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 [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 @if (resourceOptions().length) {\n <mat-select [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly()\">\n @for (resource of resourceOptions(); track resource.id) {\n <mat-option [value]=\"resource.path\">{{ resource.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [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.operation', 'Operation') }}</mat-label>\n <mat-select\n [ngModel]=\"doc().source.operation || 'group-by'\"\n (ngModelChange)=\"setOperation($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (operation of operations; 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 </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 [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 <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketSize', 'Bucket size') }}</mat-label>\n <input matInput [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 [ngModel]=\"bucketCountValue()\" (ngModelChange)=\"setBucketCount($event)\" [disabled]=\"isReadonly()\" />\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 (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 and donut charts keep only the first 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', 'Scatter charts use the first dimension as X and the first metric as Y, so keep both fields mapped.') }}\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\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\" />\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\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\" />\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 metricAggregations; 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()\">\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 @if (targetOptions().length) {\n <mat-select data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick')\" />\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 @if (targetOptions().length) {\n <mat-select data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange')\" />\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 @if (targetOptions().length) {\n <mat-select data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown')\" />\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 @if (targetOptions().length) {\n <mat-select data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter')\" />\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;color:var(--md-sys-color-on-surface, #1a1b20)}.editor-shell{display:grid;gap:18px}.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}.editor-card{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)}@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.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: i1$2.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$1.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i3$1.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i3$1.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i3$1.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", "data", "chartDocument", "filterCriteria", "queryContext", "remoteDataResolver", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "selectionChange", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6839
+ 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 </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 (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 and donut charts keep only the first 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', 'Scatter charts use the first dimension as X and the first metric as Y, so keep both fields mapped.') }}\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()\">\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.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: i1$2.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$1.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i3$1.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i3$1.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i3$1.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", "data", "chartDocument", "filterCriteria", "queryContext", "remoteDataResolver", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "pointAction", "selectionChange", "drillDown", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5262
6840
  }
5263
6841
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartConfigEditor, decorators: [{
5264
6842
  type: Component,
@@ -5271,7 +6849,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5271
6849
  MatSelectModule,
5272
6850
  MatSlideToggleModule,
5273
6851
  PraxisChartComponent
5274
- ], 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 [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 [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 @if (resourceOptions().length) {\n <mat-select [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly()\">\n @for (resource of resourceOptions(); track resource.id) {\n <mat-option [value]=\"resource.path\">{{ resource.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [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.operation', 'Operation') }}</mat-label>\n <mat-select\n [ngModel]=\"doc().source.operation || 'group-by'\"\n (ngModelChange)=\"setOperation($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (operation of operations; 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 </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 [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 <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketSize', 'Bucket size') }}</mat-label>\n <input matInput [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 [ngModel]=\"bucketCountValue()\" (ngModelChange)=\"setBucketCount($event)\" [disabled]=\"isReadonly()\" />\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 (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 and donut charts keep only the first 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', 'Scatter charts use the first dimension as X and the first metric as Y, so keep both fields mapped.') }}\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\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\" />\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\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\" />\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 metricAggregations; 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()\">\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 @if (targetOptions().length) {\n <mat-select data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick')\" />\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 @if (targetOptions().length) {\n <mat-select data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange')\" />\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 @if (targetOptions().length) {\n <mat-select data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown')\" />\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 @if (targetOptions().length) {\n <mat-select data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter')\" />\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;color:var(--md-sys-color-on-surface, #1a1b20)}.editor-shell{display:grid;gap:18px}.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}.editor-card{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)}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"] }]
6852
+ ], 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 </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 (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 and donut charts keep only the first 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', 'Scatter charts use the first dimension as X and the first metric as Y, so keep both fields mapped.') }}\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()\">\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"] }]
5275
6853
  }], 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"] }] } });
5276
6854
 
5277
6855
  var praxisChartConfigEditor = /*#__PURE__*/Object.freeze({
@@ -5280,66 +6858,118 @@ var praxisChartConfigEditor = /*#__PURE__*/Object.freeze({
5280
6858
  });
5281
6859
 
5282
6860
  class PraxisChartWidgetConfigEditor {
6861
+ i18n = inject(PraxisI18nService);
5283
6862
  inputs = null;
5284
- chartEditor;
6863
+ context = null;
6864
+ contextDiagnostics = [];
6865
+ activeChartEditor;
6866
+ chartEditorSubscription = new Subscription();
6867
+ set chartEditor(value) {
6868
+ if (this.activeChartEditor === value) {
6869
+ return;
6870
+ }
6871
+ this.chartEditorSubscription.unsubscribe();
6872
+ this.chartEditorSubscription = new Subscription();
6873
+ this.activeChartEditor = value;
6874
+ this.chartEditorDirty = false;
6875
+ this.chartEditorValid = true;
6876
+ if (value) {
6877
+ this.bindChartEditor(value);
6878
+ }
6879
+ this.updateDirty();
6880
+ this.updateValidity();
6881
+ }
6882
+ get chartEditor() {
6883
+ return this.activeChartEditor;
6884
+ }
5285
6885
  isDirty$ = new BehaviorSubject(false);
5286
6886
  isValid$ = new BehaviorSubject(true);
5287
6887
  isBusy$ = new BehaviorSubject(false);
5288
- subscription = new Subscription();
6888
+ chartEditorDirty = false;
5289
6889
  chartEditorValid = true;
5290
6890
  queryContextDirty = false;
6891
+ baselineQueryContextText = '';
5291
6892
  queryContextText = '';
5292
6893
  queryContextError = '';
5293
6894
  get chartDocument() {
5294
6895
  return this.inputs?.chartDocument ?? null;
5295
6896
  }
5296
6897
  get availableResources() {
5297
- return this.inputs?.availableResources ?? [];
6898
+ return this.context?.availableResources ?? this.inputs?.availableResources ?? [];
5298
6899
  }
5299
6900
  get availableFields() {
5300
- return this.inputs?.availableFields ?? [];
6901
+ return this.context?.availableFields ?? this.inputs?.availableFields ?? [];
5301
6902
  }
5302
6903
  get availableTargets() {
5303
- return this.inputs?.availableTargets ?? [];
5304
- }
5305
- ngAfterViewInit() {
5306
- if (!this.chartEditor) {
5307
- return;
5308
- }
5309
- this.subscription.add(this.chartEditor.isDirty$.subscribe((value) => this.isDirty$.next(value || this.queryContextDirty)));
5310
- this.subscription.add(this.chartEditor.isValid$.subscribe((value) => {
5311
- this.chartEditorValid = value;
5312
- this.updateValidity();
5313
- }));
5314
- this.subscription.add(this.chartEditor.isBusy$.subscribe((value) => this.isBusy$.next(value)));
6904
+ return this.context?.availableTargets ?? this.inputs?.availableTargets ?? [];
5315
6905
  }
5316
6906
  ngOnChanges(changes) {
5317
- if (changes['inputs']) {
5318
- this.queryContextText = this.formatQueryContext(this.inputs?.queryContext);
5319
- this.queryContextError = '';
5320
- this.queryContextDirty = false;
5321
- this.isDirty$.next(this.chartEditor?.isDirty$.value ?? false);
5322
- this.updateValidity();
6907
+ if (changes['inputs'] && (changes['inputs'].firstChange || !this.isDirty$.value)) {
6908
+ this.rebaseQueryContext();
5323
6909
  }
5324
6910
  }
5325
6911
  ngOnDestroy() {
5326
- this.subscription.unsubscribe();
6912
+ this.chartEditorSubscription.unsubscribe();
5327
6913
  }
5328
6914
  setQueryContextText(value) {
5329
6915
  this.queryContextText = value;
5330
- this.queryContextDirty = true;
6916
+ this.queryContextDirty = value !== this.baselineQueryContextText;
5331
6917
  this.queryContextError = this.validateQueryContextText(value);
5332
- this.isDirty$.next(true);
6918
+ this.updateDirty();
5333
6919
  this.updateValidity();
5334
6920
  }
5335
6921
  getSettingsValue() {
5336
- return this.buildValue(this.chartEditor?.getSettingsValue());
6922
+ return this.buildValue(this.getChartDocumentForApply());
5337
6923
  }
5338
6924
  onSave() {
5339
- return this.buildValue(this.chartEditor?.onSave?.() ?? this.chartEditor?.getSettingsValue());
6925
+ const value = this.buildValue(this.isValid$.value
6926
+ ? this.getChartDocumentForSave()
6927
+ : this.getChartDocumentForApply());
6928
+ if (this.isValid$.value) {
6929
+ this.baselineQueryContextText = this.queryContextText;
6930
+ this.queryContextDirty = false;
6931
+ this.updateDirty();
6932
+ }
6933
+ return value;
5340
6934
  }
5341
6935
  reset() {
5342
6936
  this.chartEditor?.reset();
6937
+ this.queryContextText = this.baselineQueryContextText;
6938
+ this.queryContextError = '';
6939
+ this.queryContextDirty = false;
6940
+ this.updateDirty();
6941
+ this.updateValidity();
6942
+ }
6943
+ t(key, fallback) {
6944
+ return this.i18n.resolve(resolvePraxisChartsText({ key, text: fallback }, fallback));
6945
+ }
6946
+ contextDiagnosticText(diagnostic) {
6947
+ switch (diagnostic.code) {
6948
+ case 'chart-resource-catalog-denied':
6949
+ return this.t('praxis.charts.widget.contextDiagnostics.catalogDenied', 'Access to the governed resource catalog was denied.');
6950
+ case 'chart-resource-catalog-unavailable':
6951
+ return this.t('praxis.charts.widget.contextDiagnostics.catalogUnavailable', 'The governed resource catalog is temporarily unavailable.');
6952
+ case 'chart-resource-catalog-empty':
6953
+ return this.t('praxis.charts.widget.contextDiagnostics.catalogEmpty', 'No authorable stats resources were published by the governed catalog.');
6954
+ case 'chart-resource-catalog-path-invalid':
6955
+ return this.t('praxis.charts.widget.contextDiagnostics.catalogPathInvalid', 'The resource catalog published an unsafe or non-relative operational path.');
6956
+ case 'chart-resource-capabilities-denied':
6957
+ return this.t('praxis.charts.widget.contextDiagnostics.capabilitiesDenied', 'Access to the selected resource capabilities was denied.');
6958
+ case 'chart-resource-capabilities-unavailable':
6959
+ return this.t('praxis.charts.widget.contextDiagnostics.capabilitiesUnavailable', 'The selected resource capabilities are temporarily unavailable.');
6960
+ case 'chart-resource-stats-operations-empty':
6961
+ return this.t('praxis.charts.widget.contextDiagnostics.operationsEmpty', 'The selected resource does not publish authorable stats operations.');
6962
+ case 'chart-resource-stats-fields-empty':
6963
+ return this.t('praxis.charts.widget.contextDiagnostics.fieldsEmpty', 'The selected resource does not publish governed fields for chart authoring.');
6964
+ case 'chart-resource-capabilities-identity-mismatch':
6965
+ return this.t('praxis.charts.widget.contextDiagnostics.identityMismatch', 'The selected resource identity does not match its governed capability snapshot.');
6966
+ case 'chart-resource-capabilities-path-invalid':
6967
+ return this.t('praxis.charts.widget.contextDiagnostics.capabilitiesPathInvalid', 'The selected capability snapshot did not publish a safe governed resource path.');
6968
+ case 'chart-resource-capabilities-path-mismatch':
6969
+ return this.t('praxis.charts.widget.contextDiagnostics.capabilitiesPathMismatch', 'The selected capability snapshot path does not match the operational path published by the catalog.');
6970
+ default:
6971
+ return this.t('praxis.charts.widget.contextDiagnostics.partial', 'Some governed authoring choices could not be materialized from the current page context.');
6972
+ }
5343
6973
  }
5344
6974
  buildValue(document) {
5345
6975
  const inputs = {
@@ -5347,19 +6977,57 @@ class PraxisChartWidgetConfigEditor {
5347
6977
  ...(document ? { chartDocument: document } : {}),
5348
6978
  };
5349
6979
  const queryContext = this.parseQueryContextText(this.queryContextText);
5350
- if (queryContext === undefined) {
6980
+ if (queryContext.valid && queryContext.value === undefined) {
5351
6981
  delete inputs.queryContext;
5352
6982
  }
5353
- else {
5354
- inputs.queryContext = queryContext;
6983
+ else if (queryContext.valid) {
6984
+ inputs.queryContext = queryContext.value;
5355
6985
  }
5356
6986
  if (document?.source?.kind === 'praxis.stats') {
5357
6987
  delete inputs.data;
5358
6988
  }
6989
+ delete inputs.availableResources;
6990
+ delete inputs.availableFields;
6991
+ delete inputs.availableTargets;
6992
+ delete inputs.contextDiagnostics;
5359
6993
  return {
5360
6994
  inputs,
5361
6995
  };
5362
6996
  }
6997
+ bindChartEditor(editor) {
6998
+ this.chartEditorDirty = editor.isDirty$.value;
6999
+ this.chartEditorValid = editor.isValid$.value;
7000
+ this.isBusy$.next(editor.isBusy$.value);
7001
+ this.chartEditorSubscription.add(editor.isDirty$.subscribe((value) => {
7002
+ this.chartEditorDirty = value;
7003
+ this.updateDirty();
7004
+ }));
7005
+ this.chartEditorSubscription.add(editor.isValid$.subscribe((value) => {
7006
+ this.chartEditorValid = value;
7007
+ this.updateValidity();
7008
+ }));
7009
+ this.chartEditorSubscription.add(editor.isBusy$.subscribe((value) => this.isBusy$.next(value)));
7010
+ }
7011
+ getChartDocumentForApply() {
7012
+ return this.chartDocument ? this.chartEditor?.getSettingsValue() : undefined;
7013
+ }
7014
+ getChartDocumentForSave() {
7015
+ if (!this.chartDocument) {
7016
+ return undefined;
7017
+ }
7018
+ return this.chartEditor?.onSave?.() ?? this.chartEditor?.getSettingsValue();
7019
+ }
7020
+ rebaseQueryContext() {
7021
+ this.baselineQueryContextText = this.formatQueryContext(this.inputs?.queryContext);
7022
+ this.queryContextText = this.baselineQueryContextText;
7023
+ this.queryContextError = '';
7024
+ this.queryContextDirty = false;
7025
+ this.updateDirty();
7026
+ this.updateValidity();
7027
+ }
7028
+ updateDirty() {
7029
+ this.isDirty$.next(this.chartEditorDirty || this.queryContextDirty);
7030
+ }
5363
7031
  updateValidity() {
5364
7032
  this.isValid$.next(this.chartEditorValid && !this.queryContextError);
5365
7033
  }
@@ -5378,42 +7046,70 @@ class PraxisChartWidgetConfigEditor {
5378
7046
  const parsed = JSON.parse(trimmed);
5379
7047
  return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
5380
7048
  ? ''
5381
- : 'Query context must be a JSON object.';
7049
+ : this.t('praxis.charts.widget.queryContext.mustBeObject', 'Query context must be a JSON object.');
5382
7050
  }
5383
- catch (error) {
5384
- return error instanceof Error ? error.message : 'Invalid JSON.';
7051
+ catch {
7052
+ return this.t('praxis.charts.widget.queryContext.invalidJson', 'Query context must be valid JSON.');
5385
7053
  }
5386
7054
  }
5387
7055
  parseQueryContextText(value) {
5388
7056
  const trimmed = value.trim();
5389
7057
  if (!trimmed) {
5390
- return undefined;
7058
+ return { valid: true, value: undefined };
7059
+ }
7060
+ try {
7061
+ const parsed = JSON.parse(trimmed);
7062
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
7063
+ return { valid: false };
7064
+ }
7065
+ return { valid: true, value: parsed };
7066
+ }
7067
+ catch {
7068
+ return { valid: false };
5391
7069
  }
5392
- const parsed = JSON.parse(trimmed);
5393
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
5394
- ? parsed
5395
- : null;
5396
7070
  }
5397
7071
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartWidgetConfigEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
5398
- 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" }, viewQueries: [{ propertyName: "chartEditor", first: true, predicate: ["chartEditor"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
7072
+ 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: `
5399
7073
  <section data-testid="chart-widget-config-editor">
5400
- <praxis-chart-config-editor
5401
- #chartEditor
5402
- [document]="chartDocument"
5403
- [availableResources]="availableResources"
5404
- [availableFields]="availableFields"
5405
- [availableTargets]="availableTargets"
5406
- />
7074
+ @if (contextDiagnostics.length) {
7075
+ <section
7076
+ class="chart-widget-config-editor__diagnostics"
7077
+ data-testid="chart-widget-context-diagnostics"
7078
+ role="status"
7079
+ >
7080
+ <strong>{{ t('praxis.charts.widget.contextDiagnostics.title', 'Authoring context') }}</strong>
7081
+ <ul>
7082
+ @for (diagnostic of contextDiagnostics; track diagnostic.code + ':' + (diagnostic.path || '')) {
7083
+ <li [class]="'chart-widget-config-editor__diagnostic--' + (diagnostic.severity || 'info')">
7084
+ {{ contextDiagnosticText(diagnostic) }}
7085
+ </li>
7086
+ }
7087
+ </ul>
7088
+ </section>
7089
+ }
7090
+ @if (chartDocument) {
7091
+ <praxis-chart-config-editor
7092
+ #chartEditor
7093
+ [document]="chartDocument"
7094
+ [availableResources]="availableResources"
7095
+ [availableFields]="availableFields"
7096
+ [availableTargets]="availableTargets"
7097
+ />
7098
+ } @else {
7099
+ <section class="chart-widget-config-editor__notice" data-testid="chart-widget-missing-document">
7100
+ {{ t('praxis.charts.widget.missingDocument', 'This widget does not have a canonical chart document yet. Runtime inputs are preserved until a canonical chartDocument is provided by the host.') }}
7101
+ </section>
7102
+ }
5407
7103
  <section class="chart-widget-config-editor__runtime">
5408
7104
  <label class="chart-widget-config-editor__label" for="chart-query-context">
5409
- Query context
7105
+ {{ t('praxis.charts.widget.queryContext.label', 'Query context') }}
5410
7106
  </label>
5411
7107
  <textarea
5412
7108
  id="chart-query-context"
5413
7109
  class="chart-widget-config-editor__textarea"
5414
7110
  rows="5"
5415
7111
  spellcheck="false"
5416
- aria-label="Query context"
7112
+ [attr.aria-label]="t('praxis.charts.widget.queryContext.ariaLabel', 'Chart query context')"
5417
7113
  data-testid="chart-widget-query-context"
5418
7114
  [ngModel]="queryContextText"
5419
7115
  (ngModelChange)="setQueryContextText($event)"
@@ -5425,29 +7121,51 @@ class PraxisChartWidgetConfigEditor {
5425
7121
  }
5426
7122
  </section>
5427
7123
  </section>
5428
- `, isInline: true, styles: [".chart-widget-config-editor__runtime{display:grid;gap:8px;margin-top:16px}.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}\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 });
7124
+ `, 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 });
5429
7125
  }
5430
7126
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartWidgetConfigEditor, decorators: [{
5431
7127
  type: Component,
5432
7128
  args: [{ selector: 'praxis-chart-widget-config-editor', standalone: true, imports: [FormsModule, PraxisChartConfigEditor], template: `
5433
7129
  <section data-testid="chart-widget-config-editor">
5434
- <praxis-chart-config-editor
5435
- #chartEditor
5436
- [document]="chartDocument"
5437
- [availableResources]="availableResources"
5438
- [availableFields]="availableFields"
5439
- [availableTargets]="availableTargets"
5440
- />
7130
+ @if (contextDiagnostics.length) {
7131
+ <section
7132
+ class="chart-widget-config-editor__diagnostics"
7133
+ data-testid="chart-widget-context-diagnostics"
7134
+ role="status"
7135
+ >
7136
+ <strong>{{ t('praxis.charts.widget.contextDiagnostics.title', 'Authoring context') }}</strong>
7137
+ <ul>
7138
+ @for (diagnostic of contextDiagnostics; track diagnostic.code + ':' + (diagnostic.path || '')) {
7139
+ <li [class]="'chart-widget-config-editor__diagnostic--' + (diagnostic.severity || 'info')">
7140
+ {{ contextDiagnosticText(diagnostic) }}
7141
+ </li>
7142
+ }
7143
+ </ul>
7144
+ </section>
7145
+ }
7146
+ @if (chartDocument) {
7147
+ <praxis-chart-config-editor
7148
+ #chartEditor
7149
+ [document]="chartDocument"
7150
+ [availableResources]="availableResources"
7151
+ [availableFields]="availableFields"
7152
+ [availableTargets]="availableTargets"
7153
+ />
7154
+ } @else {
7155
+ <section class="chart-widget-config-editor__notice" data-testid="chart-widget-missing-document">
7156
+ {{ t('praxis.charts.widget.missingDocument', 'This widget does not have a canonical chart document yet. Runtime inputs are preserved until a canonical chartDocument is provided by the host.') }}
7157
+ </section>
7158
+ }
5441
7159
  <section class="chart-widget-config-editor__runtime">
5442
7160
  <label class="chart-widget-config-editor__label" for="chart-query-context">
5443
- Query context
7161
+ {{ t('praxis.charts.widget.queryContext.label', 'Query context') }}
5444
7162
  </label>
5445
7163
  <textarea
5446
7164
  id="chart-query-context"
5447
7165
  class="chart-widget-config-editor__textarea"
5448
7166
  rows="5"
5449
7167
  spellcheck="false"
5450
- aria-label="Query context"
7168
+ [attr.aria-label]="t('praxis.charts.widget.queryContext.ariaLabel', 'Chart query context')"
5451
7169
  data-testid="chart-widget-query-context"
5452
7170
  [ngModel]="queryContextText"
5453
7171
  (ngModelChange)="setQueryContextText($event)"
@@ -5459,9 +7177,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5459
7177
  }
5460
7178
  </section>
5461
7179
  </section>
5462
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".chart-widget-config-editor__runtime{display:grid;gap:8px;margin-top:16px}.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}\n"] }]
7180
+ `, 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"] }]
5463
7181
  }], propDecorators: { inputs: [{
5464
7182
  type: Input
7183
+ }], context: [{
7184
+ type: Input
7185
+ }], contextDiagnostics: [{
7186
+ type: Input
5465
7187
  }], chartEditor: [{
5466
7188
  type: ViewChild,
5467
7189
  args: ['chartEditor']
@@ -5493,6 +7215,25 @@ const PRAXIS_CHART_PALETTE_DEFAULT_DATA = [
5493
7215
  { category: 'B', value: 32 },
5494
7216
  { category: 'C', value: 24 },
5495
7217
  ];
7218
+ const PRAXIS_CHART_PALETTE_DEFAULT_DOCUMENT = {
7219
+ version: PRAXIS_X_UI_CHART_AUTHORABLE_VERSION,
7220
+ kind: 'bar',
7221
+ chartId: 'palette-chart',
7222
+ title: 'Analytics preview',
7223
+ subtitle: 'Sample chart inserted from the Praxis component palette.',
7224
+ source: {
7225
+ kind: 'derived',
7226
+ },
7227
+ dimensions: [{ field: 'category', role: 'category' }],
7228
+ metrics: [{ field: 'value', aggregation: 'sum', label: 'Value' }],
7229
+ legend: { enabled: true },
7230
+ labels: { enabled: false },
7231
+ tooltip: { enabled: true },
7232
+ motion: {
7233
+ enabled: true,
7234
+ preset: 'standard',
7235
+ },
7236
+ };
5496
7237
  const PRAXIS_CHART_PHASE1_PORTS = [
5497
7238
  {
5498
7239
  id: 'queryContext',
@@ -5555,7 +7296,7 @@ const PRAXIS_CHART_PHASE1_PORTS = [
5555
7296
  label: 'Point Click',
5556
7297
  direction: 'output',
5557
7298
  semanticKind: 'event',
5558
- description: 'Interaction event emitted when a point or series is clicked.',
7299
+ description: 'Raw renderer-neutral point evidence emitted when a point or series is clicked.',
5559
7300
  cardinality: 'stream',
5560
7301
  schema: {
5561
7302
  id: 'PraxisChartPointEvent',
@@ -5564,6 +7305,20 @@ const PRAXIS_CHART_PHASE1_PORTS = [
5564
7305
  },
5565
7306
  exposure: { public: true, group: 'interaction' },
5566
7307
  },
7308
+ {
7309
+ id: 'pointAction',
7310
+ label: 'Point Action',
7311
+ direction: 'output',
7312
+ semanticKind: 'event',
7313
+ description: 'Structured configured pointClick action derived from raw point evidence.',
7314
+ cardinality: 'stream',
7315
+ schema: {
7316
+ id: 'PraxisChartPointActionEvent',
7317
+ kind: 'ts-type',
7318
+ ref: 'PraxisChartPointActionEvent',
7319
+ },
7320
+ exposure: { public: true, group: 'interaction' },
7321
+ },
5567
7322
  {
5568
7323
  id: 'selectionChange',
5569
7324
  label: 'Selection Change',
@@ -5578,6 +7333,20 @@ const PRAXIS_CHART_PHASE1_PORTS = [
5578
7333
  },
5579
7334
  exposure: { public: true, group: 'interaction' },
5580
7335
  },
7336
+ {
7337
+ id: 'drillDown',
7338
+ label: 'Drill Down',
7339
+ direction: 'output',
7340
+ semanticKind: 'event',
7341
+ description: 'Structured drill-down action payload derived from selected chart point evidence.',
7342
+ cardinality: 'stream',
7343
+ schema: {
7344
+ id: 'PraxisChartDrillDownEvent',
7345
+ kind: 'ts-type',
7346
+ ref: 'PraxisChartDrillDownEvent',
7347
+ },
7348
+ exposure: { public: true, group: 'interaction' },
7349
+ },
5581
7350
  {
5582
7351
  id: 'crossFilter',
5583
7352
  label: 'Cross Filter',
@@ -5636,6 +7405,23 @@ const PRAXIS_CHART_COMPONENT_METADATA = {
5636
7405
  component: PraxisChartWidgetConfigEditor,
5637
7406
  title: 'Configure chart',
5638
7407
  },
7408
+ authoringManifestRef: {
7409
+ componentId: 'praxis-chart',
7410
+ source: 'PRAXIS_CHARTS_AUTHORING_MANIFEST',
7411
+ },
7412
+ insertionPresets: [
7413
+ {
7414
+ id: 'canonical-chart',
7415
+ label: 'Canonical chart',
7416
+ description: 'Creates a chart widget from the canonical x-ui.chart authoring document.',
7417
+ icon: 'bar_chart',
7418
+ inputs: {
7419
+ chartDocument: PRAXIS_CHART_PALETTE_DEFAULT_DOCUMENT,
7420
+ data: PRAXIS_CHART_PALETTE_DEFAULT_DATA,
7421
+ enableCustomization: true,
7422
+ },
7423
+ },
7424
+ ],
5639
7425
  inputs: [
5640
7426
  {
5641
7427
  name: 'config',
@@ -5653,6 +7439,7 @@ const PRAXIS_CHART_COMPONENT_METADATA = {
5653
7439
  name: 'chartDocument',
5654
7440
  type: 'PraxisXUiChartContract | null',
5655
7441
  description: 'Optional canonical x-ui.chart document used as the authoring source of truth for settings-panel integration.',
7442
+ default: PRAXIS_CHART_PALETTE_DEFAULT_DOCUMENT,
5656
7443
  },
5657
7444
  {
5658
7445
  name: 'queryContext',
@@ -5678,19 +7465,19 @@ const PRAXIS_CHART_COMPONENT_METADATA = {
5678
7465
  {
5679
7466
  name: 'availableResources',
5680
7467
  type: 'ReadonlyArray<ChartEditorResourceOption>',
5681
- description: 'Catalog of remote resources exposed to the runtime chart editor when customization is enabled.',
7468
+ description: 'Transient governed resource catalog. id is the stable discovery identity and path is the root-relative operational value persisted to chartDocument.source.resource; the catalog itself is not persisted.',
5682
7469
  default: [],
5683
7470
  },
5684
7471
  {
5685
7472
  name: 'availableFields',
5686
7473
  type: 'ReadonlyArray<ChartEditorFieldOption>',
5687
- description: 'Catalog of semantic fields exposed to the runtime chart editor when customization is enabled.',
7474
+ description: 'Transient capability-backed field catalog with exact operation, aggregation and distribution terms/histogram eligibility; not intended for widget persistence.',
5688
7475
  default: [],
5689
7476
  },
5690
7477
  {
5691
7478
  name: 'availableTargets',
5692
7479
  type: 'ReadonlyArray<ChartEditorTargetOption>',
5693
- description: 'Catalog of widget or route targets exposed to chart runtime actions in the config editor.',
7480
+ description: 'Transient event-scoped targets from existing top-level links that are compatible with public, non-deprecated widget ports or explicitly writable declared page state. events[] identifies the exact authorized structured Chart events; raw pointClick never authorizes events.pointClick, which uses pointAction. Local transform.output semantic-kind inspection is provisional and does not replace a Core-validated transform-output projection (P2). The catalog is not persisted.',
5694
7481
  default: [],
5695
7482
  },
5696
7483
  ],
@@ -5698,12 +7485,22 @@ const PRAXIS_CHART_COMPONENT_METADATA = {
5698
7485
  {
5699
7486
  name: 'pointClick',
5700
7487
  type: 'PraxisChartPointEvent',
5701
- description: 'Emitted when the host wants to react to a point/series click.',
7488
+ description: 'Raw renderer-neutral point evidence emitted when the host wants to observe a point/series click.',
7489
+ },
7490
+ {
7491
+ name: 'pointAction',
7492
+ type: 'PraxisChartPointActionEvent',
7493
+ description: 'Emitted when a configured pointClick action resolves semantic filters and target evidence from a point/series click.',
5702
7494
  },
5703
7495
  {
5704
7496
  name: 'selectionChange',
5705
7497
  type: 'PraxisChartSelectionEvent',
5706
- description: 'Emitted when declarative chart selection resolves a selected point and its canonical filters.',
7498
+ description: 'Emitted when declarative single-point chart selection resolves selected point evidence and canonical filters.',
7499
+ },
7500
+ {
7501
+ name: 'drillDown',
7502
+ type: 'PraxisChartDrillDownEvent',
7503
+ description: 'Emitted when declarative drill-down resolves action, target and canonical filters from a selected chart point.',
5707
7504
  },
5708
7505
  {
5709
7506
  name: 'crossFilter',
@@ -5747,10 +7544,18 @@ function providePraxisChartsMetadata() {
5747
7544
  return {
5748
7545
  provide: ENVIRONMENT_INITIALIZER,
5749
7546
  multi: true,
5750
- useFactory: (registry) => () => {
5751
- registry.register(PRAXIS_CHART_COMPONENT_METADATA);
7547
+ useFactory: (registry, injector) => () => {
7548
+ registry.register({
7549
+ ...PRAXIS_CHART_COMPONENT_METADATA,
7550
+ configEditor: {
7551
+ ...PRAXIS_CHART_COMPONENT_METADATA.configEditor,
7552
+ contextResolver: (request) => injector
7553
+ .get(PraxisChartConfigEditorContextResolverService)
7554
+ .resolve(request),
7555
+ },
7556
+ });
5752
7557
  },
5753
- deps: [ComponentMetadataRegistry],
7558
+ deps: [ComponentMetadataRegistry, Injector],
5754
7559
  };
5755
7560
  }
5756
7561
 
@@ -5832,7 +7637,9 @@ class PraxisChartShowcaseWidgetComponent {
5832
7637
  enableCustomization = input(false, ...(ngDevMode ? [{ debugName: "enableCustomization" }] : /* istanbul ignore next */ []));
5833
7638
  viewMode = input('chart', ...(ngDevMode ? [{ debugName: "viewMode" }] : /* istanbul ignore next */ []));
5834
7639
  pointClick = output();
7640
+ pointAction = output();
5835
7641
  selectionChange = output();
7642
+ drillDown = output();
5836
7643
  crossFilter = output();
5837
7644
  queryRequest = output();
5838
7645
  loadStateChange = output();
@@ -5948,7 +7755,7 @@ class PraxisChartShowcaseWidgetComponent {
5948
7755
  });
5949
7756
  }
5950
7757
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartShowcaseWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5951
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisChartShowcaseWidgetComponent, isStandalone: true, selector: "praxis-chart-showcase-widget", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, chartDocument: { classPropertyName: "chartDocument", publicName: "chartDocument", isSignal: true, isRequired: false, transformFunction: null }, remoteDataResolver: { classPropertyName: "remoteDataResolver", publicName: "remoteDataResolver", isSignal: true, isRequired: false, transformFunction: null }, enableCustomization: { classPropertyName: "enableCustomization", publicName: "enableCustomization", isSignal: true, isRequired: false, transformFunction: null }, viewMode: { classPropertyName: "viewMode", publicName: "viewMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pointClick: "pointClick", selectionChange: "selectionChange", crossFilter: "crossFilter", queryRequest: "queryRequest", loadStateChange: "loadStateChange" }, ngImport: i0, template: `
7758
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisChartShowcaseWidgetComponent, isStandalone: true, selector: "praxis-chart-showcase-widget", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, chartDocument: { classPropertyName: "chartDocument", publicName: "chartDocument", isSignal: true, isRequired: false, transformFunction: null }, remoteDataResolver: { classPropertyName: "remoteDataResolver", publicName: "remoteDataResolver", isSignal: true, isRequired: false, transformFunction: null }, enableCustomization: { classPropertyName: "enableCustomization", publicName: "enableCustomization", isSignal: true, isRequired: false, transformFunction: null }, viewMode: { classPropertyName: "viewMode", publicName: "viewMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pointClick: "pointClick", pointAction: "pointAction", selectionChange: "selectionChange", drillDown: "drillDown", crossFilter: "crossFilter", queryRequest: "queryRequest", loadStateChange: "loadStateChange" }, ngImport: i0, template: `
5952
7759
  @if (viewMode() === 'chart') {
5953
7760
  <praxis-chart
5954
7761
  [config]="config()"
@@ -5957,7 +7764,9 @@ class PraxisChartShowcaseWidgetComponent {
5957
7764
  [remoteDataResolver]="remoteDataResolver()"
5958
7765
  [enableCustomization]="enableCustomization()"
5959
7766
  (pointClick)="pointClick.emit($event)"
7767
+ (pointAction)="pointAction.emit($event)"
5960
7768
  (selectionChange)="selectionChange.emit($event)"
7769
+ (drillDown)="drillDown.emit($event)"
5961
7770
  (crossFilter)="crossFilter.emit($event)"
5962
7771
  (queryRequest)="queryRequest.emit($event)"
5963
7772
  (loadStateChange)="loadStateChange.emit($event)"
@@ -5984,7 +7793,7 @@ class PraxisChartShowcaseWidgetComponent {
5984
7793
  (rowClick)="handleRowClick($event)"
5985
7794
  ></praxis-table>
5986
7795
  }
5987
- `, isInline: true, styles: [":host{display:block;height:100%;min-width:0}.showcase-state-card{min-height:240px;display:grid;place-content:center;gap:8px;padding:24px;border-radius:18px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#fffffff5,#f4f7fbfa);text-align:center}.showcase-state-card h4{margin:0;font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.showcase-state-card p{margin:0;color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"], dependencies: [{ kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "data", "chartDocument", "filterCriteria", "queryContext", "remoteDataResolver", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "selectionChange", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }, { kind: "component", type: PraxisTable, selector: "praxis-table", inputs: ["config", "resourcePath", "data", "tableId", "componentInstanceId", "configPersistenceStrategy", "title", "subtitle", "icon", "autoDelete", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "crudContext", "filterCriteria", "queryContext", "aiContext", "aiAssistantVoiceInputMode", "aiAssistantVoiceLanguage", "horizontalScroll", "enableCustomization", "authoringCapability", "dense"], outputs: ["rowClick", "widgetEvent", "resourceEvent", "rowDoubleClick", "rowExpansionChange", "rowAction", "toolbarAction", "bulkAction", "exportAction", "columnReorder", "columnReorderAttempt", "columnResize", "beforeDelete", "afterDelete", "deleteError", "beforeBulkDelete", "afterBulkDelete", "bulkDeleteError", "schemaStatusChange", "configChange", "metadataChange", "loadingStateChange", "collectionLinksChange", "selectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
7796
+ `, isInline: true, styles: [":host{display:block;height:100%;min-width:0}.showcase-state-card{min-height:240px;display:grid;place-content:center;gap:8px;padding:24px;border-radius:18px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#fffffff5,#f4f7fbfa);text-align:center}.showcase-state-card h4{margin:0;font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.showcase-state-card p{margin:0;color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"], dependencies: [{ kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "data", "chartDocument", "filterCriteria", "queryContext", "remoteDataResolver", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "pointAction", "selectionChange", "drillDown", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }, { kind: "component", type: PraxisTable, selector: "praxis-table", inputs: ["config", "resourcePath", "data", "tableId", "componentInstanceId", "configPersistenceStrategy", "title", "subtitle", "icon", "autoDelete", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "crudContext", "filterCriteria", "queryContext", "aiContext", "aiAssistantVoiceInputMode", "aiAssistantVoiceLanguage", "horizontalScroll", "enableCustomization", "authoringCapability", "dense"], outputs: ["rowClick", "widgetEvent", "resourceEvent", "rowDoubleClick", "rowExpansionChange", "rowAction", "toolbarAction", "bulkAction", "exportAction", "columnReorder", "columnReorderAttempt", "columnResize", "beforeDelete", "afterDelete", "deleteError", "beforeBulkDelete", "afterBulkDelete", "bulkDeleteError", "schemaStatusChange", "configChange", "metadataChange", "loadingStateChange", "collectionLinksChange", "selectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5988
7797
  }
5989
7798
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartShowcaseWidgetComponent, decorators: [{
5990
7799
  type: Component,
@@ -5997,7 +7806,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5997
7806
  [remoteDataResolver]="remoteDataResolver()"
5998
7807
  [enableCustomization]="enableCustomization()"
5999
7808
  (pointClick)="pointClick.emit($event)"
7809
+ (pointAction)="pointAction.emit($event)"
6000
7810
  (selectionChange)="selectionChange.emit($event)"
7811
+ (drillDown)="drillDown.emit($event)"
6001
7812
  (crossFilter)="crossFilter.emit($event)"
6002
7813
  (queryRequest)="queryRequest.emit($event)"
6003
7814
  (loadStateChange)="loadStateChange.emit($event)"
@@ -6025,7 +7836,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
6025
7836
  ></praxis-table>
6026
7837
  }
6027
7838
  `, styles: [":host{display:block;height:100%;min-width:0}.showcase-state-card{min-height:240px;display:grid;place-content:center;gap:8px;padding:24px;border-radius:18px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#fffffff5,#f4f7fbfa);text-align:center}.showcase-state-card h4{margin:0;font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.showcase-state-card p{margin:0;color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"] }]
6028
- }], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: true }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], chartDocument: [{ type: i0.Input, args: [{ isSignal: true, alias: "chartDocument", required: false }] }], remoteDataResolver: [{ type: i0.Input, args: [{ isSignal: true, alias: "remoteDataResolver", required: false }] }], enableCustomization: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableCustomization", required: false }] }], viewMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewMode", required: false }] }], pointClick: [{ type: i0.Output, args: ["pointClick"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], crossFilter: [{ type: i0.Output, args: ["crossFilter"] }], queryRequest: [{ type: i0.Output, args: ["queryRequest"] }], loadStateChange: [{ type: i0.Output, args: ["loadStateChange"] }] } });
7839
+ }], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: true }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], chartDocument: [{ type: i0.Input, args: [{ isSignal: true, alias: "chartDocument", required: false }] }], remoteDataResolver: [{ type: i0.Input, args: [{ isSignal: true, alias: "remoteDataResolver", required: false }] }], enableCustomization: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableCustomization", required: false }] }], viewMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewMode", 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"] }] } });
6029
7840
  function resolveShowcaseStateMode(config, remoteRuntimeState = 'idle') {
6030
7841
  if (config.preferredLoadState === 'loading') {
6031
7842
  return 'loading';
@@ -6204,13 +8015,23 @@ const PRAXIS_CHART_SHOWCASE_WIDGET_METADATA = {
6204
8015
  {
6205
8016
  name: 'pointClick',
6206
8017
  type: 'PraxisChartPointEvent',
6207
- description: 'Forwards point selection from the chart or mapped row selection from the table.',
8018
+ description: 'Forwards raw point evidence from the chart or mapped row selection from the table.',
8019
+ },
8020
+ {
8021
+ name: 'pointAction',
8022
+ type: 'PraxisChartPointActionEvent',
8023
+ description: 'Forwards configured pointClick action payloads from the chart.',
6208
8024
  },
6209
8025
  {
6210
8026
  name: 'selectionChange',
6211
8027
  type: 'PraxisChartSelectionEvent',
6212
8028
  description: 'Forwards chart selection events with mapped filter values.',
6213
8029
  },
8030
+ {
8031
+ name: 'drillDown',
8032
+ type: 'PraxisChartDrillDownEvent',
8033
+ description: 'Forwards chart drill-down action payloads for Dynamic Page composition links.',
8034
+ },
6214
8035
  {
6215
8036
  name: 'crossFilter',
6216
8037
  type: 'PraxisChartCrossFilterEvent',
@@ -6281,38 +8102,10 @@ function providePraxisCharts(options = {}) {
6281
8102
  ];
6282
8103
  }
6283
8104
 
6284
- class PraxisChartMetadataRegistrationService {
6285
- registry;
6286
- registered = false;
6287
- constructor(registry) {
6288
- this.registry = registry;
6289
- this.ensureRegistered();
6290
- }
6291
- ensureRegistered() {
6292
- if (this.registered) {
6293
- return;
6294
- }
6295
- this.registry.register(PRAXIS_CHART_COMPONENT_METADATA);
6296
- this.registry.register(PRAXIS_CHART_DRILLDOWN_PANEL_METADATA);
6297
- this.registry.register(PRAXIS_CHART_STATE_PROBE_COMPONENT_METADATA);
6298
- this.registry.register(PRAXIS_CHART_SHOWCASE_WIDGET_METADATA);
6299
- this.registered = true;
6300
- }
6301
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartMetadataRegistrationService, deps: [{ token: i1$1.ComponentMetadataRegistry }], target: i0.ɵɵFactoryTarget.Injectable });
6302
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartMetadataRegistrationService, providedIn: 'root' });
6303
- }
6304
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartMetadataRegistrationService, decorators: [{
6305
- type: Injectable,
6306
- args: [{ providedIn: 'root' }]
6307
- }], ctorParameters: () => [{ type: i1$1.ComponentMetadataRegistry }] });
6308
-
6309
8105
  class PraxisChartSchemaMapperService {
6310
- metadataRegistration;
6311
8106
  canonicalContractMapper;
6312
- constructor(metadataRegistration, canonicalContractMapper) {
6313
- this.metadataRegistration = metadataRegistration;
8107
+ constructor(canonicalContractMapper) {
6314
8108
  this.canonicalContractMapper = canonicalContractMapper;
6315
- this.metadataRegistration.ensureRegistered();
6316
8109
  }
6317
8110
  resolve(input, defaults) {
6318
8111
  const schema = this.normalize(input);
@@ -6401,8 +8194,12 @@ class PraxisChartSchemaMapperService {
6401
8194
  }
6402
8195
  const events = schema.chart?.events;
6403
8196
  const outputs = {};
6404
- if (events?.pointClick || events?.drillDown) {
8197
+ if (events?.pointClick) {
6405
8198
  outputs['pointClick'] = 'emit';
8199
+ outputs['pointAction'] = 'emit';
8200
+ }
8201
+ if (events?.drillDown) {
8202
+ outputs['drillDown'] = 'emit';
6406
8203
  }
6407
8204
  if (events?.selectionChange) {
6408
8205
  outputs['selectionChange'] = 'emit';
@@ -6412,13 +8209,13 @@ class PraxisChartSchemaMapperService {
6412
8209
  }
6413
8210
  return Object.keys(outputs).length ? outputs : undefined;
6414
8211
  }
6415
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartSchemaMapperService, deps: [{ token: PraxisChartMetadataRegistrationService }, { token: PraxisChartCanonicalContractMapperService }], target: i0.ɵɵFactoryTarget.Injectable });
8212
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartSchemaMapperService, deps: [{ token: PraxisChartCanonicalContractMapperService }], target: i0.ɵɵFactoryTarget.Injectable });
6416
8213
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartSchemaMapperService, providedIn: 'root' });
6417
8214
  }
6418
8215
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartSchemaMapperService, decorators: [{
6419
8216
  type: Injectable,
6420
8217
  args: [{ providedIn: 'root' }]
6421
- }], ctorParameters: () => [{ type: PraxisChartMetadataRegistrationService }, { type: PraxisChartCanonicalContractMapperService }] });
8218
+ }], ctorParameters: () => [{ type: PraxisChartCanonicalContractMapperService }] });
6422
8219
 
6423
8220
  class PraxisChartBackendPayloadAdapterService {
6424
8221
  chartSchemaMapper;
@@ -6472,7 +8269,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
6472
8269
  }], ctorParameters: () => [{ type: PraxisChartSchemaMapperService }, { type: PraxisChartCanonicalContractMapperService }] });
6473
8270
 
6474
8271
  class AnalyticsChartConfigAdapterService {
8272
+ i18n;
6475
8273
  statsBuilder = new AnalyticsStatsRequestBuilderService();
8274
+ constructor(i18n) {
8275
+ this.i18n = i18n;
8276
+ }
6476
8277
  toPraxisChartConfig(projection, options) {
6477
8278
  const chartType = this.resolveChartType(projection);
6478
8279
  const orientation = chartType === 'horizontal-bar' ? 'horizontal' : undefined;
@@ -6484,6 +8285,11 @@ class AnalyticsChartConfigAdapterService {
6484
8285
  if (!metrics.length) {
6485
8286
  throw new Error(`AnalyticsChartConfigAdapterService requires at least one metric for projection "${projection.id}".`);
6486
8287
  }
8288
+ const crossFilter = Boolean(projection.interactions?.crossFilter);
8289
+ const keyFilterField = dimension.keyFilterField?.trim();
8290
+ if (crossFilter && !keyFilterField) {
8291
+ throw new Error(`AnalyticsChartConfigAdapterService requires primaryDimension.keyFilterField when crossFilter is enabled for projection "${projection.id}".`);
8292
+ }
6487
8293
  return {
6488
8294
  id: projection.id,
6489
8295
  type: chartType,
@@ -6498,6 +8304,17 @@ class AnalyticsChartConfigAdapterService {
6498
8304
  pointClick: Boolean(projection.interactions?.pointSelection || projection.interactions?.drillDown),
6499
8305
  drillDown: Boolean(projection.interactions?.drillDown),
6500
8306
  selection: Boolean(projection.interactions?.pointSelection),
8307
+ crossFilter,
8308
+ ...(crossFilter
8309
+ ? {
8310
+ eventActions: {
8311
+ crossFilter: {
8312
+ action: 'emit',
8313
+ mapping: { key: keyFilterField },
8314
+ },
8315
+ },
8316
+ }
8317
+ : {}),
6501
8318
  },
6502
8319
  };
6503
8320
  }
@@ -6529,18 +8346,27 @@ class AnalyticsChartConfigAdapterService {
6529
8346
  }
6530
8347
  buildSeries(projection, chartType) {
6531
8348
  const dimension = projection.bindings.primaryDimension;
6532
- return this.getDisplayMetrics(projection).map((metric, index) => ({
6533
- id: `${projection.id}.${metric.field}.${index + 1}`,
6534
- name: metric.label ?? metric.field,
6535
- type: chartType,
6536
- categoryField: chartType === 'pie' || chartType === 'donut' ? dimension.field : undefined,
6537
- metric: {
6538
- field: metric.field,
6539
- aggregation: this.mapAggregation(metric.aggregation),
6540
- label: metric.label ?? undefined,
6541
- },
6542
- smooth: chartType === 'line' || chartType === 'area',
6543
- }));
8349
+ const comparison = projection.source.operation === 'comparison';
8350
+ return this.getDisplayMetrics(projection).flatMap((metric, index) => comparison
8351
+ ? ['current', 'previous'].map((period) => ({
8352
+ id: `${projection.id}.${metric.field}.${period}`,
8353
+ name: `${metric.label ?? metric.field} (${this.comparisonPeriodLabel(period)})`,
8354
+ type: chartType,
8355
+ categoryField: dimension.field,
8356
+ metric: { field: this.comparisonMetricField(metric.field, period), aggregation: this.mapAggregation(metric.aggregation) },
8357
+ }))
8358
+ : [{
8359
+ id: `${projection.id}.${metric.field}.${index + 1}`,
8360
+ name: metric.label ?? metric.field,
8361
+ type: chartType,
8362
+ categoryField: chartType === 'pie' || chartType === 'donut' ? dimension.field : undefined,
8363
+ metric: {
8364
+ field: metric.field,
8365
+ aggregation: this.mapAggregation(metric.aggregation),
8366
+ label: metric.label ?? undefined,
8367
+ },
8368
+ smooth: chartType === 'line' || chartType === 'area',
8369
+ }]);
6544
8370
  }
6545
8371
  buildDataSource(projection) {
6546
8372
  const executionPlan = this.statsBuilder.buildExecutionPlan(projection);
@@ -6554,7 +8380,13 @@ class AnalyticsChartConfigAdapterService {
6554
8380
  statsPath: executionPlan.statsPath,
6555
8381
  statsRequest: executionPlan.statsRequest,
6556
8382
  dimensions: executionPlan.dimensions,
6557
- metrics: this.mapExecutionMetrics(executionPlan),
8383
+ metrics: projection.source.operation === 'comparison'
8384
+ ? this.getDisplayMetrics(projection).flatMap((metric) => ['current', 'previous'].map((period) => ({
8385
+ field: this.comparisonMetricField(metric.field, period),
8386
+ aggregation: this.mapAggregation(metric.aggregation),
8387
+ alias: this.comparisonMetricField(metric.field, period),
8388
+ })))
8389
+ : this.mapExecutionMetrics(executionPlan),
6558
8390
  sort: executionPlan.sort,
6559
8391
  limit: executionPlan.limit,
6560
8392
  },
@@ -6604,13 +8436,25 @@ class AnalyticsChartConfigAdapterService {
6604
8436
  ...(projection.bindings.secondaryMetrics ?? []),
6605
8437
  ];
6606
8438
  }
6607
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: AnalyticsChartConfigAdapterService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
8439
+ comparisonMetricField(metricField, period) {
8440
+ return `__praxisComparison_${metricField}_${period}`;
8441
+ }
8442
+ comparisonPeriodLabel(period) {
8443
+ const key = period === 'current'
8444
+ ? 'praxis.charts.runtime.comparisonCurrent'
8445
+ : 'praxis.charts.runtime.comparisonPrevious';
8446
+ const fallback = period === 'current' ? 'Current' : 'Previous';
8447
+ return this.i18n?.t(key, undefined, fallback, 'charts') ?? fallback;
8448
+ }
8449
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: AnalyticsChartConfigAdapterService, deps: [{ token: i1$1.PraxisI18nService, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
6608
8450
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: AnalyticsChartConfigAdapterService, providedIn: 'root' });
6609
8451
  }
6610
8452
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: AnalyticsChartConfigAdapterService, decorators: [{
6611
8453
  type: Injectable,
6612
8454
  args: [{ providedIn: 'root' }]
6613
- }] });
8455
+ }], ctorParameters: () => [{ type: i1$1.PraxisI18nService, decorators: [{
8456
+ type: Optional
8457
+ }] }] });
6614
8458
 
6615
8459
  class AnalyticsChartContractService {
6616
8460
  analyticsSchema;
@@ -6713,7 +8557,7 @@ function normalizeError(error) {
6713
8557
 
6714
8558
  const PRAXIS_CHART_BACKEND_MOCK_BAR = {
6715
8559
  schemaMeta: {
6716
- schemaId: 'api/human-resources/vw-perfil-heroi|post|response|tenant:demo|locale:pt-BR',
8560
+ schemaId: 'api/human-resources/vw-perfil-heroi|post|response|internal:false',
6717
8561
  schemaHash: 'published-hash-hero-universe-v1',
6718
8562
  resourcePath: 'api/human-resources/vw-perfil-heroi',
6719
8563
  operation: 'post',
@@ -6743,7 +8587,7 @@ const PRAXIS_CHART_BACKEND_MOCK_BAR = {
6743
8587
  sizing: { mode: 'fixed', height: 320 },
6744
8588
  source: {
6745
8589
  kind: 'praxis.stats',
6746
- resource: 'api/human-resources/vw-perfil-heroi',
8590
+ resource: '/api/human-resources/vw-perfil-heroi',
6747
8591
  operation: 'group-by',
6748
8592
  options: {
6749
8593
  orderBy: 'value-desc',
@@ -6795,7 +8639,7 @@ const PRAXIS_CHART_BACKEND_MOCK_BAR = {
6795
8639
  };
6796
8640
  const PRAXIS_CHART_BACKEND_MOCK_TIMESERIES = {
6797
8641
  schemaMeta: {
6798
- schemaId: 'api/human-resources/vw-indicadores-incidentes|post|response|tenant:demo|locale:pt-BR',
8642
+ schemaId: 'api/human-resources/vw-indicadores-incidentes|post|response|internal:false',
6799
8643
  schemaHash: 'published-hash-incident-timeseries-v1',
6800
8644
  resourcePath: 'api/human-resources/vw-indicadores-incidentes',
6801
8645
  operation: 'post',
@@ -6819,7 +8663,7 @@ const PRAXIS_CHART_BACKEND_MOCK_TIMESERIES = {
6819
8663
  subtitle: { key: 'charts.incidents.timeline.subtitle', fallback: 'Timeseries em /vw-indicadores-incidentes' },
6820
8664
  source: {
6821
8665
  kind: 'praxis.stats',
6822
- resource: 'api/human-resources/vw-indicadores-incidentes',
8666
+ resource: '/api/human-resources/vw-indicadores-incidentes',
6823
8667
  operation: 'timeseries',
6824
8668
  options: {
6825
8669
  granularity: 'month',
@@ -6851,7 +8695,7 @@ const PRAXIS_CHART_BACKEND_MOCK_TIMESERIES = {
6851
8695
  };
6852
8696
  const PRAXIS_CHART_BACKEND_MOCK_DONUT = {
6853
8697
  schemaMeta: {
6854
- schemaId: 'api/human-resources/vw-indicadores-incidentes|post|response|tenant:demo|locale:pt-BR',
8698
+ schemaId: 'api/human-resources/vw-indicadores-incidentes|post|response|internal:false',
6855
8699
  schemaHash: 'published-hash-incident-severity-v1',
6856
8700
  resourcePath: 'api/human-resources/vw-indicadores-incidentes',
6857
8701
  operation: 'post',
@@ -6874,7 +8718,7 @@ const PRAXIS_CHART_BACKEND_MOCK_DONUT = {
6874
8718
  title: { key: 'charts.incidents.severity.title', fallback: 'Severidade de incidentes' },
6875
8719
  source: {
6876
8720
  kind: 'praxis.stats',
6877
- resource: 'api/human-resources/vw-indicadores-incidentes',
8721
+ resource: '/api/human-resources/vw-indicadores-incidentes',
6878
8722
  operation: 'distribution',
6879
8723
  options: {
6880
8724
  mode: 'terms',
@@ -6907,7 +8751,7 @@ const PRAXIS_CHART_BACKEND_MOCK_DONUT = {
6907
8751
  };
6908
8752
  const PRAXIS_CHART_BACKEND_MOCK_HORIZONTAL_BAR = {
6909
8753
  schemaMeta: {
6910
- schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|tenant:demo|locale:pt-BR',
8754
+ schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|internal:false',
6911
8755
  schemaHash: 'published-hash-payroll-department-ranking-v1',
6912
8756
  resourcePath: 'api/human-resources/vw-analytics-folha-pagamento',
6913
8757
  operation: 'post',
@@ -6933,7 +8777,7 @@ const PRAXIS_CHART_BACKEND_MOCK_HORIZONTAL_BAR = {
6933
8777
  sizing: { mode: 'fixed', height: 340 },
6934
8778
  source: {
6935
8779
  kind: 'praxis.stats',
6936
- resource: 'api/human-resources/vw-analytics-folha-pagamento',
8780
+ resource: '/api/human-resources/vw-analytics-folha-pagamento',
6937
8781
  operation: 'group-by',
6938
8782
  options: {
6939
8783
  orderBy: 'value-desc',
@@ -6969,7 +8813,7 @@ const PRAXIS_CHART_BACKEND_MOCK_HORIZONTAL_BAR = {
6969
8813
  };
6970
8814
  const PRAXIS_CHART_BACKEND_MOCK_STACKED_AREA = {
6971
8815
  schemaMeta: {
6972
- schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|tenant:demo|locale:pt-BR',
8816
+ schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|internal:false',
6973
8817
  schemaHash: 'published-hash-payroll-net-trend-stacked-v1',
6974
8818
  resourcePath: 'api/human-resources/vw-analytics-folha-pagamento',
6975
8819
  operation: 'post',
@@ -6994,7 +8838,7 @@ const PRAXIS_CHART_BACKEND_MOCK_STACKED_AREA = {
6994
8838
  sizing: { mode: 'fixed', height: 340 },
6995
8839
  source: {
6996
8840
  kind: 'praxis.stats',
6997
- resource: 'api/human-resources/vw-analytics-folha-pagamento',
8841
+ resource: '/api/human-resources/vw-analytics-folha-pagamento',
6998
8842
  operation: 'timeseries',
6999
8843
  options: {
7000
8844
  granularity: 'month',
@@ -7025,7 +8869,7 @@ const PRAXIS_CHART_BACKEND_MOCK_STACKED_AREA = {
7025
8869
  };
7026
8870
  const PRAXIS_CHART_BACKEND_MOCK_MULTI_METRIC_BAR = {
7027
8871
  schemaMeta: {
7028
- schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|tenant:demo|locale:pt-BR',
8872
+ schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|internal:false',
7029
8873
  schemaHash: 'published-hash-payroll-multi-metric-bar-v1',
7030
8874
  resourcePath: 'api/human-resources/vw-analytics-folha-pagamento',
7031
8875
  operation: 'post',
@@ -7053,7 +8897,7 @@ const PRAXIS_CHART_BACKEND_MOCK_MULTI_METRIC_BAR = {
7053
8897
  sizing: { mode: 'fixed', height: 340 },
7054
8898
  source: {
7055
8899
  kind: 'praxis.stats',
7056
- resource: 'api/human-resources/vw-analytics-folha-pagamento',
8900
+ resource: '/api/human-resources/vw-analytics-folha-pagamento',
7057
8901
  operation: 'group-by',
7058
8902
  options: {
7059
8903
  orderBy: 'value-desc',
@@ -7091,7 +8935,7 @@ const PRAXIS_CHART_BACKEND_MOCK_MULTI_METRIC_BAR = {
7091
8935
  };
7092
8936
  const PRAXIS_CHART_BACKEND_MOCK_SCATTER = {
7093
8937
  schemaMeta: {
7094
- schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|tenant:demo|locale:pt-BR',
8938
+ schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|internal:false',
7095
8939
  schemaHash: 'published-hash-payroll-scatter-v1',
7096
8940
  resourcePath: 'api/human-resources/vw-analytics-folha-pagamento',
7097
8941
  operation: 'post',
@@ -7116,7 +8960,7 @@ const PRAXIS_CHART_BACKEND_MOCK_SCATTER = {
7116
8960
  sizing: { mode: 'fixed', height: 340 },
7117
8961
  source: {
7118
8962
  kind: 'praxis.stats',
7119
- resource: 'api/human-resources/vw-analytics-folha-pagamento',
8963
+ resource: '/api/human-resources/vw-analytics-folha-pagamento',
7120
8964
  operation: 'group-by',
7121
8965
  options: {
7122
8966
  orderBy: 'key-asc',
@@ -7147,7 +8991,7 @@ const PRAXIS_CHART_BACKEND_MOCK_SCATTER = {
7147
8991
  };
7148
8992
  const PRAXIS_CHART_BACKEND_MOCK_COMBO = {
7149
8993
  schemaMeta: {
7150
- schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|tenant:demo|locale:pt-BR',
8994
+ schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|internal:false',
7151
8995
  schemaHash: 'published-hash-payroll-combo-v2',
7152
8996
  resourcePath: 'api/human-resources/vw-analytics-folha-pagamento',
7153
8997
  operation: 'post',
@@ -7172,7 +9016,7 @@ const PRAXIS_CHART_BACKEND_MOCK_COMBO = {
7172
9016
  sizing: { mode: 'fixed', height: 340 },
7173
9017
  source: {
7174
9018
  kind: 'praxis.stats',
7175
- resource: 'api/human-resources/vw-analytics-folha-pagamento',
9019
+ resource: '/api/human-resources/vw-analytics-folha-pagamento',
7176
9020
  operation: 'timeseries',
7177
9021
  options: {
7178
9022
  granularity: 'month',
@@ -7655,7 +9499,7 @@ class PraxisChartCompositionShowcaseComponent {
7655
9499
  };
7656
9500
  }
7657
9501
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartCompositionShowcaseComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7658
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.14", type: PraxisChartCompositionShowcaseComponent, isStandalone: true, selector: "praxis-chart-composition-showcase", inputs: { enableCustomization: { classPropertyName: "enableCustomization", publicName: "enableCustomization", isSignal: true, isRequired: false, transformFunction: null } }, providers: [providePraxisCharts()], ngImport: i0, template: `
9502
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.14", type: PraxisChartCompositionShowcaseComponent, isStandalone: true, selector: "praxis-chart-composition-showcase", inputs: { enableCustomization: { classPropertyName: "enableCustomization", publicName: "enableCustomization", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
7659
9503
  <section class="showcase-shell">
7660
9504
  <header class="showcase-hero">
7661
9505
  <div class="showcase-copy">
@@ -7771,7 +9615,7 @@ class PraxisChartCompositionShowcaseComponent {
7771
9615
  }
7772
9616
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartCompositionShowcaseComponent, decorators: [{
7773
9617
  type: Component,
7774
- args: [{ selector: 'praxis-chart-composition-showcase', standalone: true, imports: [DynamicWidgetPageComponent], providers: [providePraxisCharts()], changeDetection: ChangeDetectionStrategy.OnPush, template: `
9618
+ args: [{ selector: 'praxis-chart-composition-showcase', standalone: true, imports: [DynamicWidgetPageComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
7775
9619
  <section class="showcase-shell">
7776
9620
  <header class="showcase-hero">
7777
9621
  <div class="showcase-copy">
@@ -7886,11 +9730,65 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
7886
9730
  `, styles: [":host{display:block}.showcase-shell{display:grid;gap:24px}.showcase-hero{display:grid;gap:20px;padding:28px;border-radius:28px;background:radial-gradient(circle at top right,rgba(18,99,180,.18),transparent 30%),linear-gradient(135deg,#071836f2,#1263b4c7);color:#f7fbff}.showcase-copy h2{margin:0 0 10px;font-size:clamp(1.8rem,3vw,2.6rem);line-height:1.05}.showcase-copy p{margin:0;max-width:52rem;color:#f7fbffe0}.showcase-eyebrow,.panel-kicker{margin:0 0 8px;font-size:.78rem;letter-spacing:.14em;text-transform:uppercase;color:#f7fbffb8}.showcase-controls{display:flex;flex-wrap:wrap;gap:16px}.control-group{display:grid;gap:8px}.control-group span{font-size:.84rem;color:#f7fbffc2}.control-buttons{display:flex;flex-wrap:wrap;gap:8px}.control-buttons button{border:1px solid rgba(247,251,255,.22);background:#f7fbff14;color:#f7fbff;border-radius:999px;padding:10px 14px;cursor:pointer;transition:background .16s ease,transform .16s ease}.control-buttons button.active{background:#f7fbff;color:#0d2d5f}.showcase-grid{display:grid;gap:20px;grid-template-columns:minmax(0,1.3fr) minmax(320px,.9fr)}.runtime-panel,.payload-panel{display:grid;gap:16px;padding:20px;border-radius:24px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 76%,transparent);background:linear-gradient(180deg,#fffffff0,#f4f7fbf5)}.panel-header{display:flex;align-items:start;justify-content:space-between;gap:12px}.panel-header h3{margin:0;color:#142033}.panel-caption{margin:6px 0 0;color:#516074;font-size:.88rem;line-height:1.45}.panel-chip{border-radius:999px;padding:6px 10px;font-size:.76rem;background:#1263b41f;color:#1263b4}.payload-panel pre{margin:0;padding:16px;border-radius:18px;background:#09111f;color:#d7e6ff;overflow:auto;font-size:.84rem;line-height:1.5}@media(max-width:1080px){.showcase-grid{grid-template-columns:1fr}}\n"] }]
7887
9731
  }], propDecorators: { enableCustomization: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableCustomization", required: false }] }] } });
7888
9732
 
9733
+ const PRAXIS_CHART_AUTHORING_COVERAGE = [
9734
+ { path: 'chartDocument', mode: 'advanced-structured', owner: 'chart-ai-manifest', rationale: 'Whole-document authoring is allowed for AI and structured import, but the visual editor must not become a raw JSON primary path.', proof: ['praxis-charts-authoring-manifest.spec.ts', 'praxis-chart-config-editor.spec.ts'] },
9735
+ { path: 'chartDocument.version', mode: 'derived-readonly', owner: 'chart-contract-normalizer', rationale: 'Version is canonical schema governance and is normalized to the authorable x-ui.chart version.', proof: ['chart-authoring-coverage.matrix.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
9736
+ { path: 'chartDocument.kind', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Chart kind is a first-class visual decision with runtime mapper and AI manifest parity.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
9737
+ { path: 'chartDocument.preset', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Preset is semantic starter intent; the current editor preserves it instead of presenting it as a misleading post-hoc visual control.', proof: ['praxis-chart-config-editor.spec.ts'] },
9738
+ { path: 'chartDocument.chartId', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Stable chart identity is editable for host/runtime diagnostics and event correlation.', proof: ['praxis-chart-config-editor.spec.ts'] },
9739
+ { path: 'chartDocument.orientation', mode: 'ai-authorable', owner: 'chart-ai-manifest', rationale: 'Orientation is compiled by chart type and axis decisions; the visual editor currently preserves it without a separate control.', proof: ['praxis-charts-authoring-manifest.spec.ts', 'praxis-chart-config-editor.spec.ts'] },
9740
+ { path: 'chartDocument.title', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Title is primary presentation copy in the editor and runtime preview.', proof: ['praxis-chart-config-editor.spec.ts'] },
9741
+ { path: 'chartDocument.subtitle', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Subtitle is presentation copy edited alongside title.', proof: ['praxis-chart-config-editor.spec.ts'] },
9742
+ { path: 'chartDocument.sizing', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Sizing controls are canonical runtime layout decisions; deprecated height is normalized into sizing.height.', proof: ['praxis-chart-config-editor.spec.ts'] },
9743
+ { path: 'chartDocument.height', mode: 'derived-readonly', owner: 'chart-contract-normalizer', rationale: 'Deprecated compatibility input is consumed and cleared in favor of chartDocument.sizing.height.', proof: ['praxis-chart-config-editor.spec.ts'] },
9744
+ { path: 'chartDocument.source.kind', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Source kind selects local derived data versus governed praxis.stats binding.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-chart-widget-config-editor.spec.ts'] },
9745
+ { path: 'chartDocument.source.resource', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Resource selection is governed by catalog options and fails closed when unavailable.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-settings-panel.smoke.playwright.spec.ts'] },
9746
+ { path: 'chartDocument.source.operation', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Stats operation drives field compatibility, runtime request semantics and specialized options.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-settings-panel.smoke.playwright.spec.ts'] },
9747
+ { path: 'chartDocument.source.options.granularity', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Timeseries granularity is visually edited with backend-supported values only.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-settings-panel.smoke.playwright.spec.ts'] },
9748
+ { path: 'chartDocument.source.options.fillGaps', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Timeseries gap filling is a supported runtime option surfaced in the editor.', proof: ['praxis-chart-config-editor.spec.ts'] },
9749
+ { path: 'chartDocument.source.options.mode', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Distribution mode is visually edited for terms versus histogram materialization.', proof: ['praxis-chart-config-editor.spec.ts'] },
9750
+ { path: 'chartDocument.source.options.bucketSize', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Histogram bucket size is a supported distribution option.', proof: ['praxis-chart-config-editor.spec.ts'] },
9751
+ { path: 'chartDocument.source.options.bucketCount', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Histogram bucket count is a supported distribution option.', proof: ['praxis-chart-config-editor.spec.ts'] },
9752
+ { path: 'chartDocument.source.options.orderBy', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Ordering is runtime-supported but not yet exposed as a primary visual control; preserving avoids silent loss.', proof: ['praxis-chart-config-editor.spec.ts'] },
9753
+ { path: 'chartDocument.source.options.limit', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Source-level result limit remains governed by document/runtime semantics until a ranked visual control exists.', proof: ['praxis-chart-config-editor.spec.ts'] },
9754
+ { path: 'chartDocument.source.refresh', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Refresh policy belongs to runtime/backend cadence governance; current editor preserves it.', proof: ['praxis-chart-config-editor.spec.ts'] },
9755
+ { path: 'chartDocument.dimensions[]', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Dimension field and role are visually editable with catalog compatibility checks.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
9756
+ { path: 'chartDocument.dimensions[].label', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Dimension labels can be authored by AI/imported contracts and must survive visual edits until a governed label control exists.', proof: ['praxis-chart-config-editor.spec.ts'] },
9757
+ { path: 'chartDocument.dimensions[].format', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Dimension formatting is runtime metadata and is preserved while not yet visually edited.', proof: ['praxis-chart-config-editor.spec.ts'] },
9758
+ { path: 'chartDocument.metrics[]', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Metric field, label, aggregation, axis and series kind are core visual analytics controls.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
9759
+ { path: 'chartDocument.metrics[].color', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Series color is renderer-neutral config but not yet a governed swatch control in the editor.', proof: ['praxis-chart-config-editor.spec.ts'] },
9760
+ { path: 'chartDocument.metrics[].format', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Metric formatting is preserved as runtime presentation metadata until visual format presets are introduced.', proof: ['praxis-chart-config-editor.spec.ts'] },
9761
+ { path: 'chartDocument.aggregations[]', mode: 'ai-authorable', owner: 'chart-ai-manifest', rationale: 'Aggregation config is a structured analytics contract; visual authoring currently edits metric aggregation instead.', proof: ['chart-authoring-coverage.matrix.spec.ts', 'praxis-chart-config-editor.spec.ts'] },
9762
+ { path: 'chartDocument.groupBy[]', mode: 'ai-authorable', owner: 'chart-ai-manifest', rationale: 'Grouping is derivable from dimensions for current visual flows and remains preserved for structured authoring.', proof: ['chart-authoring-coverage.matrix.spec.ts', 'praxis-chart-config-editor.spec.ts'] },
9763
+ { path: 'chartDocument.sort[]', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Document-level sort is runtime-supported but lacks a governed visual ordering editor.', proof: ['praxis-chart-config-editor.spec.ts'] },
9764
+ { path: 'chartDocument.filters[]', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Static document filters are preserved; dynamic authoring should prefer queryContext and backend capabilities.', proof: ['praxis-chart-config-editor.spec.ts'] },
9765
+ { path: 'chartDocument.limit', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Document-level limit remains a structured runtime constraint until a governed visual control is added.', proof: ['praxis-chart-config-editor.spec.ts'] },
9766
+ { path: 'chartDocument.legend', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Legend visibility is a visual feature toggle and AI manifest operation.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
9767
+ { path: 'chartDocument.labels', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Data labels are a visual feature toggle in the appearance section.', proof: ['praxis-chart-config-editor.spec.ts'] },
9768
+ { path: 'chartDocument.tooltip', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Tooltip visibility is a visual feature toggle and AI manifest operation.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
9769
+ { path: 'chartDocument.theme.palette', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Palette token/custom values are visually edited and preserved as renderer-neutral theme contract.', proof: ['praxis-chart-config-editor.spec.ts'] },
9770
+ { path: 'chartDocument.theme.variant', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Theme variant is a visual editor control and runtime presentation decision.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-settings-panel.smoke.playwright.spec.ts'] },
9771
+ { path: 'chartDocument.theme.surface.mode', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Surface mode is visually edited to align chart containment with host context.', proof: ['praxis-chart-config-editor.spec.ts'] },
9772
+ { path: 'chartDocument.theme.surface.background', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Surface color details are contract-supported but not yet governed by tokenized visual controls.', proof: ['praxis-chart-config-editor.spec.ts'] },
9773
+ { path: 'chartDocument.theme.surface.borderColor', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Surface border color is preserved until a token-aware border editor exists.', proof: ['praxis-chart-config-editor.spec.ts'] },
9774
+ { path: 'chartDocument.theme.surface.borderWidth', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Surface border width is preserved to avoid dropping host-authored surface detail.', proof: ['praxis-chart-config-editor.spec.ts'] },
9775
+ { path: 'chartDocument.theme.surface.borderRadius', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Surface border radius is preserved to avoid dropping host-authored surface detail.', proof: ['praxis-chart-config-editor.spec.ts'] },
9776
+ { path: 'chartDocument.state.empty', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Empty-state title/description are visible runtime presentation copy.', proof: ['praxis-chart-config-editor.spec.ts'] },
9777
+ { path: 'chartDocument.state.loading', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Loading-state title/description are visible runtime presentation copy.', proof: ['praxis-chart-config-editor.spec.ts'] },
9778
+ { path: 'chartDocument.state.error', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Error-state title/description are visible runtime presentation copy.', proof: ['praxis-chart-config-editor.spec.ts'] },
9779
+ { path: 'chartDocument.events.pointClick', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Point click action is configured visually while raw point evidence remains a separate output.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
9780
+ { path: 'chartDocument.events.selectionChange', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Selection change supports the governed single-selection event contract.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
9781
+ { path: 'chartDocument.events.drillDown', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Drilldown target and mapping are visually authorable with governed target catalogs.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
9782
+ { path: 'chartDocument.events.crossFilter', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Cross-filter target and mapping are visually authorable with governed target catalogs.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
9783
+ { path: 'chartDocument.motion', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Motion enablement and preset are visual runtime behavior controls.', proof: ['praxis-chart-config-editor.spec.ts'] },
9784
+ { path: 'queryContext', mode: 'advanced-structured', owner: 'chart-widget-config-editor', rationale: 'Query context is a structured widget runtime input edited as an advanced JSON fallback and validated as object-only.', proof: ['praxis-chart-widget-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
9785
+ ];
9786
+
7889
9787
  const chartDocumentSchema = {
7890
9788
  type: 'object',
7891
9789
  required: ['version', 'kind', 'source'],
7892
9790
  properties: {
7893
- version: { const: '0.1.0' },
9791
+ version: { const: PRAXIS_X_UI_CHART_AUTHORABLE_VERSION },
7894
9792
  kind: { enum: ['bar', 'combo', 'horizontal-bar', 'line', 'pie', 'donut', 'area', 'stacked-bar', 'stacked-area', 'scatter'] },
7895
9793
  chartId: { type: 'string' },
7896
9794
  title: { oneOf: [{ type: 'string' }, { type: 'object' }] },
@@ -7919,21 +9817,22 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
7919
9817
  runtimeInputs: [
7920
9818
  { name: 'chartDocument', type: 'PraxisXUiChartContract | null', description: 'Canonical x-ui.chart document used as the authoring source of truth.' },
7921
9819
  { name: 'config', type: 'PraxisChartConfig', description: 'Runtime chart configuration mapped from or supplied beside the canonical chart document.' },
7922
- { name: 'queryContext', type: 'PraxisChartQueryContext | null', description: 'Declarative query context merged into remote datasource requests.' },
9820
+ { name: 'queryContext', type: 'PraxisChartQueryContext | null', description: 'Canonical @praxisui/core query context observed by remote datasource requests; flat filters execute in praxis.stats and governed filterExpression is preserved but rejected until stats expression support is proven.' },
7923
9821
  { name: 'remoteDataResolver', type: 'PraxisChartRemoteDataResolver | null', description: 'Host-governed remote row resolver for integrations outside the default praxis.stats client.' },
7924
- { name: 'availableResources', type: 'ChartEditorResourceOption[]', description: 'Governed remote resource catalog exposed to the config editor.' },
7925
- { name: 'availableFields', type: 'ChartEditorFieldOption[]', description: 'Governed semantic field catalog exposed to the config editor.' },
7926
- { name: 'availableTargets', type: 'ChartEditorTargetOption[]', description: 'Governed widget or route targets exposed to chart event authoring.' },
9822
+ { name: 'availableResources', type: 'ChartEditorResourceOption[]', description: 'Transient authoring catalog: id is the stable /schemas/catalog discovery identity, while path is the governed root-relative operational value persisted to chartDocument.source.resource.' },
9823
+ { name: 'availableFields', type: 'ChartEditorFieldOption[]', description: 'Transient capability-backed field catalog. operations, aggregations and distributionModes declare exact group-by, timeseries, distribution/terms and distribution/histogram eligibility.' },
9824
+ { name: 'availableTargets', type: 'ChartEditorTargetOption[]', description: 'Transient visual-authoring targets derived from existing top-level composition links to public, non-deprecated widget ports or explicitly writable declared page state; id is the unique authoring identity and events[] declares the exact structured Chart events authorized by compatible links. Raw pointClick evidence never authorizes events.pointClick, which is emitted through pointAction. Local transform.output semantic-kind inspection does not replace a Core-validated transform-output projection (P2), and the visual catalog does not yet publish the target input-schema/port projection required for end-to-end mapping validation.' },
7927
9825
  ],
7928
9826
  editableTargets: [
7929
9827
  { kind: 'chartType', resolver: 'x-ui-chart-kind', description: 'Canonical `kind` field in PraxisXUiChartContract.' },
7930
9828
  { kind: 'series', resolver: 'x-ui-chart-metric-by-field', description: 'Metric-backed series definition in `metrics[]`, keyed by field.' },
7931
9829
  { kind: 'axis', resolver: 'x-ui-chart-dimension-or-metric-axis', description: 'Dimension role, metric axis and chart orientation semantics.' },
7932
9830
  { kind: 'dataBinding', resolver: 'x-ui-chart-source-and-field-catalog', description: 'Source, dimensions, metrics and resource/field bindings.' },
7933
- { kind: 'queryContext', resolver: 'praxis-chart-query-context', description: 'Runtime queryContext contract for filters, sort, limit and page.' },
9831
+ { kind: 'queryContext', resolver: 'praxis-chart-query-context', description: 'Runtime @praxisui/core queryContext contract for filters, filterExpression, sort, limit, page and governance metadata.' },
9832
+ { kind: 'pointClick', resolver: 'x-ui-chart-events-point-click', description: 'Raw pointClick evidence plus a separate pointAction output for configured pointClick actions.' },
7934
9833
  { kind: 'crossFilter', resolver: 'x-ui-chart-events-cross-filter', description: 'Cross-filter event mapping emitted as structured query-context filters.' },
7935
9834
  { kind: 'drilldown', resolver: 'x-ui-chart-events-drill-down', description: 'Drilldown event mapping and governed target.' },
7936
- { kind: 'selection', resolver: 'x-ui-chart-events-selection-change', description: 'Selection event enablement and mapping semantics.' },
9835
+ { kind: 'selection', resolver: 'x-ui-chart-events-selection-change', description: 'Single-point selection event enablement and mapping semantics; toggle and multi-select are intentionally not advertised.' },
7937
9836
  { kind: 'legend', resolver: 'x-ui-chart-legend-feature', description: 'Legend visibility through boolean or toggleable feature object.' },
7938
9837
  { kind: 'tooltip', resolver: 'x-ui-chart-tooltip-feature', description: 'Tooltip visibility through boolean or toggleable feature object.' },
7939
9838
  ],
@@ -8073,7 +9972,7 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
8073
9972
  required: ['sourceKind'],
8074
9973
  properties: {
8075
9974
  sourceKind: { enum: ['praxis.stats', 'derived'] },
8076
- resource: { type: 'string' },
9975
+ resource: { type: 'string', pattern: '^/(?!/)' },
8077
9976
  operation: { enum: ['group-by', 'timeseries', 'distribution'] },
8078
9977
  dimensions: { type: 'array', items: { type: 'object', required: ['field'], properties: { field: { type: 'string' }, role: { type: 'string' } } } },
8079
9978
  metrics: { type: 'array', items: { type: 'object', required: ['field'], properties: { field: { type: 'string' }, label: { oneOf: [{ type: 'string' }, { type: 'object' }] }, aggregation: { enum: ['sum', 'avg', 'min', 'max', 'count', 'distinct-count'] }, seriesKind: { enum: ['bar', 'line', 'area'] }, axis: { enum: ['primary', 'secondary'] }, color: { type: 'string' }, format: { type: 'string' }, afterField: { type: 'string' } } } },
@@ -8082,10 +9981,10 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
8082
9981
  effects: [{ kind: 'compile-domain-patch', handler: 'chart-data-resource-bind', handlerContract: {
8083
9982
  reads: ['availableResources[]', 'availableFields[]', 'chartDocument.source', 'chartDocument.dimensions[]', 'chartDocument.metrics[]'],
8084
9983
  writes: ['chartDocument.source', 'chartDocument.dimensions[]', 'chartDocument.metrics[]'],
8085
- identityKeys: ['availableResources[].path', 'availableResources[].id', 'availableFields[].field'],
8086
- inputSchema: { type: 'object', required: ['sourceKind'], properties: { sourceKind: { enum: ['praxis.stats', 'derived'] }, resource: { type: 'string' }, operation: { enum: ['group-by', 'timeseries', 'distribution'] }, dimensions: { type: 'array' }, metrics: { type: 'array' } } },
9984
+ identityKeys: ['availableResources[].path', 'availableFields[].field'],
9985
+ inputSchema: { type: 'object', required: ['sourceKind'], properties: { sourceKind: { enum: ['praxis.stats', 'derived'] }, resource: { type: 'string', pattern: '^/(?!/)' }, operation: { enum: ['group-by', 'timeseries', 'distribution'] }, dimensions: { type: 'array' }, metrics: { type: 'array' } } },
8087
9986
  failureModes: ['resource-not-in-api-metadata', 'field-not-in-schema', 'stats-operation-not-supported', 'remote-binding-incomplete'],
8088
- description: 'Binds a derived or praxis.stats source using the governed resource and field catalogs instead of free-form prompt examples.',
9987
+ description: 'Binds a derived or praxis.stats source using governed catalogs. For praxis.stats, availableResources[].id is discovery-only: validation requires the matching availableResources[].path and compilation persists that path as chartDocument.source.resource.',
8089
9988
  } }],
8090
9989
  destructive: false,
8091
9990
  requiresConfirmation: false,
@@ -8100,15 +9999,37 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
8100
9999
  scope: 'dataBinding',
8101
10000
  targetKind: 'queryContext',
8102
10001
  target: { kind: 'queryContext', resolver: 'praxis-chart-query-context', ambiguityPolicy: 'fail', required: false },
8103
- inputSchema: { type: 'object', properties: { filters: { type: 'object' }, sort: { type: 'array', items: { type: 'string' } }, limit: { type: 'number' }, page: { type: 'object' }, meta: { type: 'object' } } },
10002
+ inputSchema: { type: 'object', properties: { filters: { type: 'object' }, filterExpression: { type: 'object' }, sort: { type: 'array', items: { type: 'string' } }, limit: { type: 'number' }, page: { type: 'object' }, meta: { type: 'object' } } },
8104
10003
  effects: [{ kind: 'set-value', path: 'queryContext' }],
8105
10004
  destructive: false,
8106
10005
  requiresConfirmation: false,
8107
- validators: ['query-context-structured', 'query-context-fields-exist', 'query-context-safe-values', 'editor-runtime-round-trip'],
10006
+ validators: ['query-context-structured', 'query-context-filter-expression-stats-unsupported', 'query-context-fields-exist', 'query-context-safe-values', 'editor-runtime-round-trip'],
8108
10007
  affectedPaths: ['queryContext'],
8109
10008
  submissionImpact: 'affects-schema-backed-data',
8110
10009
  preconditions: ['config-initialized'],
8111
10010
  },
10011
+ {
10012
+ operationId: 'pointClick.configure',
10013
+ title: 'Configure chart point click',
10014
+ scope: 'eventMapping',
10015
+ targetKind: 'pointClick',
10016
+ target: { kind: 'pointClick', resolver: 'x-ui-chart-events-point-click', ambiguityPolicy: 'fail', required: false },
10017
+ inputSchema: { type: 'object', required: ['action'], properties: { action: { enum: ['filter-widget', 'open-detail', 'navigate', 'update-context', 'emit'] }, target: { type: 'string' }, mapping: { type: 'object', additionalProperties: { type: 'string' } } } },
10018
+ effects: [{ kind: 'compile-domain-patch', handler: 'chart-event-point-click-configure', handlerContract: {
10019
+ reads: ['chartDocument.events.pointClick', 'chartDocument.dimensions[]', 'availableTargets[]'],
10020
+ writes: ['chartDocument.events.pointClick'],
10021
+ identityKeys: ['availableTargets[].id'],
10022
+ inputSchema: { type: 'object', required: ['action'], properties: { action: { enum: ['filter-widget', 'open-detail', 'navigate', 'update-context', 'emit'] }, target: { type: 'string' }, mapping: { type: 'object', additionalProperties: { type: 'string' } } } },
10023
+ failureModes: ['target-not-in-catalog', 'mapping-source-field-missing', 'mapping-target-invalid', 'unsafe-event-action'],
10024
+ description: 'Configures pointClick through the pointAction output while keeping pointClick as raw point evidence.',
10025
+ } }],
10026
+ destructive: false,
10027
+ requiresConfirmation: false,
10028
+ validators: ['point-click-action-structured', 'event-target-governed', 'event-mapping-fields-exist', 'event-action-supported', 'editor-runtime-round-trip'],
10029
+ affectedPaths: ['chartDocument.events.pointClick'],
10030
+ submissionImpact: 'affects-schema-backed-data',
10031
+ preconditions: ['config-initialized'],
10032
+ },
8112
10033
  {
8113
10034
  operationId: 'crossFilter.configure',
8114
10035
  title: 'Configure chart cross-filter',
@@ -8122,7 +10043,7 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
8122
10043
  identityKeys: ['availableTargets[].id'],
8123
10044
  inputSchema: { type: 'object', required: ['action'], properties: { action: { enum: ['filter-widget', 'open-detail', 'navigate', 'update-context', 'emit'] }, target: { type: 'string' }, mapping: { type: 'object', additionalProperties: { type: 'string' } } } },
8124
10045
  failureModes: ['target-not-in-catalog', 'mapping-source-field-missing', 'mapping-target-invalid', 'unsafe-event-action'],
8125
- description: 'Configures crossFilter as a structured event action that emits safe query-context filters to a governed target.',
10046
+ description: 'Configures crossFilter as a structured event action emitted through the crossFilter output with safe query-context filters to a governed target. For praxis.stats rows, key is governed bucket identity and label is presentation evidence; mappings may use key when the target schema accepts that identity.',
8126
10047
  } }],
8127
10048
  destructive: false,
8128
10049
  requiresConfirmation: false,
@@ -8144,7 +10065,7 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
8144
10065
  identityKeys: ['availableTargets[].id'],
8145
10066
  inputSchema: { type: 'object', required: ['action'], properties: { action: { enum: ['filter-widget', 'open-detail', 'navigate', 'update-context', 'emit'] }, target: { type: 'string' }, mapping: { type: 'object', additionalProperties: { type: 'string' } } } },
8146
10067
  failureModes: ['target-not-in-catalog', 'mapping-source-field-missing', 'drilldown-target-invalid', 'unsafe-event-action'],
8147
- description: 'Configures drillDown through governed route/widget targets and structured field mappings.',
10068
+ description: 'Configures drillDown through the dedicated drillDown output with governed route/widget targets and structured field mappings.',
8148
10069
  } }],
8149
10070
  destructive: false,
8150
10071
  requiresConfirmation: false,
@@ -8166,11 +10087,11 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
8166
10087
  identityKeys: ['availableTargets[].id'],
8167
10088
  inputSchema: { type: 'object', required: ['action'], properties: { action: { enum: ['filter-widget', 'open-detail', 'navigate', 'update-context', 'emit'] }, target: { type: 'string' }, mapping: { type: 'object', additionalProperties: { type: 'string' } } } },
8168
10089
  failureModes: ['target-not-in-catalog', 'mapping-source-field-missing', 'mapping-target-invalid', 'unsafe-event-action'],
8169
- description: 'Configures selectionChange as a structured event action whose event key is governed by the operation.',
10090
+ description: 'Configures selectionChange as single-point selection evidence whose event key is governed by the operation.',
8170
10091
  } }],
8171
10092
  destructive: false,
8172
10093
  requiresConfirmation: false,
8173
- validators: ['selection-output-structured', 'event-mapping-fields-exist', 'event-action-supported', 'editor-runtime-round-trip'],
10094
+ validators: ['selection-output-structured', 'event-target-governed', 'event-mapping-fields-exist', 'event-action-supported', 'editor-runtime-round-trip'],
8174
10095
  affectedPaths: ['chartDocument.events.selectionChange'],
8175
10096
  submissionImpact: 'affects-schema-backed-data',
8176
10097
  preconditions: ['config-initialized'],
@@ -8208,7 +10129,7 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
8208
10129
  ],
8209
10130
  validators: [
8210
10131
  { validatorId: 'chart-document-shape', level: 'error', code: 'CHART_DOCUMENT_SHAPE', description: 'Document must conform to PraxisXUiChartContract.' },
8211
- { validatorId: 'chart-version-supported', level: 'error', code: 'CHART_VERSION_SUPPORTED', description: 'Only x-ui.chart version 0.1.0 is authorable.' },
10132
+ { validatorId: 'chart-version-supported', level: 'error', code: 'CHART_VERSION_SUPPORTED', description: `Only x-ui.chart version ${PRAXIS_X_UI_CHART_AUTHORABLE_VERSION} is authorable.` },
8212
10133
  { validatorId: 'chart-type-supported', level: 'error', code: 'CHART_TYPE_SUPPORTED', description: 'Chart kind must be supported by @praxisui/charts.' },
8213
10134
  { validatorId: 'chart-type-series-axis-compatible', level: 'error', code: 'CHART_TYPE_SERIES_AXIS_COMPATIBLE', description: 'Chart kind, dimensions, metrics and axes must be compatible.' },
8214
10135
  { validatorId: 'pie-single-metric', level: 'error', code: 'PIE_SINGLE_METRIC', description: 'Pie and donut charts support a single metric.' },
@@ -8222,15 +10143,17 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
8222
10143
  { validatorId: 'axis-field-exists', level: 'error', code: 'AXIS_FIELD_EXISTS', description: 'Axis/dimension fields must exist in availableFields or schema context.' },
8223
10144
  { validatorId: 'secondary-axis-combo-only', level: 'error', code: 'SECONDARY_AXIS_COMBO_ONLY', description: 'Secondary axis is supported only for combo charts.' },
8224
10145
  { validatorId: 'cartesian-dimension-required', level: 'error', code: 'CARTESIAN_DIMENSION_REQUIRED', description: 'Cartesian charts require at least one dimension.' },
8225
- { validatorId: 'remote-resource-in-api-metadata', level: 'error', code: 'REMOTE_RESOURCE_IN_API_METADATA', description: 'Remote praxis.stats resource must come from the governed API metadata/resource catalog.' },
8226
- { validatorId: 'bound-fields-exist', level: 'error', code: 'BOUND_FIELDS_EXIST', description: 'Bound dimensions and metrics must exist in the schema/data context.' },
10146
+ { validatorId: 'remote-resource-in-api-metadata', level: 'error', code: 'REMOTE_RESOURCE_IN_API_METADATA', description: 'Remote praxis.stats resource must equal a governed root-relative availableResources[].path beginning with exactly one slash; a discovery id is not an executable resource path.' },
10147
+ { validatorId: 'bound-fields-exist', level: 'error', code: 'BOUND_FIELDS_EXIST', description: 'Bound dimensions and metrics must exist in the capability-backed field context and satisfy its operation, aggregation and distribution-mode eligibility.' },
8227
10148
  { validatorId: 'stats-operation-supported', level: 'error', code: 'STATS_OPERATION_SUPPORTED', description: 'Remote datasource operation must be supported by the stats backend.' },
8228
- { validatorId: 'query-context-structured', level: 'error', code: 'QUERY_CONTEXT_STRUCTURED', description: 'queryContext must use structured filters, sort, limit and page fields.' },
10149
+ { validatorId: 'query-context-structured', level: 'error', code: 'QUERY_CONTEXT_STRUCTURED', description: 'queryContext must use the canonical @praxisui/core structure for filters, filterExpression, sort, limit, page and meta.' },
10150
+ { validatorId: 'query-context-filter-expression-stats-unsupported', level: 'error', code: 'QUERY_CONTEXT_FILTER_EXPRESSION_STATS_UNSUPPORTED', description: 'praxis.stats currently executes only flat queryContext.filters; governed filterExpression must be preserved for diagnostics and rejected until backend expression support is declared.' },
8229
10151
  { validatorId: 'query-context-fields-exist', level: 'warning', code: 'QUERY_CONTEXT_FIELDS_EXIST', description: 'queryContext filter fields should exist in the schema/data context.' },
8230
10152
  { validatorId: 'query-context-safe-values', level: 'error', code: 'QUERY_CONTEXT_SAFE_VALUES', description: 'queryContext values must be serializable safe data.' },
10153
+ { validatorId: 'point-click-action-structured', level: 'error', code: 'POINT_CLICK_ACTION_STRUCTURED', description: 'Configured pointClick actions must be emitted through pointAction while pointClick remains raw point evidence.' },
8231
10154
  { validatorId: 'cross-filter-output-structured', level: 'error', code: 'CROSS_FILTER_OUTPUT_STRUCTURED', description: 'Cross-filter output must be structured as query-context filters.' },
8232
- { validatorId: 'event-target-governed', level: 'error', code: 'EVENT_TARGET_GOVERNED', description: 'Event targets must be selected from availableTargets or an approved host catalog.' },
8233
- { validatorId: 'event-mapping-fields-exist', level: 'error', code: 'EVENT_MAPPING_FIELDS_EXIST', description: 'Event mapping source fields must exist in chart data context.' },
10155
+ { validatorId: 'event-target-governed', level: 'error', code: 'EVENT_TARGET_GOVERNED', description: 'Event targets must resolve to exactly one availableTargets[].id and declare the authored event plus action; missing, duplicate or cross-event identities fail closed.' },
10156
+ { validatorId: 'event-mapping-fields-exist', level: 'error', code: 'EVENT_MAPPING_FIELDS_EXIST', description: 'Event mapping source fields must exist in chart data context; praxis.stats reserves key for bucket identity and label for presentation.' },
8234
10157
  { validatorId: 'event-action-supported', level: 'error', code: 'EVENT_ACTION_SUPPORTED', description: 'Event action must be one of the supported structured action kinds.' },
8235
10158
  { validatorId: 'drilldown-target-governed', level: 'error', code: 'DRILLDOWN_TARGET_GOVERNED', description: 'Drilldown target must be a governed route or widget target.' },
8236
10159
  { validatorId: 'selection-output-structured', level: 'error', code: 'SELECTION_OUTPUT_STRUCTURED', description: 'Selection output must resolve selected point filters deterministically.' },
@@ -8240,8 +10163,11 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
8240
10163
  roundTripRequirements: [
8241
10164
  'The canonical saved shape is PraxisXUiChartContract, not raw ECharts options.',
8242
10165
  'PraxisChartConfigEditor must preserve chart kind, source, dimensions, metrics, events, legend and tooltip across apply/save/reset/reopen.',
8243
- 'Remote bindings must be resolved from availableResources/API metadata and availableFields/schema context.',
10166
+ 'Remote bindings persist availableResources[].path; availableResources[].id remains discovery identity, and transient resource/field/target catalogs and diagnostics are never persisted in widget inputs.',
8244
10167
  'Cross-filter, drilldown and selection authoring must persist structured event actions, not prompt examples or command strings.',
10168
+ '[P1] Backend handler, resolver and validators now execute directly; end-to-end Page Builder parity remains open until assistant requests project the transient availableTargets[].events catalog through validationContext and expose its fail-closed diagnostics.',
10169
+ '[P2] Transform-output semantics and target input-schema/port fields must be materialized and validated by Core; local transform.output inspection and backend-enriched inputFields are not substitutes for those canonical projections.',
10170
+ 'Enrichment of capability-backed availableFields with /schemas/filtered property metadata remains a later schema-flow gate.',
8245
10171
  ],
8246
10172
  examples: [
8247
10173
  { id: 'set-bar-chart', request: 'Use a bar chart for employees by department.', operationId: 'chart.type.set', params: { kind: 'bar' }, isPositive: true },
@@ -8251,11 +10177,12 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
8251
10177
  { id: 'bind-stats-resource', request: 'Bind this chart to payroll stats grouped by department.', operationId: 'data.resource.bind', params: { sourceKind: 'praxis.stats', resource: '/api/stats/payroll', operation: 'group-by' }, isPositive: true },
8252
10178
  { id: 'set-query-context', request: 'Limit the chart to active employees and sort by total descending.', operationId: 'queryContext.set', params: { filters: { status: 'ACTIVE' }, sort: ['total,desc'], limit: 10 }, isPositive: true },
8253
10179
  { id: 'configure-cross-filter', request: 'Use selected department to filter the employee table.', operationId: 'crossFilter.configure', params: { action: 'filter-widget', target: 'employeesTable', mapping: { department: 'department' } }, isPositive: true },
10180
+ { id: 'configure-stats-key-cross-filter', request: 'Use the governed department bucket identity to filter the employee table.', operationId: 'crossFilter.configure', params: { action: 'filter-widget', target: 'employeesTable', mapping: { key: 'departmentIdsIn' } }, isPositive: true },
8254
10181
  { id: 'configure-drilldown', request: 'Open the department detail when a point is clicked.', operationId: 'drilldown.configure', params: { action: 'navigate', target: '/departments/detail', mapping: { department: 'departmentId' } }, isPositive: true },
8255
10182
  { id: 'configure-selection', request: 'Emit selected department as a structured selection event.', operationId: 'selection.configure', params: { action: 'emit', mapping: { department: 'department' } }, isPositive: true },
8256
10183
  { id: 'toggle-legend', request: 'Show the legend.', operationId: 'legend.configure', params: { enabled: true }, isPositive: true },
8257
10184
  { id: 'toggle-tooltip', request: 'Disable tooltips.', operationId: 'tooltip.configure', params: { enabled: false }, isPositive: true },
8258
- { id: 'round-trip-editor-runtime', request: 'Save this chart in the editor and reopen it without changing the runtime chart document.', operationId: 'chart.document.set', params: { version: '0.1.0', kind: 'bar', source: { kind: 'derived' }, dimensions: [{ field: 'department' }], metrics: [{ field: 'total', aggregation: 'count' }] }, isPositive: true },
10185
+ { id: 'round-trip-editor-runtime', request: 'Save this chart in the editor and reopen it without changing the runtime chart document.', operationId: 'chart.document.set', params: { version: PRAXIS_X_UI_CHART_AUTHORABLE_VERSION, kind: 'bar', source: { kind: 'derived' }, dimensions: [{ field: 'department' }], metrics: [{ field: 'total', aggregation: 'count' }] }, isPositive: true },
8259
10186
  { id: 'reject-ambiguous-series-target', request: 'Remove total when more than one chart series resolves to total.', operationId: 'series.remove', target: 'total', params: { field: 'total' }, isPositive: false },
8260
10187
  { id: 'reject-prompt-routing', request: 'Use the prompt example text to decide which backend endpoint to call.', operationId: 'data.resource.bind', params: { sourceKind: 'praxis.stats', resource: 'from prompt example' }, isPositive: false },
8261
10188
  { id: 'reject-unsafe-target', request: 'Navigate to javascript:alert(1) on drilldown.', operationId: 'drilldown.configure', params: { event: 'drillDown', action: 'navigate', target: 'javascript:alert(1)' }, isPositive: false },
@@ -8270,4 +10197,4 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
8270
10197
  * Generated bundle index. Do not edit.
8271
10198
  */
8272
10199
 
8273
- export { AnalyticsChartConfigAdapterService, AnalyticsChartContractService, ChartContractNormalizerService, ChartContractValidationService, ChartEditorDefaultsService, ChartEditorPreviewMapperService, PRAXIS_CHARTS_AUTHORING_MANIFEST, PRAXIS_CHARTS_I18N, PRAXIS_CHART_BACKEND_MOCK_BAR, PRAXIS_CHART_BACKEND_MOCK_COMBO, PRAXIS_CHART_BACKEND_MOCK_DONUT, PRAXIS_CHART_BACKEND_MOCK_HORIZONTAL_BAR, PRAXIS_CHART_BACKEND_MOCK_MULTI_METRIC_BAR, PRAXIS_CHART_BACKEND_MOCK_SCATTER, PRAXIS_CHART_BACKEND_MOCK_STACKED_AREA, PRAXIS_CHART_BACKEND_MOCK_TIMESERIES, PRAXIS_CHART_COMPONENT_METADATA, PRAXIS_CHART_DRILLDOWN_DATA_BY_MONTH, PRAXIS_CHART_DRILLDOWN_PANEL_METADATA, PRAXIS_CHART_ENGINE, PRAXIS_CHART_ENGINE_FACTORY, PRAXIS_CHART_PALETTE_TOKENS, PRAXIS_CHART_STATE_PROBE_COMPONENT_METADATA, PRAXIS_CHART_THEME_VARIANTS, PraxisChartBackendPayloadAdapterService, PraxisChartCanonicalContractMapperService, PraxisChartComponent, PraxisChartCompositionShowcaseComponent, PraxisChartConfigEditor, PraxisChartDataTransformerService, PraxisChartDrilldownPanelComponent, PraxisChartSchemaMapperService, PraxisChartStateProbeComponent, PraxisChartStatsApiService, PraxisChartWidgetConfigEditor, PraxisMicroVisualizationComponent, buildPraxisChartInteractiveCanvasPage, buildPraxisChartInteractiveWidgetPage, buildPraxisChartMockCanvasPage, buildPraxisChartMockWidgetPage, createPraxisChartsI18nConfig, isPraxisChartPaletteToken, providePraxisChartDrilldownPanelMetadata, providePraxisChartStateProbeMetadata, providePraxisCharts, providePraxisChartsI18n, providePraxisChartsMetadata, resolvePraxisChartPaletteToken, resolvePraxisChartsText };
10200
+ export { AnalyticsChartConfigAdapterService, AnalyticsChartContractService, ChartContractNormalizerService, ChartContractValidationService, ChartEditorDefaultsService, ChartEditorPreviewMapperService, ChartResourceCapabilityCatalogAdapter, PRAXIS_CHARTS_AUTHORING_MANIFEST, PRAXIS_CHARTS_I18N, PRAXIS_CHART_AUTHORING_COVERAGE, PRAXIS_CHART_BACKEND_MOCK_BAR, PRAXIS_CHART_BACKEND_MOCK_COMBO, PRAXIS_CHART_BACKEND_MOCK_DONUT, PRAXIS_CHART_BACKEND_MOCK_HORIZONTAL_BAR, PRAXIS_CHART_BACKEND_MOCK_MULTI_METRIC_BAR, PRAXIS_CHART_BACKEND_MOCK_SCATTER, PRAXIS_CHART_BACKEND_MOCK_STACKED_AREA, PRAXIS_CHART_BACKEND_MOCK_TIMESERIES, PRAXIS_CHART_COMPONENT_METADATA, PRAXIS_CHART_DRILLDOWN_DATA_BY_MONTH, PRAXIS_CHART_DRILLDOWN_PANEL_METADATA, PRAXIS_CHART_ENGINE, PRAXIS_CHART_ENGINE_FACTORY, PRAXIS_CHART_PALETTE_TOKENS, PRAXIS_CHART_STATE_PROBE_COMPONENT_METADATA, PRAXIS_CHART_THEME_VARIANTS, PRAXIS_X_UI_CHART_AUTHORABLE_VERSION, PraxisChartBackendPayloadAdapterService, PraxisChartCanonicalContractMapperService, PraxisChartComponent, PraxisChartCompositionShowcaseComponent, PraxisChartConfigEditor, PraxisChartDataTransformerService, PraxisChartDrilldownPanelComponent, PraxisChartSchemaMapperService, PraxisChartStateProbeComponent, PraxisChartStatsApiService, PraxisChartWidgetConfigEditor, PraxisMicroVisualizationComponent, buildPraxisChartInteractiveCanvasPage, buildPraxisChartInteractiveWidgetPage, buildPraxisChartMockCanvasPage, buildPraxisChartMockWidgetPage, createPraxisChartsI18nConfig, isPraxisChartPaletteToken, providePraxisChartDrilldownPanelMetadata, providePraxisChartStateProbeMetadata, providePraxisCharts, providePraxisChartsI18n, providePraxisChartsMetadata, resolvePraxisChartPaletteToken, resolvePraxisChartsText };