@firestitch/report 18.21.3 → 18.21.5

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.
Files changed (31) hide show
  1. package/app/reports/components/component-list/component-list.component.d.ts +3 -0
  2. package/app/reports/export/report-export-collector.service.d.ts +2 -9
  3. package/app/reports/export/report-export.service.d.ts +0 -1
  4. package/app/reports/export/report-pdf.service.d.ts +0 -1
  5. package/app/reports/export/report-pptx.service.d.ts +0 -3
  6. package/app/reports/export/report-text-layout.service.d.ts +0 -4
  7. package/app/reports/interfaces/report-value-label.interface.d.ts +4 -0
  8. package/app/reports/interfaces/report.interface.d.ts +2 -0
  9. package/app/reports/option-builder.d.ts +1 -0
  10. package/app/reports/value-labels.d.ts +2 -0
  11. package/esm2022/app/reports/components/component-list/component-list.component.mjs +11 -3
  12. package/esm2022/app/reports/components/component-text/component-text.component.mjs +3 -3
  13. package/esm2022/app/reports/export/report-export-collector.service.mjs +21 -22
  14. package/esm2022/app/reports/export/report-export.service.mjs +7 -34
  15. package/esm2022/app/reports/export/report-pdf.service.mjs +21 -40
  16. package/esm2022/app/reports/export/report-pptx.service.mjs +68 -117
  17. package/esm2022/app/reports/export/report-text-layout.service.mjs +5 -20
  18. package/esm2022/app/reports/interfaces/report-value-label.interface.mjs +2 -0
  19. package/esm2022/app/reports/interfaces/report.interface.mjs +1 -1
  20. package/esm2022/app/reports/mark-click.mjs +9 -6
  21. package/esm2022/app/reports/option-builder.mjs +60 -10
  22. package/esm2022/app/reports/value-labels.mjs +26 -0
  23. package/esm2022/public_api.mjs +2 -1
  24. package/fesm2022/{firestitch-report-echarts-CUVOSBvJ.mjs → firestitch-report-echarts-D7WNr0Z4.mjs} +2 -2
  25. package/fesm2022/{firestitch-report-echarts-CUVOSBvJ.mjs.map → firestitch-report-echarts-D7WNr0Z4.mjs.map} +1 -1
  26. package/fesm2022/{firestitch-report-firestitch-report-CfvBTMrG.mjs → firestitch-report-firestitch-report-DWP3aywv.mjs} +218 -245
  27. package/fesm2022/firestitch-report-firestitch-report-DWP3aywv.mjs.map +1 -0
  28. package/fesm2022/firestitch-report.mjs +1 -1
  29. package/package.json +1 -1
  30. package/public_api.d.ts +1 -0
  31. package/fesm2022/firestitch-report-firestitch-report-CfvBTMrG.mjs.map +0 -1
@@ -3687,6 +3687,32 @@ const REPORT_DATA_LABEL_FONT_SIZE = 10;
3687
3687
  // .small text or the body size.
3688
3688
  const REPORT_KPI_VALUE_FONT_SIZE = 34;
3689
3689
 
3690
+ // The words a chart's or a list's row value is shown in, when the server
3691
+ // labels its column (ComponentData.valueLabels): an enum column's
3692
+ // "consumerGoods" reads "Consumer Goods" on the axis, the legend, the
3693
+ // tooltip, the list cell, the cross-tab heading and in both exports. Rows
3694
+ // keep the stored value, which filters, sorts and clicks still use. Pure, so
3695
+ // the screen and the exports word a value alike.
3696
+ // Each column's entries as a lookup by value text, built once per entries
3697
+ // array the data carries: a list asks once per cell, a chart once per
3698
+ // category.
3699
+ const LOOKUPS = new WeakMap();
3700
+ // What `value`, a row value of `column`, reads as: the label of the entry
3701
+ // whose value matches String(value). null when the column has no labels, the
3702
+ // value has no entry or is blank, so it shows as it always has.
3703
+ function valueLabel(labels, column, value) {
3704
+ const entries = column ? labels?.[column] : undefined;
3705
+ if (!entries?.length || value === null || value === undefined) {
3706
+ return null;
3707
+ }
3708
+ let lookup = LOOKUPS.get(entries);
3709
+ if (!lookup) {
3710
+ lookup = new Map(entries.map((entry) => [String(entry.value), entry.label]));
3711
+ LOOKUPS.set(entries, lookup);
3712
+ }
3713
+ return lookup.get(String(value)) ?? null;
3714
+ }
3715
+
3690
3716
  // Room for an axis title. ECharts 5's containLabel keeps the TICK labels inside
3691
3717
  // the grid but not the axis name, so without this a vertical chart's category
3692
3718
  // title runs off the bottom edge or under a bottom legend, and its value title
@@ -3838,7 +3864,7 @@ function extractChartSeries(component, data) {
3838
3864
  const { xColumn, groupColumn } = chartColumns(config, data);
3839
3865
  const measures = chartMeasures(config, data);
3840
3866
  const rawCategories = uniqueValues(data.rows, xColumn);
3841
- const categories = rawCategories.map((category) => String(category ?? '—'));
3867
+ const categories = rawCategories.map((category) => dimensionText(data, xColumn, category));
3842
3868
  const series = [];
3843
3869
  const rawGroups = groupColumn ? uniqueValues(data.rows, groupColumn) : [];
3844
3870
  if (groupColumn) {
@@ -3846,7 +3872,7 @@ function extractChartSeries(component, data) {
3846
3872
  const measure = measures[0];
3847
3873
  for (const groupValue of rawGroups) {
3848
3874
  series.push({
3849
- name: String(groupValue ?? '—'),
3875
+ name: dimensionText(data, groupColumn, groupValue),
3850
3876
  values: categoryValues(data.rows, xColumn, rawCategories, measure.column, (row) => row[groupColumn] === groupValue),
3851
3877
  type: measure.type,
3852
3878
  measure,
@@ -3867,6 +3893,13 @@ function extractChartSeries(component, data) {
3867
3893
  }
3868
3894
  return { categories, series, rawCategories, rawGroups };
3869
3895
  }
3896
+ // A category or a split value as the chart writes it: its label when the
3897
+ // server labels its column, else the value as it came, a blank one as '—'. A
3898
+ // time column is never labeled, so its period starts stay raw for the axis
3899
+ // to format.
3900
+ function dimensionText(data, column, value) {
3901
+ return valueLabel(data.valueLabels, column, value) ?? String(value ?? '—');
3902
+ }
3870
3903
  // The column a chart's categories come from, and the one that splits its
3871
3904
  // first measure into a series per value, if any. colorBy emits one series per
3872
3905
  // category too (so each bar carries its own color + legend entry), but the
@@ -5535,10 +5568,14 @@ function seriesTooltip(measure, context) {
5535
5568
  valueFormatter: (value) => formatNumber(Array.isArray(value) ? value[0] : value, measure, context),
5536
5569
  };
5537
5570
  }
5571
+ // A pie's slices are named by categoryLineLabels(), so a monthly pie's legend
5572
+ // and tooltip read the month ("August"), never its period start
5573
+ // ("2026-08-01").
5538
5574
  function buildPieOption(component, data, donut, context) {
5539
5575
  const config = component.config;
5540
5576
  const extracted = extractChartSeries(component, data);
5541
5577
  const values = extracted.series[0]?.values ?? [];
5578
+ const names = categoryLineLabels(extracted.categories, config, data, context.locale);
5542
5579
  const legendPosition = resolveLegendPosition(config, extracted.series.length);
5543
5580
  return {
5544
5581
  tooltip: { trigger: 'item' },
@@ -5550,8 +5587,8 @@ function buildPieOption(component, data, donut, context) {
5550
5587
  center: pieCenter(legendPosition),
5551
5588
  tooltip: seriesTooltip(extracted.series[0]?.measure, context),
5552
5589
  label: { show: false },
5553
- data: extracted.categories.map((category, index) => ({
5554
- name: category,
5590
+ data: names.map((name, index) => ({
5591
+ name,
5555
5592
  value: values[index] ?? 0,
5556
5593
  })),
5557
5594
  },
@@ -5706,21 +5743,59 @@ function formatPeriodLabel(value, granularity, multiYear, locale, dateLabel = Re
5706
5743
  }
5707
5744
  return multiYear && periodYearApart(granularity, dateLabel) ? `${parts.primary} ${parts.year}` : parts.primary;
5708
5745
  }
5746
+ // A chart's categories as one-line labels, for what writes a category on a
5747
+ // single line: a pie's slices (legend, tooltip, click) and the PowerPoint
5748
+ // export's axis. On a bucketed time axis each period start reads as the axis
5749
+ // writes it, in the report's locale and the axis's date label style, a short
5750
+ // label taking its year inline only when the categories span years; any
5751
+ // other category as it is.
5752
+ function categoryLineLabels(categories, config, data, locale) {
5753
+ const granularity = bucketGranularity(data);
5754
+ if (!granularity) {
5755
+ return categories;
5756
+ }
5757
+ const multiYear = spansMultipleYears(categories);
5758
+ const dateLabel = chartDateLabel(config);
5759
+ return categories.map((category) => formatPeriodLabel(category, granularity, multiYear, locale, dateLabel));
5760
+ }
5761
+ // Between a cross-tab cell column's pivot value and its measure when the grid
5762
+ // has several measures ('2026-01-01 — revenue'), as the server names the
5763
+ // column (ReportListPivot::getCellColumn). With one measure the column is the
5764
+ // pivot value alone.
5765
+ const PIVOT_MEASURE_SEPARATOR = ' — ';
5709
5766
  // A cross-tab's column headings, the one rule the grid and the PDF/PowerPoint
5710
5767
  // exports share. The first is the row key and keeps its authored label; the
5711
- // rest are pivot VALUES. When the pivot axis is a time dimension the server
5712
- // sends the grain it bucketed at, so a period-start like "2026-01-01" reads
5713
- // "January 2026" ('janvier 2026' in fr-CA), as the chart's x-axis labels it.
5768
+ // rest are pivot VALUES, each read as the server labels the pivot column (an
5769
+ // enum's "consumerGoods" heads "Consumer Goods"). When the pivot axis is a
5770
+ // time dimension the server sends the grain it bucketed at, so a
5771
+ // period-start like "2026-01-01" reads "January 2026" ('janvier 2026' in
5772
+ // fr-CA), as the chart's x-axis labels it. A grid of several measures keeps
5773
+ // each column's measure after its value.
5714
5774
  function pivotHeadings(config, data, locale) {
5715
5775
  const columns = data.columns ?? [];
5716
- const multiYear = spansMultipleYears(columns);
5776
+ const severalMeasures = new Set(Object.values(data.cellMeasures ?? {})).size > 1;
5777
+ const cells = columns.slice(1)
5778
+ .map((column) => pivotCellParts(column, severalMeasures ? data.cellMeasures?.[column] : undefined));
5779
+ const multiYear = spansMultipleYears(cells.map((cell) => cell.value));
5717
5780
  return columns.map((column, index) => {
5718
5781
  if (index === 0) {
5719
5782
  return config.rowBy?.label || column;
5720
5783
  }
5721
- return data.granularity ? formatPeriodLabel(column, data.granularity, multiYear, locale) : column;
5784
+ const { value, measure } = cells[index - 1];
5785
+ const heading = valueLabel(data.valueLabels, config.pivotBy?.column, value)
5786
+ ?? (data.granularity ? formatPeriodLabel(value, data.granularity, multiYear, locale) : value);
5787
+ return `${heading}${measure}`;
5722
5788
  });
5723
5789
  }
5790
+ // A cross-tab cell column split into the pivot value it is named by and the
5791
+ // measure part after it ('' with one measure, or when `measure` does not end
5792
+ // the column).
5793
+ function pivotCellParts(column, measure) {
5794
+ const suffix = measure === undefined ? '' : `${PIVOT_MEASURE_SEPARATOR}${measure}`;
5795
+ return suffix && column.endsWith(suffix)
5796
+ ? { value: column.slice(0, -suffix.length), measure: suffix }
5797
+ : { value: column, measure: '' };
5798
+ }
5724
5799
  // A cross-tab's columns as the grid and the exports draw them: each headed by
5725
5800
  // pivotHeadings(), and every pivot-value column formatted by the one
5726
5801
  // config.cellFormat. The row key column stays as it came. `numeric` marks the
@@ -9375,9 +9450,10 @@ function chartPoints(component, data, context, extracted, dataIndex, series) {
9375
9450
  }
9376
9451
  return points;
9377
9452
  }
9378
- // The category as the rows hold it and as the axis wrote it. On a time axis
9379
- // it also gives the period's first day and the grain, and reads as the
9380
- // tooltip names it ('January 2026'); a pie writes its slices as they come.
9453
+ // The category as the rows hold it and as the axis wrote it (a labeled
9454
+ // column's label). On a time axis it also gives the period's first day and
9455
+ // the grain, and reads as the tooltip names it ('January 2026'); a pie's as
9456
+ // its legend names the slice (categoryLineLabels).
9381
9457
  function categoryPoint(config, data, context, extracted, column, dataIndex) {
9382
9458
  const raw = extracted.rawCategories[dataIndex];
9383
9459
  const label = extracted.categories[dataIndex];
@@ -9390,7 +9466,9 @@ function categoryPoint(config, data, context, extracted, column, dataIndex) {
9390
9466
  return {
9391
9467
  column,
9392
9468
  raw,
9393
- label: pie ? label : formatPeriodLabel(String(raw), grain, true, context.locale, chartDateLabel(config)),
9469
+ label: pie
9470
+ ? categoryLineLabels(extracted.categories, config, data, context.locale)[dataIndex] ?? label
9471
+ : formatPeriodLabel(String(raw), grain, true, context.locale, chartDateLabel(config)),
9394
9472
  period,
9395
9473
  grain,
9396
9474
  };
@@ -10431,6 +10509,12 @@ class ComponentListComponent {
10431
10509
  // the note above the list, the header tooltips and the output cells; null
10432
10510
  // until it arrives, and empty of every key for a list that has none.
10433
10511
  noteData = signal(null);
10512
+ // What each stored value of a labeled column reads as (an enum's, a
10513
+ // cross-tab's row key's), from the latest page 1: its cells show the label
10514
+ // (valueLabel), while the rows keep the value that sorts and filters. null
10515
+ // until it arrives, and for a list with none.
10516
+ valueLabels = signal(null);
10517
+ valueLabel = valueLabel;
10434
10518
  // How many rows the list shows (load more appends) out of how many its
10435
10519
  // filters keep. A cross-tab counts grid rows.
10436
10520
  rowCount = signal(null);
@@ -10600,6 +10684,7 @@ class ComponentListComponent {
10600
10684
  this.totals.set(data.totals ?? null);
10601
10685
  this.totalsNote.set(data.totalsNote ?? null);
10602
10686
  this.noteData.set(this._noteData(data));
10687
+ this.valueLabels.set(data.valueLabels ?? null);
10603
10688
  this._alignOutputColumns(data);
10604
10689
  }
10605
10690
  // The server's page size when it sends one (it clamps the request),
@@ -10700,7 +10785,7 @@ class ComponentListComponent {
10700
10785
  }
10701
10786
  }
10702
10787
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ComponentListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10703
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ComponentListComponent, isStandalone: true, selector: "app-report-component-list", inputs: { reportId: { classPropertyName: "reportId", publicName: "reportId", isSignal: true, isRequired: true, transformFunction: null }, component: { classPropertyName: "component", publicName: "component", isSignal: true, isRequired: true, transformFunction: null } }, viewQueries: [{ propertyName: "listComponent", first: true, predicate: FsListComponent, descendants: true, isSignal: true }], ngImport: i0, template: "@if (truncated()) {\n <div class=\"pivot-truncated\">\n This grid was built from a partial result, so some columns or values may be missing. Narrow it with a filter.\n </div>\n}\n\n<!-- The windows a comparison compares, and why it reads as it does. A\n windowed column's own notes are in its header's tooltip. -->\n<app-report-component-note\n [data]=\"noteData()\"\n [windowNotes]=\"false\">\n</app-report-component-note>\n\n<!-- fs-list reads a column's footer template once, as a direct child of\n fs-list-column, so a list with totals renders its columns in a branch of\n their own rather than wrapping the footer in an @if. Its header template too,\n which every column carries: an output column's tooltip says what it holds.\n Columns are tracked by alignment as well as name, since fs-list reads a\n column's alignment only when it builds the column. -->\n<fs-list\n [config]=\"listConfig\"\n [class.report-totals-empty]=\"!totals()\">\n @if (showTotals()) {\n @for (column of columns(); track column.column + ':' + column.numeric) {\n <fs-list-column\n [name]=\"column.column\"\n [title]=\"column.label || column.column\"\n [show]=\"!hidden().has(column.column)\"\n [sortable]=\"!pivoted() || $first\"\n [align]=\"column.numeric ? 'right' : 'left'\"\n [class]=\"columnClasses(column, $index, pinned())\">\n <ng-template fs-list-header>\n <ng-container\n [ngTemplateOutlet]=\"headerTpl\"\n [ngTemplateOutletContext]=\"{ column: column }\">\n </ng-container>\n </ng-template>\n <ng-template\n fs-list-cell\n let-row=\"row\">\n <ng-container\n [ngTemplateOutlet]=\"cellTpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row[column.column], row: row, column: column }\">\n </ng-container>\n </ng-template>\n <ng-template fs-list-footer>\n @if (totals()?.[column.column] !== undefined) {\n <ng-container\n [ngTemplateOutlet]=\"cellTpl\"\n [ngTemplateOutletContext]=\"{ $implicit: totals()?.[column.column], row: totals(), column: column }\">\n </ng-container>\n } @else if (column.column === totalsLabelColumn()) {\n {{ totalLabel }}\n }\n </ng-template>\n </fs-list-column>\n }\n } @else {\n @for (column of columns(); track column.column + ':' + column.numeric) {\n <fs-list-column\n [name]=\"column.column\"\n [title]=\"column.label || column.column\"\n [show]=\"!hidden().has(column.column)\"\n [sortable]=\"!pivoted() || $first\"\n [align]=\"column.numeric ? 'right' : 'left'\"\n [class]=\"columnClasses(column, $index, pinned())\">\n <ng-template fs-list-header>\n <ng-container\n [ngTemplateOutlet]=\"headerTpl\"\n [ngTemplateOutletContext]=\"{ column: column }\">\n </ng-container>\n </ng-template>\n <ng-template\n fs-list-cell\n let-row=\"row\">\n <ng-container\n [ngTemplateOutlet]=\"cellTpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row[column.column], row: row, column: column }\">\n </ng-container>\n </ng-template>\n </fs-list-column>\n }\n }\n</fs-list>\n\n@if (caption()) {\n <div class=\"list-caption small\">\n {{ caption() }}\n </div>\n}\n\n<!-- A column's heading, with what an output column holds as its tooltip (an\n empty one never shows). -->\n<ng-template\n #headerTpl\n let-column=\"column\">\n <span [matTooltip]=\"columnNotes()[column.column] ?? ''\">{{ column.label || column.column }}</span>\n</ng-template>\n\n<!-- One cell, in the body and the totals row alike: an output column by what\n it holds (its row gives a change its prior), a date column as a day in the\n report's zone and locale, any other through its number format. -->\n<ng-template\n #cellTpl\n let-value\n let-row=\"row\"\n let-column=\"column\">\n @if (outputColumns()[column.column]; as output) {\n @if (row | fsReportOutput: column : output : formatContext(); as cell) {\n <span [attr.title]=\"cell.title\">{{ cell.text }}</span>\n }\n } @else if (column.format === ReportListColumnFormat.Date) {\n {{ value | fsReportDate: timezone() : formatContext().locale }}\n } @else {\n {{ value | fsReportNumber: column : formatContext() }}\n }\n</ng-template>\n", styles: [":host{display:flex;flex-direction:column;width:100%;height:100%;min-height:0}:host ::ng-deep fs-list{display:flex;flex-direction:column;flex:1 1 auto;height:auto;min-height:0}:host ::ng-deep fs-list .fs-list-container{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;height:100%}:host ::ng-deep fs-list .fs-list-header-container{flex:0 0 auto}:host ::ng-deep fs-list .fs-list-container .fs-list-table-container{flex:1 1 auto;min-height:0;overflow-y:auto!important}:host ::ng-deep fs-list .fs-list-table thead th{position:sticky;top:0;z-index:1;background-color:#fff}:host ::ng-deep fs-list .fs-list-table tfoot td{position:sticky;bottom:0;z-index:1;background-color:#fff;font-weight:500;box-shadow:inset 0 1px #0000001f}:host ::ng-deep fs-list.report-totals-empty tfoot{display:none}:host ::ng-deep fs-list .report-pinned{position:sticky;left:var(--report-pin-left, 0px);z-index:1;background-color:#fff}:host ::ng-deep fs-list .report-pin-0{--report-pin-left: var(--report-pin-left-0, 0px)}:host ::ng-deep fs-list .report-pin-1{--report-pin-left: var(--report-pin-left-1, 0px)}:host ::ng-deep fs-list .report-pin-2{--report-pin-left: var(--report-pin-left-2, 0px)}:host ::ng-deep fs-list .report-pin-last{box-shadow:inset -1px 0 #0000001f}:host ::ng-deep fs-list .fs-list-table thead th.report-pinned{z-index:3}:host ::ng-deep fs-list .fs-list-table tfoot td.report-pinned{z-index:2}:host ::ng-deep fs-list .fs-list-table tfoot td.report-pin-last{box-shadow:inset -1px 0 #0000001f,inset 0 1px #0000001f}:host ::ng-deep fs-list th.report-nowrap,:host ::ng-deep fs-list td.report-nowrap{white-space:nowrap}:host ::ng-deep fs-list fs-list-pagination{flex:0 0 auto}:host ::ng-deep fs-list .fs-list-load-more{position:sticky;bottom:0;background-color:#fff}:host ::ng-deep fs-list .fs-list-load-more button{margin-top:0}.list-caption{flex:0 0 auto;padding-top:4px}.pivot-truncated{flex:0 0 auto;padding:8px 12px;margin-bottom:8px;border-radius:4px;background-color:#fff4e5;color:#663c00}\n"], dependencies: [{ kind: "component", type: ComponentNoteComponent, selector: "app-report-component-note", inputs: ["data", "windows", "windowDescriptions", "windowNotes"] }, { kind: "ngmodule", type: FsListModule }, { kind: "component", type: i1$4.FsListComponent, selector: "fs-list", inputs: ["config", "loaderLines", "cellRowType"], outputs: ["filtersReady"] }, { kind: "directive", type: i1$4.FsListColumnDirective, selector: "fs-list-column", inputs: ["show", "title", "name", "customizable", "sortable", "sortableDefault", "sortableDirection", "direction", "align", "width", "class"] }, { kind: "directive", type: i1$4.FsListCellDirective, selector: "[fs-list-cell],[fsListCell]", inputs: ["colspan", "align", "class", "fsListCell", "configTyping"] }, { kind: "directive", type: i1$4.FsListHeaderDirective, selector: "[fs-list-header]", inputs: ["colspan", "align", "class"] }, { kind: "directive", type: i1$4.FsListFooterDirective, selector: "[fs-list-footer]", inputs: ["colspan", "align", "class"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "pipe", type: ReportDatePipe, name: "fsReportDate" }, { kind: "pipe", type: ReportNumberPipe, name: "fsReportNumber" }, { kind: "pipe", type: ReportOutputPipe, name: "fsReportOutput" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
10788
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ComponentListComponent, isStandalone: true, selector: "app-report-component-list", inputs: { reportId: { classPropertyName: "reportId", publicName: "reportId", isSignal: true, isRequired: true, transformFunction: null }, component: { classPropertyName: "component", publicName: "component", isSignal: true, isRequired: true, transformFunction: null } }, viewQueries: [{ propertyName: "listComponent", first: true, predicate: FsListComponent, descendants: true, isSignal: true }], ngImport: i0, template: "@if (truncated()) {\n <div class=\"pivot-truncated\">\n This grid was built from a partial result, so some columns or values may be missing. Narrow it with a filter.\n </div>\n}\n\n<!-- The windows a comparison compares, and why it reads as it does. A\n windowed column's own notes are in its header's tooltip. -->\n<app-report-component-note\n [data]=\"noteData()\"\n [windowNotes]=\"false\">\n</app-report-component-note>\n\n<!-- fs-list reads a column's footer template once, as a direct child of\n fs-list-column, so a list with totals renders its columns in a branch of\n their own rather than wrapping the footer in an @if. Its header template too,\n which every column carries: an output column's tooltip says what it holds.\n Columns are tracked by alignment as well as name, since fs-list reads a\n column's alignment only when it builds the column. -->\n<fs-list\n [config]=\"listConfig\"\n [class.report-totals-empty]=\"!totals()\">\n @if (showTotals()) {\n @for (column of columns(); track column.column + ':' + column.numeric) {\n <fs-list-column\n [name]=\"column.column\"\n [title]=\"column.label || column.column\"\n [show]=\"!hidden().has(column.column)\"\n [sortable]=\"!pivoted() || $first\"\n [align]=\"column.numeric ? 'right' : 'left'\"\n [class]=\"columnClasses(column, $index, pinned())\">\n <ng-template fs-list-header>\n <ng-container\n [ngTemplateOutlet]=\"headerTpl\"\n [ngTemplateOutletContext]=\"{ column: column }\">\n </ng-container>\n </ng-template>\n <ng-template\n fs-list-cell\n let-row=\"row\">\n <ng-container\n [ngTemplateOutlet]=\"cellTpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row[column.column], row: row, column: column }\">\n </ng-container>\n </ng-template>\n <ng-template fs-list-footer>\n @if (totals()?.[column.column] !== undefined) {\n <ng-container\n [ngTemplateOutlet]=\"cellTpl\"\n [ngTemplateOutletContext]=\"{ $implicit: totals()?.[column.column], row: totals(), column: column }\">\n </ng-container>\n } @else if (column.column === totalsLabelColumn()) {\n {{ totalLabel }}\n }\n </ng-template>\n </fs-list-column>\n }\n } @else {\n @for (column of columns(); track column.column + ':' + column.numeric) {\n <fs-list-column\n [name]=\"column.column\"\n [title]=\"column.label || column.column\"\n [show]=\"!hidden().has(column.column)\"\n [sortable]=\"!pivoted() || $first\"\n [align]=\"column.numeric ? 'right' : 'left'\"\n [class]=\"columnClasses(column, $index, pinned())\">\n <ng-template fs-list-header>\n <ng-container\n [ngTemplateOutlet]=\"headerTpl\"\n [ngTemplateOutletContext]=\"{ column: column }\">\n </ng-container>\n </ng-template>\n <ng-template\n fs-list-cell\n let-row=\"row\">\n <ng-container\n [ngTemplateOutlet]=\"cellTpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row[column.column], row: row, column: column }\">\n </ng-container>\n </ng-template>\n </fs-list-column>\n }\n }\n</fs-list>\n\n@if (caption()) {\n <div class=\"list-caption small\">\n {{ caption() }}\n </div>\n}\n\n<!-- A column's heading, with what an output column holds as its tooltip (an\n empty one never shows). -->\n<ng-template\n #headerTpl\n let-column=\"column\">\n <span [matTooltip]=\"columnNotes()[column.column] ?? ''\">{{ column.label || column.column }}</span>\n</ng-template>\n\n<!-- One cell, in the body and the totals row alike: an output column by what\n it holds (its row gives a change its prior), a date column as a day in the\n report's zone and locale, a labeled column's value as its label, any other\n through its number format. -->\n<ng-template\n #cellTpl\n let-value\n let-row=\"row\"\n let-column=\"column\">\n @if (outputColumns()[column.column]; as output) {\n @if (row | fsReportOutput: column : output : formatContext(); as cell) {\n <span [attr.title]=\"cell.title\">{{ cell.text }}</span>\n }\n } @else if (column.format === ReportListColumnFormat.Date) {\n {{ value | fsReportDate: timezone() : formatContext().locale }}\n } @else {\n {{ valueLabel(valueLabels(), column.column, value) ?? (value | fsReportNumber: column : formatContext()) }}\n }\n</ng-template>\n", styles: [":host{display:flex;flex-direction:column;width:100%;height:100%;min-height:0}:host ::ng-deep fs-list{display:flex;flex-direction:column;flex:1 1 auto;height:auto;min-height:0}:host ::ng-deep fs-list .fs-list-container{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;height:100%}:host ::ng-deep fs-list .fs-list-header-container{flex:0 0 auto}:host ::ng-deep fs-list .fs-list-container .fs-list-table-container{flex:1 1 auto;min-height:0;overflow-y:auto!important}:host ::ng-deep fs-list .fs-list-table thead th{position:sticky;top:0;z-index:1;background-color:#fff}:host ::ng-deep fs-list .fs-list-table tfoot td{position:sticky;bottom:0;z-index:1;background-color:#fff;font-weight:500;box-shadow:inset 0 1px #0000001f}:host ::ng-deep fs-list.report-totals-empty tfoot{display:none}:host ::ng-deep fs-list .report-pinned{position:sticky;left:var(--report-pin-left, 0px);z-index:1;background-color:#fff}:host ::ng-deep fs-list .report-pin-0{--report-pin-left: var(--report-pin-left-0, 0px)}:host ::ng-deep fs-list .report-pin-1{--report-pin-left: var(--report-pin-left-1, 0px)}:host ::ng-deep fs-list .report-pin-2{--report-pin-left: var(--report-pin-left-2, 0px)}:host ::ng-deep fs-list .report-pin-last{box-shadow:inset -1px 0 #0000001f}:host ::ng-deep fs-list .fs-list-table thead th.report-pinned{z-index:3}:host ::ng-deep fs-list .fs-list-table tfoot td.report-pinned{z-index:2}:host ::ng-deep fs-list .fs-list-table tfoot td.report-pin-last{box-shadow:inset -1px 0 #0000001f,inset 0 1px #0000001f}:host ::ng-deep fs-list th.report-nowrap,:host ::ng-deep fs-list td.report-nowrap{white-space:nowrap}:host ::ng-deep fs-list fs-list-pagination{flex:0 0 auto}:host ::ng-deep fs-list .fs-list-load-more{position:sticky;bottom:0;background-color:#fff}:host ::ng-deep fs-list .fs-list-load-more button{margin-top:0}.list-caption{flex:0 0 auto;padding-top:4px}.pivot-truncated{flex:0 0 auto;padding:8px 12px;margin-bottom:8px;border-radius:4px;background-color:#fff4e5;color:#663c00}\n"], dependencies: [{ kind: "component", type: ComponentNoteComponent, selector: "app-report-component-note", inputs: ["data", "windows", "windowDescriptions", "windowNotes"] }, { kind: "ngmodule", type: FsListModule }, { kind: "component", type: i1$4.FsListComponent, selector: "fs-list", inputs: ["config", "loaderLines", "cellRowType"], outputs: ["filtersReady"] }, { kind: "directive", type: i1$4.FsListColumnDirective, selector: "fs-list-column", inputs: ["show", "title", "name", "customizable", "sortable", "sortableDefault", "sortableDirection", "direction", "align", "width", "class"] }, { kind: "directive", type: i1$4.FsListCellDirective, selector: "[fs-list-cell],[fsListCell]", inputs: ["colspan", "align", "class", "fsListCell", "configTyping"] }, { kind: "directive", type: i1$4.FsListHeaderDirective, selector: "[fs-list-header]", inputs: ["colspan", "align", "class"] }, { kind: "directive", type: i1$4.FsListFooterDirective, selector: "[fs-list-footer]", inputs: ["colspan", "align", "class"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "pipe", type: ReportDatePipe, name: "fsReportDate" }, { kind: "pipe", type: ReportNumberPipe, name: "fsReportNumber" }, { kind: "pipe", type: ReportOutputPipe, name: "fsReportOutput" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
10704
10789
  }
10705
10790
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ComponentListComponent, decorators: [{
10706
10791
  type: Component,
@@ -10712,7 +10797,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
10712
10797
  ReportDatePipe,
10713
10798
  ReportNumberPipe,
10714
10799
  ReportOutputPipe,
10715
- ], template: "@if (truncated()) {\n <div class=\"pivot-truncated\">\n This grid was built from a partial result, so some columns or values may be missing. Narrow it with a filter.\n </div>\n}\n\n<!-- The windows a comparison compares, and why it reads as it does. A\n windowed column's own notes are in its header's tooltip. -->\n<app-report-component-note\n [data]=\"noteData()\"\n [windowNotes]=\"false\">\n</app-report-component-note>\n\n<!-- fs-list reads a column's footer template once, as a direct child of\n fs-list-column, so a list with totals renders its columns in a branch of\n their own rather than wrapping the footer in an @if. Its header template too,\n which every column carries: an output column's tooltip says what it holds.\n Columns are tracked by alignment as well as name, since fs-list reads a\n column's alignment only when it builds the column. -->\n<fs-list\n [config]=\"listConfig\"\n [class.report-totals-empty]=\"!totals()\">\n @if (showTotals()) {\n @for (column of columns(); track column.column + ':' + column.numeric) {\n <fs-list-column\n [name]=\"column.column\"\n [title]=\"column.label || column.column\"\n [show]=\"!hidden().has(column.column)\"\n [sortable]=\"!pivoted() || $first\"\n [align]=\"column.numeric ? 'right' : 'left'\"\n [class]=\"columnClasses(column, $index, pinned())\">\n <ng-template fs-list-header>\n <ng-container\n [ngTemplateOutlet]=\"headerTpl\"\n [ngTemplateOutletContext]=\"{ column: column }\">\n </ng-container>\n </ng-template>\n <ng-template\n fs-list-cell\n let-row=\"row\">\n <ng-container\n [ngTemplateOutlet]=\"cellTpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row[column.column], row: row, column: column }\">\n </ng-container>\n </ng-template>\n <ng-template fs-list-footer>\n @if (totals()?.[column.column] !== undefined) {\n <ng-container\n [ngTemplateOutlet]=\"cellTpl\"\n [ngTemplateOutletContext]=\"{ $implicit: totals()?.[column.column], row: totals(), column: column }\">\n </ng-container>\n } @else if (column.column === totalsLabelColumn()) {\n {{ totalLabel }}\n }\n </ng-template>\n </fs-list-column>\n }\n } @else {\n @for (column of columns(); track column.column + ':' + column.numeric) {\n <fs-list-column\n [name]=\"column.column\"\n [title]=\"column.label || column.column\"\n [show]=\"!hidden().has(column.column)\"\n [sortable]=\"!pivoted() || $first\"\n [align]=\"column.numeric ? 'right' : 'left'\"\n [class]=\"columnClasses(column, $index, pinned())\">\n <ng-template fs-list-header>\n <ng-container\n [ngTemplateOutlet]=\"headerTpl\"\n [ngTemplateOutletContext]=\"{ column: column }\">\n </ng-container>\n </ng-template>\n <ng-template\n fs-list-cell\n let-row=\"row\">\n <ng-container\n [ngTemplateOutlet]=\"cellTpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row[column.column], row: row, column: column }\">\n </ng-container>\n </ng-template>\n </fs-list-column>\n }\n }\n</fs-list>\n\n@if (caption()) {\n <div class=\"list-caption small\">\n {{ caption() }}\n </div>\n}\n\n<!-- A column's heading, with what an output column holds as its tooltip (an\n empty one never shows). -->\n<ng-template\n #headerTpl\n let-column=\"column\">\n <span [matTooltip]=\"columnNotes()[column.column] ?? ''\">{{ column.label || column.column }}</span>\n</ng-template>\n\n<!-- One cell, in the body and the totals row alike: an output column by what\n it holds (its row gives a change its prior), a date column as a day in the\n report's zone and locale, any other through its number format. -->\n<ng-template\n #cellTpl\n let-value\n let-row=\"row\"\n let-column=\"column\">\n @if (outputColumns()[column.column]; as output) {\n @if (row | fsReportOutput: column : output : formatContext(); as cell) {\n <span [attr.title]=\"cell.title\">{{ cell.text }}</span>\n }\n } @else if (column.format === ReportListColumnFormat.Date) {\n {{ value | fsReportDate: timezone() : formatContext().locale }}\n } @else {\n {{ value | fsReportNumber: column : formatContext() }}\n }\n</ng-template>\n", styles: [":host{display:flex;flex-direction:column;width:100%;height:100%;min-height:0}:host ::ng-deep fs-list{display:flex;flex-direction:column;flex:1 1 auto;height:auto;min-height:0}:host ::ng-deep fs-list .fs-list-container{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;height:100%}:host ::ng-deep fs-list .fs-list-header-container{flex:0 0 auto}:host ::ng-deep fs-list .fs-list-container .fs-list-table-container{flex:1 1 auto;min-height:0;overflow-y:auto!important}:host ::ng-deep fs-list .fs-list-table thead th{position:sticky;top:0;z-index:1;background-color:#fff}:host ::ng-deep fs-list .fs-list-table tfoot td{position:sticky;bottom:0;z-index:1;background-color:#fff;font-weight:500;box-shadow:inset 0 1px #0000001f}:host ::ng-deep fs-list.report-totals-empty tfoot{display:none}:host ::ng-deep fs-list .report-pinned{position:sticky;left:var(--report-pin-left, 0px);z-index:1;background-color:#fff}:host ::ng-deep fs-list .report-pin-0{--report-pin-left: var(--report-pin-left-0, 0px)}:host ::ng-deep fs-list .report-pin-1{--report-pin-left: var(--report-pin-left-1, 0px)}:host ::ng-deep fs-list .report-pin-2{--report-pin-left: var(--report-pin-left-2, 0px)}:host ::ng-deep fs-list .report-pin-last{box-shadow:inset -1px 0 #0000001f}:host ::ng-deep fs-list .fs-list-table thead th.report-pinned{z-index:3}:host ::ng-deep fs-list .fs-list-table tfoot td.report-pinned{z-index:2}:host ::ng-deep fs-list .fs-list-table tfoot td.report-pin-last{box-shadow:inset -1px 0 #0000001f,inset 0 1px #0000001f}:host ::ng-deep fs-list th.report-nowrap,:host ::ng-deep fs-list td.report-nowrap{white-space:nowrap}:host ::ng-deep fs-list fs-list-pagination{flex:0 0 auto}:host ::ng-deep fs-list .fs-list-load-more{position:sticky;bottom:0;background-color:#fff}:host ::ng-deep fs-list .fs-list-load-more button{margin-top:0}.list-caption{flex:0 0 auto;padding-top:4px}.pivot-truncated{flex:0 0 auto;padding:8px 12px;margin-bottom:8px;border-radius:4px;background-color:#fff4e5;color:#663c00}\n"] }]
10800
+ ], template: "@if (truncated()) {\n <div class=\"pivot-truncated\">\n This grid was built from a partial result, so some columns or values may be missing. Narrow it with a filter.\n </div>\n}\n\n<!-- The windows a comparison compares, and why it reads as it does. A\n windowed column's own notes are in its header's tooltip. -->\n<app-report-component-note\n [data]=\"noteData()\"\n [windowNotes]=\"false\">\n</app-report-component-note>\n\n<!-- fs-list reads a column's footer template once, as a direct child of\n fs-list-column, so a list with totals renders its columns in a branch of\n their own rather than wrapping the footer in an @if. Its header template too,\n which every column carries: an output column's tooltip says what it holds.\n Columns are tracked by alignment as well as name, since fs-list reads a\n column's alignment only when it builds the column. -->\n<fs-list\n [config]=\"listConfig\"\n [class.report-totals-empty]=\"!totals()\">\n @if (showTotals()) {\n @for (column of columns(); track column.column + ':' + column.numeric) {\n <fs-list-column\n [name]=\"column.column\"\n [title]=\"column.label || column.column\"\n [show]=\"!hidden().has(column.column)\"\n [sortable]=\"!pivoted() || $first\"\n [align]=\"column.numeric ? 'right' : 'left'\"\n [class]=\"columnClasses(column, $index, pinned())\">\n <ng-template fs-list-header>\n <ng-container\n [ngTemplateOutlet]=\"headerTpl\"\n [ngTemplateOutletContext]=\"{ column: column }\">\n </ng-container>\n </ng-template>\n <ng-template\n fs-list-cell\n let-row=\"row\">\n <ng-container\n [ngTemplateOutlet]=\"cellTpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row[column.column], row: row, column: column }\">\n </ng-container>\n </ng-template>\n <ng-template fs-list-footer>\n @if (totals()?.[column.column] !== undefined) {\n <ng-container\n [ngTemplateOutlet]=\"cellTpl\"\n [ngTemplateOutletContext]=\"{ $implicit: totals()?.[column.column], row: totals(), column: column }\">\n </ng-container>\n } @else if (column.column === totalsLabelColumn()) {\n {{ totalLabel }}\n }\n </ng-template>\n </fs-list-column>\n }\n } @else {\n @for (column of columns(); track column.column + ':' + column.numeric) {\n <fs-list-column\n [name]=\"column.column\"\n [title]=\"column.label || column.column\"\n [show]=\"!hidden().has(column.column)\"\n [sortable]=\"!pivoted() || $first\"\n [align]=\"column.numeric ? 'right' : 'left'\"\n [class]=\"columnClasses(column, $index, pinned())\">\n <ng-template fs-list-header>\n <ng-container\n [ngTemplateOutlet]=\"headerTpl\"\n [ngTemplateOutletContext]=\"{ column: column }\">\n </ng-container>\n </ng-template>\n <ng-template\n fs-list-cell\n let-row=\"row\">\n <ng-container\n [ngTemplateOutlet]=\"cellTpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row[column.column], row: row, column: column }\">\n </ng-container>\n </ng-template>\n </fs-list-column>\n }\n }\n</fs-list>\n\n@if (caption()) {\n <div class=\"list-caption small\">\n {{ caption() }}\n </div>\n}\n\n<!-- A column's heading, with what an output column holds as its tooltip (an\n empty one never shows). -->\n<ng-template\n #headerTpl\n let-column=\"column\">\n <span [matTooltip]=\"columnNotes()[column.column] ?? ''\">{{ column.label || column.column }}</span>\n</ng-template>\n\n<!-- One cell, in the body and the totals row alike: an output column by what\n it holds (its row gives a change its prior), a date column as a day in the\n report's zone and locale, a labeled column's value as its label, any other\n through its number format. -->\n<ng-template\n #cellTpl\n let-value\n let-row=\"row\"\n let-column=\"column\">\n @if (outputColumns()[column.column]; as output) {\n @if (row | fsReportOutput: column : output : formatContext(); as cell) {\n <span [attr.title]=\"cell.title\">{{ cell.text }}</span>\n }\n } @else if (column.format === ReportListColumnFormat.Date) {\n {{ value | fsReportDate: timezone() : formatContext().locale }}\n } @else {\n {{ valueLabel(valueLabels(), column.column, value) ?? (value | fsReportNumber: column : formatContext()) }}\n }\n</ng-template>\n", styles: [":host{display:flex;flex-direction:column;width:100%;height:100%;min-height:0}:host ::ng-deep fs-list{display:flex;flex-direction:column;flex:1 1 auto;height:auto;min-height:0}:host ::ng-deep fs-list .fs-list-container{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;height:100%}:host ::ng-deep fs-list .fs-list-header-container{flex:0 0 auto}:host ::ng-deep fs-list .fs-list-container .fs-list-table-container{flex:1 1 auto;min-height:0;overflow-y:auto!important}:host ::ng-deep fs-list .fs-list-table thead th{position:sticky;top:0;z-index:1;background-color:#fff}:host ::ng-deep fs-list .fs-list-table tfoot td{position:sticky;bottom:0;z-index:1;background-color:#fff;font-weight:500;box-shadow:inset 0 1px #0000001f}:host ::ng-deep fs-list.report-totals-empty tfoot{display:none}:host ::ng-deep fs-list .report-pinned{position:sticky;left:var(--report-pin-left, 0px);z-index:1;background-color:#fff}:host ::ng-deep fs-list .report-pin-0{--report-pin-left: var(--report-pin-left-0, 0px)}:host ::ng-deep fs-list .report-pin-1{--report-pin-left: var(--report-pin-left-1, 0px)}:host ::ng-deep fs-list .report-pin-2{--report-pin-left: var(--report-pin-left-2, 0px)}:host ::ng-deep fs-list .report-pin-last{box-shadow:inset -1px 0 #0000001f}:host ::ng-deep fs-list .fs-list-table thead th.report-pinned{z-index:3}:host ::ng-deep fs-list .fs-list-table tfoot td.report-pinned{z-index:2}:host ::ng-deep fs-list .fs-list-table tfoot td.report-pin-last{box-shadow:inset -1px 0 #0000001f,inset 0 1px #0000001f}:host ::ng-deep fs-list th.report-nowrap,:host ::ng-deep fs-list td.report-nowrap{white-space:nowrap}:host ::ng-deep fs-list fs-list-pagination{flex:0 0 auto}:host ::ng-deep fs-list .fs-list-load-more{position:sticky;bottom:0;background-color:#fff}:host ::ng-deep fs-list .fs-list-load-more button{margin-top:0}.list-caption{flex:0 0 auto;padding-top:4px}.pivot-truncated{flex:0 0 auto;padding:8px 12px;margin-bottom:8px;border-radius:4px;background-color:#fff4e5;color:#663c00}\n"] }]
10716
10801
  }], ctorParameters: () => [] });
10717
10802
 
10718
10803
  // How a value chip is marked in the editor. The names follow the embeds of
@@ -11771,7 +11856,7 @@ class ComponentTextComponent {
11771
11856
  }
11772
11857
  }
11773
11858
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ComponentTextComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
11774
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ComponentTextComponent, isStandalone: true, selector: "app-report-component-text", inputs: { html: { classPropertyName: "html", publicName: "html", isSignal: true, isRequired: false, transformFunction: null }, tokens: { classPropertyName: "tokens", publicName: "tokens", isSignal: true, isRequired: true, transformFunction: null }, editing: { classPropertyName: "editing", publicName: "editing", isSignal: true, isRequired: false, transformFunction: null }, editMode: { classPropertyName: "editMode", publicName: "editMode", isSignal: true, isRequired: false, transformFunction: null }, shownElsewhere: { classPropertyName: "shownElsewhere", publicName: "shownElsewhere", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { saved: "saved", editEnded: "editEnded", valueSettings: "valueSettings" }, host: { listeners: { "keydown": "onKeydown($event)" }, properties: { "style.--text-size": "typeScale.body", "style.--text-h1-size": "typeScale.h1", "style.--text-h2-size": "typeScale.h2", "style.--text-h3-size": "typeScale.h3", "style.--text-color": "typeScale.color", "style.--text-table-border": "tableLook.border", "style.--text-table-padding": "tableLook.padding", "style.--text-table-margin": "tableLook.margin" } }, viewQueries: [{ propertyName: "_editor", first: true, predicate: FsHtmlEditorComponent, descendants: true, isSignal: true }, { propertyName: "_valuePopoverAnchor", first: true, predicate: ["valuePopoverAnchor"], descendants: true, read: ElementRef, isSignal: true }], ngImport: i0, template: "@if (editing()) {\n <!-- Floating fs-html-editor: no frame, Froala's inline toolbar pops up over\n the text. pointerdown stays here so a press in the text never starts a\n drag of the block or a pan of the canvas. -->\n <fs-html-editor\n class=\"text-content editing\"\n [config]=\"editorConfig\"\n [(ngModel)]=\"draft\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\">\n </fs-html-editor>\n\n <!-- One notice at a time: an edit stopped because it would have removed\n values, an edit that left values no text reads, or a refused command.\n mousedown keeps the editor's focus and selection, so Undo reaches it. -->\n @if (notice(); as current) {\n <div\n class=\"value-notice fs-row.gap-sm\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.preventDefault()\">\n @switch (current.kind) {\n @case ('blocked') {\n @if (current.block.removeSelection) {\n <span>\n This selection includes {{ current.block.count }} {{ current.block.count === 1 ? 'value' : 'values' }}.\n </span>\n <button\n mat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"removeBlockedSelection()\">\n Remove text and {{ current.block.count === 1 ? 'value' : current.block.count + ' values' }}\n </button>\n } @else {\n <span>To take a value out, select it, then press Delete.</span>\n }\n }\n @case ('unused') {\n <span>{{ current.count }} {{ current.count === 1 ? 'value' : 'values' }} moved to Not used</span>\n <button\n mat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"undoValuesUnused()\">\n Undo\n </button>\n }\n @case ('message') {\n <span>{{ current.text }}</span>\n }\n }\n <button\n mat-button\n type=\"button\"\n (click)=\"dismissNotice()\">\n Dismiss\n </button>\n </div>\n }\n} @else if (editMode() && isBlank()) {\n <!-- An empty block while the canvas is laid out: a hint in the block's own\n type, never its html, so nothing is typed onto it or exported. -->\n <div class=\"text-content text-hint\">Double-click to edit</div>\n} @else {\n <div\n class=\"text-content\"\n [innerHTML]=\"displayHtml()\">\n </div>\n}\n\n<!-- A clicked value chip's popover: an fs-popover on an invisible anchor laid\n over the chip (the chip itself belongs to the editor). Outside the editing\n block, so a popover still open when editing ends closes the usual way. -->\n<span\n #valuePopoverAnchor\n class=\"value-popover-anchor\"\n aria-hidden=\"true\"\n fsPopover\n trigger=\"click\"\n wrapperClass=\"value-popover-wrapper\"\n [template]=\"valuePopoverTemplate\"\n [indication]=\"false\"\n [maxWidth]=\"320\"\n [style.left.px]=\"valuePopover()?.left\"\n [style.top.px]=\"valuePopover()?.top\"\n [style.width.px]=\"valuePopover()?.width\"\n [style.height.px]=\"valuePopover()?.height\">\n</span>\n<ng-template\n #valuePopoverTemplate\n let-popover=\"popover\">\n <!-- What the value is, and its settings (where it can also be deleted); a\n chip naming no value on the page has no settings. -->\n <div class=\"value-popover fs-row\">\n <div class=\"fs-flex\">\n <div class=\"value-popover-name\">{{ valuePopoverValue()?.name ?? valuePopover()?.key }}</div>\n @if (!valuePopoverValue()) {\n <div class=\"small\">Not a value on this page</div>\n }\n </div>\n @if (valuePopoverValue()) {\n <button\n mat-icon-button\n type=\"button\"\n class=\"value-popover-settings\"\n matTooltip=\"Settings\"\n (click)=\"openValueSettings(popover)\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{display:block;position:relative;width:100%;height:100%}.text-content{display:block;width:100%;height:100%;overflow:hidden;font-family:Arial,Helvetica,sans-serif;font-size:var(--text-size);line-height:1.25;color:var(--text-color);word-wrap:break-word;letter-spacing:normal;word-spacing:normal;font-weight:400;font-style:normal;text-transform:none;white-space:normal}.text-content ::ng-deep p,.text-content ::ng-deep h1,.text-content ::ng-deep h2,.text-content ::ng-deep h3,.text-content ::ng-deep ul,.text-content ::ng-deep ol{margin:0}.text-content ::ng-deep h1{font-size:var(--text-h1-size)}.text-content ::ng-deep h2{font-size:var(--text-h2-size)}.text-content ::ng-deep h3{font-size:var(--text-h3-size)}.text-content ::ng-deep h1,.text-content ::ng-deep h2,.text-content ::ng-deep h3{font-weight:700;line-height:1.2}.text-content ::ng-deep ul,.text-content ::ng-deep ol{padding-left:1.4em}.text-content ::ng-deep p:empty:before{content:\"\\200b\"}.text-content ::ng-deep a{color:inherit}.text-content ::ng-deep table{border-collapse:collapse;margin:var(--text-table-margin) 0}.text-content ::ng-deep th,.text-content ::ng-deep td{border:var(--text-table-border);padding:var(--text-table-padding);vertical-align:middle;text-align:left}.text-content ::ng-deep th{text-align:center;font-weight:700}.text-content.editing{overflow:auto;cursor:text}.text-content.editing ::ng-deep [data-embed-key],.text-content.editing ::ng-deep [data-embed-field]{padding:0 2px;border-radius:3px;background-color:#4e79a729;-webkit-user-select:all;user-select:all}.text-content.editing ::ng-deep [data-embed-key]{cursor:pointer}.text-content.editing ::ng-deep [data-embed-key]:hover{background-color:#4e79a747}.text-content.editing ::ng-deep [data-embed-missing]{background-color:#e1575924;outline:1px dashed rgba(225,87,89,.7)}.text-content.editing ::ng-deep th,.text-content.editing ::ng-deep tfoot td{background-color:transparent}.text-content.editing ::ng-deep .fr-table-selector{display:none}.text-content.editing ::ng-deep .fr-table-selection-hover{outline:none}.text-hint{color:#9aa5b1;-webkit-user-select:none;user-select:none}.value-popover-anchor{position:absolute;pointer-events:none}::ng-deep .value-popover-wrapper .popover-content{padding:15px!important}.value-popover{gap:15px}.value-popover-name{font-weight:600}.value-popover-settings{--mdc-icon-button-state-layer-size: 24px;width:24px;height:24px;padding:0}.value-notice{position:absolute;top:100%;left:0;z-index:2;margin-top:4px;padding:2px 4px 2px 10px;border:1px solid #d9dee3;border-radius:4px;background:#fff;white-space:nowrap;font-family:Arial,Helvetica,sans-serif}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: FsHtmlEditorComponent, selector: "fs-html-editor", inputs: ["config", "disabled"] }, { kind: "directive", type: FsPopoverDirective, selector: "[fsPopover]", inputs: ["text", "template", "data", "leaveDelay", "showDelay", "maxWidth", "maxHeight", "wrapperClass", "autoShow", "autoClose", "loadingDiameter", "loading", "indication", "position", "theme", "size", "trigger", "enabled", "panel"], outputs: ["closed"], exportAs: ["fsPopover"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11859
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ComponentTextComponent, isStandalone: true, selector: "app-report-component-text", inputs: { html: { classPropertyName: "html", publicName: "html", isSignal: true, isRequired: false, transformFunction: null }, tokens: { classPropertyName: "tokens", publicName: "tokens", isSignal: true, isRequired: true, transformFunction: null }, editing: { classPropertyName: "editing", publicName: "editing", isSignal: true, isRequired: false, transformFunction: null }, editMode: { classPropertyName: "editMode", publicName: "editMode", isSignal: true, isRequired: false, transformFunction: null }, shownElsewhere: { classPropertyName: "shownElsewhere", publicName: "shownElsewhere", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { saved: "saved", editEnded: "editEnded", valueSettings: "valueSettings" }, host: { listeners: { "keydown": "onKeydown($event)" }, properties: { "style.--text-size": "typeScale.body", "style.--text-h1-size": "typeScale.h1", "style.--text-h2-size": "typeScale.h2", "style.--text-h3-size": "typeScale.h3", "style.--text-color": "typeScale.color", "style.--text-table-border": "tableLook.border", "style.--text-table-padding": "tableLook.padding", "style.--text-table-margin": "tableLook.margin" } }, viewQueries: [{ propertyName: "_editor", first: true, predicate: FsHtmlEditorComponent, descendants: true, isSignal: true }, { propertyName: "_valuePopoverAnchor", first: true, predicate: ["valuePopoverAnchor"], descendants: true, read: ElementRef, isSignal: true }], ngImport: i0, template: "@if (editing()) {\n <!-- Floating fs-html-editor: no frame, Froala's inline toolbar pops up over\n the text. pointerdown stays here so a press in the text never starts a\n drag of the block or a pan of the canvas. -->\n <fs-html-editor\n class=\"text-content editing\"\n [config]=\"editorConfig\"\n [(ngModel)]=\"draft\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\">\n </fs-html-editor>\n\n <!-- One notice at a time: an edit stopped because it would have removed\n values, an edit that left values no text reads, or a refused command.\n mousedown keeps the editor's focus and selection, so Undo reaches it. -->\n @if (notice(); as current) {\n <div\n class=\"value-notice fs-row.gap-sm\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.preventDefault()\">\n @switch (current.kind) {\n @case ('blocked') {\n @if (current.block.removeSelection) {\n <span>\n This selection includes {{ current.block.count }} {{ current.block.count === 1 ? 'value' : 'values' }}.\n </span>\n <button\n mat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"removeBlockedSelection()\">\n Remove text and {{ current.block.count === 1 ? 'value' : current.block.count + ' values' }}\n </button>\n } @else {\n <span>To take a value out, select it, then press Delete.</span>\n }\n }\n @case ('unused') {\n <span>{{ current.count }} {{ current.count === 1 ? 'value' : 'values' }} moved to Not used</span>\n <button\n mat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"undoValuesUnused()\">\n Undo\n </button>\n }\n @case ('message') {\n <span>{{ current.text }}</span>\n }\n }\n <button\n mat-button\n type=\"button\"\n (click)=\"dismissNotice()\">\n Dismiss\n </button>\n </div>\n }\n} @else if (editMode() && isBlank()) {\n <!-- An empty block while the canvas is laid out: a hint in the block's own\n type, never its html, so nothing is typed onto it or exported. -->\n <div class=\"text-content text-hint\">Double-click to edit</div>\n} @else {\n <div\n class=\"text-content\"\n [innerHTML]=\"displayHtml()\">\n </div>\n}\n\n<!-- A clicked value chip's popover: an fs-popover on an invisible anchor laid\n over the chip (the chip itself belongs to the editor). Outside the editing\n block, so a popover still open when editing ends closes the usual way. -->\n<span\n #valuePopoverAnchor\n class=\"value-popover-anchor\"\n aria-hidden=\"true\"\n fsPopover\n trigger=\"click\"\n wrapperClass=\"value-popover-wrapper\"\n [template]=\"valuePopoverTemplate\"\n [indication]=\"false\"\n [maxWidth]=\"320\"\n [style.left.px]=\"valuePopover()?.left\"\n [style.top.px]=\"valuePopover()?.top\"\n [style.width.px]=\"valuePopover()?.width\"\n [style.height.px]=\"valuePopover()?.height\">\n</span>\n<ng-template\n #valuePopoverTemplate\n let-popover=\"popover\">\n <!-- What the value is, and its settings (where it can also be deleted); a\n chip naming no value on the page has no settings. -->\n <div class=\"value-popover fs-row\">\n <div class=\"fs-flex\">\n <div class=\"value-popover-name\">{{ valuePopoverValue()?.name ?? valuePopover()?.key }}</div>\n @if (!valuePopoverValue()) {\n <div class=\"small\">Not a value on this page</div>\n }\n </div>\n @if (valuePopoverValue()) {\n <button\n mat-icon-button\n type=\"button\"\n class=\"value-popover-settings\"\n matTooltip=\"Settings\"\n (click)=\"openValueSettings(popover)\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{display:block;position:relative;width:100%;height:100%}.text-content{display:block;width:100%;height:100%;overflow:visible;font-family:Arial,Helvetica,sans-serif;font-size:var(--text-size);line-height:1.25;color:var(--text-color);word-wrap:break-word;letter-spacing:normal;word-spacing:normal;font-weight:400;font-style:normal;text-transform:none;white-space:normal}.text-content ::ng-deep p,.text-content ::ng-deep h1,.text-content ::ng-deep h2,.text-content ::ng-deep h3,.text-content ::ng-deep ul,.text-content ::ng-deep ol{margin:0}.text-content ::ng-deep h1{font-size:var(--text-h1-size)}.text-content ::ng-deep h2{font-size:var(--text-h2-size)}.text-content ::ng-deep h3{font-size:var(--text-h3-size)}.text-content ::ng-deep h1,.text-content ::ng-deep h2,.text-content ::ng-deep h3{font-weight:700;line-height:1.2}.text-content ::ng-deep ul,.text-content ::ng-deep ol{padding-left:1.4em}.text-content ::ng-deep p:empty:before{content:\"\\200b\"}.text-content ::ng-deep a{color:inherit}.text-content ::ng-deep table{border-collapse:collapse;margin:var(--text-table-margin) 0}.text-content ::ng-deep th,.text-content ::ng-deep td{border:var(--text-table-border);padding:var(--text-table-padding);vertical-align:middle;text-align:left}.text-content ::ng-deep th{text-align:center;font-weight:700}.text-content.editing{cursor:text}.text-content.editing ::ng-deep [data-embed-key],.text-content.editing ::ng-deep [data-embed-field]{padding:0 2px;border-radius:3px;background-color:#4e79a729;-webkit-user-select:all;user-select:all}.text-content.editing ::ng-deep [data-embed-key]{cursor:pointer}.text-content.editing ::ng-deep [data-embed-key]:hover{background-color:#4e79a747}.text-content.editing ::ng-deep [data-embed-missing]{background-color:#e1575924;outline:1px dashed rgba(225,87,89,.7)}.text-content.editing ::ng-deep th,.text-content.editing ::ng-deep tfoot td{background-color:transparent}.text-content.editing ::ng-deep .fr-table-selector{display:none}.text-content.editing ::ng-deep .fr-table-selection-hover{outline:none}.text-hint{color:#9aa5b1;-webkit-user-select:none;user-select:none}.value-popover-anchor{position:absolute;pointer-events:none}::ng-deep .value-popover-wrapper .popover-content{padding:15px!important}.value-popover{gap:15px}.value-popover-name{font-weight:600}.value-popover-settings{--mdc-icon-button-state-layer-size: 24px;width:24px;height:24px;padding:0}.value-notice{position:absolute;top:100%;left:0;z-index:2;margin-top:4px;padding:2px 4px 2px 10px;border:1px solid #d9dee3;border-radius:4px;background:#fff;white-space:nowrap;font-family:Arial,Helvetica,sans-serif}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: FsHtmlEditorComponent, selector: "fs-html-editor", inputs: ["config", "disabled"] }, { kind: "directive", type: FsPopoverDirective, selector: "[fsPopover]", inputs: ["text", "template", "data", "leaveDelay", "showDelay", "maxWidth", "maxHeight", "wrapperClass", "autoShow", "autoClose", "loadingDiameter", "loading", "indication", "position", "theme", "size", "trigger", "enabled", "panel"], outputs: ["closed"], exportAs: ["fsPopover"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11775
11860
  }
11776
11861
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ComponentTextComponent, decorators: [{
11777
11862
  type: Component,
@@ -11796,7 +11881,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
11796
11881
  '[style.--text-table-border]': 'tableLook.border',
11797
11882
  '[style.--text-table-padding]': 'tableLook.padding',
11798
11883
  '[style.--text-table-margin]': 'tableLook.margin',
11799
- }, template: "@if (editing()) {\n <!-- Floating fs-html-editor: no frame, Froala's inline toolbar pops up over\n the text. pointerdown stays here so a press in the text never starts a\n drag of the block or a pan of the canvas. -->\n <fs-html-editor\n class=\"text-content editing\"\n [config]=\"editorConfig\"\n [(ngModel)]=\"draft\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\">\n </fs-html-editor>\n\n <!-- One notice at a time: an edit stopped because it would have removed\n values, an edit that left values no text reads, or a refused command.\n mousedown keeps the editor's focus and selection, so Undo reaches it. -->\n @if (notice(); as current) {\n <div\n class=\"value-notice fs-row.gap-sm\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.preventDefault()\">\n @switch (current.kind) {\n @case ('blocked') {\n @if (current.block.removeSelection) {\n <span>\n This selection includes {{ current.block.count }} {{ current.block.count === 1 ? 'value' : 'values' }}.\n </span>\n <button\n mat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"removeBlockedSelection()\">\n Remove text and {{ current.block.count === 1 ? 'value' : current.block.count + ' values' }}\n </button>\n } @else {\n <span>To take a value out, select it, then press Delete.</span>\n }\n }\n @case ('unused') {\n <span>{{ current.count }} {{ current.count === 1 ? 'value' : 'values' }} moved to Not used</span>\n <button\n mat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"undoValuesUnused()\">\n Undo\n </button>\n }\n @case ('message') {\n <span>{{ current.text }}</span>\n }\n }\n <button\n mat-button\n type=\"button\"\n (click)=\"dismissNotice()\">\n Dismiss\n </button>\n </div>\n }\n} @else if (editMode() && isBlank()) {\n <!-- An empty block while the canvas is laid out: a hint in the block's own\n type, never its html, so nothing is typed onto it or exported. -->\n <div class=\"text-content text-hint\">Double-click to edit</div>\n} @else {\n <div\n class=\"text-content\"\n [innerHTML]=\"displayHtml()\">\n </div>\n}\n\n<!-- A clicked value chip's popover: an fs-popover on an invisible anchor laid\n over the chip (the chip itself belongs to the editor). Outside the editing\n block, so a popover still open when editing ends closes the usual way. -->\n<span\n #valuePopoverAnchor\n class=\"value-popover-anchor\"\n aria-hidden=\"true\"\n fsPopover\n trigger=\"click\"\n wrapperClass=\"value-popover-wrapper\"\n [template]=\"valuePopoverTemplate\"\n [indication]=\"false\"\n [maxWidth]=\"320\"\n [style.left.px]=\"valuePopover()?.left\"\n [style.top.px]=\"valuePopover()?.top\"\n [style.width.px]=\"valuePopover()?.width\"\n [style.height.px]=\"valuePopover()?.height\">\n</span>\n<ng-template\n #valuePopoverTemplate\n let-popover=\"popover\">\n <!-- What the value is, and its settings (where it can also be deleted); a\n chip naming no value on the page has no settings. -->\n <div class=\"value-popover fs-row\">\n <div class=\"fs-flex\">\n <div class=\"value-popover-name\">{{ valuePopoverValue()?.name ?? valuePopover()?.key }}</div>\n @if (!valuePopoverValue()) {\n <div class=\"small\">Not a value on this page</div>\n }\n </div>\n @if (valuePopoverValue()) {\n <button\n mat-icon-button\n type=\"button\"\n class=\"value-popover-settings\"\n matTooltip=\"Settings\"\n (click)=\"openValueSettings(popover)\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{display:block;position:relative;width:100%;height:100%}.text-content{display:block;width:100%;height:100%;overflow:hidden;font-family:Arial,Helvetica,sans-serif;font-size:var(--text-size);line-height:1.25;color:var(--text-color);word-wrap:break-word;letter-spacing:normal;word-spacing:normal;font-weight:400;font-style:normal;text-transform:none;white-space:normal}.text-content ::ng-deep p,.text-content ::ng-deep h1,.text-content ::ng-deep h2,.text-content ::ng-deep h3,.text-content ::ng-deep ul,.text-content ::ng-deep ol{margin:0}.text-content ::ng-deep h1{font-size:var(--text-h1-size)}.text-content ::ng-deep h2{font-size:var(--text-h2-size)}.text-content ::ng-deep h3{font-size:var(--text-h3-size)}.text-content ::ng-deep h1,.text-content ::ng-deep h2,.text-content ::ng-deep h3{font-weight:700;line-height:1.2}.text-content ::ng-deep ul,.text-content ::ng-deep ol{padding-left:1.4em}.text-content ::ng-deep p:empty:before{content:\"\\200b\"}.text-content ::ng-deep a{color:inherit}.text-content ::ng-deep table{border-collapse:collapse;margin:var(--text-table-margin) 0}.text-content ::ng-deep th,.text-content ::ng-deep td{border:var(--text-table-border);padding:var(--text-table-padding);vertical-align:middle;text-align:left}.text-content ::ng-deep th{text-align:center;font-weight:700}.text-content.editing{overflow:auto;cursor:text}.text-content.editing ::ng-deep [data-embed-key],.text-content.editing ::ng-deep [data-embed-field]{padding:0 2px;border-radius:3px;background-color:#4e79a729;-webkit-user-select:all;user-select:all}.text-content.editing ::ng-deep [data-embed-key]{cursor:pointer}.text-content.editing ::ng-deep [data-embed-key]:hover{background-color:#4e79a747}.text-content.editing ::ng-deep [data-embed-missing]{background-color:#e1575924;outline:1px dashed rgba(225,87,89,.7)}.text-content.editing ::ng-deep th,.text-content.editing ::ng-deep tfoot td{background-color:transparent}.text-content.editing ::ng-deep .fr-table-selector{display:none}.text-content.editing ::ng-deep .fr-table-selection-hover{outline:none}.text-hint{color:#9aa5b1;-webkit-user-select:none;user-select:none}.value-popover-anchor{position:absolute;pointer-events:none}::ng-deep .value-popover-wrapper .popover-content{padding:15px!important}.value-popover{gap:15px}.value-popover-name{font-weight:600}.value-popover-settings{--mdc-icon-button-state-layer-size: 24px;width:24px;height:24px;padding:0}.value-notice{position:absolute;top:100%;left:0;z-index:2;margin-top:4px;padding:2px 4px 2px 10px;border:1px solid #d9dee3;border-radius:4px;background:#fff;white-space:nowrap;font-family:Arial,Helvetica,sans-serif}\n"] }]
11884
+ }, template: "@if (editing()) {\n <!-- Floating fs-html-editor: no frame, Froala's inline toolbar pops up over\n the text. pointerdown stays here so a press in the text never starts a\n drag of the block or a pan of the canvas. -->\n <fs-html-editor\n class=\"text-content editing\"\n [config]=\"editorConfig\"\n [(ngModel)]=\"draft\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\">\n </fs-html-editor>\n\n <!-- One notice at a time: an edit stopped because it would have removed\n values, an edit that left values no text reads, or a refused command.\n mousedown keeps the editor's focus and selection, so Undo reaches it. -->\n @if (notice(); as current) {\n <div\n class=\"value-notice fs-row.gap-sm\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.preventDefault()\">\n @switch (current.kind) {\n @case ('blocked') {\n @if (current.block.removeSelection) {\n <span>\n This selection includes {{ current.block.count }} {{ current.block.count === 1 ? 'value' : 'values' }}.\n </span>\n <button\n mat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"removeBlockedSelection()\">\n Remove text and {{ current.block.count === 1 ? 'value' : current.block.count + ' values' }}\n </button>\n } @else {\n <span>To take a value out, select it, then press Delete.</span>\n }\n }\n @case ('unused') {\n <span>{{ current.count }} {{ current.count === 1 ? 'value' : 'values' }} moved to Not used</span>\n <button\n mat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"undoValuesUnused()\">\n Undo\n </button>\n }\n @case ('message') {\n <span>{{ current.text }}</span>\n }\n }\n <button\n mat-button\n type=\"button\"\n (click)=\"dismissNotice()\">\n Dismiss\n </button>\n </div>\n }\n} @else if (editMode() && isBlank()) {\n <!-- An empty block while the canvas is laid out: a hint in the block's own\n type, never its html, so nothing is typed onto it or exported. -->\n <div class=\"text-content text-hint\">Double-click to edit</div>\n} @else {\n <div\n class=\"text-content\"\n [innerHTML]=\"displayHtml()\">\n </div>\n}\n\n<!-- A clicked value chip's popover: an fs-popover on an invisible anchor laid\n over the chip (the chip itself belongs to the editor). Outside the editing\n block, so a popover still open when editing ends closes the usual way. -->\n<span\n #valuePopoverAnchor\n class=\"value-popover-anchor\"\n aria-hidden=\"true\"\n fsPopover\n trigger=\"click\"\n wrapperClass=\"value-popover-wrapper\"\n [template]=\"valuePopoverTemplate\"\n [indication]=\"false\"\n [maxWidth]=\"320\"\n [style.left.px]=\"valuePopover()?.left\"\n [style.top.px]=\"valuePopover()?.top\"\n [style.width.px]=\"valuePopover()?.width\"\n [style.height.px]=\"valuePopover()?.height\">\n</span>\n<ng-template\n #valuePopoverTemplate\n let-popover=\"popover\">\n <!-- What the value is, and its settings (where it can also be deleted); a\n chip naming no value on the page has no settings. -->\n <div class=\"value-popover fs-row\">\n <div class=\"fs-flex\">\n <div class=\"value-popover-name\">{{ valuePopoverValue()?.name ?? valuePopover()?.key }}</div>\n @if (!valuePopoverValue()) {\n <div class=\"small\">Not a value on this page</div>\n }\n </div>\n @if (valuePopoverValue()) {\n <button\n mat-icon-button\n type=\"button\"\n class=\"value-popover-settings\"\n matTooltip=\"Settings\"\n (click)=\"openValueSettings(popover)\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{display:block;position:relative;width:100%;height:100%}.text-content{display:block;width:100%;height:100%;overflow:visible;font-family:Arial,Helvetica,sans-serif;font-size:var(--text-size);line-height:1.25;color:var(--text-color);word-wrap:break-word;letter-spacing:normal;word-spacing:normal;font-weight:400;font-style:normal;text-transform:none;white-space:normal}.text-content ::ng-deep p,.text-content ::ng-deep h1,.text-content ::ng-deep h2,.text-content ::ng-deep h3,.text-content ::ng-deep ul,.text-content ::ng-deep ol{margin:0}.text-content ::ng-deep h1{font-size:var(--text-h1-size)}.text-content ::ng-deep h2{font-size:var(--text-h2-size)}.text-content ::ng-deep h3{font-size:var(--text-h3-size)}.text-content ::ng-deep h1,.text-content ::ng-deep h2,.text-content ::ng-deep h3{font-weight:700;line-height:1.2}.text-content ::ng-deep ul,.text-content ::ng-deep ol{padding-left:1.4em}.text-content ::ng-deep p:empty:before{content:\"\\200b\"}.text-content ::ng-deep a{color:inherit}.text-content ::ng-deep table{border-collapse:collapse;margin:var(--text-table-margin) 0}.text-content ::ng-deep th,.text-content ::ng-deep td{border:var(--text-table-border);padding:var(--text-table-padding);vertical-align:middle;text-align:left}.text-content ::ng-deep th{text-align:center;font-weight:700}.text-content.editing{cursor:text}.text-content.editing ::ng-deep [data-embed-key],.text-content.editing ::ng-deep [data-embed-field]{padding:0 2px;border-radius:3px;background-color:#4e79a729;-webkit-user-select:all;user-select:all}.text-content.editing ::ng-deep [data-embed-key]{cursor:pointer}.text-content.editing ::ng-deep [data-embed-key]:hover{background-color:#4e79a747}.text-content.editing ::ng-deep [data-embed-missing]{background-color:#e1575924;outline:1px dashed rgba(225,87,89,.7)}.text-content.editing ::ng-deep th,.text-content.editing ::ng-deep tfoot td{background-color:transparent}.text-content.editing ::ng-deep .fr-table-selector{display:none}.text-content.editing ::ng-deep .fr-table-selection-hover{outline:none}.text-hint{color:#9aa5b1;-webkit-user-select:none;user-select:none}.value-popover-anchor{position:absolute;pointer-events:none}::ng-deep .value-popover-wrapper .popover-content{padding:15px!important}.value-popover{gap:15px}.value-popover-name{font-weight:600}.value-popover-settings{--mdc-icon-button-state-layer-size: 24px;width:24px;height:24px;padding:0}.value-notice{position:absolute;top:100%;left:0;z-index:2;margin-top:4px;padding:2px 4px 2px 10px;border:1px solid #d9dee3;border-radius:4px;background:#fff;white-space:nowrap;font-family:Arial,Helvetica,sans-serif}\n"] }]
11800
11885
  }], ctorParameters: () => [] });
11801
11886
 
11802
11887
  // Resize handles, named by compass direction.
@@ -15199,12 +15284,11 @@ const KPI_EXPORT_MIN_SCALE = 0.5;
15199
15284
  // failures.
15200
15285
  const EXPORT_CONCURRENCY = 3;
15201
15286
  // What the export gates call the header and footer: when a value they show
15202
- // came back without its number, and when one of their text blocks does not
15203
- // fit its box.
15287
+ // came back without its number, and when one of their blocks could not be
15288
+ // drawn.
15204
15289
  const HEADER_FOOTER_NAME = 'Header & footer';
15205
- // How much of a text block's words the export gates quote to name a block that
15206
- // does not fit its box.
15207
- const OVERFLOW_SNIPPET_LENGTH = 40;
15290
+ // How much of a text block's words the export gates quote to name the block.
15291
+ const TEXT_SNIPPET_LENGTH = 40;
15208
15292
  // What a component with nothing to draw prints in the exported file.
15209
15293
  //
15210
15294
  // The two cases must not read alike. A tile that returned no rows is a finding
@@ -15238,16 +15322,11 @@ function exportTitle(component, tokens, search, crumbs, filtered = false) {
15238
15322
  function isFailed(item) {
15239
15323
  return !item.data && item.component.type !== 'text';
15240
15324
  }
15241
- // A text block that does not fit its box in a built file, as the overflow gate
15242
- // names it: under its page (HEADER_FOOTER_NAME for a header or footer block)
15243
- // and by the start of its words, on one line. Shared by the PDF and the deck.
15244
- function textOverflow(component, page, tokens) {
15325
+ // A text block as the export gates name it: by the start of its words, on one
15326
+ // line.
15327
+ function textSnippet(component, tokens) {
15245
15328
  const text = textPlain(component.config?.html ?? '', tokens).replace(/\s+/g, ' ').trim();
15246
- return {
15247
- componentId: component.id,
15248
- page,
15249
- snippet: text.length > OVERFLOW_SNIPPET_LENGTH ? `${text.slice(0, OVERFLOW_SNIPPET_LENGTH).trimEnd()}…` : text,
15250
- };
15329
+ return text.length > TEXT_SNIPPET_LENGTH ? `${text.slice(0, TEXT_SNIPPET_LENGTH).trimEnd()}…` : text;
15251
15330
  }
15252
15331
  // The lines under the report's name on the PDF title page and the title slide:
15253
15332
  // the export date, the active filters, then what the numbers are measured in.
@@ -15264,15 +15343,17 @@ function coverLines(collected) {
15264
15343
  // An exported list cell, reading like the on-screen cell, through the same
15265
15344
  // functions as the screen: a date column renders in the report's zone and
15266
15345
  // locale, and a column with number-format keys (a cross-tab's cells through
15267
- // its cellFormat) in the report's locale and currency. Any other value goes
15268
- // out verbatim, and an empty one as ''. CSV never comes through here: its
15269
- // values stay raw.
15346
+ // its cellFormat) in the report's locale and currency. A value of a column
15347
+ // the data labels (`labels`, ComponentData.valueLabels: an enum's, a
15348
+ // cross-tab's row key's) reads as its label. Any other value goes out
15349
+ // verbatim, and an empty one as ''. CSV never comes through here: the server
15350
+ // writes it, labels included.
15270
15351
  //
15271
15352
  // An output column is written by what it holds, as the screen's cell is
15272
15353
  // (formatOutputValue): a signed change, a percent change, a change of a rate
15273
15354
  // in points, a rank, a share. Its row gives a change the prior beside it, so
15274
15355
  // a missing baseline ('—') reads apart from a zero one ('n/a').
15275
- function exportCell(value, column, timezone, context, row) {
15356
+ function exportCell(value, column, timezone, context, row, labels) {
15276
15357
  const output = column.output;
15277
15358
  if (output) {
15278
15359
  return formatOutputValue(value, output.output, column, context, {
@@ -15288,13 +15369,15 @@ function exportCell(value, column, timezone, context, row) {
15288
15369
  if (column.format === ReportListColumnFormat.Date) {
15289
15370
  return formatReportDate(value, timezone, context.locale);
15290
15371
  }
15291
- return hasNumberFormat(column) ? formatNumber(value, column, context, { empty: '' }) : String(value);
15372
+ return valueLabel(labels, column.column, value)
15373
+ ?? (hasNumberFormat(column) ? formatNumber(value, column, context, { empty: '' }) : String(value));
15292
15374
  }
15293
15375
  // The columns an exported list table draws. A cross-tab's arrive with its
15294
15376
  // rows (the row key first, then one per pivot value), since only the result
15295
15377
  // knows the pivot values, and are built by pivotColumnSpecs() as on screen:
15296
- // headed by its periods in the report's locale, each pivot value formatted by
15297
- // cellFormat. A plain list draws its configured columns but the ones the
15378
+ // headed by its pivot values as pivotHeadings() reads them (their labels, its
15379
+ // periods in the report's locale), each pivot value formatted by cellFormat.
15380
+ // A plain list draws its configured columns but the ones the
15298
15381
  // viewer hid (`hidden`, kept to listHiddenColumns, so never all of them),
15299
15382
  // else every column the query returned, as it came. Output columns (the
15300
15383
  // data's outputs, a cross-tab cell through cellMeasures) carry how they are
@@ -15856,7 +15939,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
15856
15939
  // in the report's format context, as on screen.
15857
15940
  async function renderChartImage(component, data, width, height, context, pixelRatio = 3) {
15858
15941
  // The same tree-shaken echarts build + 'report' theme the live canvas uses.
15859
- const echarts = (await import('./firestitch-report-echarts-CUVOSBvJ.mjs')).default;
15942
+ const echarts = (await import('./firestitch-report-echarts-D7WNr0Z4.mjs')).default;
15860
15943
  const host = document.createElement('div');
15861
15944
  host.style.width = `${width}px`;
15862
15945
  host.style.height = `${height}px`;
@@ -15881,17 +15964,12 @@ async function renderChartImage(component, data, width, height, context, pixelRa
15881
15964
  }
15882
15965
  }
15883
15966
 
15884
- // How far past the bottom of its box a block may reach and still fit: two
15885
- // pixels, so the rounding of the last line's edge never counts as overflow.
15886
- // The deck keeps a table's row on the same terms.
15887
- const OVERFLOW_SLACK = 2 / PX_PER_INCH;
15888
15967
  // Where the canvas puts each part of a text block, for the exports. A
15889
15968
  // browser wraps the words and sizes a table's columns by its own layout, which
15890
15969
  // neither jsPDF nor PowerPoint reproduces. So the block is rendered offscreen
15891
15970
  // with the canvas's own component, at the canvas's 96px per inch and unscaled,
15892
15971
  // and the exports place what they draw where it sat: a table at its top, in
15893
15972
  // its column widths and row heights, and the words between tables at theirs.
15894
- // It also tells whether the block runs past the bottom of its box.
15895
15973
  //
15896
15974
  // Provided beside the export services, so the block renders under the same
15897
15975
  // injector the canvas's blocks do.
@@ -15986,32 +16064,22 @@ function blockBox(component, text) {
15986
16064
  }
15987
16065
  // Split the rendered block at its tables, as textBlocks splits the same html,
15988
16066
  // so the nth segment is the nth text part and the nth table the nth table
15989
- // part. How far the block reaches is the bottom of its last part that draws
15990
- // anything.
16067
+ // part.
15991
16068
  function measureBlock(content) {
15992
16069
  const box = content.getBoundingClientRect();
15993
16070
  const parts = splitAtTables(content);
15994
16071
  const segments = [];
15995
16072
  const tables = [];
15996
- let usedHeight = 0;
15997
16073
  parts.forEach((part, index) => {
15998
16074
  if (Array.isArray(part)) {
15999
- const segment = measureSegment(drawnNodes(part, index === parts.length - 1), box);
16000
- segments.push(segment);
16001
- usedHeight = segment?.bottom ?? usedHeight;
16075
+ segments.push(measureSegment(drawnNodes(part, index === parts.length - 1), box));
16002
16076
  }
16003
16077
  else {
16004
- const table = measureTable(part, box);
16005
- tables.push(table);
16006
- usedHeight = table.top + table.height;
16078
+ tables.push(measureTable(part, box));
16007
16079
  }
16008
16080
  });
16009
- const contentHeight = inches(box.height);
16010
16081
  return {
16011
16082
  contentWidth: inches(box.width),
16012
- contentHeight,
16013
- usedHeight,
16014
- overflow: usedHeight > contentHeight + OVERFLOW_SLACK,
16015
16083
  segments,
16016
16084
  tables,
16017
16085
  };
@@ -16020,7 +16088,7 @@ function measureBlock(content) {
16020
16088
  // blocks draws nothing anywhere. A blank line at the start of a block is
16021
16089
  // drawn, on the canvas and by textBlocks alike, but textBlocks drops the blank
16022
16090
  // lines at the very end of a block, so they are not measured either: a
16023
- // trailing empty paragraph never makes a block overflow.
16091
+ // trailing empty paragraph never adds to the last segment's height.
16024
16092
  function drawnNodes(nodes, blockEnd) {
16025
16093
  const skipsLast = blockEnd ? isBlank : isVoid;
16026
16094
  let start = 0;
@@ -16185,9 +16253,6 @@ const TEXT_LINE_HEIGHT = 1.25;
16185
16253
  // How far a list item's text sits from the block's edge, points; the marker
16186
16254
  // hangs in that space.
16187
16255
  const LIST_INDENT = 16;
16188
- // What the export's overflow gate says the PDF does with a text block that does
16189
- // not fit its box.
16190
- const PDF_OVERFLOW_NOTE = 'The PDF cuts it off at the bottom of its box.';
16191
16256
  // How far a table's group label steps in per level, points.
16192
16257
  const GRID_INDENT$1 = 8;
16193
16258
  // Builds the PDF client-side with jspdf. The report's unit is inches and PDFs
@@ -16207,14 +16272,12 @@ const GRID_INDENT$1 = 8;
16207
16272
  // without end trying to place it.
16208
16273
  //
16209
16274
  // A component that fails to draw is marked in its box and the file goes on
16210
- // (_guarded); build() hands back the ones that did, as it does the text it
16211
- // cut.
16275
+ // (_guarded); build() hands back the ones that did.
16212
16276
  //
16213
16277
  // A text block's tables are drawn by autoTable too, from the line its words
16214
16278
  // reach, in the column widths the canvas gives them (ReportTextLayoutService).
16215
- // A text block is cut at the bottom of its box, as the canvas clips it, and
16216
- // build() hands the file back unsaved with the blocks it cut, so the export
16217
- // can ask about them before it saves.
16279
+ // A text block is never cut at the bottom of its box: what runs past it draws
16280
+ // on below, as on the canvas, down to the page's edge.
16218
16281
  class ReportPdfService {
16219
16282
  _textLayout = inject(ReportTextLayoutService);
16220
16283
  async build(collected) {
@@ -16237,8 +16300,6 @@ class ReportPdfService {
16237
16300
  }
16238
16301
  }
16239
16302
  return {
16240
- overflows: [...context.textOverflows.values()],
16241
- overflowNote: PDF_OVERFLOW_NOTE,
16242
16303
  undrawn: [...context.undrawn.values()],
16243
16304
  save: () => {
16244
16305
  context.doc.save(`${this._slug(collected.report.name)}.pdf`);
@@ -16266,7 +16327,6 @@ class ReportPdfService {
16266
16327
  masterValues: this._pdfValues(collected.masterValues),
16267
16328
  filterFields: collected.filterFields,
16268
16329
  masterFilters: layerFilterFields(collected.filterFields, report.filterGroups ?? [], null),
16269
- textOverflows: new Map(),
16270
16330
  undrawn: new Map(),
16271
16331
  };
16272
16332
  }
@@ -16284,8 +16344,8 @@ class ReportPdfService {
16284
16344
  if (item.component.type !== 'text') {
16285
16345
  await this._renderComponent(context, collectedPage.page, item, tokens, overflows);
16286
16346
  }
16287
- else if (await this._renderText(context, item.component, pageTokens)) {
16288
- this._recordOverflow(context, item.component, tokens.pageName, pageTokens);
16347
+ else {
16348
+ await this._renderText(context, item.component, pageTokens);
16289
16349
  }
16290
16350
  });
16291
16351
  }
@@ -16316,7 +16376,7 @@ class ReportPdfService {
16316
16376
  componentId: component.id,
16317
16377
  page,
16318
16378
  name: component.type === 'text'
16319
- ? textOverflow(component, page, tokens).snippet
16379
+ ? textSnippet(component, tokens)
16320
16380
  : exportTitle(component, tokens),
16321
16381
  });
16322
16382
  }
@@ -16343,20 +16403,10 @@ class ReportPdfService {
16343
16403
  const masterTokens = { ...tokens, values: context.masterValues, filters: context.masterFilters };
16344
16404
  for (const component of (context.report.master?.components ?? []).filter(isDrawn)) {
16345
16405
  await this._guarded(context, component, HEADER_FOOTER_NAME, masterTokens, true, async () => {
16346
- if (await this._renderText(context, component, masterTokens)) {
16347
- this._recordOverflow(context, component, HEADER_FOOTER_NAME, masterTokens);
16348
- }
16406
+ await this._renderText(context, component, masterTokens);
16349
16407
  });
16350
16408
  }
16351
16409
  }
16352
- // Lists a text block that was cut, under the page the export's overflow gate
16353
- // names it by. A block cut again on a later page (a header or footer) keeps
16354
- // the first entry.
16355
- _recordOverflow(context, component, page, tokens) {
16356
- if (!context.textOverflows.has(component.id)) {
16357
- context.textOverflows.set(component.id, textOverflow(component, page, tokens));
16358
- }
16359
- }
16360
16410
  _titlePage(doc, collected, pageW, pageH) {
16361
16411
  doc.setFont('helvetica', 'bold');
16362
16412
  doc.setFontSize(28);
@@ -16371,11 +16421,11 @@ class ReportPdfService {
16371
16421
  // words between its tables are drawn line by line (_drawParagraphs) and each
16372
16422
  // table by autoTable from the line the words above it reach
16373
16423
  // (_drawTextTable). Only a block with a table is measured, for the edges
16374
- // and column widths the canvas gives its tables. Whatever runs past
16375
- // the block's height is left off — the canvas clips the block there too.
16376
- // Returns whether anything was.
16424
+ // and column widths the canvas gives its tables. Words and table rows that
16425
+ // run past the block's height draw on below it, as on the canvas; only the
16426
+ // page's edge stops them.
16377
16427
  async _renderText(context, component, tokens) {
16378
- const box = this._textBox(component);
16428
+ const box = this._textBox(component, context.pageH);
16379
16429
  const blocks = textBlocks(component.config?.html ?? '', tokens);
16380
16430
  const layout = blocks.some((block) => block.kind === 'table')
16381
16431
  ? await this._textLayout.measure(component, tokens)
@@ -16391,23 +16441,22 @@ class ReportPdfService {
16391
16441
  ? this._drawParagraphs(context.doc, block.paragraphs, box, cursorY)
16392
16442
  : this._drawTextTable(context, block.table, layout?.tables[tableIndex++] ?? null, box, onMargin ? cursorY : cursorY + TEXT_TABLE_MARGIN);
16393
16443
  if (drawn.cut) {
16394
- return true;
16444
+ return;
16395
16445
  }
16396
16446
  cursorY = drawn.cursorY;
16397
16447
  onMargin = block.kind === 'table' || (onMargin && !block.paragraphs.length);
16398
16448
  }
16399
- return false;
16400
16449
  }
16401
16450
  // Where a text block's words go: inside its padding, each edge 1/8" unless
16402
- // set.
16403
- _textBox(component) {
16451
+ // set, down to the bottom of the page (pageH).
16452
+ _textBox(component, pageH) {
16404
16453
  const padding = component.config?.padding;
16405
16454
  const inset = (value) => (value ?? DEFAULT_COMPONENT_PADDING) * PT;
16406
16455
  return {
16407
16456
  left: component.x * PT + inset(padding?.left),
16408
16457
  top: component.y * PT + inset(padding?.top),
16409
16458
  width: component.w * PT - inset(padding?.left) - inset(padding?.right),
16410
- bottom: (component.y + component.h) * PT - inset(padding?.bottom),
16459
+ bottom: pageH,
16411
16460
  };
16412
16461
  }
16413
16462
  // Paragraphs drawn line by line from cursorY down: each one's runs are broken
@@ -16471,9 +16520,9 @@ class ReportPdfService {
16471
16520
  // evenly. A row is as tall as its cells' words wrapped as _drawParagraphs
16472
16521
  // wraps them, and the words are drawn into each cell once autoTable has
16473
16522
  // drawn its grid lines and fill (_drawTextCell), so they keep their runs'
16474
- // fonts and colors. Only the rows the box holds are drawn, and a table that
16475
- // loses any, or runs past its page anyway, is cut. The cursor it leaves is
16476
- // TEXT_TABLE_MARGIN under it.
16523
+ // fonts and colors. Only the rows that fit above the box's bottom, the
16524
+ // page's edge, are drawn, and a table that loses any, or runs past its page
16525
+ // anyway, is cut. The cursor it leaves is TEXT_TABLE_MARGIN under it.
16477
16526
  _drawTextTable(context, table, layout, box, top) {
16478
16527
  if (top >= box.bottom) {
16479
16528
  return { cursorY: top, cut: true };
@@ -16841,7 +16890,7 @@ class ReportPdfService {
16841
16890
  // totals row (null without one). The columns the viewer hid are left out.
16842
16891
  _tableCells(context, component, data, hiddenColumns) {
16843
16892
  const columns = listColumns(component, data, context.formatContext.locale, hiddenColumns);
16844
- const cells = data.rows.map((row) => columns.map((column) => this._pdfText(exportCell(row[column.column], column, context.timezone, context.formatContext, row))));
16893
+ const cells = data.rows.map((row) => columns.map((column) => this._pdfText(exportCell(row[column.column], column, context.timezone, context.formatContext, row, data.valueLabels))));
16845
16894
  const foot = listTotalsRow(data, columns, context.timezone, context.formatContext);
16846
16895
  return {
16847
16896
  cells,
@@ -17362,10 +17411,6 @@ const PPTX_CONSTANT_LINE_SIZE = 1;
17362
17411
  // What the upper edge of a reference band is called in the data sheet and the
17363
17412
  // legend, after the band's own label on its lower edge.
17364
17413
  const PPTX_BAND_UPPER_SUFFIX = ' (upper)';
17365
- // What the export's overflow gate says the deck does with a text block that
17366
- // does not fit its box.
17367
- const PPTX_OVERFLOW_NOTE = 'In the PowerPoint it runs past the bottom of its box, and table rows past it are '
17368
- + 'left out.';
17369
17414
  // The shortest text box a text block's words get between its tables, inches,
17370
17415
  // however close the canvas sets their top and bottom.
17371
17416
  const TEXT_SEGMENT_MIN_H = 0.05;
@@ -17416,15 +17461,10 @@ class ReportPptxService {
17416
17461
  pptx.layout = 'REPORT';
17417
17462
  pptx.title = report.name;
17418
17463
  this._titleSlide(pptx, collected, pageW, pageH);
17419
- // The text blocks that do not fit their box, by component id, so a header
17420
- // or footer block is listed once, not once per slide.
17421
- const overflows = new Map();
17422
17464
  for (const index of collected.pages.keys()) {
17423
- await this._renderPage(pptx, collected, index, overflows);
17465
+ await this._renderPage(pptx, collected, index);
17424
17466
  }
17425
17467
  return {
17426
- overflows: [...overflows.values()],
17427
- overflowNote: PPTX_OVERFLOW_NOTE,
17428
17468
  // A slide's shapes are laid out by PowerPoint, so a component cannot
17429
17469
  // fail to draw here as a PDF table can.
17430
17470
  undrawn: [],
@@ -17437,7 +17477,7 @@ class ReportPptxService {
17437
17477
  // components sit on top of them exactly as on the canvas. Each layer's text
17438
17478
  // fills its value tokens with that layer's values: the master's or the
17439
17479
  // page's.
17440
- async _renderPage(pptx, collected, index, overflows) {
17480
+ async _renderPage(pptx, collected, index) {
17441
17481
  const report = collected.report;
17442
17482
  const collectedPage = collected.pages[index];
17443
17483
  const slide = pptx.addSlide();
@@ -17445,37 +17485,27 @@ class ReportPptxService {
17445
17485
  const pageTokens = { ...tokens, values: collectedPage.values };
17446
17486
  const timezone = resolveReportTimezone(report);
17447
17487
  if (!collectedPage.page.masterHidden) {
17448
- await this._renderMaster(slide, collected, tokens, overflows);
17488
+ await this._renderMaster(slide, collected, tokens);
17449
17489
  }
17450
17490
  for (const item of collectedPage.components) {
17451
17491
  if (item.component.type !== 'text') {
17452
17492
  this._renderComponent(pptx, slide, item, tokens, timezone, collected.formatContext);
17453
17493
  }
17454
- else if (await this._renderText(slide, item.component, pageTokens)) {
17455
- this._recordOverflow(overflows, item.component, tokens.pageName, pageTokens);
17494
+ else {
17495
+ await this._renderText(slide, item.component, pageTokens);
17456
17496
  }
17457
17497
  }
17458
17498
  }
17459
17499
  // The header and footer, their value tokens filled with the master's
17460
17500
  // values. They read no page filter: its token prints the em dash there.
17461
- async _renderMaster(slide, collected, tokens, overflows) {
17501
+ async _renderMaster(slide, collected, tokens) {
17462
17502
  const masterTokens = {
17463
17503
  ...tokens,
17464
17504
  values: collected.masterValues,
17465
17505
  filters: layerFilterFields(collected.filterFields, collected.report.filterGroups ?? [], null),
17466
17506
  };
17467
17507
  for (const component of (collected.report.master?.components ?? []).filter(isDrawn)) {
17468
- if (await this._renderText(slide, component, masterTokens)) {
17469
- this._recordOverflow(overflows, component, HEADER_FOOTER_NAME, masterTokens);
17470
- }
17471
- }
17472
- }
17473
- // Lists a text block that does not fit its box, under the page the export's
17474
- // overflow gate names it by. A block listed on an earlier slide (a header or
17475
- // footer) keeps that entry.
17476
- _recordOverflow(overflows, component, page, tokens) {
17477
- if (!overflows.has(component.id)) {
17478
- overflows.set(component.id, textOverflow(component, page, tokens));
17508
+ await this._renderText(slide, component, masterTokens);
17479
17509
  }
17480
17510
  }
17481
17511
  _titleSlide(pptx, collected, pageW, pageH) {
@@ -17497,28 +17527,28 @@ class ReportPptxService {
17497
17527
  });
17498
17528
  }
17499
17529
  // A text block as native PowerPoint text, editable in the deck, each run
17500
- // carrying its own bold/italic/color/size. The block is measured as the
17501
- // canvas lays it out (ReportTextLayoutService). A block without a table is
17502
- // ONE text box (_addTextBox). A block with tables is a stack inside its
17503
- // padding (_renderTextStack). Returns whether anything runs past the bottom
17504
- // of the box or was left out.
17530
+ // carrying its own bold/italic/color/size. A block without a table is ONE
17531
+ // text box (_addTextBox). A block with tables is measured as the canvas lays
17532
+ // it out (ReportTextLayoutService) and drawn as a stack inside its padding
17533
+ // (_renderTextStack). Nothing is cut: what runs past the bottom of the box
17534
+ // draws on below it, as on the canvas.
17505
17535
  async _renderText(slide, component, tokens) {
17506
- const html = component.config?.html ?? '';
17507
- const blocks = textBlocks(html, tokens);
17508
- const layout = await this._textLayout.measure(component, tokens);
17536
+ const blocks = textBlocks(component.config?.html ?? '', tokens);
17509
17537
  const textCount = blocks.filter((block) => block.kind === 'text').length;
17510
17538
  const tableCount = blocks.length - textCount;
17539
+ const layout = tableCount ? await this._textLayout.measure(component, tokens) : null;
17511
17540
  // The measure finds a block's parts one for one with textBlocks, unless the
17512
17541
  // canvas rendered the html differently. A block whose parts it did not
17513
17542
  // find falls back to the one text box, its tables read as lines of text.
17514
- if (!tableCount || layout.tables.length !== tableCount || layout.segments.length !== textCount) {
17543
+ if (!layout || layout.tables.length !== tableCount || layout.segments.length !== textCount) {
17515
17544
  this._addTextBox(slide, component, blockParagraphs(blocks));
17516
- return layout.overflow;
17545
+ return;
17517
17546
  }
17518
- return this._renderTextStack(slide, component, blocks, layout) || layout.overflow;
17547
+ this._renderTextStack(slide, component, blocks, layout);
17519
17548
  }
17520
17549
  // The words as ONE text box over the whole block. Its inset is the block's
17521
- // padding, so the words land where they sat on screen.
17550
+ // padding, so the words land where they sat on screen, and with no autofit
17551
+ // PowerPoint runs what does not fit on past the box's bottom.
17522
17552
  _addTextBox(slide, component, paragraphs) {
17523
17553
  const runs = this._paragraphRuns(paragraphs);
17524
17554
  if (!runs.length) {
@@ -17538,33 +17568,28 @@ class ReportPptxService {
17538
17568
  // words before, between and after its tables, and a native table for each
17539
17569
  // table, each at the top the canvas gives it. PowerPoint spaces lines a
17540
17570
  // little differently from the canvas, so placing every part afresh keeps
17541
- // that drift from adding up down the block. Returns whether any part was
17542
- // left out.
17571
+ // that drift from adding up down the block.
17543
17572
  _renderTextStack(slide, component, blocks, layout) {
17544
17573
  const padding = component.config?.padding;
17545
17574
  const x = component.x + (padding?.left ?? DEFAULT_COMPONENT_PADDING);
17546
17575
  const y = component.y + (padding?.top ?? DEFAULT_COMPONENT_PADDING);
17547
17576
  let segmentIndex = 0;
17548
17577
  let tableIndex = 0;
17549
- let flagged = false;
17550
17578
  for (const block of blocks) {
17551
- const leftOut = block.kind === 'text'
17552
- ? this._addTextSegment(slide, block.paragraphs, layout.segments[segmentIndex++], layout, x, y)
17553
- : this._addTextTable(slide, block.table, layout.tables[tableIndex++], layout, x, y);
17554
- flagged = flagged || leftOut;
17579
+ if (block.kind === 'text') {
17580
+ this._addTextSegment(slide, block.paragraphs, layout.segments[segmentIndex++], layout, x, y);
17581
+ }
17582
+ else {
17583
+ this._addTextTable(slide, block.table, layout.tables[tableIndex++], x, y);
17584
+ }
17555
17585
  }
17556
- return flagged;
17557
17586
  }
17558
17587
  // The words between two tables (or before the first, or after the last) as
17559
17588
  // a text box from the top of their first line to the bottom of their last,
17560
- // as wide as the block's content. A stretch that draws nothing is skipped,
17561
- // and one that starts below the box is left out. Returns whether it was.
17589
+ // as wide as the block's content. A stretch that draws nothing is skipped.
17562
17590
  _addTextSegment(slide, paragraphs, segment, layout, x, y) {
17563
17591
  if (!segment || !paragraphs.some((paragraph) => paragraph.runs.length)) {
17564
- return false;
17565
- }
17566
- if (segment.top >= layout.contentHeight) {
17567
- return true;
17592
+ return;
17568
17593
  }
17569
17594
  slide.addText(this._paragraphRuns(paragraphs), {
17570
17595
  x, y: y + segment.top, w: layout.contentWidth, h: Math.max(TEXT_SEGMENT_MIN_H, segment.bottom - segment.top),
@@ -17573,54 +17598,37 @@ class ReportPptxService {
17573
17598
  fontFace: 'Arial',
17574
17599
  fit: 'none',
17575
17600
  });
17576
- return false;
17577
17601
  }
17578
17602
  // A table as a native, editable PowerPoint table where the canvas puts it,
17579
17603
  // in its column widths and row heights, in the one look every renderer
17580
- // draws (TEXT_TABLE_* in text.ts). Only the rows that end inside the box are
17581
- // kept. Returns whether any row was left out.
17582
- _addTextTable(slide, table, tableLayout, layout, x, y) {
17583
- const kept = this._keptRows(tableLayout, layout.contentHeight);
17584
- if (kept) {
17585
- slide.addTable(this._tableRows(table, kept), {
17586
- x: x + tableLayout.left,
17587
- y: y + tableLayout.top,
17588
- w: tableLayout.width,
17589
- colW: tableLayout.columnWidths,
17590
- rowH: tableLayout.rowHeights.slice(0, kept),
17591
- autoPage: false,
17592
- fontFace: 'Arial',
17593
- fontSize: DEFAULT_TEXT_SIZE,
17594
- color: DEFAULT_TEXT_COLOR.replace('#', ''),
17595
- border: { type: 'solid', pt: TEXT_TABLE_BORDER_WIDTH, color: TEXT_TABLE_BORDER_COLOR.replace('#', '') },
17596
- margin: TEXT_TABLE_CELL_MARGIN,
17597
- });
17604
+ // draws (TEXT_TABLE_* in text.ts). Every row is kept.
17605
+ _addTextTable(slide, table, tableLayout, x, y) {
17606
+ if (!table.rows.length) {
17607
+ return;
17598
17608
  }
17599
- return kept < table.rows.length;
17609
+ slide.addTable(this._tableRows(table), {
17610
+ x: x + tableLayout.left,
17611
+ y: y + tableLayout.top,
17612
+ w: tableLayout.width,
17613
+ colW: tableLayout.columnWidths,
17614
+ rowH: tableLayout.rowHeights,
17615
+ autoPage: false,
17616
+ fontFace: 'Arial',
17617
+ fontSize: DEFAULT_TEXT_SIZE,
17618
+ color: DEFAULT_TEXT_COLOR.replace('#', ''),
17619
+ border: { type: 'solid', pt: TEXT_TABLE_BORDER_WIDTH, color: TEXT_TABLE_BORDER_COLOR.replace('#', '') },
17620
+ margin: TEXT_TABLE_CELL_MARGIN,
17621
+ });
17600
17622
  }
17601
- // How many of a table's rows, from the top, end inside the box.
17602
- _keptRows(tableLayout, contentHeight) {
17603
- let bottom = tableLayout.top;
17604
- let kept = 0;
17605
- for (const rowHeight of tableLayout.rowHeights) {
17606
- bottom += rowHeight;
17607
- if (bottom > contentHeight + OVERFLOW_SLACK) {
17608
- break;
17609
- }
17610
- kept++;
17611
- }
17612
- return kept;
17613
- }
17614
- // The kept rows as pptxgenjs cells, each row in grid order. A PowerPoint
17615
- // table is a full grid, and pptxgenjs counts its columns on the first row
17616
- // and slips a merged cell's continuation into the row below at that cell's
17617
- // place in its own row. So a cell is written in its top left slot only, a
17618
- // slot no cell holds gets an empty cell, and a cell's spans end where they
17619
- // would reach past the kept rows or into a slot another cell holds
17620
- // (claimSlots).
17621
- _tableRows(table, kept) {
17622
- const slots = Array.from({ length: kept }, () => Array(table.columns).fill(null));
17623
- table.rows.slice(0, kept).forEach((row) => row.cells.forEach((cell) => claimSlots(slots, cell)));
17623
+ // The rows as pptxgenjs cells, each row in grid order. A PowerPoint table is
17624
+ // a full grid, and pptxgenjs counts its columns on the first row and slips a
17625
+ // merged cell's continuation into the row below at that cell's place in its
17626
+ // own row. So a cell is written in its top left slot only, a slot no cell
17627
+ // holds gets an empty cell, and a cell's spans end where they would reach
17628
+ // past the last row or into a slot another cell holds (claimSlots).
17629
+ _tableRows(table) {
17630
+ const slots = Array.from({ length: table.rows.length }, () => Array(table.columns).fill(null));
17631
+ table.rows.forEach((row) => row.cells.forEach((cell) => claimSlots(slots, cell)));
17624
17632
  return slots.map((row, rowIndex) => row.flatMap((cell, column) => {
17625
17633
  if (!cell) {
17626
17634
  return [{ text: '' }];
@@ -17772,7 +17780,9 @@ class ReportPptxService {
17772
17780
  };
17773
17781
  const pie = chartType === 'pie' || chartType === 'donut';
17774
17782
  const colorBy = isColorBy(config);
17775
- const labels = this._categoryLabels(extracted.categories, data.granularity, context.locale, chartDateLabel(config));
17783
+ // Compact period labels, as the screen's pie names its slices: PowerPoint
17784
+ // can't do the axis's styled second line, so the year is inline.
17785
+ const labels = categoryLineLabels(extracted.categories, config, data, context.locale);
17776
17786
  // A 100% stacked chart's primary bars and areas plot the shares the screen
17777
17787
  // draws, stacked: PowerPoint's own percentStacked grouping is dropped on an
17778
17788
  // area chart, so one rule covers both. The data sheet holds the shares;
@@ -18179,7 +18189,7 @@ class ReportPptxService {
18179
18189
  const config = component.config;
18180
18190
  const waterfall = extractWaterfall(component, data);
18181
18191
  const measure = config.measures?.[0];
18182
- const labels = this._categoryLabels(waterfall.steps.map((step) => step.category), data.granularity, context.locale, chartDateLabel(config));
18192
+ const labels = categoryLineLabels(waterfall.steps.map((step) => step.category), config, data, context.locale);
18183
18193
  const series = this._waterfallSeries(waterfall.steps);
18184
18194
  // The references are constant lines beside the stacked bars, on the one
18185
18195
  // value axis. Without any the chart is the one stacked bar chart, without
@@ -18246,17 +18256,6 @@ class ReportPptxService {
18246
18256
  }
18247
18257
  return [base, ...[...above, ...below].filter((entry) => entry.values.some((value) => value !== null))];
18248
18258
  }
18249
- // Compact period labels match the on-screen axis, in the report's locale and
18250
- // the axis's date label style (a short label takes its year only when the
18251
- // data spans years); PowerPoint can't do a styled second line, so it's
18252
- // inline.
18253
- _categoryLabels(categories, granularity, locale, dateLabel) {
18254
- if (!granularity) {
18255
- return categories;
18256
- }
18257
- const multiYear = spansMultipleYears(categories);
18258
- return categories.map((category) => formatPeriodLabel(category, granularity, multiYear, locale, dateLabel));
18259
- }
18260
18259
  // The axis label and title settings of a chart with one value axis, and that
18261
18260
  // axis's range (valueAxis). A pie has no value axis to title. Without a
18262
18261
  // valueAxis.label the value axis is titled by the first measure's label, as
@@ -18312,13 +18311,14 @@ class ReportPptxService {
18312
18311
  options: { bold: true, color: 'FFFFFF', fill: { color: '4E79A7' }, ...align(index) },
18313
18312
  }));
18314
18313
  // A date or formatted-number cell is a value written in the report's
18315
- // locale; every other cell prints the query's text as it came.
18314
+ // locale; every other cell prints the query's text as it came, or its
18315
+ // label (exportCell).
18316
18316
  const valueLocale = (index) => {
18317
18317
  const column = columns[index];
18318
18318
  return column.numeric || column.format === ReportListColumnFormat.Date ? context.locale : undefined;
18319
18319
  };
18320
18320
  const body = rows.map((row) => columns.map((column, index) => ({
18321
- text: exportCell(row[column.column], column, timezone, context, row),
18321
+ text: exportCell(row[column.column], column, timezone, context, row, data.valueLabels),
18322
18322
  options: pptxRunOptions(align(index), valueLocale(index)),
18323
18323
  })));
18324
18324
  if (remaining > 0) {
@@ -18581,9 +18581,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
18581
18581
  // Gives a text table's cell the grid slots it covers from its top left: as
18582
18582
  // many columns as are free in its top row, and as many rows down as are free
18583
18583
  // across those columns, at most its own spans. A slot is free until a cell
18584
- // placed before it holds it, and the grid holds only the kept rows. So a span
18585
- // the box cuts ends at the last kept row, and one the html makes overlap
18586
- // another cell (which a browser draws on top of it) ends where they meet.
18584
+ // placed before it holds it. So a span the html takes past the last row ends
18585
+ // there, and one it makes overlap another cell (which a browser draws on top
18586
+ // of it) ends where they meet.
18587
18587
  function claimSlots(slots, cell) {
18588
18588
  const free = (row, column) => row < slots.length && column < slots[row].length && !slots[row][column];
18589
18589
  const columns = (count) => Array.from({ length: count }, (_, offset) => cell.column + offset);
@@ -18609,8 +18609,7 @@ const RETRY = 'retry';
18609
18609
  const DOWNLOAD = 'download';
18610
18610
  // Runs a report export end to end: collect every tile and value, refuse to
18611
18611
  // write a file that is missing any of them without saying so, build it, refuse
18612
- // to save one that could not draw a component, or whose text does not fit its
18613
- // boxes, without saying so, then save.
18612
+ // to save one that could not draw a component without saying so, then save.
18614
18613
  //
18615
18614
  // The gates are the reason this sits between the collector and the renderers
18616
18615
  // rather than in either of them. A deck with five blank pages is worse than no
@@ -18621,9 +18620,9 @@ const DOWNLOAD = 'download';
18621
18620
  // this is the only place that sees it.
18622
18621
  //
18623
18622
  // Failures are decided before the build, which is the expensive part (every
18624
- // chart drawn offscreen, every text block measured), so neither a Retry nor a
18625
- // Cancel pays for one. Text that does not fit its box is known only after it:
18626
- // the build is what measures it.
18623
+ // chart drawn offscreen, every text table measured), so neither a Retry nor a
18624
+ // Cancel pays for one. A component that could not be drawn is known only after
18625
+ // it: the build is what draws it.
18627
18626
  class ReportExportService {
18628
18627
  _collector = inject(ReportExportCollectorService);
18629
18628
  _pdfService = inject(ReportPdfService);
@@ -18649,12 +18648,11 @@ class ReportExportService {
18649
18648
  ? this._run(report, listRows, build)
18650
18649
  : this._buildAndSave(collected, build)))));
18651
18650
  }
18652
- // Builds the file, then saves it once the undrawn and overflow gates let it
18653
- // through.
18651
+ // Builds the file, then saves it once the undrawn gate lets it through.
18654
18652
  _buildAndSave(collected, build) {
18655
18653
  return from(build(collected))
18656
18654
  .pipe(switchMap((built) => this._undrawnGate(built)
18657
- .pipe(switchMap(() => this._overflowGate(built)), switchMap(() => from(Promise.resolve(built.save()))))));
18655
+ .pipe(switchMap(() => from(Promise.resolve(built.save()))))));
18658
18656
  }
18659
18657
  // Resolves to DOWNLOAD when the export is whole (or the user accepted an
18660
18658
  // incomplete one) and to RETRY when they want another attempt. Cancelling
@@ -18704,31 +18702,6 @@ class ReportExportService {
18704
18702
  })
18705
18703
  .pipe(catchError(() => EMPTY));
18706
18704
  }
18707
- // Resolves to DOWNLOAD when every text block fits its box in the built file,
18708
- // or the user accepted one that does not. There is no Retry: the words are
18709
- // the report's own, so a second attempt would build the same file. Each block
18710
- // is named by its page and the start of its words, and the note says what
18711
- // this format does with the rest. Cancelling ends the export quietly, as at
18712
- // the failure gate, and the built file is dropped unsaved.
18713
- _overflowGate(built) {
18714
- if (!built.overflows.length) {
18715
- return of(DOWNLOAD);
18716
- }
18717
- // A block with no words to quote (an empty table, say) is named by its
18718
- // page alone, not by an empty pair of quotes.
18719
- const blocks = built.overflows
18720
- .map((overflow) => `<strong>${escapeHtml(overflow.page)}</strong>`
18721
- + (overflow.snippet ? ` (“${escapeHtml(overflow.snippet)}”)` : ''));
18722
- return this._prompt.confirm({
18723
- title: 'Text does not fit',
18724
- template: `Some text does not fit its box: ${blocks.join(', ')}. ${built.overflowNote}`,
18725
- buttons: [
18726
- { label: 'Download Anyway', value: DOWNLOAD, color: 'primary' },
18727
- { label: 'Cancel', cancel: true, color: '' },
18728
- ],
18729
- })
18730
- .pipe(catchError(() => EMPTY));
18731
- }
18732
18705
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportExportService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
18733
18706
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportExportService });
18734
18707
  }
@@ -19549,7 +19522,7 @@ class FsAiReportComponent {
19549
19522
  ReportAiChatData,
19550
19523
  { provide: FsAiChatData, useExisting: ReportAiChatData },
19551
19524
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
19552
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CUVOSBvJ.mjs').then((module) => module.default) }),
19525
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-D7WNr0Z4.mjs').then((module) => module.default) }),
19553
19526
  // The bar's date pickers start their weeks on Monday and list the built-in
19554
19527
  // presets; a preset only sets the days.
19555
19528
  provideReportDatePicker(),
@@ -19573,7 +19546,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
19573
19546
  ReportAiChatData,
19574
19547
  { provide: FsAiChatData, useExisting: ReportAiChatData },
19575
19548
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
19576
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CUVOSBvJ.mjs').then((module) => module.default) }),
19549
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-D7WNr0Z4.mjs').then((module) => module.default) }),
19577
19550
  // The bar's date pickers start their weeks on Monday and list the built-in
19578
19551
  // presets; a preset only sets the days.
19579
19552
  provideReportDatePicker(),
@@ -19666,4 +19639,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
19666
19639
  */
19667
19640
 
19668
19641
  export { FsAiReportsComponent as F, GRANULARITY_OPTIONS as G, REPORT_CHART_COLORS_CSS as R, REPORT_CHART_FONT_FAMILY as a, REPORT_AXIS_LABEL_FONT_SIZE as b, FsAiReportComponent as c, ReportData as d, ReportService as e, ReportFilterStateService as f, ReportNodeType as g, ReportState as h, ReportValueFormat as i, ReportValueFormula as j, ReportListColumnFormat as k, ReportNumberUnits as l, ReportChartDataLabels as m, ReportChartColor as n, ReportChartLineStyle as o, ReportChartMarkStat as p, ReportChartDateLabel as q, ReportChartSpanThrough as r, ReportChangePolarity as s };
19669
- //# sourceMappingURL=firestitch-report-firestitch-report-CfvBTMrG.mjs.map
19642
+ //# sourceMappingURL=firestitch-report-firestitch-report-DWP3aywv.mjs.map