eru-grid 0.0.52 → 0.0.54

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.
@@ -106,8 +106,9 @@ const INHERITED_FIELD_KEYS = [
106
106
  'is_hyp', 'hypl_nm',
107
107
  // Scales
108
108
  'start_value', 'end_value', 'emoji_value', 'is_perc',
109
- // Attachment constraints
109
+ // Attachment constraints and destination
110
110
  'max_files', 'allowed_file_types', 'max_file_size',
111
+ 'storage_name', 'folder_name', 'default_upload',
111
112
  // Phone
112
113
  'default_country',
113
114
  // Cardinality and picker behaviour
@@ -129,18 +130,54 @@ const INHERITED_FIELD_KEYS = [
129
130
  * multi-currency dataset, and how wide a date may be spelled — a dense report
130
131
  * wants 'dd-MM-yy' where a detail grid over the same field wants the model's.
131
132
  *
133
+ * `label` is here for the same reason: the model's name is the right default
134
+ * and the right thing to fall back to, but a narrow column, or one whose
135
+ * meaning is already obvious from its neighbours, often wants a shorter heading
136
+ * in this grid without renaming the field for every grid that maps it.
137
+ *
132
138
  * Kept INSIDE the inherited list rather than removed from it, because that list
133
139
  * is also what eru-studio's properties panel reads to show a bound field's real
134
140
  * value; dropping a key there would make the panel and the page disagree.
135
141
  */
136
142
  /**
137
- * Datatypes that describe how a column is DRAWN rather than what it holds.
138
- *
139
- * A composite shows other columns; a page renders a layout. The data model has
140
- * no such field type, so a mapped column keeps these instead of inheriting the
141
- * model's datatype over them (see buildMappedColumnPatch).
143
+ * The datatypes that used to encode a renderer, kept only to recognise columns
144
+ * saved that way. Nothing is offered these any more — see `cell_render`, which
145
+ * replaced them, and `normalizeCellRender`, which migrates them.
142
146
  */
143
147
  const PRESENTATION_DATATYPES = new Set(['composite', 'page']);
148
+ /**
149
+ * A column's renderer, migrating the legacy encoding where it was a datatype.
150
+ *
151
+ * A column saved that way has already lost the datatype it held — there is
152
+ * nothing to recover here. A mapped one picks its real type back up from the
153
+ * data model on the next resolve; an unmapped one falls back to text, which is
154
+ * what its underlying value was being rendered as anyway.
155
+ */
156
+ function normalizeCellRender(field) {
157
+ const explicit = field?.cell_render;
158
+ if (explicit === 'composite' || explicit === 'page' || explicit === 'default')
159
+ return explicit;
160
+ const legacy = String(field?.datatype ?? '');
161
+ return legacy === 'composite' || legacy === 'page' ? legacy : 'default';
162
+ }
163
+ /**
164
+ * The datatype that governs how a column's VALUE is formatted.
165
+ *
166
+ * Normally that is `datatype`, but a presentation type says how the column is
167
+ * drawn and not what it holds, so for those the real type comes from
168
+ * `presentation_base_datatype`. A composite's primary part is the composite
169
+ * column itself — without this it formatted as an untyped value and a date came
170
+ * out as its raw stored string.
171
+ */
172
+ function effectiveValueDatatype(field) {
173
+ if (!field?.datatype)
174
+ return undefined;
175
+ const datatype = normalizeDatatype(field.datatype);
176
+ if (PRESENTATION_DATATYPES.has(datatype) && field.presentation_base_datatype) {
177
+ return normalizeDatatype(field.presentation_base_datatype);
178
+ }
179
+ return datatype;
180
+ }
144
181
  /**
145
182
  * Datatypes that colour their own value from the data — a status pill, a
146
183
  * priority chip, a progress band.
@@ -157,7 +194,8 @@ const SELF_COLOURED_DATATYPES = new Set(['status', 'priority', 'tag', 'progress'
157
194
  */
158
195
  const ACTION_COLUMN_MIN_WIDTH = 56;
159
196
  const SEEDED_FIELD_KEYS = [
160
- 'dynamic_number', 'display_number_as', 'symbol_field', 'date_format',
197
+ 'dynamic_number', 'display_number_as', 'symbol_field', 'date_format', 'label',
198
+ 'storage_name', 'folder_name', 'default_upload',
161
199
  ];
162
200
  /**
163
201
  * Datatype aliases the data model emits that mean an existing DataTypes value.
@@ -736,8 +774,6 @@ const DATA_TYPES = [
736
774
  'tag',
737
775
  'rating',
738
776
  'website',
739
- 'composite',
740
- 'page',
741
777
  ];
742
778
  /**
743
779
  * Colour tokens a grid column may pick, mirroring eru-studio's picker but over
@@ -974,12 +1010,108 @@ function formatNumberValue(value, cfg, options) {
974
1010
  maximumFractionDigits: decimalPlaces,
975
1011
  })}`;
976
1012
  }
1013
+ /**
1014
+ * The canonical stored form of a date: `yyyy-MM-dd`, in LOCAL time.
1015
+ *
1016
+ * Built from the local getters rather than `toISOString()`, which converts to
1017
+ * UTC and would move a date picked late in the day to the next (or previous)
1018
+ * one. This is the shape a value is stored in, whatever pattern the column
1019
+ * chooses to display it under, and it matches eru-studio's date component.
1020
+ */
1021
+ function toDateOnlyString(date) {
1022
+ if (!date || isNaN(date.getTime()))
1023
+ return '';
1024
+ const pad = (n) => (n < 10 ? `0${n}` : String(n));
1025
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
1026
+ }
1027
+ /**
1028
+ * The canonical stored form of a datetime: `yyyy-MM-dd HH:mm:ss`, in LOCAL
1029
+ * time — the date-only form plus a 24-hour clock, with seconds always present
1030
+ * so the stored shape does not depend on the display pattern.
1031
+ */
1032
+ function toDateTimeString(date) {
1033
+ if (!date || isNaN(date.getTime()))
1034
+ return '';
1035
+ const pad = (n) => (n < 10 ? `0${n}` : String(n));
1036
+ return `${toDateOnlyString(date)} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
1037
+ }
1038
+ /**
1039
+ * Render a datetime with a token pattern (`dd-MMM-yyyy hh:mm:ss`).
1040
+ *
1041
+ * The date half goes through formatDateWithPattern, so `MMM` yields a month
1042
+ * name — the datetime cell used to build its own numeric date string and had no
1043
+ * `MMM` branch at all, which is why picking DD-MMM-YYYY HH:MM:SS still rendered
1044
+ * digits. The time half is 24-hour, with seconds only when the pattern asks.
1045
+ */
1046
+ function formatDateTimeWithPattern(date, pattern) {
1047
+ if (!date || isNaN(date.getTime()))
1048
+ return '';
1049
+ const normalized = normalizeDateTimeFormat(pattern) || 'dd-MM-yyyy hh:mm';
1050
+ const timeAt = normalized.toLowerCase().indexOf('hh');
1051
+ const datePart = timeAt === -1 ? normalized : normalized.slice(0, timeAt).trimEnd();
1052
+ const timePart = timeAt === -1 ? '' : normalized.slice(timeAt).toLowerCase();
1053
+ const dateText = formatDateWithPattern(date, datePart);
1054
+ if (!timePart)
1055
+ return dateText;
1056
+ const pad = (n) => (n < 10 ? `0${n}` : String(n));
1057
+ const time = timePart.includes('ss')
1058
+ ? `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
1059
+ : `${pad(date.getHours())}:${pad(date.getMinutes())}`;
1060
+ return `${dateText} ${time}`;
1061
+ }
1062
+ /**
1063
+ * Best-effort parse of a stored datetime value, keeping the time.
1064
+ *
1065
+ * Distinct from parseCellDate, which deliberately pins a bare ISO date to local
1066
+ * midnight so a date column never shifts a day across timezones — correct
1067
+ * there, but it threw the clock away, so a datetime column could never show
1068
+ * anything but 00:00.
1069
+ *
1070
+ * An ISO timestamp is tried first and handed to the platform parser: it is
1071
+ * unambiguous, it carries an offset, and letting a `dd-MM-yyyy` column pattern
1072
+ * see it first would read `2026-09-05` as day 2026. A timestamp with an offset
1073
+ * (`...Z`) therefore reads back as the local wall-clock time of that instant.
1074
+ */
1075
+ function parseCellDateTime(value, pattern) {
1076
+ if (value === null || value === undefined || value === '')
1077
+ return null;
1078
+ if (value instanceof Date)
1079
+ return isNaN(value.getTime()) ? null : value;
1080
+ const text = String(value).trim();
1081
+ if (/^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/.test(text)) {
1082
+ const iso = new Date(text);
1083
+ if (!isNaN(iso.getTime()))
1084
+ return iso;
1085
+ }
1086
+ if (pattern) {
1087
+ // Split the stored value the same way the pattern is split: date half,
1088
+ // then a HH:mm[:ss] time half.
1089
+ const bits = text.split(/\s+/);
1090
+ const normalized = normalizeDateTimeFormat(pattern);
1091
+ const timeAt = normalized.toLowerCase().indexOf('hh');
1092
+ const datePattern = timeAt === -1 ? normalized : normalized.slice(0, timeAt).trimEnd();
1093
+ // A date pattern may itself contain spaces (`dd MMM yyyy`), so the time is
1094
+ // the last segment when it looks like a clock, and the date is the rest.
1095
+ const hasTime = bits.length > 1 && /^\d{1,2}:\d{2}(:\d{2})?$/.test(bits[bits.length - 1]);
1096
+ const datePart = (hasTime ? bits.slice(0, -1) : bits).join(' ');
1097
+ const date = parseDateWithPattern(datePart, datePattern);
1098
+ if (date) {
1099
+ if (hasTime) {
1100
+ const [h, m, sec = 0] = bits[bits.length - 1].split(':').map(Number);
1101
+ date.setHours(h || 0, m || 0, sec || 0, 0);
1102
+ }
1103
+ return date;
1104
+ }
1105
+ }
1106
+ const parsed = new Date(text);
1107
+ return isNaN(parsed.getTime()) ? null : parsed;
1108
+ }
977
1109
  /**
978
1110
  * Best-effort parse of a stored date value, trying the column's own pattern
979
1111
  * before falling back to ISO and then the platform parser. Returns null rather
980
1112
  * than an Invalid Date so callers can render an empty cell.
981
1113
  */
982
- function parseCellDate(value, pattern) {
1114
+ function parseCellDate(value, pattern, keepTime = false) {
983
1115
  if (value === null || value === undefined || value === '')
984
1116
  return null;
985
1117
  if (value instanceof Date)
@@ -990,9 +1122,22 @@ function parseCellDate(value, pattern) {
990
1122
  if (viaPattern)
991
1123
  return viaPattern;
992
1124
  }
993
- const iso = /^(\d{4})-(\d{2})-(\d{2})/.exec(text);
994
- if (iso)
995
- return new Date(Number(iso[1]), Number(iso[2]) - 1, Number(iso[3]));
1125
+ // `keepTime` is off by default so a date field, and the before/after rules,
1126
+ // keep comparing at midnight the way they always have. A datetime asks for it:
1127
+ // this branch used to capture the calendar part alone, so every ISO timestamp
1128
+ // came back at 00:00 and formatCellValue printed that as the time — a
1129
+ // composite's 10:16 part read "00:00" while the same field read correctly in
1130
+ // its own column.
1131
+ //
1132
+ // The components are taken literally rather than through Date's own ISO
1133
+ // handling, which would shift a `Z` value into the viewer's zone and could
1134
+ // land it on a different day than the one the value spells out.
1135
+ const iso = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2}))?)?/.exec(text);
1136
+ if (iso) {
1137
+ return keepTime
1138
+ ? new Date(Number(iso[1]), Number(iso[2]) - 1, Number(iso[3]), Number(iso[4] ?? 0), Number(iso[5] ?? 0), Number(iso[6] ?? 0))
1139
+ : new Date(Number(iso[1]), Number(iso[2]) - 1, Number(iso[3]));
1140
+ }
996
1141
  const parsed = new Date(text);
997
1142
  return isNaN(parsed.getTime()) ? null : parsed;
998
1143
  }
@@ -1007,7 +1152,7 @@ function parseCellDate(value, pattern) {
1007
1152
  function formatCellValue(value, field) {
1008
1153
  if (value === null || value === undefined || value === '')
1009
1154
  return '';
1010
- const datatype = field?.datatype ? normalizeDatatype(field.datatype) : undefined;
1155
+ const datatype = effectiveValueDatatype(field);
1011
1156
  switch (datatype) {
1012
1157
  case 'number':
1013
1158
  return formatNumberValue(value, field);
@@ -1021,7 +1166,7 @@ function formatCellValue(value, field) {
1021
1166
  case 'datetime': {
1022
1167
  const pattern = normalizeDateTimeFormat(field?.date_format) || 'dd-MM-yyyy hh:mm';
1023
1168
  const datePart = pattern.split(/\s+(?=hh)/i)[0];
1024
- const date = parseCellDate(value, datePart);
1169
+ const date = parseCellDate(value, datePart, true);
1025
1170
  if (!date)
1026
1171
  return String(value);
1027
1172
  const pad = (n) => (n < 10 ? `0${n}` : String(n));
@@ -1143,7 +1288,10 @@ class PivotTransformService {
1143
1288
  const enableColumnSubtotals = gridConfiguration?.config?.enableColumnSubtotals ?? false;
1144
1289
  const enableGrandTotal = gridConfiguration?.config?.enableGrandTotal ?? false;
1145
1290
  const enableColumnGrandTotal = gridConfiguration?.config?.enableColumnGrandTotal ?? false;
1146
- if (enableRowSubtotals || enableColumnSubtotals || enableGrandTotal || enableColumnGrandTotal) {
1291
+ // Nothing to total when there are no rows — a grand total of an empty set
1292
+ // would render a row of zeros under a result that has no data.
1293
+ if (pivotRows.length > 0 &&
1294
+ (enableRowSubtotals || enableColumnSubtotals || enableGrandTotal || enableColumnGrandTotal)) {
1147
1295
  pivotRows = this.addSubtotals(pivotRows, configuration, pivotColumns, gridConfiguration, sourceData);
1148
1296
  }
1149
1297
  // Step 5: Generate column definitions (including subtotal columns)
@@ -3696,6 +3844,17 @@ const NULL_GROUP_KEY = '__NULL_GROUP__';
3696
3844
  function toGroupKey(groupId) {
3697
3845
  return groupId === null || groupId === undefined ? NULL_GROUP_KEY : groupId;
3698
3846
  }
3847
+ /**
3848
+ * The id a row is selected by. Entity rows carry `entity_id`; array/query rows
3849
+ * that were given a synthetic key carry `id`. Every read and write of the
3850
+ * selection set must go through this — the checkbox used to store `row.id`
3851
+ * while the checked-state read `row.entity_id`, so ticking an entity row put
3852
+ * `undefined` in the set and the box never rendered checked.
3853
+ */
3854
+ function rowSelectionKey(row) {
3855
+ const id = row?.entity_id ?? row?.id;
3856
+ return id === null || id === undefined ? '' : String(id);
3857
+ }
3699
3858
  class EruGridStore {
3700
3859
  // Inject services
3701
3860
  gridConfigService = inject(GridConfigService);
@@ -3707,6 +3866,11 @@ class EruGridStore {
3707
3866
  _rows = signal([], ...(ngDevMode ? [{ debugName: "_rows" }] : []));
3708
3867
  _groupRows = signal(new Map(), ...(ngDevMode ? [{ debugName: "_groupRows" }] : []));
3709
3868
  _selectedRowIds = signal(new Set(), ...(ngDevMode ? [{ debugName: "_selectedRowIds" }] : []));
3869
+ // "All N matching records are selected", the second stage of the header
3870
+ // checkbox. The grid only ever holds the pages it has loaded, so this flag is
3871
+ // the only way to say the user meant the whole result set: consumers send
3872
+ // their filter criteria instead of the id list when it is set.
3873
+ _selectAllMatching = signal(false, ...(ngDevMode ? [{ debugName: "_selectAllMatching" }] : []));
3710
3874
  // The single "active" board card (last clicked) — stored by row-object
3711
3875
  // reference so it works for any data source (entity rows have entity_id,
3712
3876
  // query rows may not). Drives the board-card selected-state highlight;
@@ -3735,6 +3899,11 @@ class EruGridStore {
3735
3899
  // Pivot-specific signals
3736
3900
  _pivotConfiguration = signal(null, ...(ngDevMode ? [{ debugName: "_pivotConfiguration" }] : []));
3737
3901
  _pivotResult = signal(null, ...(ngDevMode ? [{ debugName: "_pivotResult" }] : []));
3902
+ /**
3903
+ * Widths for generated pivot leaves that have no field to hold them.
3904
+ * Keyed by leaf name — see updateColumnWidth.
3905
+ */
3906
+ _pivotLeafWidths = signal(new Map(), ...(ngDevMode ? [{ debugName: "_pivotLeafWidths" }] : []));
3738
3907
  _drilldown = signal(null, ...(ngDevMode ? [{ debugName: "_drilldown" }] : []));
3739
3908
  _actionClick = signal(null, ...(ngDevMode ? [{ debugName: "_actionClick" }] : []));
3740
3909
  _columnResize = signal(null, ...(ngDevMode ? [{ debugName: "_columnResize" }] : []));
@@ -3813,6 +3982,7 @@ class EruGridStore {
3813
3982
  return result;
3814
3983
  }, ...(ngDevMode ? [{ debugName: "groupRowsSummary" }] : []));
3815
3984
  selectedRowIds = this._selectedRowIds.asReadonly();
3985
+ selectAllMatching = this._selectAllMatching.asReadonly();
3816
3986
  activeBoardRow = this._activeBoardRow.asReadonly();
3817
3987
  configuration = this._configuration.asReadonly();
3818
3988
  isLoading = this._isLoading.asReadonly();
@@ -3823,6 +3993,7 @@ class EruGridStore {
3823
3993
  // Pivot-specific readonly signals
3824
3994
  pivotConfiguration = this._pivotConfiguration.asReadonly();
3825
3995
  pivotResult = this._pivotResult.asReadonly();
3996
+ pivotLeafWidths = this._pivotLeafWidths.asReadonly();
3826
3997
  drilldown = this._drilldown.asReadonly();
3827
3998
  actionClick = this._actionClick.asReadonly();
3828
3999
  columnResize = this._columnResize.asReadonly();
@@ -3838,7 +4009,35 @@ class EruGridStore {
3838
4009
  // Excel-download request readonly signal (exposed to consumers)
3839
4010
  excelDownloadRequest = this._excelDownloadRequest.asReadonly();
3840
4011
  // Computed properties
3841
- selectedRows = computed(() => this.rows().filter(row => this.selectedRowIds().has(row.entity_id)), ...(ngDevMode ? [{ debugName: "selectedRows" }] : []));
4012
+ /**
4013
+ * Every row currently in memory that can be selected, deduped by selection key.
4014
+ *
4015
+ * Selection has to read both row stores: `_rows` is only populated when the
4016
+ * consumer mirrors data in via setRows()/addRows(), while the grouped,
4017
+ * server-paginated renderer fills `_groupRows` through addRowsForGroup(). The
4018
+ * select-all handler read `rows()` alone, which is empty in the normal paged
4019
+ * path — so select-all was a no-op there.
4020
+ */
4021
+ selectableRows = computed(() => {
4022
+ const seen = new Set();
4023
+ const out = [];
4024
+ const push = (row) => {
4025
+ const id = rowSelectionKey(row);
4026
+ if (!id || seen.has(id))
4027
+ return;
4028
+ seen.add(id);
4029
+ out.push(row);
4030
+ };
4031
+ this.rows().forEach(push);
4032
+ this.groupRows().forEach(rows => rows.forEach(push));
4033
+ return out;
4034
+ }, ...(ngDevMode ? [{ debugName: "selectableRows" }] : []));
4035
+ selectedRows = computed(() => this.selectableRows().filter(row => this.selectedRowIds().has(rowSelectionKey(row))), ...(ngDevMode ? [{ debugName: "selectedRows" }] : []));
4036
+ /**
4037
+ * How many records the current groups say exist server-side, loaded or not.
4038
+ * This is the "all N matching" number the select-all banner offers.
4039
+ */
4040
+ totalMatchingCount = computed(() => this.groups().reduce((sum, group) => sum + (group.totalRowCount || 0), 0), ...(ngDevMode ? [{ debugName: "totalMatchingCount" }] : []));
3842
4041
  totalSelectedCount = computed(() => this.selectedRowIds().size, ...(ngDevMode ? [{ debugName: "totalSelectedCount" }] : []));
3843
4042
  hasSelection = computed(() => this.selectedRowIds().size > 0, ...(ngDevMode ? [{ debugName: "hasSelection" }] : []));
3844
4043
  expandedGroups = computed(() => this.groups().filter(group => group.isExpanded), ...(ngDevMode ? [{ debugName: "expandedGroups" }] : []));
@@ -3926,7 +4125,14 @@ class EruGridStore {
3926
4125
  // Resolve data-model datatype aliases (r_status -> status) once here, so
3927
4126
  // every downstream switch, validator and design-panel lookup sees the
3928
4127
  // canonical value.
3929
- datatype: normalizeDatatype(column.datatype),
4128
+ datatype: normalizeDatatype(
4129
+ // A column saved under the old encoding carries 'composite'/'page' in
4130
+ // the datatype slot. normalizeCellRender has just read the renderer off
4131
+ // it; leaving the value there would keep it out of every datatype
4132
+ // switch, so it falls back to text. A mapped column overwrites this
4133
+ // with its real type on the next resolve.
4134
+ PRESENTATION_DATATYPES.has(String(column.datatype)) ? 'textbox' : column.datatype),
4135
+ cell_render: normalizeCellRender(column),
3930
4136
  field_size: this.columnConstraintsService.getDefaultFieldSize(column, config)
3931
4137
  }));
3932
4138
  // Sort columns by grid_index to ensure proper order in table mode
@@ -4018,6 +4224,28 @@ class EruGridStore {
4018
4224
  getConfiguration() {
4019
4225
  return this.configuration();
4020
4226
  }
4227
+ /**
4228
+ * Reorder by column name rather than by rendered position.
4229
+ *
4230
+ * The header renders `displayColumns` — `columns()` minus the hidden columns
4231
+ * and minus the group-by field — so the `$index` of a header cell is not the
4232
+ * index of that column in `_columns`. Passing the rendered index straight to
4233
+ * `reorderColumns` moved whichever column occupied that slot in the full
4234
+ * list, which is why dragging a column near the end appeared to move its
4235
+ * neighbour instead. Names survive every filter, so resolve them here.
4236
+ */
4237
+ reorderColumnsByName(fromName, toName) {
4238
+ const currentColumns = this.columns();
4239
+ const fromIndex = currentColumns.findIndex(col => col.name === fromName);
4240
+ const toIndex = currentColumns.findIndex(col => col.name === toName);
4241
+ // Pivot mode renders generated column definitions that do not exist in
4242
+ // `_columns`; there is nothing to reorder in that case.
4243
+ if (fromIndex === -1 || toIndex === -1) {
4244
+ console.error('Unknown column in reorder', { fromName, toName });
4245
+ return;
4246
+ }
4247
+ this.reorderColumns(fromIndex, toIndex);
4248
+ }
4021
4249
  reorderColumns(fromIndex, toIndex) {
4022
4250
  const currentColumns = [...this.columns()];
4023
4251
  if (fromIndex < 0 || fromIndex >= currentColumns.length ||
@@ -4182,8 +4410,20 @@ class EruGridStore {
4182
4410
  });
4183
4411
  return;
4184
4412
  }
4185
- const columns = this.columns().map(col => col.name === owner ? { ...col, field_size: field_size } : col);
4186
- this._columns.set(columns);
4413
+ const columns = this.columns();
4414
+ if (!columns.some(col => col.name === owner)) {
4415
+ // A pivot dimension the grid has no column for — the author put a field
4416
+ // in the pivot's rows that is not in the column set. There is no owner to
4417
+ // hold the width, so it is held against the leaf name. Without this the
4418
+ // resize reached the cells (the directive styles them directly) but
4419
+ // nothing else: the next change detection re-asserted the old width, and
4420
+ // the table total, which is summed from the leaves, never moved.
4421
+ const next = new Map(this._pivotLeafWidths());
4422
+ next.set(owner, field_size);
4423
+ this._pivotLeafWidths.set(next);
4424
+ return;
4425
+ }
4426
+ this._columns.set(columns.map(col => (col.name === owner ? { ...col, field_size: field_size } : col)));
4187
4427
  }
4188
4428
  setCellValueChange(change) {
4189
4429
  this._cellValueChange.set(change);
@@ -4289,18 +4529,35 @@ class EruGridStore {
4289
4529
  }
4290
4530
  selectAllRowsInGroup(groupId) {
4291
4531
  const selectedRowIds = new Set(this.selectedRowIds());
4292
- const groupRows = this.getRowsForGroup(groupId);
4293
- groupRows.forEach(row => selectedRowIds.add(row.entity_id));
4532
+ this.getRowsForGroup(groupId).forEach(row => {
4533
+ const id = rowSelectionKey(row);
4534
+ if (id)
4535
+ selectedRowIds.add(id);
4536
+ });
4294
4537
  this._selectedRowIds.set(selectedRowIds);
4295
4538
  }
4296
4539
  deselectAllRowsInGroup(groupId) {
4297
4540
  const selectedRowIds = new Set(this.selectedRowIds());
4298
- const groupRows = this.getRowsForGroup(groupId);
4299
- groupRows.forEach(row => selectedRowIds.delete(row.entity_id));
4541
+ this.getRowsForGroup(groupId).forEach(row => selectedRowIds.delete(rowSelectionKey(row)));
4542
+ this._selectedRowIds.set(selectedRowIds);
4543
+ }
4544
+ /** Select every row loaded so far, across all groups. Stage one of select-all. */
4545
+ selectAllLoadedRows() {
4546
+ const selectedRowIds = new Set(this.selectedRowIds());
4547
+ this.selectableRows().forEach(row => selectedRowIds.add(rowSelectionKey(row)));
4300
4548
  this._selectedRowIds.set(selectedRowIds);
4301
4549
  }
4550
+ /**
4551
+ * Stage two of select-all: the user accepted "select all N matching". The set
4552
+ * of ids is left as-is (it still only holds what is loaded) — the flag is what
4553
+ * tells the consumer to act on the filter rather than on the ids.
4554
+ */
4555
+ setSelectAllMatching(value) {
4556
+ this._selectAllMatching.set(value);
4557
+ }
4302
4558
  clearSelection() {
4303
4559
  this._selectedRowIds.set(new Set());
4560
+ this._selectAllMatching.set(false);
4304
4561
  }
4305
4562
  // Configuration methods
4306
4563
  setConfiguration(configuration) {
@@ -4411,7 +4668,7 @@ class EruGridStore {
4411
4668
  isGroupSelected(groupId) {
4412
4669
  const groupRows = this.getRowsForGroup(groupId);
4413
4670
  return groupRows.length > 0 &&
4414
- groupRows.every(row => this.selectedRowIds().has(row.entity_id));
4671
+ groupRows.every(row => this.selectedRowIds().has(rowSelectionKey(row)));
4415
4672
  }
4416
4673
  getRowsForGroup(groupId) {
4417
4674
  const key = toGroupKey(groupId);
@@ -4483,9 +4740,18 @@ class EruGridStore {
4483
4740
  this._groupRows().forEach(rows => allGroupRows.push(...rows));
4484
4741
  sourceData = allGroupRows;
4485
4742
  }
4486
- if (!pivotConfig || sourceData.length === 0) {
4743
+ if (!pivotConfig)
4487
4744
  return;
4488
- }
4745
+ // No source rows still goes through the transform, producing a pivot with
4746
+ // the row-dimension columns and no data rows.
4747
+ //
4748
+ // Two things this must NOT do. Bailing out leaves the previous filter's
4749
+ // rows and subtotals on screen, since the pivot view reads _pivotResult and
4750
+ // not the row stores that were already cleared. Publishing null empties it
4751
+ // but drops the pivot's own column definitions, so displayColumns() falls
4752
+ // back to the raw source columns and an empty pivot reads as a table. The
4753
+ // transform on an empty set yields neither: column values and measures
4754
+ // cannot be derived from nothing, so only the row dimensions remain.
4489
4755
  this.setLoading(true);
4490
4756
  this.setError(null);
4491
4757
  try {
@@ -4759,6 +5025,15 @@ class EruGridStore {
4759
5025
  }
4760
5026
  resetGroupRows() {
4761
5027
  this._groupRows.set(new Map());
5028
+ // A fresh data set invalidates the selection: the ids may not exist under
5029
+ // the new filter/grouping, and an "all N matching" claim was made against
5030
+ // the criteria that just changed. Paging does NOT come through here — it
5031
+ // appends via addRowsForGroup — so a selection still survives scrolling.
5032
+ this.clearSelection();
5033
+ // In pivot mode the view renders from _pivotResult, not from the row
5034
+ // stores, so clearing the rows alone left the old pivot on screen.
5035
+ if (this.isPivotMode())
5036
+ this.transformToPivot();
4762
5037
  }
4763
5038
  /**
4764
5039
  * Add rows for a specific group (called by consumer with API data)
@@ -5089,6 +5364,22 @@ class EruGridService {
5089
5364
  get hasSelection() {
5090
5365
  return this.eruGridStore.hasSelection;
5091
5366
  }
5367
+ get selectedRows() {
5368
+ return this.eruGridStore.selectedRows;
5369
+ }
5370
+ get totalSelectedCount() {
5371
+ return this.eruGridStore.totalSelectedCount;
5372
+ }
5373
+ /** True once the user accepted "select all N matching" — act on the filter, not the ids. */
5374
+ get selectAllMatching() {
5375
+ return this.eruGridStore.selectAllMatching;
5376
+ }
5377
+ get totalMatchingCount() {
5378
+ return this.eruGridStore.totalMatchingCount;
5379
+ }
5380
+ clearSelection() {
5381
+ this.eruGridStore.clearSelection();
5382
+ }
5092
5383
  // Theme management
5093
5384
  get theme() {
5094
5385
  return this.themeService;
@@ -5563,21 +5854,25 @@ class CurrencyComponent {
5563
5854
  }
5564
5855
  onBlur() {
5565
5856
  const value = this.currentValue();
5566
- const numValue = Number(value);
5567
- const cfg = this.config();
5568
- const validation = this.validateField(numValue);
5857
+ // Number('') and Number(null) are both 0, so treating a blank as a number
5858
+ // would emit 0 out of every empty cell the user tabs through. Validation
5859
+ // still runs on NaN, so a mandatory blank is caught.
5860
+ const isBlank = value === null || value === undefined || value === '';
5861
+ const numValue = isBlank ? NaN : Number(value);
5862
+ // Blank goes to validation as-is: it is an empty cell, not the unparseable
5863
+ // number NaN would report it as.
5864
+ const validation = this.validateField(isBlank ? value : numValue);
5569
5865
  if (!validation.isValid && validation.error) {
5570
5866
  this.error.set(validation.error);
5571
5867
  this.validationError.emit(validation.error);
5868
+ return;
5572
5869
  }
5573
- else {
5574
- this.error.set('');
5575
- // Force decimal places as specified in config
5576
- const decimalPlaces = cfg?.decimal ?? 2;
5577
- const formattedValue = parseFloat(numValue.toFixed(decimalPlaces));
5578
- this.currentValue.set(formattedValue);
5579
- this.blur.emit(formattedValue);
5580
- }
5870
+ this.error.set('');
5871
+ // `decimal` is DISPLAY precision — the view branch applies it through
5872
+ // formatNumberSignal. Rounding here put the rounded number back into
5873
+ // currentValue, which is the value the cell SAVES, so 10.5 in a 0-decimal
5874
+ // column became 11 for good after any edit.
5875
+ this.blur.emit(isBlank ? null : numValue);
5581
5876
  }
5582
5877
  onValueChange(event) {
5583
5878
  this.currentValue.set(event);
@@ -5706,21 +6001,25 @@ class NumberComponent {
5706
6001
  }
5707
6002
  onBlur() {
5708
6003
  const value = this.currentValue();
5709
- const numValue = Number(value);
5710
- const cfg = this.config();
5711
- const validation = this.validateField(numValue);
6004
+ // Number('') and Number(null) are both 0, so treating a blank as a number
6005
+ // would emit 0 out of every empty cell the user tabs through. Validation
6006
+ // still runs on NaN, so a mandatory blank is caught.
6007
+ const isBlank = value === null || value === undefined || value === '';
6008
+ const numValue = isBlank ? NaN : Number(value);
6009
+ // Blank goes to validation as-is: it is an empty cell, not the unparseable
6010
+ // number NaN would report it as.
6011
+ const validation = this.validateField(isBlank ? value : numValue);
5712
6012
  if (!validation.isValid && validation.error) {
5713
6013
  this.error.set(validation.error);
5714
6014
  this.validationError.emit(validation.error);
6015
+ return;
5715
6016
  }
5716
- else {
5717
- this.error.set('');
5718
- // Force decimal places as specified in config
5719
- const decimalPlaces = cfg?.decimal ?? 2;
5720
- const formattedValue = parseFloat(numValue.toFixed(decimalPlaces));
5721
- this.currentValue.set(formattedValue);
5722
- this.blur.emit(formattedValue);
5723
- }
6017
+ this.error.set('');
6018
+ // `decimal` is DISPLAY precision — the view branch applies it through
6019
+ // formatNumberSignal. Rounding here put the rounded number back into
6020
+ // currentValue, which is the value the cell SAVES, so 10.5 in a 0-decimal
6021
+ // column became 11 for good after any edit.
6022
+ this.blur.emit(isBlank ? null : numValue);
5724
6023
  }
5725
6024
  onValueChange(event) {
5726
6025
  this.currentValue.set(event);
@@ -7101,14 +7400,22 @@ class DateComponent {
7101
7400
  }
7102
7401
  onDateChange(date) {
7103
7402
  this.currentValue.set(date ? new Date(date) : null);
7104
- const formattedDate = date ? this.formatDate(date) : '';
7105
- this.valueChange.emit(formattedDate);
7106
- this.dateChange.emit(formattedDate);
7403
+ // The canonical `yyyy-MM-dd`, NOT the column's display pattern. Emitting
7404
+ // the pattern stored '05-Sep-2026' as the value, so the stored shape
7405
+ // changed on every edit and re-reading it depended on the column still
7406
+ // carrying the pattern it was written under. eru-studio's date component
7407
+ // canonicalises the same way (storeValueFor -> toDateOnlyString).
7408
+ const stored = date ? toDateOnlyString(date) : '';
7409
+ this.valueChange.emit(stored);
7410
+ this.dateChange.emit(stored);
7107
7411
  }
7108
7412
  onDrillableClick(event) {
7109
7413
  if (this.isDrillable()) {
7110
7414
  event.stopPropagation();
7111
- this.drilldownClick.emit(this.getDisplayValue());
7415
+ // The stored value, not the formatted text — a consumer turns a drill
7416
+ // into a filter, and '05-Sep-2026' matches nothing server-side.
7417
+ const value = this.currentValue();
7418
+ this.drilldownClick.emit(value instanceof Date ? toDateOnlyString(value) : (value ?? ''));
7112
7419
  }
7113
7420
  }
7114
7421
  /** Render a date in the column's configured format, via the shared token
@@ -7327,12 +7634,15 @@ class DatetimeComponent {
7327
7634
  effect(() => {
7328
7635
  const value = this.value();
7329
7636
  if (typeof value === 'string' && value) {
7330
- // Parse datetime string (dd-MM-yyyy HH:mm format)
7331
- const parsed = this.parseDateTimeString(value);
7637
+ // Any stored shape: the column's own pattern, or an ISO timestamp
7638
+ // straight from the server (`2026-09-05T07:35:40.265175Z`), which the
7639
+ // old whitespace-splitting parser could not read at all — it returned
7640
+ // null and the raw string was rendered verbatim.
7641
+ const parsed = parseCellDateTime(value, this.dtFormat());
7332
7642
  if (parsed) {
7333
- this.selectedDate.set(parsed.date);
7334
- this.selectedTime.set(parsed.time);
7335
- this.currentValue.set(parsed.date);
7643
+ this.selectedDate.set(parsed);
7644
+ this.selectedTime.set(this.formatTime(parsed));
7645
+ this.currentValue.set(parsed);
7336
7646
  }
7337
7647
  else {
7338
7648
  this.currentValue.set(value);
@@ -7405,29 +7715,26 @@ class DatetimeComponent {
7405
7715
  this.drilldownClick.emit(value);
7406
7716
  }
7407
7717
  }
7408
- // Column-configured format, e.g. 'dd-mm-yyyy hh:mm:ss'. `date_format` is the
7718
+ // Column-configured format, e.g. 'dd-MMM-yyyy hh:mm:ss'. `date_format` is the
7409
7719
  // key the data model actually persists the datetime pattern under;
7410
- // `dateTimeFormat` is the legacy alias. Lowercased because the model
7411
- // uppercases the value on save. Defaults to dd-mm-yyyy hh:mm:ss.
7720
+ // `dateTimeFormat` is the legacy alias. Normalised rather than lowercased —
7721
+ // the model uppercases on save, and lowercasing collapsed `MMM` into `mmm`,
7722
+ // erasing the month-name/minute distinction the token casing carries.
7412
7723
  dtFormat = computed(() => {
7413
7724
  const cfg = this.config();
7414
- return String(cfg?.date_format || cfg?.dateTimeFormat || 'dd-mm-yyyy hh:mm:ss').toLowerCase();
7725
+ return normalizeDateTimeFormat(cfg?.date_format || cfg?.dateTimeFormat) || 'dd-MM-yyyy hh:mm:ss';
7415
7726
  }, ...(ngDevMode ? [{ debugName: "dtFormat" }] : []));
7727
+ /**
7728
+ * The canonical stored form of an edit: `yyyy-MM-dd HH:mm:ss`.
7729
+ *
7730
+ * It used to be built from the DISPLAY pattern, which made the stored shape
7731
+ * depend on how the column happened to be formatted and dropped the seconds
7732
+ * whenever the pattern had no `ss` — so editing a server timestamp under a
7733
+ * `hh:mm` pattern silently truncated it. Display goes through
7734
+ * formatDateTimeWithPattern; storage is always this one shape.
7735
+ */
7416
7736
  formatDateTime(date) {
7417
- if (!date)
7418
- return '';
7419
- const pad = (n) => n < 10 ? '0' + n : n.toString();
7420
- const dd = pad(date.getDate());
7421
- const mm = pad(date.getMonth() + 1);
7422
- const yyyy = date.getFullYear().toString();
7423
- const fmt = this.dtFormat();
7424
- const datePart = fmt.startsWith('mm-dd') ? `${mm}-${dd}-${yyyy}`
7425
- : fmt.startsWith('yyyy') ? `${yyyy}-${mm}-${dd}`
7426
- : `${dd}-${mm}-${yyyy}`;
7427
- const hh = pad(date.getHours());
7428
- const min = pad(date.getMinutes());
7429
- const timePart = fmt.includes('ss') ? `${hh}:${min}:${pad(date.getSeconds())}` : `${hh}:${min}`;
7430
- return `${datePart} ${timePart}`;
7737
+ return toDateTimeString(date);
7431
7738
  }
7432
7739
  formatTime(date) {
7433
7740
  const hours = date.getHours();
@@ -7435,49 +7742,20 @@ class DatetimeComponent {
7435
7742
  const pad = (n) => n < 10 ? '0' + n : n.toString();
7436
7743
  return `${pad(hours)}:${pad(minutes)}`;
7437
7744
  }
7438
- parseDateTimeString(dateTimeString) {
7439
- // Tolerant parse: accepts any supported date order (year located by its
7440
- // 4-digit segment) and a HH:mm or HH:mm:ss time part.
7441
- const parts = dateTimeString.trim().split(/\s+/);
7442
- if (parts.length < 2)
7443
- return null;
7444
- const dateParts = parts[0].split('-');
7445
- const timePart = parts[1];
7446
- if (dateParts.length !== 3)
7447
- return null;
7448
- const nums = dateParts.map(p => parseInt(p, 10));
7449
- if (nums.some(isNaN))
7450
- return null;
7451
- const yearIdx = dateParts.findIndex(p => p.length === 4);
7452
- let year, month, day;
7453
- if (yearIdx === 0) {
7454
- [year, month, day] = [nums[0], nums[1] - 1, nums[2]];
7455
- }
7456
- else if (this.dtFormat().startsWith('mm-dd')) {
7457
- [month, day, year] = [nums[0] - 1, nums[1], nums[2]];
7458
- }
7459
- else {
7460
- [day, month, year] = [nums[0], nums[1] - 1, nums[2]];
7461
- }
7462
- const timeBits = timePart.split(':').map(t => parseInt(t, 10));
7463
- if (timeBits.length < 2 || timeBits.some(isNaN))
7464
- return null;
7465
- const [hours, minutes, seconds = 0] = timeBits;
7466
- const date = new Date(year, month, day, hours, minutes, seconds);
7467
- if (date.getDate() === day && date.getMonth() === month && date.getFullYear() === year) {
7468
- const pad = (n) => n < 10 ? '0' + n : n.toString();
7469
- return { date, time: `${pad(hours)}:${pad(minutes)}` };
7470
- }
7471
- return null;
7472
- }
7745
+ /**
7746
+ * Seconds are carried through when the time string has them. The picker only
7747
+ * offers HH:mm, so an untouched value keeps the seconds it arrived with
7748
+ * rather than being silently zeroed on a date-only change.
7749
+ */
7473
7750
  combineDateAndTime(date, time) {
7474
7751
  const timeParts = time.split(':');
7475
7752
  const hours = parseInt(timeParts[0], 10) || 0;
7476
7753
  const minutes = parseInt(timeParts[1], 10) || 0;
7754
+ const seconds = timeParts.length > 2 ? (parseInt(timeParts[2], 10) || 0) : this.selectedDate()?.getSeconds() ?? 0;
7477
7755
  const combined = new Date(date);
7478
7756
  combined.setHours(hours);
7479
7757
  combined.setMinutes(minutes);
7480
- combined.setSeconds(0);
7758
+ combined.setSeconds(seconds);
7481
7759
  combined.setMilliseconds(0);
7482
7760
  return combined;
7483
7761
  }
@@ -7488,10 +7766,14 @@ class DatetimeComponent {
7488
7766
  const val = this.currentValue();
7489
7767
  if (!val)
7490
7768
  return '';
7491
- if (typeof val === 'string')
7492
- return val;
7493
- if (val instanceof Date) {
7494
- return this.formatDateTime(val);
7769
+ const pattern = this.dtFormat();
7770
+ if (val instanceof Date)
7771
+ return formatDateTimeWithPattern(val, pattern);
7772
+ // A string only reaches here when it could not be parsed; show it as-is
7773
+ // rather than an empty cell, so a bad stored value stays visible.
7774
+ if (typeof val === 'string') {
7775
+ const parsed = parseCellDateTime(val, pattern);
7776
+ return parsed ? formatDateTimeWithPattern(parsed, pattern) : val;
7495
7777
  }
7496
7778
  return '';
7497
7779
  }
@@ -7589,22 +7871,44 @@ class DurationComponent {
7589
7871
  if (this.childOverlayOpen()) {
7590
7872
  return;
7591
7873
  }
7592
- const value = this.getCurrentValueInMinutes();
7593
- this.currentValue.set(value);
7594
- this.blur.emit(value);
7595
- this.isOverlayOpen.set(false);
7596
- this.editModeChange.emit(false);
7874
+ this.commitAndClose();
7597
7875
  }
7598
7876
  onOverlayClick(event) {
7599
7877
  event.stopPropagation();
7600
7878
  }
7601
7879
  onBlur() {
7880
+ this.commitAndClose();
7881
+ }
7882
+ /**
7883
+ * Close the editor, emitting only when the duration actually changed.
7884
+ *
7885
+ * It used to emit unconditionally, and since the editor works in minutes that
7886
+ * rewrote the stored representation on a no-op: a cell holding '02:30' became
7887
+ * 150 merely by opening the overlay and dismissing it. Opening a cell is not
7888
+ * an edit.
7889
+ */
7890
+ commitAndClose() {
7602
7891
  const value = this.getCurrentValueInMinutes();
7603
- this.currentValue.set(value);
7604
- this.blur.emit(value);
7892
+ if (value !== this.storedMinutes()) {
7893
+ this.currentValue.set(value);
7894
+ this.blur.emit(value);
7895
+ }
7605
7896
  this.isOverlayOpen.set(false);
7606
7897
  this.editModeChange.emit(false);
7607
7898
  }
7899
+ /** The incoming value in minutes, for comparison against an edit. */
7900
+ storedMinutes() {
7901
+ const value = this.value();
7902
+ if (value === null || value === undefined || value === '')
7903
+ return null;
7904
+ if (typeof value === 'number')
7905
+ return value;
7906
+ const text = String(value);
7907
+ if (this.isTimeFormat(text))
7908
+ return this.timeToMinutes(text);
7909
+ const num = parseInt(text, 10);
7910
+ return isNaN(num) ? null : num;
7911
+ }
7608
7912
  onDrillableClick(event) {
7609
7913
  if (this.isDrillable()) {
7610
7914
  event.stopPropagation();
@@ -8097,7 +8401,29 @@ class ProgressComponent {
8097
8401
  editModeChange = new EventEmitter();
8098
8402
  // Internal state
8099
8403
  currentValue = signal(0, ...(ngDevMode ? [{ debugName: "currentValue" }] : []));
8100
- // Computed value that always returns a number (defaults to 0)
8404
+ /** Configured scale, defaulting to the 0-100 percentage scale. */
8405
+ scaleMin = computed(() => {
8406
+ const v = Number(this.config()?.start_value);
8407
+ return Number.isFinite(v) ? v : 0;
8408
+ }, ...(ngDevMode ? [{ debugName: "scaleMin" }] : []));
8409
+ scaleMax = computed(() => {
8410
+ const v = Number(this.config()?.end_value);
8411
+ return Number.isFinite(v) && v > this.scaleMin() ? v : 100;
8412
+ }, ...(ngDevMode ? [{ debugName: "scaleMax" }] : []));
8413
+ /**
8414
+ * The value the slider edits — the raw stored number, only coerced when it is
8415
+ * not a number at all.
8416
+ *
8417
+ * Deliberately NOT displayValue(): that clamps to the scale for the bar, and
8418
+ * seeding the control with it meant a stored 150 opened the slider at 100 and
8419
+ * the first drag wrote the clamp back as the real value.
8420
+ */
8421
+ editValue = computed(() => {
8422
+ const val = this.currentValue();
8423
+ const num = typeof val === 'number' ? val : Number(val);
8424
+ return Number.isFinite(num) ? num : this.scaleMin();
8425
+ }, ...(ngDevMode ? [{ debugName: "editValue" }] : []));
8426
+ // Bar geometry only — clamped to the scale so the fill never overflows.
8101
8427
  displayValue = computed(() => {
8102
8428
  const val = this.currentValue();
8103
8429
  // Handle null, undefined, or invalid numbers - default to 0
@@ -8107,12 +8433,23 @@ class ProgressComponent {
8107
8433
  // Convert to number if needed
8108
8434
  const numVal = typeof val === 'number' ? val : Number(val);
8109
8435
  // Handle NaN or invalid numbers
8110
- if (isNaN(numVal) || numVal < 0) {
8111
- return 0;
8436
+ if (isNaN(numVal)) {
8437
+ return this.scaleMin();
8112
8438
  }
8113
- // Clamp between 0 and 100
8114
- return Math.max(0, Math.min(100, numVal));
8439
+ // Clamp to the configured scale
8440
+ return Math.max(this.scaleMin(), Math.min(this.scaleMax(), numVal));
8115
8441
  }, ...(ngDevMode ? [{ debugName: "displayValue" }] : []));
8442
+ /**
8443
+ * Bar fill as a percentage of the configured scale. The bar's width is always
8444
+ * 0-100% of its track, whatever units the column counts in — a 3 on a 0-5
8445
+ * scale fills 60%, not 3%.
8446
+ */
8447
+ fillPercent = computed(() => {
8448
+ const span = this.scaleMax() - this.scaleMin();
8449
+ if (span <= 0)
8450
+ return 0;
8451
+ return ((this.displayValue() - this.scaleMin()) / span) * 100;
8452
+ }, ...(ngDevMode ? [{ debugName: "fillPercent" }] : []));
8116
8453
  // Bar colour from the column's configured value ranges (e.g. 0-20 green).
8117
8454
  // Applies in both editable and view/disabled modes. Null when no range
8118
8455
  // matches, so the bar falls back to the default theme colour.
@@ -8141,24 +8478,18 @@ class ProgressComponent {
8141
8478
  // Sync signal-based input with internal state
8142
8479
  effect(() => {
8143
8480
  const value = this.value();
8144
- // Always set to 0 if null/undefined/NaN/invalid, otherwise use the value
8145
- if (value === null || value === undefined || isNaN(value) || value < 0) {
8146
- this.currentValue.set(0);
8147
- }
8148
- else {
8149
- this.currentValue.set(value);
8150
- }
8481
+ // Only a non-number is coerced. A stored value outside the scale — a 150
8482
+ // on a 0-100 column, or a negative is kept as it is and merely drawn
8483
+ // clamped, so opening the cell cannot quietly replace it.
8484
+ this.currentValue.set(value === null || value === undefined || isNaN(value) ? this.scaleMin() : value);
8151
8485
  });
8152
8486
  }
8153
8487
  ngOnInit() {
8154
8488
  // Initialize value if provided, otherwise default to 0
8155
8489
  const initialValue = this.value();
8156
- if (initialValue === null || initialValue === undefined || isNaN(initialValue) || initialValue < 0) {
8157
- this.currentValue.set(0);
8158
- }
8159
- else {
8160
- this.currentValue.set(initialValue);
8161
- }
8490
+ this.currentValue.set(initialValue === null || initialValue === undefined || isNaN(initialValue)
8491
+ ? this.scaleMin()
8492
+ : initialValue);
8162
8493
  }
8163
8494
  onActivate() {
8164
8495
  if (this.isEditable()) {
@@ -8187,11 +8518,11 @@ class ProgressComponent {
8187
8518
  return cfg[property];
8188
8519
  }
8189
8520
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: ProgressComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
8190
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: ProgressComponent, isStandalone: true, selector: "eru-progress", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, isEditable: { classPropertyName: "isEditable", publicName: "isEditable", isSignal: true, isRequired: false, transformFunction: null }, isActive: { classPropertyName: "isActive", publicName: "isActive", isSignal: true, isRequired: false, transformFunction: null }, isDrillable: { classPropertyName: "isDrillable", publicName: "isDrillable", isSignal: true, isRequired: false, transformFunction: null }, columnWidth: { classPropertyName: "columnWidth", publicName: "columnWidth", isSignal: true, isRequired: false, transformFunction: null }, fieldSize: { classPropertyName: "fieldSize", publicName: "fieldSize", isSignal: true, isRequired: false, transformFunction: null }, eruGridStore: { classPropertyName: "eruGridStore", publicName: "eruGridStore", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange", blur: "blur", focus: "focus", drilldownClick: "drilldownClick", editModeChange: "editModeChange" }, ngImport: i0, template: "@if (isActive()) {\n<div class=\"progress-input-container\"\n [style.--mat-slider-active-track-color]=\"barColor()\"\n [style.--mat-slider-disabled-active-track-color]=\"barColor()\"\n [style.--mat-slider-with-tick-marks-active-container-color]=\"barColor()\"\n [style.--mat-slider-handle-color]=\"barColor()\"\n [style.--mat-slider-focus-handle-color]=\"barColor()\"\n [style.--mat-slider-hover-handle-color]=\"barColor()\"\n [style.--mat-slider-disabled-handle-color]=\"barColor()\"\n [style.--mat-slider-ripple-color]=\"barColor()\"\n (dblclick)=\"onActivate(); $event.stopPropagation()\">\n <span class=\"progress-value\" [style.color]=\"barColor()\">{{valueLabel()}}</span>\n <!-- Only ever one of these on screen (the active cell), so it keeps the\n tick marks and the rest of its appearance exactly as before. -->\n <mat-slider\n class=\"progress-slider\"\n [min]=\"0\"\n [max]=\"100\"\n [step]=\"1\"\n [discrete]=\"true\"\n [showTickMarks]=\"true\"\n [disabled]=\"!isEditable()\">\n <input\n matSliderThumb\n [ngModel]=\"displayValue()\"\n (ngModelChange)=\"onValueChange($event)\"\n (blur)=\"onBlur()\"\n [disabled]=\"!isEditable()\">\n </mat-slider>\n</div>\n} @else {\n<!-- Read-only: a plain track + fill rather than a disabled mat-slider.\n The slider mounted here even when the cell was not being edited, and with\n step=1 over 0-100 its tick marks alone were 101 DOM nodes per cell \u2014 on a\n 25-row grid that was 2,525 nodes and the single largest cost in rendering\n the table. It also read as interactive when it was not: the thumb invited\n a drag that did nothing. Double-click still activates, which mounts the\n real slider above. -->\n<div class=\"progress-view-container\" (dblclick)=\"onActivate(); $event.stopPropagation()\">\n <span class=\"progress-value\" [style.color]=\"barColor()\">{{valueLabel()}}</span>\n <div class=\"progress-view-track\">\n <div class=\"progress-view-fill\" [style.width.%]=\"displayValue()\"\n [style.background]=\"barColor()\"></div>\n </div>\n</div>\n}\n", styles: [":root{--mat-slider-with-overlap-handle-outline-color: var(--grid-primary, #6750a4) !important;--mat-slider-with-tick-marks-active-container-color: var(--grid-primary, #6750a4) !important;--mat-slider-handle-height: 12px;--mat-slider-handle-width: 12px;--mat-slider-disabled-active-track-color: var(--grid-primary, #6750a4);--mat-slider-active-track-color: var(--grid-primary, #6750a4)}:host{display:block;height:100%;width:100%}.progress-bar-container{width:100%;height:100%;min-height:30px;position:relative;background:#e0e0e0;border-radius:4px;cursor:pointer}.progress-bar{height:100%;background:var(--grid-primary, #6750a4);transition:width .3s;border-radius:4px}.progress-text{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:var(--cell-font-size, var(--grid-font-size-body, 12px));font-weight:var(--cell-font-weight, 500);color:var(--grid-on-surface, #1d1b20);pointer-events:none;z-index:1}.drillable-value{color:inherit;text-decoration:underline;cursor:pointer;pointer-events:auto}.drillable-value:hover{opacity:.8}.progress-input-container{display:flex;flex-direction:row-reverse;align-items:center;padding:4px;width:100%;box-sizing:border-box;overflow:visible;max-height:30px}.progress-input-container.disabled{cursor:pointer}.progress-input-container.disabled .progress-slider{pointer-events:none}.progress-view-container{display:flex;flex-direction:row-reverse;align-items:center;gap:8px;padding:4px;width:100%;box-sizing:border-box;max-height:30px;cursor:pointer}.progress-view-track{flex:1 1 0%;min-width:0;height:4px;border-radius:2px;background:var(--grid-surface-variant, #e0e0e0);overflow:hidden}.progress-view-fill{height:100%;border-radius:2px;background:var(--grid-primary, #6750a4);transition:width .3s}.progress-value{font-size:var(--cell-font-size, var(--grid-font-size-body, 12px));font-weight:var(--cell-font-weight, 500);color:var(--grid-on-surface, #1d1b20);flex:0 0 auto;min-width:35px;text-align:right;white-space:nowrap;flex-shrink:0}.progress-slider{flex:1 1 0%;min-width:0!important;max-width:100%!important;overflow:visible}.progress-slider .mdc-slider__track{left:0!important}.progress-slider .mdc-slider{padding-left:0!important;margin-left:0!important}.progress-slider .mdc-slider__value-indicator,.progress-slider .mdc-slider__value-indicator-container{display:none!important;visibility:hidden!important}.progress-slider .mdc-slider__thumb:before,.progress-slider .mdc-slider__thumb:after{display:none!important;content:none!important}.progress-slider .mdc-slider__thumb-knob:before,.progress-slider .mdc-slider__thumb-knob:after{display:none!important;content:none!important}.progress-slider [class*=value-indicator],.progress-slider [class*=tooltip]{display:none!important}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatSliderModule }, { kind: "component", type: i2$4.MatSlider, selector: "mat-slider", inputs: ["disabled", "discrete", "showTickMarks", "min", "color", "disableRipple", "max", "step", "displayWith"], exportAs: ["matSlider"] }, { kind: "directive", type: i2$4.MatSliderThumb, selector: "input[matSliderThumb]", inputs: ["value"], outputs: ["valueChange", "dragStart", "dragEnd"], exportAs: ["matSliderThumb"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
8521
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: ProgressComponent, isStandalone: true, selector: "eru-progress", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, isEditable: { classPropertyName: "isEditable", publicName: "isEditable", isSignal: true, isRequired: false, transformFunction: null }, isActive: { classPropertyName: "isActive", publicName: "isActive", isSignal: true, isRequired: false, transformFunction: null }, isDrillable: { classPropertyName: "isDrillable", publicName: "isDrillable", isSignal: true, isRequired: false, transformFunction: null }, columnWidth: { classPropertyName: "columnWidth", publicName: "columnWidth", isSignal: true, isRequired: false, transformFunction: null }, fieldSize: { classPropertyName: "fieldSize", publicName: "fieldSize", isSignal: true, isRequired: false, transformFunction: null }, eruGridStore: { classPropertyName: "eruGridStore", publicName: "eruGridStore", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange", blur: "blur", focus: "focus", drilldownClick: "drilldownClick", editModeChange: "editModeChange" }, ngImport: i0, template: "@if (isActive()) {\n<div class=\"progress-input-container\"\n [style.--mat-slider-active-track-color]=\"barColor()\"\n [style.--mat-slider-disabled-active-track-color]=\"barColor()\"\n [style.--mat-slider-with-tick-marks-active-container-color]=\"barColor()\"\n [style.--mat-slider-handle-color]=\"barColor()\"\n [style.--mat-slider-focus-handle-color]=\"barColor()\"\n [style.--mat-slider-hover-handle-color]=\"barColor()\"\n [style.--mat-slider-disabled-handle-color]=\"barColor()\"\n [style.--mat-slider-ripple-color]=\"barColor()\"\n (dblclick)=\"onActivate(); $event.stopPropagation()\">\n <span class=\"progress-value\" [style.color]=\"barColor()\">{{valueLabel()}}</span>\n <!-- Only ever one of these on screen (the active cell), so it keeps the\n tick marks and the rest of its appearance exactly as before. -->\n <mat-slider\n class=\"progress-slider\"\n [min]=\"scaleMin()\"\n [max]=\"scaleMax()\"\n [step]=\"1\"\n [discrete]=\"true\"\n [showTickMarks]=\"true\"\n [disabled]=\"!isEditable()\">\n <input\n matSliderThumb\n [ngModel]=\"editValue()\"\n (ngModelChange)=\"onValueChange($event)\"\n (blur)=\"onBlur()\"\n [disabled]=\"!isEditable()\">\n </mat-slider>\n</div>\n} @else {\n<!-- Read-only: a plain track + fill rather than a disabled mat-slider.\n The slider mounted here even when the cell was not being edited, and with\n step=1 over 0-100 its tick marks alone were 101 DOM nodes per cell \u2014 on a\n 25-row grid that was 2,525 nodes and the single largest cost in rendering\n the table. It also read as interactive when it was not: the thumb invited\n a drag that did nothing. Double-click still activates, which mounts the\n real slider above. -->\n<div class=\"progress-view-container\" (dblclick)=\"onActivate(); $event.stopPropagation()\">\n <span class=\"progress-value\" [style.color]=\"barColor()\">{{valueLabel()}}</span>\n <div class=\"progress-view-track\">\n <div class=\"progress-view-fill\" [style.width.%]=\"fillPercent()\"\n [style.background]=\"barColor()\"></div>\n </div>\n</div>\n}\n", styles: [":root{--mat-slider-with-overlap-handle-outline-color: var(--grid-primary, #6750a4) !important;--mat-slider-with-tick-marks-active-container-color: var(--grid-primary, #6750a4) !important;--mat-slider-handle-height: 12px;--mat-slider-handle-width: 12px;--mat-slider-disabled-active-track-color: var(--grid-primary, #6750a4);--mat-slider-active-track-color: var(--grid-primary, #6750a4)}:host{display:block;height:100%;width:100%}.progress-bar-container{width:100%;height:100%;min-height:30px;position:relative;background:#e0e0e0;border-radius:4px;cursor:pointer}.progress-bar{height:100%;background:var(--grid-primary, #6750a4);transition:width .3s;border-radius:4px}.progress-text{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:var(--cell-font-size, var(--grid-font-size-body, 12px));font-weight:var(--cell-font-weight, 500);color:var(--grid-on-surface, #1d1b20);pointer-events:none;z-index:1}.drillable-value{color:inherit;text-decoration:underline;cursor:pointer;pointer-events:auto}.drillable-value:hover{opacity:.8}.progress-input-container{display:flex;flex-direction:row-reverse;align-items:center;padding:4px;width:100%;box-sizing:border-box;overflow:visible;max-height:30px}.progress-input-container.disabled{cursor:pointer}.progress-input-container.disabled .progress-slider{pointer-events:none}.progress-view-container{display:flex;flex-direction:row-reverse;align-items:center;gap:8px;padding:4px;width:100%;box-sizing:border-box;max-height:30px;cursor:pointer}.progress-view-track{flex:1 1 0%;min-width:0;height:4px;border-radius:2px;background:var(--grid-surface-variant, #e0e0e0);overflow:hidden}.progress-view-fill{height:100%;border-radius:2px;background:var(--grid-primary, #6750a4);transition:width .3s}.progress-value{font-size:var(--cell-font-size, var(--grid-font-size-body, 12px));font-weight:var(--cell-font-weight, 500);color:var(--grid-on-surface, #1d1b20);flex:0 0 auto;min-width:35px;text-align:right;white-space:nowrap;flex-shrink:0}.progress-slider{flex:1 1 0%;min-width:0!important;max-width:100%!important;overflow:visible}.progress-slider .mdc-slider__track{left:0!important}.progress-slider .mdc-slider{padding-left:0!important;margin-left:0!important}.progress-slider .mdc-slider__value-indicator,.progress-slider .mdc-slider__value-indicator-container{display:none!important;visibility:hidden!important}.progress-slider .mdc-slider__thumb:before,.progress-slider .mdc-slider__thumb:after{display:none!important;content:none!important}.progress-slider .mdc-slider__thumb-knob:before,.progress-slider .mdc-slider__thumb-knob:after{display:none!important;content:none!important}.progress-slider [class*=value-indicator],.progress-slider [class*=tooltip]{display:none!important}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatSliderModule }, { kind: "component", type: i2$4.MatSlider, selector: "mat-slider", inputs: ["disabled", "discrete", "showTickMarks", "min", "color", "disableRipple", "max", "step", "displayWith"], exportAs: ["matSlider"] }, { kind: "directive", type: i2$4.MatSliderThumb, selector: "input[matSliderThumb]", inputs: ["value"], outputs: ["valueChange", "dragStart", "dragEnd"], exportAs: ["matSliderThumb"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
8191
8522
  }
8192
8523
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: ProgressComponent, decorators: [{
8193
8524
  type: Component,
8194
- args: [{ selector: 'eru-progress', standalone: true, imports: [FormsModule, MatSliderModule], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "@if (isActive()) {\n<div class=\"progress-input-container\"\n [style.--mat-slider-active-track-color]=\"barColor()\"\n [style.--mat-slider-disabled-active-track-color]=\"barColor()\"\n [style.--mat-slider-with-tick-marks-active-container-color]=\"barColor()\"\n [style.--mat-slider-handle-color]=\"barColor()\"\n [style.--mat-slider-focus-handle-color]=\"barColor()\"\n [style.--mat-slider-hover-handle-color]=\"barColor()\"\n [style.--mat-slider-disabled-handle-color]=\"barColor()\"\n [style.--mat-slider-ripple-color]=\"barColor()\"\n (dblclick)=\"onActivate(); $event.stopPropagation()\">\n <span class=\"progress-value\" [style.color]=\"barColor()\">{{valueLabel()}}</span>\n <!-- Only ever one of these on screen (the active cell), so it keeps the\n tick marks and the rest of its appearance exactly as before. -->\n <mat-slider\n class=\"progress-slider\"\n [min]=\"0\"\n [max]=\"100\"\n [step]=\"1\"\n [discrete]=\"true\"\n [showTickMarks]=\"true\"\n [disabled]=\"!isEditable()\">\n <input\n matSliderThumb\n [ngModel]=\"displayValue()\"\n (ngModelChange)=\"onValueChange($event)\"\n (blur)=\"onBlur()\"\n [disabled]=\"!isEditable()\">\n </mat-slider>\n</div>\n} @else {\n<!-- Read-only: a plain track + fill rather than a disabled mat-slider.\n The slider mounted here even when the cell was not being edited, and with\n step=1 over 0-100 its tick marks alone were 101 DOM nodes per cell \u2014 on a\n 25-row grid that was 2,525 nodes and the single largest cost in rendering\n the table. It also read as interactive when it was not: the thumb invited\n a drag that did nothing. Double-click still activates, which mounts the\n real slider above. -->\n<div class=\"progress-view-container\" (dblclick)=\"onActivate(); $event.stopPropagation()\">\n <span class=\"progress-value\" [style.color]=\"barColor()\">{{valueLabel()}}</span>\n <div class=\"progress-view-track\">\n <div class=\"progress-view-fill\" [style.width.%]=\"displayValue()\"\n [style.background]=\"barColor()\"></div>\n </div>\n</div>\n}\n", styles: [":root{--mat-slider-with-overlap-handle-outline-color: var(--grid-primary, #6750a4) !important;--mat-slider-with-tick-marks-active-container-color: var(--grid-primary, #6750a4) !important;--mat-slider-handle-height: 12px;--mat-slider-handle-width: 12px;--mat-slider-disabled-active-track-color: var(--grid-primary, #6750a4);--mat-slider-active-track-color: var(--grid-primary, #6750a4)}:host{display:block;height:100%;width:100%}.progress-bar-container{width:100%;height:100%;min-height:30px;position:relative;background:#e0e0e0;border-radius:4px;cursor:pointer}.progress-bar{height:100%;background:var(--grid-primary, #6750a4);transition:width .3s;border-radius:4px}.progress-text{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:var(--cell-font-size, var(--grid-font-size-body, 12px));font-weight:var(--cell-font-weight, 500);color:var(--grid-on-surface, #1d1b20);pointer-events:none;z-index:1}.drillable-value{color:inherit;text-decoration:underline;cursor:pointer;pointer-events:auto}.drillable-value:hover{opacity:.8}.progress-input-container{display:flex;flex-direction:row-reverse;align-items:center;padding:4px;width:100%;box-sizing:border-box;overflow:visible;max-height:30px}.progress-input-container.disabled{cursor:pointer}.progress-input-container.disabled .progress-slider{pointer-events:none}.progress-view-container{display:flex;flex-direction:row-reverse;align-items:center;gap:8px;padding:4px;width:100%;box-sizing:border-box;max-height:30px;cursor:pointer}.progress-view-track{flex:1 1 0%;min-width:0;height:4px;border-radius:2px;background:var(--grid-surface-variant, #e0e0e0);overflow:hidden}.progress-view-fill{height:100%;border-radius:2px;background:var(--grid-primary, #6750a4);transition:width .3s}.progress-value{font-size:var(--cell-font-size, var(--grid-font-size-body, 12px));font-weight:var(--cell-font-weight, 500);color:var(--grid-on-surface, #1d1b20);flex:0 0 auto;min-width:35px;text-align:right;white-space:nowrap;flex-shrink:0}.progress-slider{flex:1 1 0%;min-width:0!important;max-width:100%!important;overflow:visible}.progress-slider .mdc-slider__track{left:0!important}.progress-slider .mdc-slider{padding-left:0!important;margin-left:0!important}.progress-slider .mdc-slider__value-indicator,.progress-slider .mdc-slider__value-indicator-container{display:none!important;visibility:hidden!important}.progress-slider .mdc-slider__thumb:before,.progress-slider .mdc-slider__thumb:after{display:none!important;content:none!important}.progress-slider .mdc-slider__thumb-knob:before,.progress-slider .mdc-slider__thumb-knob:after{display:none!important;content:none!important}.progress-slider [class*=value-indicator],.progress-slider [class*=tooltip]{display:none!important}\n"] }]
8525
+ args: [{ selector: 'eru-progress', standalone: true, imports: [FormsModule, MatSliderModule], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "@if (isActive()) {\n<div class=\"progress-input-container\"\n [style.--mat-slider-active-track-color]=\"barColor()\"\n [style.--mat-slider-disabled-active-track-color]=\"barColor()\"\n [style.--mat-slider-with-tick-marks-active-container-color]=\"barColor()\"\n [style.--mat-slider-handle-color]=\"barColor()\"\n [style.--mat-slider-focus-handle-color]=\"barColor()\"\n [style.--mat-slider-hover-handle-color]=\"barColor()\"\n [style.--mat-slider-disabled-handle-color]=\"barColor()\"\n [style.--mat-slider-ripple-color]=\"barColor()\"\n (dblclick)=\"onActivate(); $event.stopPropagation()\">\n <span class=\"progress-value\" [style.color]=\"barColor()\">{{valueLabel()}}</span>\n <!-- Only ever one of these on screen (the active cell), so it keeps the\n tick marks and the rest of its appearance exactly as before. -->\n <mat-slider\n class=\"progress-slider\"\n [min]=\"scaleMin()\"\n [max]=\"scaleMax()\"\n [step]=\"1\"\n [discrete]=\"true\"\n [showTickMarks]=\"true\"\n [disabled]=\"!isEditable()\">\n <input\n matSliderThumb\n [ngModel]=\"editValue()\"\n (ngModelChange)=\"onValueChange($event)\"\n (blur)=\"onBlur()\"\n [disabled]=\"!isEditable()\">\n </mat-slider>\n</div>\n} @else {\n<!-- Read-only: a plain track + fill rather than a disabled mat-slider.\n The slider mounted here even when the cell was not being edited, and with\n step=1 over 0-100 its tick marks alone were 101 DOM nodes per cell \u2014 on a\n 25-row grid that was 2,525 nodes and the single largest cost in rendering\n the table. It also read as interactive when it was not: the thumb invited\n a drag that did nothing. Double-click still activates, which mounts the\n real slider above. -->\n<div class=\"progress-view-container\" (dblclick)=\"onActivate(); $event.stopPropagation()\">\n <span class=\"progress-value\" [style.color]=\"barColor()\">{{valueLabel()}}</span>\n <div class=\"progress-view-track\">\n <div class=\"progress-view-fill\" [style.width.%]=\"fillPercent()\"\n [style.background]=\"barColor()\"></div>\n </div>\n</div>\n}\n", styles: [":root{--mat-slider-with-overlap-handle-outline-color: var(--grid-primary, #6750a4) !important;--mat-slider-with-tick-marks-active-container-color: var(--grid-primary, #6750a4) !important;--mat-slider-handle-height: 12px;--mat-slider-handle-width: 12px;--mat-slider-disabled-active-track-color: var(--grid-primary, #6750a4);--mat-slider-active-track-color: var(--grid-primary, #6750a4)}:host{display:block;height:100%;width:100%}.progress-bar-container{width:100%;height:100%;min-height:30px;position:relative;background:#e0e0e0;border-radius:4px;cursor:pointer}.progress-bar{height:100%;background:var(--grid-primary, #6750a4);transition:width .3s;border-radius:4px}.progress-text{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:var(--cell-font-size, var(--grid-font-size-body, 12px));font-weight:var(--cell-font-weight, 500);color:var(--grid-on-surface, #1d1b20);pointer-events:none;z-index:1}.drillable-value{color:inherit;text-decoration:underline;cursor:pointer;pointer-events:auto}.drillable-value:hover{opacity:.8}.progress-input-container{display:flex;flex-direction:row-reverse;align-items:center;padding:4px;width:100%;box-sizing:border-box;overflow:visible;max-height:30px}.progress-input-container.disabled{cursor:pointer}.progress-input-container.disabled .progress-slider{pointer-events:none}.progress-view-container{display:flex;flex-direction:row-reverse;align-items:center;gap:8px;padding:4px;width:100%;box-sizing:border-box;max-height:30px;cursor:pointer}.progress-view-track{flex:1 1 0%;min-width:0;height:4px;border-radius:2px;background:var(--grid-surface-variant, #e0e0e0);overflow:hidden}.progress-view-fill{height:100%;border-radius:2px;background:var(--grid-primary, #6750a4);transition:width .3s}.progress-value{font-size:var(--cell-font-size, var(--grid-font-size-body, 12px));font-weight:var(--cell-font-weight, 500);color:var(--grid-on-surface, #1d1b20);flex:0 0 auto;min-width:35px;text-align:right;white-space:nowrap;flex-shrink:0}.progress-slider{flex:1 1 0%;min-width:0!important;max-width:100%!important;overflow:visible}.progress-slider .mdc-slider__track{left:0!important}.progress-slider .mdc-slider{padding-left:0!important;margin-left:0!important}.progress-slider .mdc-slider__value-indicator,.progress-slider .mdc-slider__value-indicator-container{display:none!important;visibility:hidden!important}.progress-slider .mdc-slider__thumb:before,.progress-slider .mdc-slider__thumb:after{display:none!important;content:none!important}.progress-slider .mdc-slider__thumb-knob:before,.progress-slider .mdc-slider__thumb-knob:after{display:none!important;content:none!important}.progress-slider [class*=value-indicator],.progress-slider [class*=tooltip]{display:none!important}\n"] }]
8195
8526
  }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], isEditable: [{ type: i0.Input, args: [{ isSignal: true, alias: "isEditable", required: false }] }], isActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "isActive", required: false }] }], isDrillable: [{ type: i0.Input, args: [{ isSignal: true, alias: "isDrillable", required: false }] }], columnWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnWidth", required: false }] }], fieldSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "fieldSize", required: false }] }], eruGridStore: [{ type: i0.Input, args: [{ isSignal: true, alias: "eruGridStore", required: false }] }], valueChange: [{
8196
8527
  type: Output
8197
8528
  }], blur: [{
@@ -10453,6 +10784,8 @@ class AttachmentComponent {
10453
10784
  drilldownClick = new EventEmitter();
10454
10785
  editModeChange = new EventEmitter();
10455
10786
  fileAdded = new EventEmitter();
10787
+ /** The file's contents and where it is meant to go, for a host that uploads. */
10788
+ fileUpload = new EventEmitter();
10456
10789
  fileDeleted = new EventEmitter();
10457
10790
  fileViewed = new EventEmitter();
10458
10791
  // Internal state
@@ -10664,8 +10997,14 @@ class AttachmentComponent {
10664
10997
  reader.readAsDataURL(file);
10665
10998
  }
10666
10999
  uploadFile(base64String, fileName) {
10667
- // This can be extended to call an upload service
10668
- // For now, just emit the file data
11000
+ const cfg = this.config();
11001
+ this.fileUpload.emit({
11002
+ fileName,
11003
+ file: base64String,
11004
+ storage_name: cfg.storageName ?? '',
11005
+ folder_name: cfg.folderName ?? '',
11006
+ default_upload: cfg.defaultUpload !== false,
11007
+ });
10669
11008
  }
10670
11009
  viewFile(file, event) {
10671
11010
  if (event) {
@@ -10722,7 +11061,7 @@ class AttachmentComponent {
10722
11061
  return file;
10723
11062
  }
10724
11063
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AttachmentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10725
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: AttachmentComponent, isStandalone: true, selector: "eru-attachment", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, isEditable: { classPropertyName: "isEditable", publicName: "isEditable", isSignal: true, isRequired: false, transformFunction: null }, isActive: { classPropertyName: "isActive", publicName: "isActive", isSignal: true, isRequired: false, transformFunction: null }, isDrillable: { classPropertyName: "isDrillable", publicName: "isDrillable", isSignal: true, isRequired: false, transformFunction: null }, columnWidth: { classPropertyName: "columnWidth", publicName: "columnWidth", isSignal: true, isRequired: false, transformFunction: null }, eruGridStore: { classPropertyName: "eruGridStore", publicName: "eruGridStore", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange", blur: "blur", focus: "focus", drilldownClick: "drilldownClick", editModeChange: "editModeChange", fileAdded: "fileAdded", fileDeleted: "fileDeleted", fileViewed: "fileViewed" }, viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true }], ngImport: i0, template: "<div class=\"attachment-overlay-container\">\n <div \n class=\"attachment-cell-wrapper\"\n cdkOverlayOrigin \n #attachmentTrigger=\"cdkOverlayOrigin\"\n [class.attachment-display-editable]=\"isEditable() && !isActive()\"\n [class.attachment-display-viewable]=\"!isEditable() && hasFiles()\"\n (dblclick)=\"onActivate()\">\n @if(hasFiles()) {\n <div class=\"cell-attachment-container\">\n @if (isDrillable() && !isActive()) {\n <span class=\"drillable-value\" (click)=\"onDrillableClick($event)\">\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" clip-rule=\"evenodd\"\n d=\"M19 12.5C19 14.985 15.866 17 12 17C8.134 17 5 14.985 5 12.5C5 10.015 8.134 8 12 8C15.866 8 19 10.015 19 12.5Z\"\n stroke=\"#7C818C\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n <path fill-rule=\"evenodd\" clip-rule=\"evenodd\"\n d=\"M13.75 12.5001C13.7716 13.1394 13.4429 13.7397 12.8925 14.0657C12.3422 14.3918 11.6578 14.3918 11.1075 14.0657C10.5571 13.7397 10.2284 13.1394 10.25 12.5001C10.2284 11.8608 10.5571 11.2606 11.1075 10.9345C11.6578 10.6084 12.3422 10.6084 12.8925 10.9345C13.4429 11.2606 13.7716 11.8608 13.75 12.5001V12.5001Z\"\n stroke=\"#7C818C\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n </svg>\n </span>\n } @else {\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" clip-rule=\"evenodd\"\n d=\"M19 12.5C19 14.985 15.866 17 12 17C8.134 17 5 14.985 5 12.5C5 10.015 8.134 8 12 8C15.866 8 19 10.015 19 12.5Z\"\n stroke=\"#7C818C\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n <path fill-rule=\"evenodd\" clip-rule=\"evenodd\"\n d=\"M13.75 12.5001C13.7716 13.1394 13.4429 13.7397 12.8925 14.0657C12.3422 14.3918 11.6578 14.3918 11.1075 14.0657C10.5571 13.7397 10.2284 13.1394 10.25 12.5001C10.2284 11.8608 10.5571 11.2606 11.1075 10.9345C11.6578 10.6084 12.3422 10.6084 12.8925 10.9345C13.4429 11.2606 13.7716 11.8608 13.75 12.5001V12.5001Z\"\n stroke=\"#7C818C\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n </svg>\n }\n </div>\n } @else {\n <div class=\"cell-attachment-container\">\n @if (isDrillable() && !isActive()) {\n <span class=\"drillable-value\" (click)=\"onDrillableClick($event)\">\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path\n d=\"M14.67 11.053L10.68 15.315C10.3416 15.6932 9.85986 15.9119 9.35236 15.9178C8.84487 15.9237 8.35821 15.7162 8.01104 15.346C7.24412 14.5454 7.257 13.2788 8.04004 12.494L13.399 6.763C13.9902 6.10491 14.8315 5.72677 15.7161 5.72163C16.6006 5.71649 17.4463 6.08482 18.045 6.736C19.3222 8.14736 19.3131 10.2995 18.024 11.7L12.342 17.771C11.5334 18.5827 10.4265 19.0261 9.28113 18.9971C8.13575 18.9682 7.05268 18.4695 6.28604 17.618C4.5337 15.6414 4.57705 12.6549 6.38604 10.73L11.753 5\"\n stroke=\"#363B44\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n </svg>\n </span>\n } @else {\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path\n d=\"M14.67 11.053L10.68 15.315C10.3416 15.6932 9.85986 15.9119 9.35236 15.9178C8.84487 15.9237 8.35821 15.7162 8.01104 15.346C7.24412 14.5454 7.257 13.2788 8.04004 12.494L13.399 6.763C13.9902 6.10491 14.8315 5.72677 15.7161 5.72163C16.6006 5.71649 17.4463 6.08482 18.045 6.736C19.3222 8.14736 19.3131 10.2995 18.024 11.7L12.342 17.771C11.5334 18.5827 10.4265 19.0261 9.28113 18.9971C8.13575 18.9682 7.05268 18.4695 6.28604 17.618C4.5337 15.6414 4.57705 12.6549 6.38604 10.73L11.753 5\"\n stroke=\"#363B44\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n </svg>\n }\n </div>\n }\n </div>\n\n <ng-template \n cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"attachmentTrigger\"\n [cdkConnectedOverlayOpen]=\"isOverlayOpen() && (isActive() || !isEditable())\"\n [cdkConnectedOverlayHasBackdrop]=\"true\"\n [cdkConnectedOverlayBackdropClass]=\"'attachment-backdrop'\"\n (backdropClick)=\"onOverlayBackdropClick()\"\n (detach)=\"closeOverlay()\">\n <div \n class=\"attachment-menu-overlay\" \n [style.width.px]=\"columnWidth() || config().columnWidth || 300\" \n [style.min-width.px]=\"300\"\n (click)=\"onOverlayClick($event)\">\n <div class=\"attachment-container\" (click)=\"$event.stopPropagation()\">\n @if (rejectionMessage()) {\n <div class=\"attachment-rejection\" role=\"alert\">{{ rejectionMessage() }}</div>\n }\n @if (isEditable()) {\n <!-- Header Section -->\n <div class=\"attachment-header\">\n <span class=\"attachment-title\">Add or Drag files</span>\n <button\n mat-flat-button\n (click)=\"onUploadClick(); $event.stopPropagation()\"\n class=\"attachment-upload-btn\"\n [disabled]=\"config().disabled\">\n Upload\n </button>\n </div>\n\n <!-- Drop zone -->\n <div\n class=\"attachment-drop-zone\"\n (drop)=\"onFileDrop($event)\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (paste)=\"onPaste($event)\"\n (click)=\"onUploadClick(); $event.stopPropagation()\"\n [class.dragging]=\"isDragging()\">\n <div class=\"attachment-drop-content\">\n <div class=\"attachment-drop-icon\">+</div>\n <div class=\"attachment-drop-text\">\n <div class=\"primary-text\">Click to upload or drag files here</div>\n <div class=\"secondary-text\">Support multiple files</div>\n </div>\n </div>\n </div>\n\n <!-- Hidden file input -->\n <input\n #fileInput\n type=\"file\"\n multiple\n (change)=\"onFileInputChange($event)\"\n style=\"display: none;\">\n } @else {\n <!-- Read-only header -->\n <div class=\"attachment-header\">\n <span class=\"attachment-title\">Attachments</span>\n </div>\n }\n\n <!-- File list -->\n @if (hasFiles()) {\n <div class=\"attachment-file-list\">\n @for (file of currentValue(); track $index) {\n <div class=\"attachment-file-item\">\n <div class=\"file-info\">\n <mat-icon class=\"file-status-icon done-icon\">check_circle</mat-icon>\n <span class=\"file-name\">{{ getFileName(file) }}</span>\n </div>\n <div class=\"file-actions\">\n <button\n mat-icon-button\n class=\"file-action-btn\"\n (click)=\"viewFile(file, $event)\"\n title=\"View\"\n type=\"button\">\n <mat-icon>visibility</mat-icon>\n </button>\n @if (isEditable()) {\n <button\n mat-icon-button\n class=\"file-action-btn\"\n (click)=\"deleteFile(file, $event)\"\n title=\"Delete\"\n type=\"button\">\n <mat-icon>delete_outline</mat-icon>\n </button>\n }\n </div>\n </div>\n }\n </div>\n }\n </div>\n </div>\n </ng-template>\n</div>\n\n", styles: [".attachment-rejection{padding:8px 10px;margin-bottom:8px;border-radius:6px;background:#fef2f2;color:var(--cell-color, #b91c1c);font-size:var(--cell-font-size, 12px);line-height:1.4}.attachment-overlay-container{width:100%;height:100%}.attachment-cell-wrapper{display:flex;align-items:center;justify-content:center;padding:4px;min-height:32px;cursor:default}.attachment-cell-wrapper.attachment-display-editable{cursor:pointer}.attachment-cell-wrapper.attachment-display-editable:hover{background-color:#0000000a}.attachment-cell-wrapper.attachment-display-viewable{cursor:pointer}.attachment-cell-wrapper.attachment-display-viewable:hover{background-color:#0000000a}.cell-attachment-container{display:flex;align-items:center;justify-content:center;width:100%;height:100%}.drillable-value{display:flex;align-items:center;justify-content:center;cursor:pointer}.drillable-value svg{text-decoration:none}.drillable-value:hover{opacity:.8}.attachment-backdrop{background:transparent}.attachment-menu-overlay{background:#fff;border-radius:8px;box-shadow:0 4px 12px #00000026;overflow:hidden;position:relative;z-index:1000}.attachment-container{padding:16px}.attachment-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.attachment-title{font-size:var(--cell-font-size, var(--grid-font-size-body, 12px));font-weight:var(--cell-font-weight, 500);color:var(--cell-color, var(--grid-on-surface, #1c1b1f))}.attachment-upload-btn{font-size:var(--cell-font-size, var(--grid-font-size-body, 12px))!important;min-width:50px}.attachment-drop-zone{border:2px dashed var(--grid-outline-variant, #cac4d0);border-radius:8px;padding:40px 20px;text-align:center;cursor:pointer;transition:all .3s ease;background:var(--grid-surface-variant, #e7e0ec);margin:16px 0}.attachment-drop-zone:hover{border-color:var(--grid-primary, #6750a4);background:var(--grid-surface-container, #f3edf7)}.attachment-drop-zone.dragging{border-color:var(--grid-primary, #6750a4);background:var(--grid-surface-container, #f3edf7);transform:scale(1.01)}.attachment-drop-content{display:flex;flex-direction:column;align-items:center;gap:12px}.attachment-drop-icon{font-size:var(--cell-font-size, 48px);color:var(--cell-color, var(--grid-primary, #6750a4));font-weight:var(--cell-font-weight, 300);line-height:1}.attachment-drop-text .primary-text{font-family:var(--grid-font-family, \"Poppins\");font-size:var(--cell-font-size, 14px);color:var(--cell-color, var(--grid-on-surface-variant, #49454f));font-weight:var(--cell-font-weight, 400);margin:0}.attachment-drop-text .secondary-text{font-family:var(--grid-font-family, \"Poppins\");font-size:var(--cell-font-size, 12px);color:var(--cell-color, var(--grid-on-surface-variant, #49454f));margin-top:4px;display:block}.attachment-file-list{display:block;margin-top:16px;max-height:300px;overflow-y:auto}.attachment-file-item{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;border-radius:4px;margin-bottom:8px;background:var(--grid-surface-variant, #e7e0ec);transition:background-color .2s}.attachment-file-item:hover{background:var(--grid-surface-container, #f3edf7)}.file-info{display:flex;align-items:center;gap:8px;flex:1;min-width:0}.file-status-icon{font-size:var(--cell-font-size, 18px)!important;width:18px!important;height:18px!important;flex-shrink:0}.file-status-icon.done-icon{color:var(--cell-color, #22C55E)}.file-name{font-size:var(--cell-font-size, var(--grid-font-size-body, 12px));color:var(--cell-color, var(--grid-on-surface, #1c1b1f));overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.file-actions{display:flex;gap:8px;flex-shrink:0}.file-action-btn{width:28px!important;height:28px!important;padding:0!important;display:flex!important;align-items:center;justify-content:center}.file-action-btn .mat-icon{font-size:var(--cell-font-size, 18px);width:18px;height:18px;color:var(--cell-color, var(--grid-on-surface-variant, #49454f))}.file-action-btn:hover .mat-icon{color:var(--cell-color, var(--grid-on-surface, #1c1b1f))}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "ngmodule", type: MatInputModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1$2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i1$2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: OverlayModule }, { kind: "directive", type: i1$3.CdkConnectedOverlay, selector: "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", inputs: ["cdkConnectedOverlayOrigin", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPositionStrategy", "cdkConnectedOverlayOffsetX", "cdkConnectedOverlayOffsetY", "cdkConnectedOverlayWidth", "cdkConnectedOverlayHeight", "cdkConnectedOverlayMinWidth", "cdkConnectedOverlayMinHeight", "cdkConnectedOverlayBackdropClass", "cdkConnectedOverlayPanelClass", "cdkConnectedOverlayViewportMargin", "cdkConnectedOverlayScrollStrategy", "cdkConnectedOverlayOpen", "cdkConnectedOverlayDisableClose", "cdkConnectedOverlayTransformOriginOn", "cdkConnectedOverlayHasBackdrop", "cdkConnectedOverlayLockPosition", "cdkConnectedOverlayFlexibleDimensions", "cdkConnectedOverlayGrowAfterOpen", "cdkConnectedOverlayPush", "cdkConnectedOverlayDisposeOnNavigation", "cdkConnectedOverlayUsePopover", "cdkConnectedOverlayMatchWidth", "cdkConnectedOverlay"], outputs: ["backdropClick", "positionChange", "attach", "detach", "overlayKeydown", "overlayOutsideClick"], exportAs: ["cdkConnectedOverlay"] }, { kind: "directive", type: i1$3.CdkOverlayOrigin, selector: "[cdk-overlay-origin], [overlay-origin], [cdkOverlayOrigin]", exportAs: ["cdkOverlayOrigin"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
11064
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: AttachmentComponent, isStandalone: true, selector: "eru-attachment", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, isEditable: { classPropertyName: "isEditable", publicName: "isEditable", isSignal: true, isRequired: false, transformFunction: null }, isActive: { classPropertyName: "isActive", publicName: "isActive", isSignal: true, isRequired: false, transformFunction: null }, isDrillable: { classPropertyName: "isDrillable", publicName: "isDrillable", isSignal: true, isRequired: false, transformFunction: null }, columnWidth: { classPropertyName: "columnWidth", publicName: "columnWidth", isSignal: true, isRequired: false, transformFunction: null }, eruGridStore: { classPropertyName: "eruGridStore", publicName: "eruGridStore", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange", blur: "blur", focus: "focus", drilldownClick: "drilldownClick", editModeChange: "editModeChange", fileAdded: "fileAdded", fileUpload: "fileUpload", fileDeleted: "fileDeleted", fileViewed: "fileViewed" }, viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true }], ngImport: i0, template: "<div class=\"attachment-overlay-container\">\n <div \n class=\"attachment-cell-wrapper\"\n cdkOverlayOrigin \n #attachmentTrigger=\"cdkOverlayOrigin\"\n [class.attachment-display-editable]=\"isEditable() && !isActive()\"\n [class.attachment-display-viewable]=\"!isEditable() && hasFiles()\"\n (dblclick)=\"onActivate()\">\n @if(hasFiles()) {\n <div class=\"cell-attachment-container\">\n @if (isDrillable() && !isActive()) {\n <span class=\"drillable-value\" (click)=\"onDrillableClick($event)\">\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" clip-rule=\"evenodd\"\n d=\"M19 12.5C19 14.985 15.866 17 12 17C8.134 17 5 14.985 5 12.5C5 10.015 8.134 8 12 8C15.866 8 19 10.015 19 12.5Z\"\n stroke=\"#7C818C\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n <path fill-rule=\"evenodd\" clip-rule=\"evenodd\"\n d=\"M13.75 12.5001C13.7716 13.1394 13.4429 13.7397 12.8925 14.0657C12.3422 14.3918 11.6578 14.3918 11.1075 14.0657C10.5571 13.7397 10.2284 13.1394 10.25 12.5001C10.2284 11.8608 10.5571 11.2606 11.1075 10.9345C11.6578 10.6084 12.3422 10.6084 12.8925 10.9345C13.4429 11.2606 13.7716 11.8608 13.75 12.5001V12.5001Z\"\n stroke=\"#7C818C\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n </svg>\n </span>\n } @else {\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" clip-rule=\"evenodd\"\n d=\"M19 12.5C19 14.985 15.866 17 12 17C8.134 17 5 14.985 5 12.5C5 10.015 8.134 8 12 8C15.866 8 19 10.015 19 12.5Z\"\n stroke=\"#7C818C\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n <path fill-rule=\"evenodd\" clip-rule=\"evenodd\"\n d=\"M13.75 12.5001C13.7716 13.1394 13.4429 13.7397 12.8925 14.0657C12.3422 14.3918 11.6578 14.3918 11.1075 14.0657C10.5571 13.7397 10.2284 13.1394 10.25 12.5001C10.2284 11.8608 10.5571 11.2606 11.1075 10.9345C11.6578 10.6084 12.3422 10.6084 12.8925 10.9345C13.4429 11.2606 13.7716 11.8608 13.75 12.5001V12.5001Z\"\n stroke=\"#7C818C\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n </svg>\n }\n </div>\n } @else {\n <div class=\"cell-attachment-container\">\n @if (isDrillable() && !isActive()) {\n <span class=\"drillable-value\" (click)=\"onDrillableClick($event)\">\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path\n d=\"M14.67 11.053L10.68 15.315C10.3416 15.6932 9.85986 15.9119 9.35236 15.9178C8.84487 15.9237 8.35821 15.7162 8.01104 15.346C7.24412 14.5454 7.257 13.2788 8.04004 12.494L13.399 6.763C13.9902 6.10491 14.8315 5.72677 15.7161 5.72163C16.6006 5.71649 17.4463 6.08482 18.045 6.736C19.3222 8.14736 19.3131 10.2995 18.024 11.7L12.342 17.771C11.5334 18.5827 10.4265 19.0261 9.28113 18.9971C8.13575 18.9682 7.05268 18.4695 6.28604 17.618C4.5337 15.6414 4.57705 12.6549 6.38604 10.73L11.753 5\"\n stroke=\"#363B44\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n </svg>\n </span>\n } @else {\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path\n d=\"M14.67 11.053L10.68 15.315C10.3416 15.6932 9.85986 15.9119 9.35236 15.9178C8.84487 15.9237 8.35821 15.7162 8.01104 15.346C7.24412 14.5454 7.257 13.2788 8.04004 12.494L13.399 6.763C13.9902 6.10491 14.8315 5.72677 15.7161 5.72163C16.6006 5.71649 17.4463 6.08482 18.045 6.736C19.3222 8.14736 19.3131 10.2995 18.024 11.7L12.342 17.771C11.5334 18.5827 10.4265 19.0261 9.28113 18.9971C8.13575 18.9682 7.05268 18.4695 6.28604 17.618C4.5337 15.6414 4.57705 12.6549 6.38604 10.73L11.753 5\"\n stroke=\"#363B44\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n </svg>\n }\n </div>\n }\n </div>\n\n <ng-template \n cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"attachmentTrigger\"\n [cdkConnectedOverlayOpen]=\"isOverlayOpen() && (isActive() || !isEditable())\"\n [cdkConnectedOverlayHasBackdrop]=\"true\"\n [cdkConnectedOverlayBackdropClass]=\"'attachment-backdrop'\"\n (backdropClick)=\"onOverlayBackdropClick()\"\n (detach)=\"closeOverlay()\">\n <div \n class=\"attachment-menu-overlay\" \n [style.width.px]=\"columnWidth() || config().columnWidth || 300\" \n [style.min-width.px]=\"300\"\n (click)=\"onOverlayClick($event)\">\n <div class=\"attachment-container\" (click)=\"$event.stopPropagation()\">\n @if (rejectionMessage()) {\n <div class=\"attachment-rejection\" role=\"alert\">{{ rejectionMessage() }}</div>\n }\n @if (isEditable()) {\n <!-- Header Section -->\n <div class=\"attachment-header\">\n <span class=\"attachment-title\">Add or Drag files</span>\n <button\n mat-flat-button\n (click)=\"onUploadClick(); $event.stopPropagation()\"\n class=\"attachment-upload-btn\"\n [disabled]=\"config().disabled\">\n Upload\n </button>\n </div>\n\n <!-- Drop zone -->\n <div\n class=\"attachment-drop-zone\"\n (drop)=\"onFileDrop($event)\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (paste)=\"onPaste($event)\"\n (click)=\"onUploadClick(); $event.stopPropagation()\"\n [class.dragging]=\"isDragging()\">\n <div class=\"attachment-drop-content\">\n <div class=\"attachment-drop-icon\">+</div>\n <div class=\"attachment-drop-text\">\n <div class=\"primary-text\">Click to upload or drag files here</div>\n <div class=\"secondary-text\">Support multiple files</div>\n </div>\n </div>\n </div>\n\n <!-- Hidden file input -->\n <input\n #fileInput\n type=\"file\"\n multiple\n (change)=\"onFileInputChange($event)\"\n style=\"display: none;\">\n } @else {\n <!-- Read-only header -->\n <div class=\"attachment-header\">\n <span class=\"attachment-title\">Attachments</span>\n </div>\n }\n\n <!-- File list -->\n @if (hasFiles()) {\n <div class=\"attachment-file-list\">\n @for (file of currentValue(); track $index) {\n <div class=\"attachment-file-item\">\n <div class=\"file-info\">\n <mat-icon class=\"file-status-icon done-icon\">check_circle</mat-icon>\n <span class=\"file-name\">{{ getFileName(file) }}</span>\n </div>\n <div class=\"file-actions\">\n <button\n mat-icon-button\n class=\"file-action-btn\"\n (click)=\"viewFile(file, $event)\"\n title=\"View\"\n type=\"button\">\n <mat-icon>visibility</mat-icon>\n </button>\n @if (isEditable()) {\n <button\n mat-icon-button\n class=\"file-action-btn\"\n (click)=\"deleteFile(file, $event)\"\n title=\"Delete\"\n type=\"button\">\n <mat-icon>delete_outline</mat-icon>\n </button>\n }\n </div>\n </div>\n }\n </div>\n }\n </div>\n </div>\n </ng-template>\n</div>\n\n", styles: [".attachment-rejection{padding:8px 10px;margin-bottom:8px;border-radius:6px;background:#fef2f2;color:var(--cell-color, #b91c1c);font-size:var(--cell-font-size, 12px);line-height:1.4}.attachment-overlay-container{width:100%;height:100%}.attachment-cell-wrapper{display:flex;align-items:center;justify-content:center;padding:4px;min-height:32px;cursor:default}.attachment-cell-wrapper.attachment-display-editable{cursor:pointer}.attachment-cell-wrapper.attachment-display-editable:hover{background-color:#0000000a}.attachment-cell-wrapper.attachment-display-viewable{cursor:pointer}.attachment-cell-wrapper.attachment-display-viewable:hover{background-color:#0000000a}.cell-attachment-container{display:flex;align-items:center;justify-content:center;width:100%;height:100%}.drillable-value{display:flex;align-items:center;justify-content:center;cursor:pointer}.drillable-value svg{text-decoration:none}.drillable-value:hover{opacity:.8}.attachment-backdrop{background:transparent}.attachment-menu-overlay{background:#fff;border-radius:8px;box-shadow:0 4px 12px #00000026;overflow:hidden;position:relative;z-index:1000}.attachment-container{padding:16px}.attachment-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.attachment-title{font-size:var(--cell-font-size, var(--grid-font-size-body, 12px));font-weight:var(--cell-font-weight, 500);color:var(--cell-color, var(--grid-on-surface, #1c1b1f))}.attachment-upload-btn{font-size:var(--cell-font-size, var(--grid-font-size-body, 12px))!important;min-width:50px}.attachment-drop-zone{border:2px dashed var(--grid-outline-variant, #cac4d0);border-radius:8px;padding:40px 20px;text-align:center;cursor:pointer;transition:all .3s ease;background:var(--grid-surface-variant, #e7e0ec);margin:16px 0}.attachment-drop-zone:hover{border-color:var(--grid-primary, #6750a4);background:var(--grid-surface-container, #f3edf7)}.attachment-drop-zone.dragging{border-color:var(--grid-primary, #6750a4);background:var(--grid-surface-container, #f3edf7);transform:scale(1.01)}.attachment-drop-content{display:flex;flex-direction:column;align-items:center;gap:12px}.attachment-drop-icon{font-size:var(--cell-font-size, 48px);color:var(--cell-color, var(--grid-primary, #6750a4));font-weight:var(--cell-font-weight, 300);line-height:1}.attachment-drop-text .primary-text{font-family:var(--grid-font-family, \"Poppins\");font-size:var(--cell-font-size, 14px);color:var(--cell-color, var(--grid-on-surface-variant, #49454f));font-weight:var(--cell-font-weight, 400);margin:0}.attachment-drop-text .secondary-text{font-family:var(--grid-font-family, \"Poppins\");font-size:var(--cell-font-size, 12px);color:var(--cell-color, var(--grid-on-surface-variant, #49454f));margin-top:4px;display:block}.attachment-file-list{display:block;margin-top:16px;max-height:300px;overflow-y:auto}.attachment-file-item{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;border-radius:4px;margin-bottom:8px;background:var(--grid-surface-variant, #e7e0ec);transition:background-color .2s}.attachment-file-item:hover{background:var(--grid-surface-container, #f3edf7)}.file-info{display:flex;align-items:center;gap:8px;flex:1;min-width:0}.file-status-icon{font-size:var(--cell-font-size, 18px)!important;width:18px!important;height:18px!important;flex-shrink:0}.file-status-icon.done-icon{color:var(--cell-color, #22C55E)}.file-name{font-size:var(--cell-font-size, var(--grid-font-size-body, 12px));color:var(--cell-color, var(--grid-on-surface, #1c1b1f));overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.file-actions{display:flex;gap:8px;flex-shrink:0}.file-action-btn{width:28px!important;height:28px!important;padding:0!important;display:flex!important;align-items:center;justify-content:center}.file-action-btn .mat-icon{font-size:var(--cell-font-size, 18px);width:18px;height:18px;color:var(--cell-color, var(--grid-on-surface-variant, #49454f))}.file-action-btn:hover .mat-icon{color:var(--cell-color, var(--grid-on-surface, #1c1b1f))}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "ngmodule", type: MatInputModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1$2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i1$2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: OverlayModule }, { kind: "directive", type: i1$3.CdkConnectedOverlay, selector: "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", inputs: ["cdkConnectedOverlayOrigin", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPositionStrategy", "cdkConnectedOverlayOffsetX", "cdkConnectedOverlayOffsetY", "cdkConnectedOverlayWidth", "cdkConnectedOverlayHeight", "cdkConnectedOverlayMinWidth", "cdkConnectedOverlayMinHeight", "cdkConnectedOverlayBackdropClass", "cdkConnectedOverlayPanelClass", "cdkConnectedOverlayViewportMargin", "cdkConnectedOverlayScrollStrategy", "cdkConnectedOverlayOpen", "cdkConnectedOverlayDisableClose", "cdkConnectedOverlayTransformOriginOn", "cdkConnectedOverlayHasBackdrop", "cdkConnectedOverlayLockPosition", "cdkConnectedOverlayFlexibleDimensions", "cdkConnectedOverlayGrowAfterOpen", "cdkConnectedOverlayPush", "cdkConnectedOverlayDisposeOnNavigation", "cdkConnectedOverlayUsePopover", "cdkConnectedOverlayMatchWidth", "cdkConnectedOverlay"], outputs: ["backdropClick", "positionChange", "attach", "detach", "overlayKeydown", "overlayOutsideClick"], exportAs: ["cdkConnectedOverlay"] }, { kind: "directive", type: i1$3.CdkOverlayOrigin, selector: "[cdk-overlay-origin], [overlay-origin], [cdkOverlayOrigin]", exportAs: ["cdkOverlayOrigin"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
10726
11065
  }
10727
11066
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AttachmentComponent, decorators: [{
10728
11067
  type: Component,
@@ -10746,6 +11085,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
10746
11085
  type: Output
10747
11086
  }], fileAdded: [{
10748
11087
  type: Output
11088
+ }], fileUpload: [{
11089
+ type: Output
10749
11090
  }], fileDeleted: [{
10750
11091
  type: Output
10751
11092
  }], fileViewed: [{
@@ -10773,6 +11114,11 @@ class CompositeComponent {
10773
11114
  drilldownClick = output();
10774
11115
  inline = computed(() => this.column()?.composite_direction === 'inline', ...(ngDevMode ? [{ debugName: "inline" }] : []));
10775
11116
  separator = computed(() => this.column()?.composite_separator ?? '', ...(ngDevMode ? [{ debugName: "separator" }] : []));
11117
+ /** Which side each part's label sits on, or 'none' for bare values. */
11118
+ labelPosition = computed(() => {
11119
+ const p = this.column()?.composite_label_position;
11120
+ return p === 'left' || p === 'right' ? p : 'none';
11121
+ }, ...(ngDevMode ? [{ debugName: "labelPosition" }] : []));
10776
11122
  primaryCss = computed(() => cellTextStyleToCss(this.column()?.cell_style), ...(ngDevMode ? [{ debugName: "primaryCss" }] : []));
10777
11123
  secondaryCss = computed(() => cellTextStyleToCss(this.column()?.cell_style_secondary ?? DEFAULT_SECONDARY_STYLE), ...(ngDevMode ? [{ debugName: "secondaryCss" }] : []));
10778
11124
  /**
@@ -10787,11 +11133,17 @@ class CompositeComponent {
10787
11133
  const names = (column.composite_fields || []).filter(Boolean);
10788
11134
  const byName = new Map((this.columns() || []).map(field => [field.name, field]));
10789
11135
  const row = this.row();
10790
- const parts = names.map(name => {
11136
+ const labels = column.composite_labels || [];
11137
+ const parts = names.map((name, index) => {
10791
11138
  const field = byName.get(name);
11139
+ // Blank means no label for THIS part, never the field's own label as a
11140
+ // fallback: a composite is usually built from columns whose labels are
11141
+ // what the header already says, so falling back printed "Upload Date"
11142
+ // in front of the upload date. Labelling one part and not another is a
11143
+ // normal thing to want.
10792
11144
  return {
10793
11145
  name,
10794
- label: field?.label || name,
11146
+ label: String(labels[index] ?? '').trim(),
10795
11147
  text: formatCellValue(readRowValue(row, name), field),
10796
11148
  };
10797
11149
  });
@@ -10808,11 +11160,11 @@ class CompositeComponent {
10808
11160
  this.drilldownClick.emit(part.text);
10809
11161
  }
10810
11162
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: CompositeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10811
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: CompositeComponent, isStandalone: true, selector: "eru-composite", inputs: { row: { classPropertyName: "row", publicName: "row", isSignal: true, isRequired: false, transformFunction: null }, column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: false, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, isDrillable: { classPropertyName: "isDrillable", publicName: "isDrillable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { drilldownClick: "drilldownClick" }, ngImport: i0, template: "<div class=\"eru-composite\" [class.eru-composite--inline]=\"inline()\">\n @for (part of parts(); track part.name; let first = $first; let last = $last) {\n <span class=\"eru-composite__part\" [class.eru-composite__part--secondary]=\"!first\"\n [class.eru-composite__part--drillable]=\"isDrillable() && first\" [ngStyle]=\"first ? primaryCss() : secondaryCss()\"\n [title]=\"part.text\" (click)=\"onDrilldown($event, part)\">{{ part.text }}</span>\n @if (inline() && !last && separator()) {\n <span class=\"eru-composite__sep\" [ngStyle]=\"secondaryCss()\">{{ separator() }}</span>\n }\n }\n</div>\n", styles: [".eru-composite{display:flex;flex-direction:column;justify-content:center;gap:3px;min-width:0;width:100%;height:100%;line-height:1.3;box-sizing:border-box;padding:0 var(--grid-cell-inset-x, 8px)}.eru-composite--inline{flex-direction:row;align-items:baseline;justify-content:flex-start;gap:4px}.eru-composite__part{display:block;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.eru-composite__part--drillable{color:var(--grid-primary);cursor:pointer;text-decoration:underline}.eru-composite__sep{flex:0 0 auto}.eru-composite__part--secondary{line-height:1.15}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$4.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
11163
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: CompositeComponent, isStandalone: true, selector: "eru-composite", inputs: { row: { classPropertyName: "row", publicName: "row", isSignal: true, isRequired: false, transformFunction: null }, column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: false, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, isDrillable: { classPropertyName: "isDrillable", publicName: "isDrillable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { drilldownClick: "drilldownClick" }, ngImport: i0, template: "<div class=\"eru-composite\" [class.eru-composite--inline]=\"inline()\"\n [class.eru-composite--labelled]=\"labelPosition() !== 'none'\">\n @for (part of parts(); track part.name; let first = $first; let last = $last) {\n <span class=\"eru-composite__slot\">\n @if (labelPosition() === 'left' && part.label) {\n <span class=\"eru-composite__label\" [ngStyle]=\"secondaryCss()\">{{ part.label }}</span>\n }\n <span class=\"eru-composite__part\" [class.eru-composite__part--secondary]=\"!first\"\n [class.eru-composite__part--drillable]=\"isDrillable() && first\" [ngStyle]=\"first ? primaryCss() : secondaryCss()\"\n [title]=\"part.text\" (click)=\"onDrilldown($event, part)\">{{ part.text }}</span>\n @if (labelPosition() === 'right' && part.label) {\n <span class=\"eru-composite__label\" [ngStyle]=\"secondaryCss()\">{{ part.label }}</span>\n }\n </span>\n @if (inline() && !last && separator()) {\n <span class=\"eru-composite__sep\" [ngStyle]=\"secondaryCss()\">{{ separator() }}</span>\n }\n }\n</div>\n", styles: [".eru-composite{display:flex;flex-direction:column;justify-content:center;gap:3px;min-width:0;width:100%;height:100%;line-height:1.3;box-sizing:border-box;padding:0 var(--grid-cell-inset-x, 8px)}.eru-composite--inline{flex-direction:row;align-items:baseline;justify-content:flex-start;gap:4px}.eru-composite__part{display:block;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.eru-composite__part--drillable{color:var(--grid-primary);cursor:pointer;text-decoration:underline}.eru-composite__sep{flex:0 0 auto}.eru-composite__part--secondary{line-height:1.15}.eru-composite__slot{display:contents}.eru-composite--labelled .eru-composite__slot{display:inline-flex;align-items:baseline;gap:4px;min-width:0}.eru-composite__label{flex:0 0 auto;white-space:nowrap;opacity:.75}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$4.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
10812
11164
  }
10813
11165
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: CompositeComponent, decorators: [{
10814
11166
  type: Component,
10815
- args: [{ selector: 'eru-composite', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<div class=\"eru-composite\" [class.eru-composite--inline]=\"inline()\">\n @for (part of parts(); track part.name; let first = $first; let last = $last) {\n <span class=\"eru-composite__part\" [class.eru-composite__part--secondary]=\"!first\"\n [class.eru-composite__part--drillable]=\"isDrillable() && first\" [ngStyle]=\"first ? primaryCss() : secondaryCss()\"\n [title]=\"part.text\" (click)=\"onDrilldown($event, part)\">{{ part.text }}</span>\n @if (inline() && !last && separator()) {\n <span class=\"eru-composite__sep\" [ngStyle]=\"secondaryCss()\">{{ separator() }}</span>\n }\n }\n</div>\n", styles: [".eru-composite{display:flex;flex-direction:column;justify-content:center;gap:3px;min-width:0;width:100%;height:100%;line-height:1.3;box-sizing:border-box;padding:0 var(--grid-cell-inset-x, 8px)}.eru-composite--inline{flex-direction:row;align-items:baseline;justify-content:flex-start;gap:4px}.eru-composite__part{display:block;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.eru-composite__part--drillable{color:var(--grid-primary);cursor:pointer;text-decoration:underline}.eru-composite__sep{flex:0 0 auto}.eru-composite__part--secondary{line-height:1.15}\n"] }]
11167
+ args: [{ selector: 'eru-composite', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<div class=\"eru-composite\" [class.eru-composite--inline]=\"inline()\"\n [class.eru-composite--labelled]=\"labelPosition() !== 'none'\">\n @for (part of parts(); track part.name; let first = $first; let last = $last) {\n <span class=\"eru-composite__slot\">\n @if (labelPosition() === 'left' && part.label) {\n <span class=\"eru-composite__label\" [ngStyle]=\"secondaryCss()\">{{ part.label }}</span>\n }\n <span class=\"eru-composite__part\" [class.eru-composite__part--secondary]=\"!first\"\n [class.eru-composite__part--drillable]=\"isDrillable() && first\" [ngStyle]=\"first ? primaryCss() : secondaryCss()\"\n [title]=\"part.text\" (click)=\"onDrilldown($event, part)\">{{ part.text }}</span>\n @if (labelPosition() === 'right' && part.label) {\n <span class=\"eru-composite__label\" [ngStyle]=\"secondaryCss()\">{{ part.label }}</span>\n }\n </span>\n @if (inline() && !last && separator()) {\n <span class=\"eru-composite__sep\" [ngStyle]=\"secondaryCss()\">{{ separator() }}</span>\n }\n }\n</div>\n", styles: [".eru-composite{display:flex;flex-direction:column;justify-content:center;gap:3px;min-width:0;width:100%;height:100%;line-height:1.3;box-sizing:border-box;padding:0 var(--grid-cell-inset-x, 8px)}.eru-composite--inline{flex-direction:row;align-items:baseline;justify-content:flex-start;gap:4px}.eru-composite__part{display:block;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.eru-composite__part--drillable{color:var(--grid-primary);cursor:pointer;text-decoration:underline}.eru-composite__sep{flex:0 0 auto}.eru-composite__part--secondary{line-height:1.15}.eru-composite__slot{display:contents}.eru-composite--labelled .eru-composite__slot{display:inline-flex;align-items:baseline;gap:4px;min-width:0}.eru-composite__label{flex:0 0 auto;white-space:nowrap;opacity:.75}\n"] }]
10816
11168
  }], propDecorators: { row: [{ type: i0.Input, args: [{ isSignal: true, alias: "row", required: false }] }], column: [{ type: i0.Input, args: [{ isSignal: true, alias: "column", required: false }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], isDrillable: [{ type: i0.Input, args: [{ isSignal: true, alias: "isDrillable", required: false }] }], drilldownClick: [{ type: i0.Output, args: ["drilldownClick"] }] } });
10817
11169
  const DEFAULT_SECONDARY_STYLE = {
10818
11170
  font_size: 11,
@@ -10884,8 +11236,14 @@ class DataCellComponent {
10884
11236
  * applies them per part — setting the primary here as well would leak its
10885
11237
  * weight onto the secondary value, which only overrides size and colour.
10886
11238
  */
11239
+ /**
11240
+ * Which renderer draws this cell. Read off the column rather than taken as an
11241
+ * input so every existing `<data-cell>` call site keeps working — they all
11242
+ * already pass `column`.
11243
+ */
11244
+ cellRender = computed(() => normalizeCellRender(this.column()), ...(ngDevMode ? [{ debugName: "cellRender" }] : []));
10887
11245
  containerStyle = computed(() => {
10888
- if (this.columnDatatype() === 'composite')
11246
+ if (this.cellRender() === 'composite')
10889
11247
  return {};
10890
11248
  const base = cellTextStyleToCss(this.column()?.cell_style);
10891
11249
  return { ...base, ...this.ruleStyle() };
@@ -11365,9 +11723,29 @@ class DataCellComponent {
11365
11723
  maxFiles: config?.max_files,
11366
11724
  allowedFileTypes: config?.allowed_file_types,
11367
11725
  maxFileSize: config?.max_file_size,
11368
- columnWidth: config?.column_width || this.currentColumnWidth()
11726
+ columnWidth: config?.column_width || this.currentColumnWidth(),
11727
+ storageName: config?.storage_name || '',
11728
+ folderName: this.resolvedFolderName(config?.folder_name),
11729
+ defaultUpload: config?.default_upload !== false
11369
11730
  };
11370
11731
  }, ...(ngDevMode ? [{ debugName: "getAttachmentConfig" }] : []));
11732
+ /**
11733
+ * `folder_name` on the column names a SIBLING field; the folder is that
11734
+ * field's value on THIS row, so an attachment column files each record under
11735
+ * its own key. Resolved here rather than in the attachment component so the
11736
+ * component never has to know how a row nests its values.
11737
+ */
11738
+ resolvedFolderName(fieldName) {
11739
+ const name = String(fieldName || '').trim();
11740
+ if (!name)
11741
+ return '';
11742
+ const r = this.row();
11743
+ const fromEntityData = r?.entity_data && typeof r.entity_data === 'object'
11744
+ ? r.entity_data[name]
11745
+ : undefined;
11746
+ const raw = fromEntityData !== undefined ? fromEntityData : r?.[name];
11747
+ return raw === undefined || raw === null ? '' : String(raw).trim();
11748
+ }
11371
11749
  onAttachmentBlur(value) {
11372
11750
  this.currentValue.set(value);
11373
11751
  this.eruGridStore().setActiveCell(null);
@@ -11617,15 +11995,19 @@ class DataCellComponent {
11617
11995
  onNumberBlur() {
11618
11996
  const value = this.currentValue();
11619
11997
  // An empty cell stays empty. Number('') and Number(null) are both 0, so
11620
- // rounding unconditionally would write 0 into every blank numeric cell the
11621
- // user tabs through. Validation still runs, so a mandatory blank is caught.
11998
+ // coercing unconditionally would treat every blank numeric cell the user
11999
+ // tabs through as 0. Validation still runs, so a mandatory blank is caught.
11622
12000
  const isBlank = value === null || value === undefined || value === '';
11623
12001
  const numValue = isBlank ? NaN : Number(value);
11624
12002
  this.validateField(numValue);
12003
+ // The value is stored as the NUMBER the user typed. `decimal` is display
12004
+ // precision and belongs to the view branch alone: rounding here wrote the
12005
+ // rounded value into currentValue — which is what deactivation persists —
12006
+ // so 10.5 in a 0-decimal column was saved as 11 and the real value was
12007
+ // gone. It also flipped the stored type to string, since toFixed returns
12008
+ // one.
11625
12009
  if (!isBlank && !isNaN(numValue) && !this.error()) {
11626
- const config = this.columnCellConfiguration();
11627
- const decimalPlaces = config?.decimal || 0;
11628
- this.currentValue.set(numValue.toFixed(decimalPlaces));
12010
+ this.currentValue.set(numValue);
11629
12011
  }
11630
12012
  }
11631
12013
  getPriorityIcon = computed(() => {
@@ -12370,7 +12752,7 @@ class DataCellComponent {
12370
12752
  useValue: cellValidators
12371
12753
  },
12372
12754
  ...MATERIAL_PROVIDERS
12373
- ], viewQueries: [{ propertyName: "attachmentTrigger", first: true, predicate: ["attachmentTrigger"], descendants: true }, { propertyName: "singleSelectTrigger", first: true, predicate: ["singleSelectTrigger"], descendants: true }, { propertyName: "multiSelectTrigger", first: true, predicate: ["multiSelectTrigger"], descendants: true }, { propertyName: "peopleTrigger", first: true, predicate: ["peopleTrigger"], descendants: true }], ngImport: i0, template: "<div class=\"container\" [ngStyle]=\"containerStyle()\">\n @switch (columnDatatype()) {\n @case ('textbox') {\n <eru-textbox [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\"\n [externalError]=\"error()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onTextboxBlur($event)\"\n (editModeChange)=\"onTextboxEditModeChange($event)\" (drilldownClick)=\"onTextboxDrilldown($event)\">\n </eru-textbox>\n }\n @case ('currency') {\n <eru-currency [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\" [row]=\"row()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [replaceZeroValue]=\"replaceZeroValue\" (valueChange)=\"onValueChange($event)\" (blur)=\"onCurrencyBlur($event)\"\n (editModeChange)=\"onCurrencyEditModeChange($event)\" (drilldownClick)=\"onCurrencyDrilldown($event)\">\n </eru-currency>\n }\n @case ('number') {\n <eru-number [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [replaceZeroValue]=\"replaceZeroValue\" (valueChange)=\"onValueChange($event)\" (blur)=\"onNumberBlurHandler($event)\"\n (editModeChange)=\"onNumberEditModeChange($event)\" (drilldownClick)=\"onNumberDrilldown($event)\">\n </eru-number>\n }\n @case ('location') {\n <eru-location [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\"\n [externalError]=\"error()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onLocationBlurHandler($event)\"\n (editModeChange)=\"onLocationEditModeChange($event)\" (drilldownClick)=\"onLocationDrilldown($event)\">\n </eru-location>\n }\n @case ('email') {\n <eru-email [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\"\n [externalError]=\"error()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onEmailBlurHandler($event)\"\n (editModeChange)=\"onEmailEditModeChange($event)\" (drilldownClick)=\"onEmailDrilldown($event)\">\n </eru-email>\n }\n @case ('textarea') {\n <eru-textarea [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\"\n [externalError]=\"error()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onTextareaBlur($event)\"\n (editModeChange)=\"onTextareaEditModeChange($event)\" (drilldownClick)=\"onTextareaDrilldown($event)\">\n </eru-textarea>\n }\n @case ('website') {\n <eru-website [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\"\n [externalError]=\"error()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onWebsiteBlur($event)\"\n (editModeChange)=\"onWebsiteEditModeChange($event)\" (drilldownClick)=\"onWebsiteDrilldown($event)\">\n </eru-website>\n }\n <!-- @case ('dropdown_multi_select') {\n <div class=\"cell-display-text\" (dblclick)=\"toggleOverlayMenu($event)\" #multiSelectTrigger\n [class.cell-display-text-editable]=\"isEditable()\">\n @if (drillable()) {\n <span class=\"drillable-value\" (click)=\"onDrillableClick($event)\">{{formattedMultiSelectValue(currentColumnWidth()) || 'Click to select'}}</span>\n } @else {\n {{formattedMultiSelectValue(currentColumnWidth()) || 'Click to select'}}\n }\n </div>\n } -->\n\n <!-- @case ('dropdown_single_select') {\n <div class=\"cell-display-text\" (dblclick)=\"toggleOverlayMenu($event)\" cdkOverlayOrigin #singleSelectTrigger=\"cdkOverlayOrigin\"\n [class.cell-display-text-editable]=\"isEditable()\">\n @if (drillable()) {\n <span class=\"drillable-value\" (click)=\"onDrillableClick($event)\">{{currentValue()}}</span>\n } @else {\n {{currentValue()}}\n }\n </div>\n } -->\n\n @case ('checkbox') {\n <eru-checkbox [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\"\n [isDrillable]=\"canDrill()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onCheckboxBlur($event)\"\n (editModeChange)=\"onCheckboxEditModeChange($event)\" (drilldownClick)=\"onCheckboxDrilldown($event)\">\n </eru-checkbox>\n }\n\n @case ('people') {\n <eru-people [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [personCardTemplate]=\"personCardTemplate()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [eruGridStore]=\"eruGridStore()\" (valueChange)=\"onValueChange($event)\"\n (blur)=\"onPeopleBlur($event)\" (editModeChange)=\"onPeopleEditModeChange($event)\"\n (drilldownClick)=\"onPeopleDrilldown($event)\">\n </eru-people>\n }\n\n\n @case ('date') {\n <eru-date [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\"\n [isDrillable]=\"canDrill()\" (valueChange)=\"onValueChange($event)\" (editModeChange)=\"onDateEditModeChange($event)\"\n (drilldownClick)=\"onDateDrilldown($event)\">\n </eru-date>\n }\n\n @case ('datetime') {\n <eru-datetime [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\"\n [isDrillable]=\"canDrill()\" (valueChange)=\"onValueChange($event)\"\n (editModeChange)=\"onDatetimeEditModeChange($event)\" (drilldownClick)=\"onDatetimeDrilldown($event)\">\n </eru-datetime>\n }\n\n @case ('time') {\n <!-- The time-picker component existed but had no datatype and no case here, so\n a time field fell through to the plain-text renderer.\n\n Mounted only while the cell is being edited, matching date/datetime. It\n carries a mat-form-field, and rendering one per row costs a form field on\n every visible time cell to show what is really just \"HH:mm\" text. The two\n other places that embed eru-time-picker (duration, datetime) already only\n mount it inside their own open editors. -->\n @if (isActive()) {\n <eru-time-picker [value]=\"(currentValue() ?? '') + ''\"\n [disabled]=\"!isEditable() || mode() !== 'table'\"\n [placeholder]=\"'HH:mm'\"\n (valueChange)=\"onValueChange($event)\">\n </eru-time-picker>\n } @else {\n <div class=\"time-display\" [class.time-display-editable]=\"isEditable() && mode() === 'table'\"\n (dblclick)=\"onTimeActivate()\">{{currentValue() || ''}}</div>\n }\n }\n\n @case ('duration') {\n <eru-duration [value]=\"currentValue()\" [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\"\n [isDrillable]=\"canDrill()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onDurationBlur($event)\"\n (editModeChange)=\"onDurationEditModeChange($event)\" (drilldownClick)=\"onDurationDrilldown($event)\">\n </eru-duration>\n }\n\n\n @case ('priority') {\n <eru-priority [value]=\"currentValue()\" [config]=\"getPriorityConfig()\" [eruGridStore]=\"eruGridStore()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [isEditable]=\"isEditable() && mode() === 'table'\"\n [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\" (valueChange)=\"onValueChange($event)\"\n (blur)=\"onPriorityBlur($event)\" (editModeChange)=\"onPriorityEditModeChange($event)\"\n (drilldownClick)=\"onPriorityDrilldown($event)\">\n </eru-priority>\n }\n @case ('progress') {\n <eru-progress [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\"\n (valueChange)=\"onValueChange($event)\" (blur)=\"onProgressBlur($event)\" (focus)=\"onProgressFocus()\"\n (editModeChange)=\"onProgressEditModeChange($event)\" (drilldownClick)=\"onProgressDrilldown($event)\">\n </eru-progress>\n }\n\n @case ('rating') {\n <eru-rating [value]=\"currentValue()\" [config]=\"getRatingConfig()\" [isEditable]=\"isEditable() && mode() === 'table'\"\n [isActive]=\"isActive()\" (valueChange)=\"onValueChange($event)\" (editModeChange)=\"onRatingEditModeChange($event)\">\n </eru-rating>\n }\n\n @case ('status') {\n <eru-status [value]=\"currentValue()\" [config]=\"getStatusConfig()\" [isEditable]=\"isEditable() && mode() === 'table'\"\n [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\" [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\"\n [eruGridStore]=\"eruGridStore()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onStatusBlur($event)\"\n (editModeChange)=\"onStatusEditModeChange($event)\" (drilldownClick)=\"onStatusDrilldown($event)\">\n </eru-status>\n }\n\n @case ('tag') {\n <eru-tag [value]=\"currentValue()\" [config]=\"getTagConfig()\" [eruGridStore]=\"eruGridStore()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [isEditable]=\"isEditable() && mode() === 'table'\"\n [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onTagBlur($event)\"\n (editModeChange)=\"onTagEditModeChange($event)\" (drilldownClick)=\"onTagDrilldown($event)\">\n </eru-tag>\n }\n\n @case ('phone') {\n <eru-phone [value]=\"currentValue()\" [defaultCountry]=\"columnCellConfiguration()?.default_country || 'US'\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" (valueChange)=\"onValueChange($event)\"\n (blur)=\"onPhoneBlur($event)\" (editModeChange)=\"onPhoneEditModeChange($event)\"\n (drilldownClick)=\"onPhoneDrilldown($event)\">\n </eru-phone>\n }\n\n @case ('dropdown_single_select') {\n <eru-select [value]=\"currentValue()\" [config]=\"getSelectConfig()\" [row]=\"row()\" [isEditable]=\"isEditable() && mode() === 'table'\"\n [isActive]=\"isActive()\" [multiple]=\"false\" [isDrillable]=\"canDrill()\" [columnWidth]=\"currentColumnWidth()\"\n [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\" (valueChange)=\"onValueChange($event)\"\n (blur)=\"onSelectBlur($event)\" (editModeChange)=\"onSelectEditModeChange($event)\"\n (drilldownClick)=\"onSelectDrilldown($event)\">\n </eru-select>\n }\n @case ('dropdown_multi_select') {\n <eru-select [value]=\"currentValue()\" [config]=\"getSelectConfig()\" [row]=\"row()\" [isEditable]=\"isEditable() && mode() === 'table'\"\n [isActive]=\"isActive()\" [multiple]=\"true\" [isDrillable]=\"canDrill()\" [columnWidth]=\"currentColumnWidth()\"\n [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\" (valueChange)=\"onValueChange($event)\"\n (blur)=\"onSelectBlur($event)\" (editModeChange)=\"onSelectEditModeChange($event)\"\n (drilldownClick)=\"onSelectDrilldown($event)\">\n </eru-select>\n }\n\n @case ('attachment') {\n <eru-attachment [value]=\"currentValue()\" [config]=\"getAttachmentConfig()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [eruGridStore]=\"eruGridStore()\" (valueChange)=\"onValueChange($event)\"\n (blur)=\"onAttachmentBlur($event)\" (editModeChange)=\"onAttachmentEditModeChange($event)\"\n (drilldownClick)=\"onAttachmentDrilldown($event)\">\n </eru-attachment>\n }\n @case ('composite') {\n <eru-composite [row]=\"row()\" [column]=\"column()\" [columns]=\"siblingColumns()\" [isDrillable]=\"canDrill()\"\n (drilldownClick)=\"onCompositeDrilldown($event)\">\n </eru-composite>\n }\n @case ('page') {\n @if (cellTemplate()) {\n <ng-container *ngTemplateOutlet=\"cellTemplate()!; context: cellTemplateContext()\"></ng-container>\n } @else {\n <div class=\"cell-default-display\">{{value()}}</div>\n }\n }\n @default {\n <div class=\"cell-default-display\">\n @if (canDrill()) {\n <span class=\"drillable-value\" (click)=\"onDrillableClick($event)\">{{value()}}</span>\n } @else {\n {{value()}}\n }\n </div>\n }\n }\n\n</div>\n\n\n<!-- <ng-template cdkConnectedOverlay\n[cdkConnectedOverlayOrigin]=\"singleSelectTrigger\"\n[cdkConnectedOverlayOpen]=\"showOverlayMenu('dropdown_single_select')\"\n(detach)=\"isOpen = showOverlayMenu('dropdown_single_select')\">\n <div class=\"dropdown-menu\" cdkMenu [style.width.px]=\"currentColumnWidth()\" (closed)=\"singleOptionClosed()\">\n <div class=\"listbox-container\">\n <mat-form-field appearance=\"outline\" class=\"search-form-field\">\n <input matInput type=\"search\" placeholder=\"Search...\" (click)=\"$event.stopPropagation()\" [ngModel]=\"optionSearchText()\"\n (ngModelChange)=\"optionSearchText.set($event)\">\n </mat-form-field>\n <ul cdkListbox [ngModel]=\"currentValue()\" (ngModelChange)=\"selectedSingleSelect($event)\"\n aria-labelledby=\"listbox-label\" class=\"listbox\">\n <li [cdkOption]=\"'None'\" class=\"listbox-option notext-overflow\">\n None\n </li>\n @for (option of filteredOptions(); track option.value || option.name) {\n <li [cdkOption]=\"option.value || option.name\" class=\"listbox-option notext-overflow\">\n {{option.label || option.name}}\n </li>\n }\n </ul>\n </div>\n </div>\n</ng-template> -->\n\n<!-- <ng-template \ncdkConnectedOverlay\n[cdkConnectedOverlayOrigin]=\"multiSelectTrigger\"\n[cdkConnectedOverlayOpen]=\"showOverlayMenu('dropdown_multi_select')\"\n(detach)=\"isOpen = showOverlayMenu('dropdown_multi_select')\">\n <div class=\"dropdown-menu\" cdkMenu [style.width.px]=\"currentColumnWidth()\" (closed)=\"singleOptionClosed()\">\n <div class=\"listbox-container\">\n <mat-form-field appearance=\"outline\" class=\"search-form-field\">\n <input matInput type=\"search\" placeholder=\"Search...\" [ngModel]=\"optionSearchText()\"\n (ngModelChange)=\"optionSearchText.set($event)\" (click)=\"$event.stopPropagation()\">\n </mat-form-field>\n \n <div class=\"select-all-container\" (click)=\"$event.stopPropagation()\">\n <mat-checkbox \n [checked]=\"isAllSelected()\" \n [indeterminate]=\"isIndeterminate()\"\n (change)=\"toggleSelectAll($event.checked)\">\n <span class=\"select-all-text\">Select All</span>\n </mat-checkbox>\n </div>\n \n <ul cdkListboxMultiple=\"true\" cdkListboxUseActiveDescendant cdkListbox [ngModel]=\"currentValue()\"\n (ngModelChange)=\"selectedMultiSelect($event)\" aria-labelledby=\"listbox-labssel\" class=\"listbox\" (click)=\"$event.stopPropagation()\">\n <li [cdkOption]=\"'None'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('None')\" (click)=\"$event.stopPropagation();appendMultiSelect('None')\"></mat-checkbox>\n <span class=\"option-text\">None</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 1'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 1')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 1')\"></mat-checkbox>\n <span class=\"option-text\">option 1</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 2'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 2')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 2')\"></mat-checkbox>\n <span class=\"option-text\">option 2</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 3'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 3')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 3')\"></mat-checkbox>\n <span class=\"option-text\">option 3</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 4'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 4')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 4')\"></mat-checkbox>\n <span class=\"option-text\">option 4</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 5'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 5')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 5')\"></mat-checkbox>\n <span class=\"option-text\">option 5</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 6'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 6')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 6')\"></mat-checkbox>\n <span class=\"option-text\">option 6</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 7'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 7')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 7')\"></mat-checkbox>\n <span class=\"option-text\">option 7</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 8'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 8')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 8')\"></mat-checkbox>\n <span class=\"option-text\">option 8</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 9'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 9')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 9')\"></mat-checkbox>\n <span class=\"option-text\">option 9</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 10'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 10')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 10')\"></mat-checkbox>\n <span class=\"option-text\">option 10</span>\n </div>\n </li>\n @for (option of filteredOptions(); track option) {\n <li [cdkOption]=\"option.value\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected(option.value)\" (click)=\"$event.stopPropagation();appendMultiSelect(option.value)\"></mat-checkbox>\n <span class=\"option-text\">{{option.label}}</span>\n </div>\n </li>\n }\n </ul>\n </div>\n </div>\n</ng-template> -->", styles: [":host{display:block;height:100%;width:100%;position:relative;overflow:hidden!important}.container{height:calc(100% - 2px);width:calc(100% - 2px);position:relative!important;overflow:hidden!important;max-width:100%!important;box-sizing:border-box!important}.container .cell-display-text{text-overflow:ellipsis!important;overflow:hidden!important;white-space:nowrap!important;max-width:100%!important;width:100%!important;display:block!important}.inputRef{height:inherit;width:inherit;border:none}.inputRef:focus{outline:none}.cell-checkbox{text-align:center}.cell-form-field{width:100%!important;height:100%!important;padding:0!important;margin:0!important}.cell-form-field .mat-mdc-form-field-outline,.cell-form-field .mat-mdc-form-field-subscript-wrapper,.cell-form-field .mat-mdc-form-field-text-suffix{display:none!important}.cell-form-field .mat-mdc-form-field-wrapper,.cell-form-field .mat-mdc-form-field-wrapper .mat-mdc-form-field-flex{width:100%!important;height:100%!important;padding:0!important;margin:0!important}.cell-form-field .mat-mdc-form-field-wrapper .mat-mdc-form-field-flex .mat-mdc-form-field-infix{width:100%!important;height:100%!important;padding:0!important;margin:0!important;min-height:auto!important;border-top:none!important}.cell-form-field input[matInput]{width:100%!important;height:100%!important;padding:2px!important;margin:0!important;border:none!important;outline:none!important;background:transparent!important;font-size:14px!important;line-height:normal!important;box-sizing:border-box!important;max-width:none!important;min-width:0!important;flex:none!important}.dropdown-menu{width:100%}.cell-display-text-editable{cursor:pointer!important}.cell-display-text{width:100%!important;height:100%!important;min-height:20px!important;display:block!important;padding:4px 8px!important;font-family:var(--grid-font-family, \"Poppins\")!important;font-size:var(--grid-font-size-body, 12px)!important;color:var(--grid-on-surface, #1d1b20)!important;background:transparent!important;border:none!important;outline:none!important;text-overflow:ellipsis!important;overflow:hidden!important;white-space:nowrap!important;max-width:100%!important;box-sizing:border-box!important;transition:background-color .2s ease!important;line-height:1.4!important}.cell-display-text,.cell-display-text>*{text-overflow:ellipsis!important;overflow:hidden!important;white-space:nowrap!important;max-width:100%!important;width:100%!important;display:block!important}.cell-display-text:empty:before{content:\"Click to select\"!important;color:var(--grid-on-surface-variant, #49454f)!important;font-style:italic!important}.cell-display-number{text-align:var(--grid-number-text-align, right)}.aggregation .cell-display-number{text-align:var(--grid-aggregation-text-align, right)}.assignee-avatars{display:flex;align-items:center;padding:4px 8px;min-height:20px}.no-assignees{color:var(--grid-on-surface-variant, #49454f);font-style:italic;font-size:12px}.option-content{display:flex;align-items:center;gap:8px;width:100%}.option-avatar{width:24px;height:24px;border-radius:50%;background-color:var(--grid-primary, #6750a4);color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;flex-shrink:0}.option-text{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.checkmark{color:var(--grid-primary, #6750a4);font-weight:700;font-size:14px;flex-shrink:0}.drillable-value{color:var(--grid-primary, #6750a4);text-decoration:underline;text-decoration-color:var(--grid-primary, #6750a4);text-decoration-thickness:1px;text-underline-offset:2px;transition:all .2s ease}.drillable-link{cursor:pointer;padding:4px}.time-display{width:100%;height:100%;min-height:20px;display:block;padding:4px 8px;font-family:var(--grid-font-family, \"Poppins\");font-size:var(--grid-font-size-body, 12px);color:var(--grid-on-surface, #1d1b20);text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.time-display-editable{cursor:pointer}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$4.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1$4.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "ngmodule", type: MatInputModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "ngmodule", type: MatSliderModule }, { kind: "ngmodule", type: MatIconModule }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: OverlayModule }, { kind: "ngmodule", type: MatDatepickerModule }, { kind: "ngmodule", type: MatNativeDateModule }, { kind: "component", type: CurrencyComponent, selector: "eru-currency", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "replaceZeroValue", "placeholder", "row"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "validationError", "editModeChange"] }, { kind: "component", type: NumberComponent, selector: "eru-number", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "replaceZeroValue", "placeholder"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "validationError", "editModeChange"] }, { kind: "component", type: TextboxComponent, selector: "eru-textbox", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "columnWidth", "fieldSize", "eruGridStore", "externalError"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "validationError", "editModeChange"] }, { kind: "component", type: EmailComponent, selector: "eru-email", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "columnWidth", "fieldSize", "eruGridStore", "placeholder", "externalError"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "validationError", "editModeChange"] }, { kind: "component", type: TextareaComponent, selector: "eru-textarea", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "columnWidth", "fieldSize", "eruGridStore", "externalError"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "validationError", "editModeChange"] }, { kind: "component", type: WebsiteComponent, selector: "eru-website", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "columnWidth", "fieldSize", "eruGridStore", "externalError"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "validationError", "editModeChange"] }, { kind: "component", type: LocationComponent, selector: "eru-location", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "columnWidth", "fieldSize", "eruGridStore", "externalError"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "validationError", "editModeChange"] }, { kind: "component", type: CheckboxComponent, selector: "eru-checkbox", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "label"], outputs: ["valueChange", "change", "blur", "focus", "drilldownClick", "editModeChange"] }, { kind: "component", type: DateComponent, selector: "eru-date", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "placeholder"], outputs: ["valueChange", "dateChange", "drilldownClick", "editModeChange"] }, { kind: "component", type: DatetimeComponent, selector: "eru-datetime", inputs: ["value", "isEditable", "isActive", "isDrillable", "placeholder", "config"], outputs: ["valueChange", "datetimeChange", "drilldownClick", "editModeChange"] }, { kind: "component", type: DurationComponent, selector: "eru-duration", inputs: ["value", "isEditable", "isActive", "isDrillable", "placeholder"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "editModeChange"] }, { kind: "component", type: PhoneComponent, selector: "eru-phone", inputs: ["value", "defaultCountry", "isEditable", "isActive", "isDrillable", "columnWidth", "fieldSize"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "editModeChange"] }, { kind: "component", type: ProgressComponent, selector: "eru-progress", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "columnWidth", "fieldSize", "eruGridStore"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "editModeChange"] }, { kind: "component", type: RatingComponent, selector: "eru-rating", inputs: ["value", "config", "isEditable", "isActive"], outputs: ["valueChange", "editModeChange"] }, { kind: "component", type: SelectComponent, selector: "eru-select", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "columnWidth", "fieldSize", "multiple", "eruGridStore", "row"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "editModeChange"] }, { kind: "component", type: StatusComponent, selector: "eru-status", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "columnWidth", "fieldSize", "eruGridStore"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "editModeChange"] }, { kind: "component", type: TagComponent, selector: "eru-tag", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "columnWidth", "fieldSize", "eruGridStore"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "editModeChange", "newTagAdded"] }, { kind: "component", type: PeopleComponent, selector: "eru-people", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "columnWidth", "eruGridStore", "personCardTemplate"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "editModeChange"] }, { kind: "component", type: PriorityComponent, selector: "eru-priority", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "columnWidth", "fieldSize", "eruGridStore"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "editModeChange"] }, { kind: "component", type: AttachmentComponent, selector: "eru-attachment", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "columnWidth", "eruGridStore"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "editModeChange", "fileAdded", "fileDeleted", "fileViewed"] }, { kind: "component", type: TimePickerComponent, selector: "eru-time-picker", inputs: ["value", "label", "placeholder", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: CompositeComponent, selector: "eru-composite", inputs: ["row", "column", "columns", "isDrillable"], outputs: ["drilldownClick"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
12755
+ ], viewQueries: [{ propertyName: "attachmentTrigger", first: true, predicate: ["attachmentTrigger"], descendants: true }, { propertyName: "singleSelectTrigger", first: true, predicate: ["singleSelectTrigger"], descendants: true }, { propertyName: "multiSelectTrigger", first: true, predicate: ["multiSelectTrigger"], descendants: true }, { propertyName: "peopleTrigger", first: true, predicate: ["peopleTrigger"], descendants: true }], ngImport: i0, template: "<div class=\"container\" [ngStyle]=\"containerStyle()\">\n <!-- How the cell is drawn is a separate axis from what it holds: a composite\n or page column keeps its own datatype (and its formatting controls), so\n the renderer is chosen before the datatype switch, not inside it. -->\n @if (cellRender() === 'composite') {\n <eru-composite [row]=\"row()\" [column]=\"column()\" [columns]=\"siblingColumns()\" [isDrillable]=\"canDrill()\"\n (drilldownClick)=\"onCompositeDrilldown($event)\">\n </eru-composite>\n } @else if (cellRender() === 'page') {\n @if (cellTemplate()) {\n <ng-container *ngTemplateOutlet=\"cellTemplate()!; context: cellTemplateContext()\"></ng-container>\n } @else {\n <div class=\"cell-default-display\">{{value()}}</div>\n }\n } @else {\n @switch (columnDatatype()) {\n @case ('textbox') {\n <eru-textbox [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\"\n [externalError]=\"error()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onTextboxBlur($event)\"\n (editModeChange)=\"onTextboxEditModeChange($event)\" (drilldownClick)=\"onTextboxDrilldown($event)\">\n </eru-textbox>\n }\n @case ('currency') {\n <eru-currency [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\" [row]=\"row()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [replaceZeroValue]=\"replaceZeroValue\" (valueChange)=\"onValueChange($event)\" (blur)=\"onCurrencyBlur($event)\"\n (editModeChange)=\"onCurrencyEditModeChange($event)\" (drilldownClick)=\"onCurrencyDrilldown($event)\">\n </eru-currency>\n }\n @case ('number') {\n <eru-number [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [replaceZeroValue]=\"replaceZeroValue\" (valueChange)=\"onValueChange($event)\" (blur)=\"onNumberBlurHandler($event)\"\n (editModeChange)=\"onNumberEditModeChange($event)\" (drilldownClick)=\"onNumberDrilldown($event)\">\n </eru-number>\n }\n @case ('location') {\n <eru-location [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\"\n [externalError]=\"error()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onLocationBlurHandler($event)\"\n (editModeChange)=\"onLocationEditModeChange($event)\" (drilldownClick)=\"onLocationDrilldown($event)\">\n </eru-location>\n }\n @case ('email') {\n <eru-email [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\"\n [externalError]=\"error()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onEmailBlurHandler($event)\"\n (editModeChange)=\"onEmailEditModeChange($event)\" (drilldownClick)=\"onEmailDrilldown($event)\">\n </eru-email>\n }\n @case ('textarea') {\n <eru-textarea [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\"\n [externalError]=\"error()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onTextareaBlur($event)\"\n (editModeChange)=\"onTextareaEditModeChange($event)\" (drilldownClick)=\"onTextareaDrilldown($event)\">\n </eru-textarea>\n }\n @case ('website') {\n <eru-website [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\"\n [externalError]=\"error()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onWebsiteBlur($event)\"\n (editModeChange)=\"onWebsiteEditModeChange($event)\" (drilldownClick)=\"onWebsiteDrilldown($event)\">\n </eru-website>\n }\n <!-- @case ('dropdown_multi_select') {\n <div class=\"cell-display-text\" (dblclick)=\"toggleOverlayMenu($event)\" #multiSelectTrigger\n [class.cell-display-text-editable]=\"isEditable()\">\n @if (drillable()) {\n <span class=\"drillable-value\" (click)=\"onDrillableClick($event)\">{{formattedMultiSelectValue(currentColumnWidth()) || 'Click to select'}}</span>\n } @else {\n {{formattedMultiSelectValue(currentColumnWidth()) || 'Click to select'}}\n }\n </div>\n } -->\n\n <!-- @case ('dropdown_single_select') {\n <div class=\"cell-display-text\" (dblclick)=\"toggleOverlayMenu($event)\" cdkOverlayOrigin #singleSelectTrigger=\"cdkOverlayOrigin\"\n [class.cell-display-text-editable]=\"isEditable()\">\n @if (drillable()) {\n <span class=\"drillable-value\" (click)=\"onDrillableClick($event)\">{{currentValue()}}</span>\n } @else {\n {{currentValue()}}\n }\n </div>\n } -->\n\n @case ('checkbox') {\n <eru-checkbox [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\"\n [isDrillable]=\"canDrill()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onCheckboxBlur($event)\"\n (editModeChange)=\"onCheckboxEditModeChange($event)\" (drilldownClick)=\"onCheckboxDrilldown($event)\">\n </eru-checkbox>\n }\n\n @case ('people') {\n <eru-people [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [personCardTemplate]=\"personCardTemplate()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [eruGridStore]=\"eruGridStore()\" (valueChange)=\"onValueChange($event)\"\n (blur)=\"onPeopleBlur($event)\" (editModeChange)=\"onPeopleEditModeChange($event)\"\n (drilldownClick)=\"onPeopleDrilldown($event)\">\n </eru-people>\n }\n\n\n @case ('date') {\n <eru-date [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\"\n [isDrillable]=\"canDrill()\" (valueChange)=\"onValueChange($event)\" (editModeChange)=\"onDateEditModeChange($event)\"\n (drilldownClick)=\"onDateDrilldown($event)\">\n </eru-date>\n }\n\n @case ('datetime') {\n <eru-datetime [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\"\n [isDrillable]=\"canDrill()\" (valueChange)=\"onValueChange($event)\"\n (editModeChange)=\"onDatetimeEditModeChange($event)\" (drilldownClick)=\"onDatetimeDrilldown($event)\">\n </eru-datetime>\n }\n\n @case ('time') {\n <!-- The time-picker component existed but had no datatype and no case here, so\n a time field fell through to the plain-text renderer.\n\n Mounted only while the cell is being edited, matching date/datetime. It\n carries a mat-form-field, and rendering one per row costs a form field on\n every visible time cell to show what is really just \"HH:mm\" text. The two\n other places that embed eru-time-picker (duration, datetime) already only\n mount it inside their own open editors. -->\n @if (isActive()) {\n <eru-time-picker [value]=\"(currentValue() ?? '') + ''\"\n [disabled]=\"!isEditable() || mode() !== 'table'\"\n [placeholder]=\"'HH:mm'\"\n (valueChange)=\"onValueChange($event)\">\n </eru-time-picker>\n } @else {\n <div class=\"time-display\" [class.time-display-editable]=\"isEditable() && mode() === 'table'\"\n (dblclick)=\"onTimeActivate()\">{{currentValue() || ''}}</div>\n }\n }\n\n @case ('duration') {\n <eru-duration [value]=\"currentValue()\" [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\"\n [isDrillable]=\"canDrill()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onDurationBlur($event)\"\n (editModeChange)=\"onDurationEditModeChange($event)\" (drilldownClick)=\"onDurationDrilldown($event)\">\n </eru-duration>\n }\n\n\n @case ('priority') {\n <eru-priority [value]=\"currentValue()\" [config]=\"getPriorityConfig()\" [eruGridStore]=\"eruGridStore()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [isEditable]=\"isEditable() && mode() === 'table'\"\n [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\" (valueChange)=\"onValueChange($event)\"\n (blur)=\"onPriorityBlur($event)\" (editModeChange)=\"onPriorityEditModeChange($event)\"\n (drilldownClick)=\"onPriorityDrilldown($event)\">\n </eru-priority>\n }\n @case ('progress') {\n <eru-progress [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\"\n (valueChange)=\"onValueChange($event)\" (blur)=\"onProgressBlur($event)\" (focus)=\"onProgressFocus()\"\n (editModeChange)=\"onProgressEditModeChange($event)\" (drilldownClick)=\"onProgressDrilldown($event)\">\n </eru-progress>\n }\n\n @case ('rating') {\n <eru-rating [value]=\"currentValue()\" [config]=\"getRatingConfig()\" [isEditable]=\"isEditable() && mode() === 'table'\"\n [isActive]=\"isActive()\" (valueChange)=\"onValueChange($event)\" (editModeChange)=\"onRatingEditModeChange($event)\">\n </eru-rating>\n }\n\n @case ('status') {\n <eru-status [value]=\"currentValue()\" [config]=\"getStatusConfig()\" [isEditable]=\"isEditable() && mode() === 'table'\"\n [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\" [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\"\n [eruGridStore]=\"eruGridStore()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onStatusBlur($event)\"\n (editModeChange)=\"onStatusEditModeChange($event)\" (drilldownClick)=\"onStatusDrilldown($event)\">\n </eru-status>\n }\n\n @case ('tag') {\n <eru-tag [value]=\"currentValue()\" [config]=\"getTagConfig()\" [eruGridStore]=\"eruGridStore()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [isEditable]=\"isEditable() && mode() === 'table'\"\n [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onTagBlur($event)\"\n (editModeChange)=\"onTagEditModeChange($event)\" (drilldownClick)=\"onTagDrilldown($event)\">\n </eru-tag>\n }\n\n @case ('phone') {\n <eru-phone [value]=\"currentValue()\" [defaultCountry]=\"columnCellConfiguration()?.default_country || 'US'\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" (valueChange)=\"onValueChange($event)\"\n (blur)=\"onPhoneBlur($event)\" (editModeChange)=\"onPhoneEditModeChange($event)\"\n (drilldownClick)=\"onPhoneDrilldown($event)\">\n </eru-phone>\n }\n\n @case ('dropdown_single_select') {\n <eru-select [value]=\"currentValue()\" [config]=\"getSelectConfig()\" [row]=\"row()\" [isEditable]=\"isEditable() && mode() === 'table'\"\n [isActive]=\"isActive()\" [multiple]=\"false\" [isDrillable]=\"canDrill()\" [columnWidth]=\"currentColumnWidth()\"\n [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\" (valueChange)=\"onValueChange($event)\"\n (blur)=\"onSelectBlur($event)\" (editModeChange)=\"onSelectEditModeChange($event)\"\n (drilldownClick)=\"onSelectDrilldown($event)\">\n </eru-select>\n }\n @case ('dropdown_multi_select') {\n <eru-select [value]=\"currentValue()\" [config]=\"getSelectConfig()\" [row]=\"row()\" [isEditable]=\"isEditable() && mode() === 'table'\"\n [isActive]=\"isActive()\" [multiple]=\"true\" [isDrillable]=\"canDrill()\" [columnWidth]=\"currentColumnWidth()\"\n [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\" (valueChange)=\"onValueChange($event)\"\n (blur)=\"onSelectBlur($event)\" (editModeChange)=\"onSelectEditModeChange($event)\"\n (drilldownClick)=\"onSelectDrilldown($event)\">\n </eru-select>\n }\n\n @case ('attachment') {\n <eru-attachment [value]=\"currentValue()\" [config]=\"getAttachmentConfig()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [eruGridStore]=\"eruGridStore()\" (valueChange)=\"onValueChange($event)\"\n (blur)=\"onAttachmentBlur($event)\" (editModeChange)=\"onAttachmentEditModeChange($event)\"\n (drilldownClick)=\"onAttachmentDrilldown($event)\">\n </eru-attachment>\n }\n @default {\n <div class=\"cell-default-display\">\n @if (canDrill()) {\n <span class=\"drillable-value\" (click)=\"onDrillableClick($event)\">{{value()}}</span>\n } @else {\n {{value()}}\n }\n </div>\n }\n }\n }\n\n</div>\n\n\n<!-- <ng-template cdkConnectedOverlay\n[cdkConnectedOverlayOrigin]=\"singleSelectTrigger\"\n[cdkConnectedOverlayOpen]=\"showOverlayMenu('dropdown_single_select')\"\n(detach)=\"isOpen = showOverlayMenu('dropdown_single_select')\">\n <div class=\"dropdown-menu\" cdkMenu [style.width.px]=\"currentColumnWidth()\" (closed)=\"singleOptionClosed()\">\n <div class=\"listbox-container\">\n <mat-form-field appearance=\"outline\" class=\"search-form-field\">\n <input matInput type=\"search\" placeholder=\"Search...\" (click)=\"$event.stopPropagation()\" [ngModel]=\"optionSearchText()\"\n (ngModelChange)=\"optionSearchText.set($event)\">\n </mat-form-field>\n <ul cdkListbox [ngModel]=\"currentValue()\" (ngModelChange)=\"selectedSingleSelect($event)\"\n aria-labelledby=\"listbox-label\" class=\"listbox\">\n <li [cdkOption]=\"'None'\" class=\"listbox-option notext-overflow\">\n None\n </li>\n @for (option of filteredOptions(); track option.value || option.name) {\n <li [cdkOption]=\"option.value || option.name\" class=\"listbox-option notext-overflow\">\n {{option.label || option.name}}\n </li>\n }\n </ul>\n </div>\n </div>\n</ng-template> -->\n\n<!-- <ng-template \ncdkConnectedOverlay\n[cdkConnectedOverlayOrigin]=\"multiSelectTrigger\"\n[cdkConnectedOverlayOpen]=\"showOverlayMenu('dropdown_multi_select')\"\n(detach)=\"isOpen = showOverlayMenu('dropdown_multi_select')\">\n <div class=\"dropdown-menu\" cdkMenu [style.width.px]=\"currentColumnWidth()\" (closed)=\"singleOptionClosed()\">\n <div class=\"listbox-container\">\n <mat-form-field appearance=\"outline\" class=\"search-form-field\">\n <input matInput type=\"search\" placeholder=\"Search...\" [ngModel]=\"optionSearchText()\"\n (ngModelChange)=\"optionSearchText.set($event)\" (click)=\"$event.stopPropagation()\">\n </mat-form-field>\n \n <div class=\"select-all-container\" (click)=\"$event.stopPropagation()\">\n <mat-checkbox \n [checked]=\"isAllSelected()\" \n [indeterminate]=\"isIndeterminate()\"\n (change)=\"toggleSelectAll($event.checked)\">\n <span class=\"select-all-text\">Select All</span>\n </mat-checkbox>\n </div>\n \n <ul cdkListboxMultiple=\"true\" cdkListboxUseActiveDescendant cdkListbox [ngModel]=\"currentValue()\"\n (ngModelChange)=\"selectedMultiSelect($event)\" aria-labelledby=\"listbox-labssel\" class=\"listbox\" (click)=\"$event.stopPropagation()\">\n <li [cdkOption]=\"'None'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('None')\" (click)=\"$event.stopPropagation();appendMultiSelect('None')\"></mat-checkbox>\n <span class=\"option-text\">None</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 1'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 1')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 1')\"></mat-checkbox>\n <span class=\"option-text\">option 1</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 2'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 2')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 2')\"></mat-checkbox>\n <span class=\"option-text\">option 2</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 3'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 3')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 3')\"></mat-checkbox>\n <span class=\"option-text\">option 3</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 4'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 4')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 4')\"></mat-checkbox>\n <span class=\"option-text\">option 4</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 5'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 5')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 5')\"></mat-checkbox>\n <span class=\"option-text\">option 5</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 6'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 6')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 6')\"></mat-checkbox>\n <span class=\"option-text\">option 6</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 7'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 7')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 7')\"></mat-checkbox>\n <span class=\"option-text\">option 7</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 8'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 8')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 8')\"></mat-checkbox>\n <span class=\"option-text\">option 8</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 9'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 9')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 9')\"></mat-checkbox>\n <span class=\"option-text\">option 9</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 10'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 10')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 10')\"></mat-checkbox>\n <span class=\"option-text\">option 10</span>\n </div>\n </li>\n @for (option of filteredOptions(); track option) {\n <li [cdkOption]=\"option.value\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected(option.value)\" (click)=\"$event.stopPropagation();appendMultiSelect(option.value)\"></mat-checkbox>\n <span class=\"option-text\">{{option.label}}</span>\n </div>\n </li>\n }\n </ul>\n </div>\n </div>\n</ng-template> -->", styles: [":host{display:block;height:100%;width:100%;position:relative;overflow:hidden!important}.container{height:calc(100% - 2px);width:calc(100% - 2px);position:relative!important;overflow:hidden!important;max-width:100%!important;box-sizing:border-box!important}.container .cell-display-text{text-overflow:ellipsis!important;overflow:hidden!important;white-space:nowrap!important;max-width:100%!important;width:100%!important;display:block!important}.inputRef{height:inherit;width:inherit;border:none}.inputRef:focus{outline:none}.cell-checkbox{text-align:center}.cell-form-field{width:100%!important;height:100%!important;padding:0!important;margin:0!important}.cell-form-field .mat-mdc-form-field-outline,.cell-form-field .mat-mdc-form-field-subscript-wrapper,.cell-form-field .mat-mdc-form-field-text-suffix{display:none!important}.cell-form-field .mat-mdc-form-field-wrapper,.cell-form-field .mat-mdc-form-field-wrapper .mat-mdc-form-field-flex{width:100%!important;height:100%!important;padding:0!important;margin:0!important}.cell-form-field .mat-mdc-form-field-wrapper .mat-mdc-form-field-flex .mat-mdc-form-field-infix{width:100%!important;height:100%!important;padding:0!important;margin:0!important;min-height:auto!important;border-top:none!important}.cell-form-field input[matInput]{width:100%!important;height:100%!important;padding:2px!important;margin:0!important;border:none!important;outline:none!important;background:transparent!important;font-size:14px!important;line-height:normal!important;box-sizing:border-box!important;max-width:none!important;min-width:0!important;flex:none!important}.dropdown-menu{width:100%}.cell-display-text-editable{cursor:pointer!important}.cell-display-text{width:100%!important;height:100%!important;min-height:20px!important;display:block!important;padding:4px 8px!important;font-family:var(--grid-font-family, \"Poppins\")!important;font-size:var(--grid-font-size-body, 12px)!important;color:var(--grid-on-surface, #1d1b20)!important;background:transparent!important;border:none!important;outline:none!important;text-overflow:ellipsis!important;overflow:hidden!important;white-space:nowrap!important;max-width:100%!important;box-sizing:border-box!important;transition:background-color .2s ease!important;line-height:1.4!important}.cell-display-text,.cell-display-text>*{text-overflow:ellipsis!important;overflow:hidden!important;white-space:nowrap!important;max-width:100%!important;width:100%!important;display:block!important}.cell-display-text:empty:before{content:\"Click to select\"!important;color:var(--grid-on-surface-variant, #49454f)!important;font-style:italic!important}.cell-display-number{text-align:var(--grid-number-text-align, right)}.aggregation .cell-display-number{text-align:var(--grid-aggregation-text-align, right)}.assignee-avatars{display:flex;align-items:center;padding:4px 8px;min-height:20px}.no-assignees{color:var(--grid-on-surface-variant, #49454f);font-style:italic;font-size:12px}.option-content{display:flex;align-items:center;gap:8px;width:100%}.option-avatar{width:24px;height:24px;border-radius:50%;background-color:var(--grid-primary, #6750a4);color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;flex-shrink:0}.option-text{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.checkmark{color:var(--grid-primary, #6750a4);font-weight:700;font-size:14px;flex-shrink:0}.drillable-value{color:var(--grid-primary, #6750a4);text-decoration:underline;text-decoration-color:var(--grid-primary, #6750a4);text-decoration-thickness:1px;text-underline-offset:2px;transition:all .2s ease}.drillable-link{cursor:pointer;padding:4px}.time-display{width:100%;height:100%;min-height:20px;display:block;padding:4px 8px;font-family:var(--grid-font-family, \"Poppins\");font-size:var(--grid-font-size-body, 12px);color:var(--grid-on-surface, #1d1b20);text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.time-display-editable{cursor:pointer}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$4.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1$4.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "ngmodule", type: MatInputModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "ngmodule", type: MatSliderModule }, { kind: "ngmodule", type: MatIconModule }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: OverlayModule }, { kind: "ngmodule", type: MatDatepickerModule }, { kind: "ngmodule", type: MatNativeDateModule }, { kind: "component", type: CurrencyComponent, selector: "eru-currency", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "replaceZeroValue", "placeholder", "row"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "validationError", "editModeChange"] }, { kind: "component", type: NumberComponent, selector: "eru-number", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "replaceZeroValue", "placeholder"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "validationError", "editModeChange"] }, { kind: "component", type: TextboxComponent, selector: "eru-textbox", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "columnWidth", "fieldSize", "eruGridStore", "externalError"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "validationError", "editModeChange"] }, { kind: "component", type: EmailComponent, selector: "eru-email", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "columnWidth", "fieldSize", "eruGridStore", "placeholder", "externalError"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "validationError", "editModeChange"] }, { kind: "component", type: TextareaComponent, selector: "eru-textarea", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "columnWidth", "fieldSize", "eruGridStore", "externalError"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "validationError", "editModeChange"] }, { kind: "component", type: WebsiteComponent, selector: "eru-website", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "columnWidth", "fieldSize", "eruGridStore", "externalError"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "validationError", "editModeChange"] }, { kind: "component", type: LocationComponent, selector: "eru-location", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "columnWidth", "fieldSize", "eruGridStore", "externalError"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "validationError", "editModeChange"] }, { kind: "component", type: CheckboxComponent, selector: "eru-checkbox", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "label"], outputs: ["valueChange", "change", "blur", "focus", "drilldownClick", "editModeChange"] }, { kind: "component", type: DateComponent, selector: "eru-date", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "placeholder"], outputs: ["valueChange", "dateChange", "drilldownClick", "editModeChange"] }, { kind: "component", type: DatetimeComponent, selector: "eru-datetime", inputs: ["value", "isEditable", "isActive", "isDrillable", "placeholder", "config"], outputs: ["valueChange", "datetimeChange", "drilldownClick", "editModeChange"] }, { kind: "component", type: DurationComponent, selector: "eru-duration", inputs: ["value", "isEditable", "isActive", "isDrillable", "placeholder"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "editModeChange"] }, { kind: "component", type: PhoneComponent, selector: "eru-phone", inputs: ["value", "defaultCountry", "isEditable", "isActive", "isDrillable", "columnWidth", "fieldSize"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "editModeChange"] }, { kind: "component", type: ProgressComponent, selector: "eru-progress", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "columnWidth", "fieldSize", "eruGridStore"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "editModeChange"] }, { kind: "component", type: RatingComponent, selector: "eru-rating", inputs: ["value", "config", "isEditable", "isActive"], outputs: ["valueChange", "editModeChange"] }, { kind: "component", type: SelectComponent, selector: "eru-select", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "columnWidth", "fieldSize", "multiple", "eruGridStore", "row"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "editModeChange"] }, { kind: "component", type: StatusComponent, selector: "eru-status", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "columnWidth", "fieldSize", "eruGridStore"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "editModeChange"] }, { kind: "component", type: TagComponent, selector: "eru-tag", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "columnWidth", "fieldSize", "eruGridStore"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "editModeChange", "newTagAdded"] }, { kind: "component", type: PeopleComponent, selector: "eru-people", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "columnWidth", "eruGridStore", "personCardTemplate"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "editModeChange"] }, { kind: "component", type: PriorityComponent, selector: "eru-priority", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "columnWidth", "fieldSize", "eruGridStore"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "editModeChange"] }, { kind: "component", type: AttachmentComponent, selector: "eru-attachment", inputs: ["value", "config", "isEditable", "isActive", "isDrillable", "columnWidth", "eruGridStore"], outputs: ["valueChange", "blur", "focus", "drilldownClick", "editModeChange", "fileAdded", "fileUpload", "fileDeleted", "fileViewed"] }, { kind: "component", type: TimePickerComponent, selector: "eru-time-picker", inputs: ["value", "label", "placeholder", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: CompositeComponent, selector: "eru-composite", inputs: ["row", "column", "columns", "isDrillable"], outputs: ["drilldownClick"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
12374
12756
  }
12375
12757
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: DataCellComponent, decorators: [{
12376
12758
  type: Component,
@@ -12419,7 +12801,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
12419
12801
  ...MATERIAL_PROVIDERS
12420
12802
  ], host: {
12421
12803
  'class': 'data-cell-component'
12422
- }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<div class=\"container\" [ngStyle]=\"containerStyle()\">\n @switch (columnDatatype()) {\n @case ('textbox') {\n <eru-textbox [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\"\n [externalError]=\"error()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onTextboxBlur($event)\"\n (editModeChange)=\"onTextboxEditModeChange($event)\" (drilldownClick)=\"onTextboxDrilldown($event)\">\n </eru-textbox>\n }\n @case ('currency') {\n <eru-currency [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\" [row]=\"row()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [replaceZeroValue]=\"replaceZeroValue\" (valueChange)=\"onValueChange($event)\" (blur)=\"onCurrencyBlur($event)\"\n (editModeChange)=\"onCurrencyEditModeChange($event)\" (drilldownClick)=\"onCurrencyDrilldown($event)\">\n </eru-currency>\n }\n @case ('number') {\n <eru-number [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [replaceZeroValue]=\"replaceZeroValue\" (valueChange)=\"onValueChange($event)\" (blur)=\"onNumberBlurHandler($event)\"\n (editModeChange)=\"onNumberEditModeChange($event)\" (drilldownClick)=\"onNumberDrilldown($event)\">\n </eru-number>\n }\n @case ('location') {\n <eru-location [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\"\n [externalError]=\"error()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onLocationBlurHandler($event)\"\n (editModeChange)=\"onLocationEditModeChange($event)\" (drilldownClick)=\"onLocationDrilldown($event)\">\n </eru-location>\n }\n @case ('email') {\n <eru-email [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\"\n [externalError]=\"error()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onEmailBlurHandler($event)\"\n (editModeChange)=\"onEmailEditModeChange($event)\" (drilldownClick)=\"onEmailDrilldown($event)\">\n </eru-email>\n }\n @case ('textarea') {\n <eru-textarea [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\"\n [externalError]=\"error()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onTextareaBlur($event)\"\n (editModeChange)=\"onTextareaEditModeChange($event)\" (drilldownClick)=\"onTextareaDrilldown($event)\">\n </eru-textarea>\n }\n @case ('website') {\n <eru-website [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\"\n [externalError]=\"error()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onWebsiteBlur($event)\"\n (editModeChange)=\"onWebsiteEditModeChange($event)\" (drilldownClick)=\"onWebsiteDrilldown($event)\">\n </eru-website>\n }\n <!-- @case ('dropdown_multi_select') {\n <div class=\"cell-display-text\" (dblclick)=\"toggleOverlayMenu($event)\" #multiSelectTrigger\n [class.cell-display-text-editable]=\"isEditable()\">\n @if (drillable()) {\n <span class=\"drillable-value\" (click)=\"onDrillableClick($event)\">{{formattedMultiSelectValue(currentColumnWidth()) || 'Click to select'}}</span>\n } @else {\n {{formattedMultiSelectValue(currentColumnWidth()) || 'Click to select'}}\n }\n </div>\n } -->\n\n <!-- @case ('dropdown_single_select') {\n <div class=\"cell-display-text\" (dblclick)=\"toggleOverlayMenu($event)\" cdkOverlayOrigin #singleSelectTrigger=\"cdkOverlayOrigin\"\n [class.cell-display-text-editable]=\"isEditable()\">\n @if (drillable()) {\n <span class=\"drillable-value\" (click)=\"onDrillableClick($event)\">{{currentValue()}}</span>\n } @else {\n {{currentValue()}}\n }\n </div>\n } -->\n\n @case ('checkbox') {\n <eru-checkbox [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\"\n [isDrillable]=\"canDrill()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onCheckboxBlur($event)\"\n (editModeChange)=\"onCheckboxEditModeChange($event)\" (drilldownClick)=\"onCheckboxDrilldown($event)\">\n </eru-checkbox>\n }\n\n @case ('people') {\n <eru-people [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [personCardTemplate]=\"personCardTemplate()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [eruGridStore]=\"eruGridStore()\" (valueChange)=\"onValueChange($event)\"\n (blur)=\"onPeopleBlur($event)\" (editModeChange)=\"onPeopleEditModeChange($event)\"\n (drilldownClick)=\"onPeopleDrilldown($event)\">\n </eru-people>\n }\n\n\n @case ('date') {\n <eru-date [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\"\n [isDrillable]=\"canDrill()\" (valueChange)=\"onValueChange($event)\" (editModeChange)=\"onDateEditModeChange($event)\"\n (drilldownClick)=\"onDateDrilldown($event)\">\n </eru-date>\n }\n\n @case ('datetime') {\n <eru-datetime [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\"\n [isDrillable]=\"canDrill()\" (valueChange)=\"onValueChange($event)\"\n (editModeChange)=\"onDatetimeEditModeChange($event)\" (drilldownClick)=\"onDatetimeDrilldown($event)\">\n </eru-datetime>\n }\n\n @case ('time') {\n <!-- The time-picker component existed but had no datatype and no case here, so\n a time field fell through to the plain-text renderer.\n\n Mounted only while the cell is being edited, matching date/datetime. It\n carries a mat-form-field, and rendering one per row costs a form field on\n every visible time cell to show what is really just \"HH:mm\" text. The two\n other places that embed eru-time-picker (duration, datetime) already only\n mount it inside their own open editors. -->\n @if (isActive()) {\n <eru-time-picker [value]=\"(currentValue() ?? '') + ''\"\n [disabled]=\"!isEditable() || mode() !== 'table'\"\n [placeholder]=\"'HH:mm'\"\n (valueChange)=\"onValueChange($event)\">\n </eru-time-picker>\n } @else {\n <div class=\"time-display\" [class.time-display-editable]=\"isEditable() && mode() === 'table'\"\n (dblclick)=\"onTimeActivate()\">{{currentValue() || ''}}</div>\n }\n }\n\n @case ('duration') {\n <eru-duration [value]=\"currentValue()\" [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\"\n [isDrillable]=\"canDrill()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onDurationBlur($event)\"\n (editModeChange)=\"onDurationEditModeChange($event)\" (drilldownClick)=\"onDurationDrilldown($event)\">\n </eru-duration>\n }\n\n\n @case ('priority') {\n <eru-priority [value]=\"currentValue()\" [config]=\"getPriorityConfig()\" [eruGridStore]=\"eruGridStore()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [isEditable]=\"isEditable() && mode() === 'table'\"\n [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\" (valueChange)=\"onValueChange($event)\"\n (blur)=\"onPriorityBlur($event)\" (editModeChange)=\"onPriorityEditModeChange($event)\"\n (drilldownClick)=\"onPriorityDrilldown($event)\">\n </eru-priority>\n }\n @case ('progress') {\n <eru-progress [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\"\n (valueChange)=\"onValueChange($event)\" (blur)=\"onProgressBlur($event)\" (focus)=\"onProgressFocus()\"\n (editModeChange)=\"onProgressEditModeChange($event)\" (drilldownClick)=\"onProgressDrilldown($event)\">\n </eru-progress>\n }\n\n @case ('rating') {\n <eru-rating [value]=\"currentValue()\" [config]=\"getRatingConfig()\" [isEditable]=\"isEditable() && mode() === 'table'\"\n [isActive]=\"isActive()\" (valueChange)=\"onValueChange($event)\" (editModeChange)=\"onRatingEditModeChange($event)\">\n </eru-rating>\n }\n\n @case ('status') {\n <eru-status [value]=\"currentValue()\" [config]=\"getStatusConfig()\" [isEditable]=\"isEditable() && mode() === 'table'\"\n [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\" [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\"\n [eruGridStore]=\"eruGridStore()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onStatusBlur($event)\"\n (editModeChange)=\"onStatusEditModeChange($event)\" (drilldownClick)=\"onStatusDrilldown($event)\">\n </eru-status>\n }\n\n @case ('tag') {\n <eru-tag [value]=\"currentValue()\" [config]=\"getTagConfig()\" [eruGridStore]=\"eruGridStore()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [isEditable]=\"isEditable() && mode() === 'table'\"\n [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onTagBlur($event)\"\n (editModeChange)=\"onTagEditModeChange($event)\" (drilldownClick)=\"onTagDrilldown($event)\">\n </eru-tag>\n }\n\n @case ('phone') {\n <eru-phone [value]=\"currentValue()\" [defaultCountry]=\"columnCellConfiguration()?.default_country || 'US'\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" (valueChange)=\"onValueChange($event)\"\n (blur)=\"onPhoneBlur($event)\" (editModeChange)=\"onPhoneEditModeChange($event)\"\n (drilldownClick)=\"onPhoneDrilldown($event)\">\n </eru-phone>\n }\n\n @case ('dropdown_single_select') {\n <eru-select [value]=\"currentValue()\" [config]=\"getSelectConfig()\" [row]=\"row()\" [isEditable]=\"isEditable() && mode() === 'table'\"\n [isActive]=\"isActive()\" [multiple]=\"false\" [isDrillable]=\"canDrill()\" [columnWidth]=\"currentColumnWidth()\"\n [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\" (valueChange)=\"onValueChange($event)\"\n (blur)=\"onSelectBlur($event)\" (editModeChange)=\"onSelectEditModeChange($event)\"\n (drilldownClick)=\"onSelectDrilldown($event)\">\n </eru-select>\n }\n @case ('dropdown_multi_select') {\n <eru-select [value]=\"currentValue()\" [config]=\"getSelectConfig()\" [row]=\"row()\" [isEditable]=\"isEditable() && mode() === 'table'\"\n [isActive]=\"isActive()\" [multiple]=\"true\" [isDrillable]=\"canDrill()\" [columnWidth]=\"currentColumnWidth()\"\n [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\" (valueChange)=\"onValueChange($event)\"\n (blur)=\"onSelectBlur($event)\" (editModeChange)=\"onSelectEditModeChange($event)\"\n (drilldownClick)=\"onSelectDrilldown($event)\">\n </eru-select>\n }\n\n @case ('attachment') {\n <eru-attachment [value]=\"currentValue()\" [config]=\"getAttachmentConfig()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [eruGridStore]=\"eruGridStore()\" (valueChange)=\"onValueChange($event)\"\n (blur)=\"onAttachmentBlur($event)\" (editModeChange)=\"onAttachmentEditModeChange($event)\"\n (drilldownClick)=\"onAttachmentDrilldown($event)\">\n </eru-attachment>\n }\n @case ('composite') {\n <eru-composite [row]=\"row()\" [column]=\"column()\" [columns]=\"siblingColumns()\" [isDrillable]=\"canDrill()\"\n (drilldownClick)=\"onCompositeDrilldown($event)\">\n </eru-composite>\n }\n @case ('page') {\n @if (cellTemplate()) {\n <ng-container *ngTemplateOutlet=\"cellTemplate()!; context: cellTemplateContext()\"></ng-container>\n } @else {\n <div class=\"cell-default-display\">{{value()}}</div>\n }\n }\n @default {\n <div class=\"cell-default-display\">\n @if (canDrill()) {\n <span class=\"drillable-value\" (click)=\"onDrillableClick($event)\">{{value()}}</span>\n } @else {\n {{value()}}\n }\n </div>\n }\n }\n\n</div>\n\n\n<!-- <ng-template cdkConnectedOverlay\n[cdkConnectedOverlayOrigin]=\"singleSelectTrigger\"\n[cdkConnectedOverlayOpen]=\"showOverlayMenu('dropdown_single_select')\"\n(detach)=\"isOpen = showOverlayMenu('dropdown_single_select')\">\n <div class=\"dropdown-menu\" cdkMenu [style.width.px]=\"currentColumnWidth()\" (closed)=\"singleOptionClosed()\">\n <div class=\"listbox-container\">\n <mat-form-field appearance=\"outline\" class=\"search-form-field\">\n <input matInput type=\"search\" placeholder=\"Search...\" (click)=\"$event.stopPropagation()\" [ngModel]=\"optionSearchText()\"\n (ngModelChange)=\"optionSearchText.set($event)\">\n </mat-form-field>\n <ul cdkListbox [ngModel]=\"currentValue()\" (ngModelChange)=\"selectedSingleSelect($event)\"\n aria-labelledby=\"listbox-label\" class=\"listbox\">\n <li [cdkOption]=\"'None'\" class=\"listbox-option notext-overflow\">\n None\n </li>\n @for (option of filteredOptions(); track option.value || option.name) {\n <li [cdkOption]=\"option.value || option.name\" class=\"listbox-option notext-overflow\">\n {{option.label || option.name}}\n </li>\n }\n </ul>\n </div>\n </div>\n</ng-template> -->\n\n<!-- <ng-template \ncdkConnectedOverlay\n[cdkConnectedOverlayOrigin]=\"multiSelectTrigger\"\n[cdkConnectedOverlayOpen]=\"showOverlayMenu('dropdown_multi_select')\"\n(detach)=\"isOpen = showOverlayMenu('dropdown_multi_select')\">\n <div class=\"dropdown-menu\" cdkMenu [style.width.px]=\"currentColumnWidth()\" (closed)=\"singleOptionClosed()\">\n <div class=\"listbox-container\">\n <mat-form-field appearance=\"outline\" class=\"search-form-field\">\n <input matInput type=\"search\" placeholder=\"Search...\" [ngModel]=\"optionSearchText()\"\n (ngModelChange)=\"optionSearchText.set($event)\" (click)=\"$event.stopPropagation()\">\n </mat-form-field>\n \n <div class=\"select-all-container\" (click)=\"$event.stopPropagation()\">\n <mat-checkbox \n [checked]=\"isAllSelected()\" \n [indeterminate]=\"isIndeterminate()\"\n (change)=\"toggleSelectAll($event.checked)\">\n <span class=\"select-all-text\">Select All</span>\n </mat-checkbox>\n </div>\n \n <ul cdkListboxMultiple=\"true\" cdkListboxUseActiveDescendant cdkListbox [ngModel]=\"currentValue()\"\n (ngModelChange)=\"selectedMultiSelect($event)\" aria-labelledby=\"listbox-labssel\" class=\"listbox\" (click)=\"$event.stopPropagation()\">\n <li [cdkOption]=\"'None'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('None')\" (click)=\"$event.stopPropagation();appendMultiSelect('None')\"></mat-checkbox>\n <span class=\"option-text\">None</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 1'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 1')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 1')\"></mat-checkbox>\n <span class=\"option-text\">option 1</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 2'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 2')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 2')\"></mat-checkbox>\n <span class=\"option-text\">option 2</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 3'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 3')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 3')\"></mat-checkbox>\n <span class=\"option-text\">option 3</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 4'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 4')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 4')\"></mat-checkbox>\n <span class=\"option-text\">option 4</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 5'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 5')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 5')\"></mat-checkbox>\n <span class=\"option-text\">option 5</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 6'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 6')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 6')\"></mat-checkbox>\n <span class=\"option-text\">option 6</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 7'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 7')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 7')\"></mat-checkbox>\n <span class=\"option-text\">option 7</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 8'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 8')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 8')\"></mat-checkbox>\n <span class=\"option-text\">option 8</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 9'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 9')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 9')\"></mat-checkbox>\n <span class=\"option-text\">option 9</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 10'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 10')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 10')\"></mat-checkbox>\n <span class=\"option-text\">option 10</span>\n </div>\n </li>\n @for (option of filteredOptions(); track option) {\n <li [cdkOption]=\"option.value\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected(option.value)\" (click)=\"$event.stopPropagation();appendMultiSelect(option.value)\"></mat-checkbox>\n <span class=\"option-text\">{{option.label}}</span>\n </div>\n </li>\n }\n </ul>\n </div>\n </div>\n</ng-template> -->", styles: [":host{display:block;height:100%;width:100%;position:relative;overflow:hidden!important}.container{height:calc(100% - 2px);width:calc(100% - 2px);position:relative!important;overflow:hidden!important;max-width:100%!important;box-sizing:border-box!important}.container .cell-display-text{text-overflow:ellipsis!important;overflow:hidden!important;white-space:nowrap!important;max-width:100%!important;width:100%!important;display:block!important}.inputRef{height:inherit;width:inherit;border:none}.inputRef:focus{outline:none}.cell-checkbox{text-align:center}.cell-form-field{width:100%!important;height:100%!important;padding:0!important;margin:0!important}.cell-form-field .mat-mdc-form-field-outline,.cell-form-field .mat-mdc-form-field-subscript-wrapper,.cell-form-field .mat-mdc-form-field-text-suffix{display:none!important}.cell-form-field .mat-mdc-form-field-wrapper,.cell-form-field .mat-mdc-form-field-wrapper .mat-mdc-form-field-flex{width:100%!important;height:100%!important;padding:0!important;margin:0!important}.cell-form-field .mat-mdc-form-field-wrapper .mat-mdc-form-field-flex .mat-mdc-form-field-infix{width:100%!important;height:100%!important;padding:0!important;margin:0!important;min-height:auto!important;border-top:none!important}.cell-form-field input[matInput]{width:100%!important;height:100%!important;padding:2px!important;margin:0!important;border:none!important;outline:none!important;background:transparent!important;font-size:14px!important;line-height:normal!important;box-sizing:border-box!important;max-width:none!important;min-width:0!important;flex:none!important}.dropdown-menu{width:100%}.cell-display-text-editable{cursor:pointer!important}.cell-display-text{width:100%!important;height:100%!important;min-height:20px!important;display:block!important;padding:4px 8px!important;font-family:var(--grid-font-family, \"Poppins\")!important;font-size:var(--grid-font-size-body, 12px)!important;color:var(--grid-on-surface, #1d1b20)!important;background:transparent!important;border:none!important;outline:none!important;text-overflow:ellipsis!important;overflow:hidden!important;white-space:nowrap!important;max-width:100%!important;box-sizing:border-box!important;transition:background-color .2s ease!important;line-height:1.4!important}.cell-display-text,.cell-display-text>*{text-overflow:ellipsis!important;overflow:hidden!important;white-space:nowrap!important;max-width:100%!important;width:100%!important;display:block!important}.cell-display-text:empty:before{content:\"Click to select\"!important;color:var(--grid-on-surface-variant, #49454f)!important;font-style:italic!important}.cell-display-number{text-align:var(--grid-number-text-align, right)}.aggregation .cell-display-number{text-align:var(--grid-aggregation-text-align, right)}.assignee-avatars{display:flex;align-items:center;padding:4px 8px;min-height:20px}.no-assignees{color:var(--grid-on-surface-variant, #49454f);font-style:italic;font-size:12px}.option-content{display:flex;align-items:center;gap:8px;width:100%}.option-avatar{width:24px;height:24px;border-radius:50%;background-color:var(--grid-primary, #6750a4);color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;flex-shrink:0}.option-text{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.checkmark{color:var(--grid-primary, #6750a4);font-weight:700;font-size:14px;flex-shrink:0}.drillable-value{color:var(--grid-primary, #6750a4);text-decoration:underline;text-decoration-color:var(--grid-primary, #6750a4);text-decoration-thickness:1px;text-underline-offset:2px;transition:all .2s ease}.drillable-link{cursor:pointer;padding:4px}.time-display{width:100%;height:100%;min-height:20px;display:block;padding:4px 8px;font-family:var(--grid-font-family, \"Poppins\");font-size:var(--grid-font-size-body, 12px);color:var(--grid-on-surface, #1d1b20);text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.time-display-editable{cursor:pointer}\n"] }]
12804
+ }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<div class=\"container\" [ngStyle]=\"containerStyle()\">\n <!-- How the cell is drawn is a separate axis from what it holds: a composite\n or page column keeps its own datatype (and its formatting controls), so\n the renderer is chosen before the datatype switch, not inside it. -->\n @if (cellRender() === 'composite') {\n <eru-composite [row]=\"row()\" [column]=\"column()\" [columns]=\"siblingColumns()\" [isDrillable]=\"canDrill()\"\n (drilldownClick)=\"onCompositeDrilldown($event)\">\n </eru-composite>\n } @else if (cellRender() === 'page') {\n @if (cellTemplate()) {\n <ng-container *ngTemplateOutlet=\"cellTemplate()!; context: cellTemplateContext()\"></ng-container>\n } @else {\n <div class=\"cell-default-display\">{{value()}}</div>\n }\n } @else {\n @switch (columnDatatype()) {\n @case ('textbox') {\n <eru-textbox [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\"\n [externalError]=\"error()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onTextboxBlur($event)\"\n (editModeChange)=\"onTextboxEditModeChange($event)\" (drilldownClick)=\"onTextboxDrilldown($event)\">\n </eru-textbox>\n }\n @case ('currency') {\n <eru-currency [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\" [row]=\"row()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [replaceZeroValue]=\"replaceZeroValue\" (valueChange)=\"onValueChange($event)\" (blur)=\"onCurrencyBlur($event)\"\n (editModeChange)=\"onCurrencyEditModeChange($event)\" (drilldownClick)=\"onCurrencyDrilldown($event)\">\n </eru-currency>\n }\n @case ('number') {\n <eru-number [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [replaceZeroValue]=\"replaceZeroValue\" (valueChange)=\"onValueChange($event)\" (blur)=\"onNumberBlurHandler($event)\"\n (editModeChange)=\"onNumberEditModeChange($event)\" (drilldownClick)=\"onNumberDrilldown($event)\">\n </eru-number>\n }\n @case ('location') {\n <eru-location [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\"\n [externalError]=\"error()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onLocationBlurHandler($event)\"\n (editModeChange)=\"onLocationEditModeChange($event)\" (drilldownClick)=\"onLocationDrilldown($event)\">\n </eru-location>\n }\n @case ('email') {\n <eru-email [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\"\n [externalError]=\"error()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onEmailBlurHandler($event)\"\n (editModeChange)=\"onEmailEditModeChange($event)\" (drilldownClick)=\"onEmailDrilldown($event)\">\n </eru-email>\n }\n @case ('textarea') {\n <eru-textarea [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\"\n [externalError]=\"error()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onTextareaBlur($event)\"\n (editModeChange)=\"onTextareaEditModeChange($event)\" (drilldownClick)=\"onTextareaDrilldown($event)\">\n </eru-textarea>\n }\n @case ('website') {\n <eru-website [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\"\n [externalError]=\"error()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onWebsiteBlur($event)\"\n (editModeChange)=\"onWebsiteEditModeChange($event)\" (drilldownClick)=\"onWebsiteDrilldown($event)\">\n </eru-website>\n }\n <!-- @case ('dropdown_multi_select') {\n <div class=\"cell-display-text\" (dblclick)=\"toggleOverlayMenu($event)\" #multiSelectTrigger\n [class.cell-display-text-editable]=\"isEditable()\">\n @if (drillable()) {\n <span class=\"drillable-value\" (click)=\"onDrillableClick($event)\">{{formattedMultiSelectValue(currentColumnWidth()) || 'Click to select'}}</span>\n } @else {\n {{formattedMultiSelectValue(currentColumnWidth()) || 'Click to select'}}\n }\n </div>\n } -->\n\n <!-- @case ('dropdown_single_select') {\n <div class=\"cell-display-text\" (dblclick)=\"toggleOverlayMenu($event)\" cdkOverlayOrigin #singleSelectTrigger=\"cdkOverlayOrigin\"\n [class.cell-display-text-editable]=\"isEditable()\">\n @if (drillable()) {\n <span class=\"drillable-value\" (click)=\"onDrillableClick($event)\">{{currentValue()}}</span>\n } @else {\n {{currentValue()}}\n }\n </div>\n } -->\n\n @case ('checkbox') {\n <eru-checkbox [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\"\n [isDrillable]=\"canDrill()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onCheckboxBlur($event)\"\n (editModeChange)=\"onCheckboxEditModeChange($event)\" (drilldownClick)=\"onCheckboxDrilldown($event)\">\n </eru-checkbox>\n }\n\n @case ('people') {\n <eru-people [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [personCardTemplate]=\"personCardTemplate()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [eruGridStore]=\"eruGridStore()\" (valueChange)=\"onValueChange($event)\"\n (blur)=\"onPeopleBlur($event)\" (editModeChange)=\"onPeopleEditModeChange($event)\"\n (drilldownClick)=\"onPeopleDrilldown($event)\">\n </eru-people>\n }\n\n\n @case ('date') {\n <eru-date [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\"\n [isDrillable]=\"canDrill()\" (valueChange)=\"onValueChange($event)\" (editModeChange)=\"onDateEditModeChange($event)\"\n (drilldownClick)=\"onDateDrilldown($event)\">\n </eru-date>\n }\n\n @case ('datetime') {\n <eru-datetime [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\"\n [isDrillable]=\"canDrill()\" (valueChange)=\"onValueChange($event)\"\n (editModeChange)=\"onDatetimeEditModeChange($event)\" (drilldownClick)=\"onDatetimeDrilldown($event)\">\n </eru-datetime>\n }\n\n @case ('time') {\n <!-- The time-picker component existed but had no datatype and no case here, so\n a time field fell through to the plain-text renderer.\n\n Mounted only while the cell is being edited, matching date/datetime. It\n carries a mat-form-field, and rendering one per row costs a form field on\n every visible time cell to show what is really just \"HH:mm\" text. The two\n other places that embed eru-time-picker (duration, datetime) already only\n mount it inside their own open editors. -->\n @if (isActive()) {\n <eru-time-picker [value]=\"(currentValue() ?? '') + ''\"\n [disabled]=\"!isEditable() || mode() !== 'table'\"\n [placeholder]=\"'HH:mm'\"\n (valueChange)=\"onValueChange($event)\">\n </eru-time-picker>\n } @else {\n <div class=\"time-display\" [class.time-display-editable]=\"isEditable() && mode() === 'table'\"\n (dblclick)=\"onTimeActivate()\">{{currentValue() || ''}}</div>\n }\n }\n\n @case ('duration') {\n <eru-duration [value]=\"currentValue()\" [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\"\n [isDrillable]=\"canDrill()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onDurationBlur($event)\"\n (editModeChange)=\"onDurationEditModeChange($event)\" (drilldownClick)=\"onDurationDrilldown($event)\">\n </eru-duration>\n }\n\n\n @case ('priority') {\n <eru-priority [value]=\"currentValue()\" [config]=\"getPriorityConfig()\" [eruGridStore]=\"eruGridStore()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [isEditable]=\"isEditable() && mode() === 'table'\"\n [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\" (valueChange)=\"onValueChange($event)\"\n (blur)=\"onPriorityBlur($event)\" (editModeChange)=\"onPriorityEditModeChange($event)\"\n (drilldownClick)=\"onPriorityDrilldown($event)\">\n </eru-priority>\n }\n @case ('progress') {\n <eru-progress [value]=\"currentValue()\" [config]=\"columnCellConfiguration()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\"\n (valueChange)=\"onValueChange($event)\" (blur)=\"onProgressBlur($event)\" (focus)=\"onProgressFocus()\"\n (editModeChange)=\"onProgressEditModeChange($event)\" (drilldownClick)=\"onProgressDrilldown($event)\">\n </eru-progress>\n }\n\n @case ('rating') {\n <eru-rating [value]=\"currentValue()\" [config]=\"getRatingConfig()\" [isEditable]=\"isEditable() && mode() === 'table'\"\n [isActive]=\"isActive()\" (valueChange)=\"onValueChange($event)\" (editModeChange)=\"onRatingEditModeChange($event)\">\n </eru-rating>\n }\n\n @case ('status') {\n <eru-status [value]=\"currentValue()\" [config]=\"getStatusConfig()\" [isEditable]=\"isEditable() && mode() === 'table'\"\n [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\" [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\"\n [eruGridStore]=\"eruGridStore()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onStatusBlur($event)\"\n (editModeChange)=\"onStatusEditModeChange($event)\" (drilldownClick)=\"onStatusDrilldown($event)\">\n </eru-status>\n }\n\n @case ('tag') {\n <eru-tag [value]=\"currentValue()\" [config]=\"getTagConfig()\" [eruGridStore]=\"eruGridStore()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" [isEditable]=\"isEditable() && mode() === 'table'\"\n [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\" (valueChange)=\"onValueChange($event)\" (blur)=\"onTagBlur($event)\"\n (editModeChange)=\"onTagEditModeChange($event)\" (drilldownClick)=\"onTagDrilldown($event)\">\n </eru-tag>\n }\n\n @case ('phone') {\n <eru-phone [value]=\"currentValue()\" [defaultCountry]=\"columnCellConfiguration()?.default_country || 'US'\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [fieldSize]=\"fieldSize()\" (valueChange)=\"onValueChange($event)\"\n (blur)=\"onPhoneBlur($event)\" (editModeChange)=\"onPhoneEditModeChange($event)\"\n (drilldownClick)=\"onPhoneDrilldown($event)\">\n </eru-phone>\n }\n\n @case ('dropdown_single_select') {\n <eru-select [value]=\"currentValue()\" [config]=\"getSelectConfig()\" [row]=\"row()\" [isEditable]=\"isEditable() && mode() === 'table'\"\n [isActive]=\"isActive()\" [multiple]=\"false\" [isDrillable]=\"canDrill()\" [columnWidth]=\"currentColumnWidth()\"\n [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\" (valueChange)=\"onValueChange($event)\"\n (blur)=\"onSelectBlur($event)\" (editModeChange)=\"onSelectEditModeChange($event)\"\n (drilldownClick)=\"onSelectDrilldown($event)\">\n </eru-select>\n }\n @case ('dropdown_multi_select') {\n <eru-select [value]=\"currentValue()\" [config]=\"getSelectConfig()\" [row]=\"row()\" [isEditable]=\"isEditable() && mode() === 'table'\"\n [isActive]=\"isActive()\" [multiple]=\"true\" [isDrillable]=\"canDrill()\" [columnWidth]=\"currentColumnWidth()\"\n [fieldSize]=\"fieldSize()\" [eruGridStore]=\"eruGridStore()\" (valueChange)=\"onValueChange($event)\"\n (blur)=\"onSelectBlur($event)\" (editModeChange)=\"onSelectEditModeChange($event)\"\n (drilldownClick)=\"onSelectDrilldown($event)\">\n </eru-select>\n }\n\n @case ('attachment') {\n <eru-attachment [value]=\"currentValue()\" [config]=\"getAttachmentConfig()\"\n [isEditable]=\"isEditable() && mode() === 'table'\" [isActive]=\"isActive()\" [isDrillable]=\"canDrill()\"\n [columnWidth]=\"currentColumnWidth()\" [eruGridStore]=\"eruGridStore()\" (valueChange)=\"onValueChange($event)\"\n (blur)=\"onAttachmentBlur($event)\" (editModeChange)=\"onAttachmentEditModeChange($event)\"\n (drilldownClick)=\"onAttachmentDrilldown($event)\">\n </eru-attachment>\n }\n @default {\n <div class=\"cell-default-display\">\n @if (canDrill()) {\n <span class=\"drillable-value\" (click)=\"onDrillableClick($event)\">{{value()}}</span>\n } @else {\n {{value()}}\n }\n </div>\n }\n }\n }\n\n</div>\n\n\n<!-- <ng-template cdkConnectedOverlay\n[cdkConnectedOverlayOrigin]=\"singleSelectTrigger\"\n[cdkConnectedOverlayOpen]=\"showOverlayMenu('dropdown_single_select')\"\n(detach)=\"isOpen = showOverlayMenu('dropdown_single_select')\">\n <div class=\"dropdown-menu\" cdkMenu [style.width.px]=\"currentColumnWidth()\" (closed)=\"singleOptionClosed()\">\n <div class=\"listbox-container\">\n <mat-form-field appearance=\"outline\" class=\"search-form-field\">\n <input matInput type=\"search\" placeholder=\"Search...\" (click)=\"$event.stopPropagation()\" [ngModel]=\"optionSearchText()\"\n (ngModelChange)=\"optionSearchText.set($event)\">\n </mat-form-field>\n <ul cdkListbox [ngModel]=\"currentValue()\" (ngModelChange)=\"selectedSingleSelect($event)\"\n aria-labelledby=\"listbox-label\" class=\"listbox\">\n <li [cdkOption]=\"'None'\" class=\"listbox-option notext-overflow\">\n None\n </li>\n @for (option of filteredOptions(); track option.value || option.name) {\n <li [cdkOption]=\"option.value || option.name\" class=\"listbox-option notext-overflow\">\n {{option.label || option.name}}\n </li>\n }\n </ul>\n </div>\n </div>\n</ng-template> -->\n\n<!-- <ng-template \ncdkConnectedOverlay\n[cdkConnectedOverlayOrigin]=\"multiSelectTrigger\"\n[cdkConnectedOverlayOpen]=\"showOverlayMenu('dropdown_multi_select')\"\n(detach)=\"isOpen = showOverlayMenu('dropdown_multi_select')\">\n <div class=\"dropdown-menu\" cdkMenu [style.width.px]=\"currentColumnWidth()\" (closed)=\"singleOptionClosed()\">\n <div class=\"listbox-container\">\n <mat-form-field appearance=\"outline\" class=\"search-form-field\">\n <input matInput type=\"search\" placeholder=\"Search...\" [ngModel]=\"optionSearchText()\"\n (ngModelChange)=\"optionSearchText.set($event)\" (click)=\"$event.stopPropagation()\">\n </mat-form-field>\n \n <div class=\"select-all-container\" (click)=\"$event.stopPropagation()\">\n <mat-checkbox \n [checked]=\"isAllSelected()\" \n [indeterminate]=\"isIndeterminate()\"\n (change)=\"toggleSelectAll($event.checked)\">\n <span class=\"select-all-text\">Select All</span>\n </mat-checkbox>\n </div>\n \n <ul cdkListboxMultiple=\"true\" cdkListboxUseActiveDescendant cdkListbox [ngModel]=\"currentValue()\"\n (ngModelChange)=\"selectedMultiSelect($event)\" aria-labelledby=\"listbox-labssel\" class=\"listbox\" (click)=\"$event.stopPropagation()\">\n <li [cdkOption]=\"'None'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('None')\" (click)=\"$event.stopPropagation();appendMultiSelect('None')\"></mat-checkbox>\n <span class=\"option-text\">None</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 1'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 1')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 1')\"></mat-checkbox>\n <span class=\"option-text\">option 1</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 2'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 2')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 2')\"></mat-checkbox>\n <span class=\"option-text\">option 2</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 3'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 3')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 3')\"></mat-checkbox>\n <span class=\"option-text\">option 3</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 4'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 4')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 4')\"></mat-checkbox>\n <span class=\"option-text\">option 4</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 5'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 5')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 5')\"></mat-checkbox>\n <span class=\"option-text\">option 5</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 6'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 6')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 6')\"></mat-checkbox>\n <span class=\"option-text\">option 6</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 7'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 7')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 7')\"></mat-checkbox>\n <span class=\"option-text\">option 7</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 8'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 8')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 8')\"></mat-checkbox>\n <span class=\"option-text\">option 8</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 9'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 9')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 9')\"></mat-checkbox>\n <span class=\"option-text\">option 9</span>\n </div>\n </li>\n <li [cdkOption]=\"'option 10'\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected('option 10')\" (click)=\"$event.stopPropagation();appendMultiSelect('option 10')\"></mat-checkbox>\n <span class=\"option-text\">option 10</span>\n </div>\n </li>\n @for (option of filteredOptions(); track option) {\n <li [cdkOption]=\"option.value\" class=\"multi-listbox-option notext-overflow\">\n <div class=\"option-content\">\n <mat-checkbox [checked]=\"isOptionSelected(option.value)\" (click)=\"$event.stopPropagation();appendMultiSelect(option.value)\"></mat-checkbox>\n <span class=\"option-text\">{{option.label}}</span>\n </div>\n </li>\n }\n </ul>\n </div>\n </div>\n</ng-template> -->", styles: [":host{display:block;height:100%;width:100%;position:relative;overflow:hidden!important}.container{height:calc(100% - 2px);width:calc(100% - 2px);position:relative!important;overflow:hidden!important;max-width:100%!important;box-sizing:border-box!important}.container .cell-display-text{text-overflow:ellipsis!important;overflow:hidden!important;white-space:nowrap!important;max-width:100%!important;width:100%!important;display:block!important}.inputRef{height:inherit;width:inherit;border:none}.inputRef:focus{outline:none}.cell-checkbox{text-align:center}.cell-form-field{width:100%!important;height:100%!important;padding:0!important;margin:0!important}.cell-form-field .mat-mdc-form-field-outline,.cell-form-field .mat-mdc-form-field-subscript-wrapper,.cell-form-field .mat-mdc-form-field-text-suffix{display:none!important}.cell-form-field .mat-mdc-form-field-wrapper,.cell-form-field .mat-mdc-form-field-wrapper .mat-mdc-form-field-flex{width:100%!important;height:100%!important;padding:0!important;margin:0!important}.cell-form-field .mat-mdc-form-field-wrapper .mat-mdc-form-field-flex .mat-mdc-form-field-infix{width:100%!important;height:100%!important;padding:0!important;margin:0!important;min-height:auto!important;border-top:none!important}.cell-form-field input[matInput]{width:100%!important;height:100%!important;padding:2px!important;margin:0!important;border:none!important;outline:none!important;background:transparent!important;font-size:14px!important;line-height:normal!important;box-sizing:border-box!important;max-width:none!important;min-width:0!important;flex:none!important}.dropdown-menu{width:100%}.cell-display-text-editable{cursor:pointer!important}.cell-display-text{width:100%!important;height:100%!important;min-height:20px!important;display:block!important;padding:4px 8px!important;font-family:var(--grid-font-family, \"Poppins\")!important;font-size:var(--grid-font-size-body, 12px)!important;color:var(--grid-on-surface, #1d1b20)!important;background:transparent!important;border:none!important;outline:none!important;text-overflow:ellipsis!important;overflow:hidden!important;white-space:nowrap!important;max-width:100%!important;box-sizing:border-box!important;transition:background-color .2s ease!important;line-height:1.4!important}.cell-display-text,.cell-display-text>*{text-overflow:ellipsis!important;overflow:hidden!important;white-space:nowrap!important;max-width:100%!important;width:100%!important;display:block!important}.cell-display-text:empty:before{content:\"Click to select\"!important;color:var(--grid-on-surface-variant, #49454f)!important;font-style:italic!important}.cell-display-number{text-align:var(--grid-number-text-align, right)}.aggregation .cell-display-number{text-align:var(--grid-aggregation-text-align, right)}.assignee-avatars{display:flex;align-items:center;padding:4px 8px;min-height:20px}.no-assignees{color:var(--grid-on-surface-variant, #49454f);font-style:italic;font-size:12px}.option-content{display:flex;align-items:center;gap:8px;width:100%}.option-avatar{width:24px;height:24px;border-radius:50%;background-color:var(--grid-primary, #6750a4);color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;flex-shrink:0}.option-text{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.checkmark{color:var(--grid-primary, #6750a4);font-weight:700;font-size:14px;flex-shrink:0}.drillable-value{color:var(--grid-primary, #6750a4);text-decoration:underline;text-decoration-color:var(--grid-primary, #6750a4);text-decoration-thickness:1px;text-underline-offset:2px;transition:all .2s ease}.drillable-link{cursor:pointer;padding:4px}.time-display{width:100%;height:100%;min-height:20px;display:block;padding:4px 8px;font-family:var(--grid-font-family, \"Poppins\");font-size:var(--grid-font-size-body, 12px);color:var(--grid-on-surface, #1d1b20);text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.time-display-editable{cursor:pointer}\n"] }]
12423
12805
  }], ctorParameters: () => [], propDecorators: { attachmentTrigger: [{
12424
12806
  type: ViewChild,
12425
12807
  args: ['attachmentTrigger']
@@ -12697,7 +13079,15 @@ class ColumnDragDirective {
12697
13079
  el;
12698
13080
  renderer;
12699
13081
  eruGridStore;
12700
- columnIndex = null;
13082
+ /**
13083
+ * The dragged column's `name`, not its rendered index. The header only ever
13084
+ * renders the *visible* columns (hidden ones and the group-by column are
13085
+ * filtered out of `displayColumns`), so a rendered index does not line up
13086
+ * with the store's full column array — reordering by index moved whichever
13087
+ * column happened to sit at that slot in the unfiltered list. The name is
13088
+ * stable across every filtering layer.
13089
+ */
13090
+ columnKey = null;
12701
13091
  dragEl;
12702
13092
  dragHandle;
12703
13093
  isDragging = false;
@@ -12715,7 +13105,7 @@ class ColumnDragDirective {
12715
13105
  // Find the drag handle element - use setTimeout to ensure it's rendered
12716
13106
  setTimeout(() => {
12717
13107
  this.dragHandle = this.dragEl.querySelector('.column-drag-handle');
12718
- if (this.columnIndex !== null && this.columnIndex !== undefined) {
13108
+ if (this.columnKey !== null && this.columnKey !== undefined) {
12719
13109
  // Set draggable attribute on the entire column header
12720
13110
  this.renderer.setAttribute(this.dragEl, 'draggable', 'true');
12721
13111
  if (this.dragHandle) {
@@ -12740,8 +13130,8 @@ class ColumnDragDirective {
12740
13130
  };
12741
13131
  document.addEventListener('mouseup', this.mouseUpListener);
12742
13132
  }
12743
- else if (this.columnIndex === null || this.columnIndex === undefined) {
12744
- // If columnIndex is null, remove draggable attribute
13133
+ else if (this.columnKey === null || this.columnKey === undefined) {
13134
+ // If columnKey is null, remove draggable attribute
12745
13135
  this.renderer.removeAttribute(this.dragEl, 'draggable');
12746
13136
  }
12747
13137
  }, 0);
@@ -12756,7 +13146,7 @@ class ColumnDragDirective {
12756
13146
  }
12757
13147
  }
12758
13148
  onDragStart(event) {
12759
- // Only allow drag if it was initiated by the handle OR if we have a valid columnIndex
13149
+ // Only allow drag if it was initiated by the handle OR if we have a valid columnKey
12760
13150
  // Check if event originated from drag handle
12761
13151
  const target = event.target;
12762
13152
  const isFromHandle = target.classList.contains('column-drag-handle') ||
@@ -12766,9 +13156,9 @@ class ColumnDragDirective {
12766
13156
  event.preventDefault();
12767
13157
  return;
12768
13158
  }
12769
- // Validate column index
12770
- if (this.columnIndex === null || this.columnIndex === undefined) {
12771
- console.error('Invalid column index for drag');
13159
+ // Validate column key
13160
+ if (this.columnKey === null || this.columnKey === undefined) {
13161
+ console.error('Invalid column key for drag');
12772
13162
  event.preventDefault();
12773
13163
  return;
12774
13164
  }
@@ -12782,7 +13172,7 @@ class ColumnDragDirective {
12782
13172
  // Allow moving
12783
13173
  event.dataTransfer.effectAllowed = 'move';
12784
13174
  // Set drag data
12785
- event.dataTransfer.setData('text/plain', this.columnIndex.toString());
13175
+ event.dataTransfer.setData('text/plain', this.columnKey);
12786
13176
  // Set drag image to the entire column header
12787
13177
  event.dataTransfer.setDragImage(this.dragEl, 0, 0);
12788
13178
  }
@@ -12808,26 +13198,18 @@ class ColumnDragDirective {
12808
13198
  }
12809
13199
  }
12810
13200
  onDrop(event) {
12811
- // Validate column index
12812
- if (this.columnIndex === null || this.columnIndex === undefined) {
12813
- console.error('Invalid column index for drop');
13201
+ // Validate column key
13202
+ if (this.columnKey === null || this.columnKey === undefined) {
13203
+ console.error('Invalid column key for drop');
12814
13204
  event.preventDefault();
12815
13205
  return;
12816
13206
  }
12817
13207
  event.preventDefault();
12818
- // Get the dragged column index
12819
- let fromIndex;
12820
- try {
12821
- fromIndex = parseInt(event.dataTransfer?.getData('text/plain') || '0', 10);
12822
- }
12823
- catch (error) {
12824
- console.error('Error parsing drag data', error);
12825
- return;
12826
- }
12827
- const toIndex = this.columnIndex;
13208
+ const fromKey = event.dataTransfer?.getData('text/plain') || '';
13209
+ const toKey = this.columnKey;
12828
13210
  // Reorder columns if different
12829
- if (fromIndex !== toIndex) {
12830
- this.eruGridStore.reorderColumns(fromIndex, toIndex);
13211
+ if (fromKey && fromKey !== toKey) {
13212
+ this.eruGridStore.reorderColumnsByName(fromKey, toKey);
12831
13213
  }
12832
13214
  }
12833
13215
  onDragEnd(event) {
@@ -12836,7 +13218,7 @@ class ColumnDragDirective {
12836
13218
  this.isDragging = false;
12837
13219
  }
12838
13220
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: ColumnDragDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: EruGridStore }], target: i0.ɵɵFactoryTarget.Directive });
12839
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.2", type: ColumnDragDirective, isStandalone: true, selector: "[columnDraggable]", inputs: { columnIndex: ["columnDraggable", "columnIndex"] }, host: { listeners: { "dragstart": "onDragStart($event)", "dragover": "onDragOver($event)", "drop": "onDrop($event)", "dragend": "onDragEnd($event)" } }, ngImport: i0 });
13221
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.2", type: ColumnDragDirective, isStandalone: true, selector: "[columnDraggable]", inputs: { columnKey: ["columnDraggable", "columnKey"] }, host: { listeners: { "dragstart": "onDragStart($event)", "dragover": "onDragOver($event)", "drop": "onDrop($event)", "dragend": "onDragEnd($event)" } }, ngImport: i0 });
12840
13222
  }
12841
13223
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: ColumnDragDirective, decorators: [{
12842
13224
  type: Directive,
@@ -12844,7 +13226,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
12844
13226
  selector: '[columnDraggable]',
12845
13227
  standalone: true
12846
13228
  }]
12847
- }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: EruGridStore }], propDecorators: { columnIndex: [{
13229
+ }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: EruGridStore }], propDecorators: { columnKey: [{
12848
13230
  type: Input,
12849
13231
  args: ['columnDraggable']
12850
13232
  }], onDragStart: [{
@@ -12870,6 +13252,17 @@ const UNIVERSAL_FIELDS = [
12870
13252
  control: 'select',
12871
13253
  options: DATA_TYPES.map(d => ({ value: d, label: d })),
12872
13254
  },
13255
+ {
13256
+ key: 'cell_render',
13257
+ label: 'Cell rendering',
13258
+ control: 'select',
13259
+ defaultValue: 'default',
13260
+ options: [
13261
+ { value: 'default', label: 'By data type' },
13262
+ { value: 'composite', label: 'Composite (several fields in one cell)' },
13263
+ { value: 'page', label: 'Page layout' },
13264
+ ],
13265
+ },
12873
13266
  { key: 'tool_tip', label: 'Tooltip', control: 'text' },
12874
13267
  { key: 'description', label: 'Description', control: 'text' },
12875
13268
  { key: 'default', label: 'Default value', control: 'text' },
@@ -12911,6 +13304,38 @@ const COMPOSITE_FIELDS = [
12911
13304
  control: 'text',
12912
13305
  showWhen: f => f.composite_direction === 'inline',
12913
13306
  },
13307
+ {
13308
+ key: 'composite_label_position',
13309
+ label: 'Part labels',
13310
+ control: 'select',
13311
+ defaultValue: 'none',
13312
+ options: [
13313
+ { value: 'none', label: 'Hidden' },
13314
+ { value: 'left', label: 'Before the value' },
13315
+ { value: 'right', label: 'After the value' },
13316
+ ],
13317
+ },
13318
+ {
13319
+ key: 'composite_labels',
13320
+ label: 'Primary label',
13321
+ control: 'text_slot',
13322
+ slot: 0,
13323
+ showWhen: f => f.composite_label_position === 'left' || f.composite_label_position === 'right',
13324
+ },
13325
+ {
13326
+ key: 'composite_labels',
13327
+ label: 'Secondary label',
13328
+ control: 'text_slot',
13329
+ slot: 1,
13330
+ showWhen: f => f.composite_label_position === 'left' || f.composite_label_position === 'right',
13331
+ },
13332
+ {
13333
+ key: 'composite_labels',
13334
+ label: 'Third label',
13335
+ control: 'text_slot',
13336
+ slot: 2,
13337
+ showWhen: f => f.composite_label_position === 'left' || f.composite_label_position === 'right',
13338
+ },
12914
13339
  { key: 'cell_style_secondary', label: 'Secondary value style', control: 'text_style' },
12915
13340
  ];
12916
13341
  const PAGE_CELL_FIELDS = [
@@ -13117,6 +13542,9 @@ const DATATYPE_FIELDS = {
13117
13542
  { key: 'max_files', label: 'Max files', control: 'number' },
13118
13543
  { key: 'allowed_file_types', label: 'Allowed types (one per line)', control: 'list' },
13119
13544
  { key: 'max_file_size', label: 'Max file size (bytes)', control: 'number' },
13545
+ { key: 'default_upload', label: 'Default upload', control: 'checkbox', defaultValue: true },
13546
+ { key: 'storage_name', label: 'Storage name', control: 'text', showWhen: f => f.default_upload !== false },
13547
+ { key: 'folder_name', label: 'Folder from field', control: 'text', showWhen: f => f.default_upload !== false },
13120
13548
  ],
13121
13549
  phone: [{ key: 'default_country', label: 'Default country (ISO)', control: 'text' }],
13122
13550
  people: [
@@ -13138,8 +13566,6 @@ const DATATYPE_FIELDS = {
13138
13566
  ],
13139
13567
  textbox: TEXTBOX_FIELDS,
13140
13568
  textarea: TEXTBOX_FIELDS,
13141
- composite: COMPOSITE_FIELDS,
13142
- page: PAGE_CELL_FIELDS,
13143
13569
  checkbox: [
13144
13570
  { key: 'value_true', label: 'Stored value when checked', control: 'text' },
13145
13571
  { key: 'value_false', label: 'Stored value when unchecked', control: 'text' },
@@ -13154,25 +13580,36 @@ const DATATYPE_FIELDS = {
13154
13580
  * Datatype choices offered for a column.
13155
13581
  *
13156
13582
  * A mapped column's type belongs to the data model, so the full list would only
13157
- * offer changes that `buildMappedColumnPatch` overwrites on the next resolve.
13158
- * The presentation types are the exception it honours they say how the column
13159
- * is drawn, not what it holds so a mapped column is offered its own type plus
13160
- * those, and nothing else.
13583
+ * offer changes that `buildMappedColumnPatch` overwrites on the next resolve
13584
+ * it is offered its own type and nothing else. Composite and page used to be
13585
+ * the exception here; they are `cell_render` now, a separate axis that a mapped
13586
+ * column is free to set without touching the type it inherits.
13161
13587
  */
13162
13588
  function datatypeOptions(field) {
13163
13589
  const isMapped = !!field.mapped_entity && !!field.mapped_field;
13164
- const types = isMapped
13165
- ? DATA_TYPES.filter(d => d === field.datatype || PRESENTATION_DATATYPES.has(d))
13166
- : DATA_TYPES;
13590
+ const types = isMapped ? DATA_TYPES.filter(d => d === field.datatype) : DATA_TYPES;
13167
13591
  return types.map(d => ({ value: d, label: d }));
13168
13592
  }
13593
+ /**
13594
+ * Controls for the chosen renderer, shown alongside — not instead of — the
13595
+ * datatype's own. That is the point of the split: a composite column is still a
13596
+ * currency or a date column underneath, and its symbol, decimals and format stay
13597
+ * editable while the composite is on, and keep their values when it is off.
13598
+ */
13599
+ function cellRenderFields(field) {
13600
+ switch (normalizeCellRender(field)) {
13601
+ case 'composite': return COMPOSITE_FIELDS;
13602
+ case 'page': return PAGE_CELL_FIELDS;
13603
+ default: return [];
13604
+ }
13605
+ }
13169
13606
  function getMetaFields(field) {
13170
13607
  if (!field)
13171
13608
  return [];
13172
13609
  const extra = field.datatype ? DATATYPE_FIELDS[field.datatype] ?? [] : [];
13173
13610
  const excluded = new Set(field.datatype ? GOVERNANCE_EXCLUSIONS[field.datatype] ?? [] : []);
13174
13611
  const governance = GOVERNANCE_FIELDS.filter(def => !excluded.has(def.key));
13175
- return [...UNIVERSAL_FIELDS, ...extra, ...governance]
13612
+ return [...UNIVERSAL_FIELDS, ...cellRenderFields(field), ...extra, ...governance]
13176
13613
  .filter(def => !def.showWhen || def.showWhen(field))
13177
13614
  .map(def => (def.key === 'datatype' ? { ...def, options: datatypeOptions(field) } : def));
13178
13615
  }
@@ -13557,9 +13994,14 @@ class ColumnDesignPanelComponent {
13557
13994
  return arr[def.slot ?? 0] ?? null;
13558
13995
  }
13559
13996
  /**
13560
- * Writes one position of an ordered field list. Trailing empties are dropped
13561
- * so clearing the third slot leaves a two-part composite rather than a list
13562
- * with a hole in it, which `parts` would otherwise have to filter.
13997
+ * Writes one position of an ordered list. Trailing empties are dropped so
13998
+ * clearing the third slot leaves a two-part composite rather than a list with
13999
+ * a hole in it, which `parts` would otherwise have to filter.
14000
+ *
14001
+ * Interior holes are only collapsed for a `field_slot`. A `text_slot` list is
14002
+ * read positionally against another one — composite_labels against
14003
+ * composite_fields — so compacting it would slide every later label onto the
14004
+ * wrong part the moment an earlier one was left blank.
13563
14005
  */
13564
14006
  onSlotChange(def, value) {
13565
14007
  const slot = def.slot ?? 0;
@@ -13570,7 +14012,7 @@ class ColumnDesignPanelComponent {
13570
14012
  arr[slot] = value || null;
13571
14013
  while (arr.length > 0 && !arr[arr.length - 1])
13572
14014
  arr.pop();
13573
- this.patch(def.key, arr.filter(Boolean));
14015
+ this.patch(def.key, def.control === 'text_slot' ? arr.map(v => v ?? '') : arr.filter(Boolean));
13574
14016
  }
13575
14017
  // ── Text-style control ─────────────────────────────────────────────────
13576
14018
  styleValue(key, prop) {
@@ -13609,8 +14051,37 @@ class ColumnDesignPanelComponent {
13609
14051
  this.patch(key, value === '' || value === null ? null : Number(value));
13610
14052
  }
13611
14053
  onSelect(key, value) {
14054
+ if (key === 'datatype') {
14055
+ this.changeDatatype(value);
14056
+ return;
14057
+ }
13612
14058
  this.patch(key, value);
13613
14059
  }
14060
+ /**
14061
+ * Changing a column's datatype, with the presentation types handled specially.
14062
+ *
14063
+ * `composite` and `page` say how the column is DRAWN, not what it holds, so
14064
+ * switching to one must remember the real type — otherwise the value has
14065
+ * nothing to format itself by, which is why a composed date column reverted
14066
+ * to its raw stored string. Switching back to a real type clears the memo.
14067
+ */
14068
+ changeDatatype(value) {
14069
+ const current = this.field();
14070
+ const next = String(value ?? '');
14071
+ const wasPresentation = PRESENTATION_DATATYPES.has(String(current?.datatype ?? ''));
14072
+ const isPresentation = PRESENTATION_DATATYPES.has(next);
14073
+ const name = this.gridStore.selectedDesignColumn();
14074
+ if (!name)
14075
+ return;
14076
+ const patch = { datatype: next };
14077
+ if (isPresentation && !wasPresentation) {
14078
+ patch.presentation_base_datatype = current?.datatype;
14079
+ }
14080
+ else if (!isPresentation) {
14081
+ patch.presentation_base_datatype = null;
14082
+ }
14083
+ this.gridStore.updateColumnMeta(name, patch);
14084
+ }
13614
14085
  onCheckbox(key, checked) {
13615
14086
  this.patch(key, checked);
13616
14087
  }
@@ -13685,7 +14156,7 @@ class ColumnDesignPanelComponent {
13685
14156
  this.gridStore.selectDesignColumn(null);
13686
14157
  }
13687
14158
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: ColumnDesignPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
13688
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: ColumnDesignPanelComponent, isStandalone: true, selector: "eru-column-design-panel", ngImport: i0, template: "@if (isOpen()) {\n<div class=\"design-panel-backdrop\" (click)=\"close()\"></div>\n}\n<aside class=\"column-design-panel\" [class.open]=\"isOpen()\">\n @if (field(); as col) {\n <header class=\"design-panel-header\">\n <div class=\"design-panel-title\">\n <mat-icon>tune</mat-icon>\n <span>{{ col.label || col.name }}</span>\n </div>\n <button mat-icon-button (click)=\"close()\" title=\"Close\">\n <mat-icon>close</mat-icon>\n </button>\n </header>\n\n <div class=\"design-panel-body\">\n <!-- Which field is being designed. A pivot with one measure renders no\n measure-name row, so its single header cell stands for the column\n dimension and the measure at once and can only open one of them; this\n reaches the other. Also the way to a header too narrow to click, or a\n hidden column. Only shown when there is more than one thing to pick. -->\n @if (designTargets().length > 0 && targetCount() > 1) {\n <div class=\"design-field design-target\">\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Editing</mat-label>\n <mat-select [ngModel]=\"gridStore.selectedDesignColumn()\"\n (ngModelChange)=\"gridStore.selectDesignColumn($event)\">\n @for (group of designTargets(); track group.group) {\n <mat-optgroup [label]=\"group.group\">\n @for (target of group.fields; track target.name) {\n <mat-option [value]=\"target.name\">\n {{ target.label || target.name }}\n @if (target.is_hidden) {<span class=\"design-target-hidden\">hidden</span>}\n </mat-option>\n }\n </mat-optgroup>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n\n <!-- Where this column's metadata comes from: typed in here, or inherited\n from a field in the host app's data model so it stays defined once. -->\n <div class=\"design-field design-meta-source\">\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Metadata source</mat-label>\n <mat-select [ngModel]=\"metaSource()\" (ngModelChange)=\"onMetaSourceChange($event)\">\n <mat-option value=\"manual\">Defined here</mat-option>\n <mat-option value=\"entity\">Mapped to data model field</mat-option>\n </mat-select>\n </mat-form-field>\n\n @if (metaSource() === 'entity') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Entity</mat-label>\n <mat-select [ngModel]=\"getValue('mapped_entity')\" (ngModelChange)=\"onEntityChange($event)\"\n (openedChange)=\"$event && requestEntities()\">\n @for (opt of entityOptions(); track opt.value) {\n <mat-option [value]=\"opt.value\">{{ opt.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (getValue('mapped_entity')) {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Field</mat-label>\n <mat-select [ngModel]=\"getValue('mapped_field')\" (ngModelChange)=\"onEntityFieldChange($event)\">\n @for (opt of entityFieldOptions(); track opt.value) {\n <mat-option [value]=\"opt.value\">{{ opt.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @if (isEntityMapped()) {\n <p class=\"design-meta-hint\">\n Colours, options and formatting come from the data model. Edit them there to update\n every grid that maps this field. The date format, abbreviation, display scale and\n symbol field start from the model but can be changed here for this grid alone.\n </p>\n }\n }\n </div>\n\n @for (def of metaFields(); track trackMetaField(def)) {\n <div class=\"design-field\" [class.design-field-inherited]=\"isInherited(def.key)\"\n [title]=\"isInherited(def.key) ? 'Inherited from the mapped data model field' : ''\">\n @switch (def.control) {\n\n @case ('text') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>{{ def.label }}</mat-label>\n <input matInput [ngModel]=\"getValue(def.key)\" (ngModelChange)=\"onText(def.key, $event)\" />\n </mat-form-field>\n }\n\n @case ('number') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>{{ def.label }}</mat-label>\n <input matInput type=\"number\" [ngModel]=\"getValue(def.key)\" (ngModelChange)=\"onNumber(def.key, $event)\" />\n </mat-form-field>\n }\n\n @case ('select') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>{{ def.label }}</mat-label>\n <mat-select [ngModel]=\"getValue(def.key) ?? def.defaultValue\" (ngModelChange)=\"onSelect(def.key, $event)\">\n @for (opt of def.options; track opt.value) {\n <mat-option [value]=\"opt.value\">{{ opt.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @case ('field_select') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>{{ def.label }}</mat-label>\n <mat-select [ngModel]=\"getValue(def.key)\" (ngModelChange)=\"onSelect(def.key, $event)\">\n <mat-option [value]=\"null\">None</mat-option>\n @for (opt of fieldSelectOptions(); track opt.value) {\n <mat-option [value]=\"opt.value\">{{ opt.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @case ('field_slot') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>{{ def.label }}</mat-label>\n <mat-select [ngModel]=\"slotValue(def)\" (ngModelChange)=\"onSlotChange(def, $event)\">\n <mat-option [value]=\"null\">None</mat-option>\n @for (opt of fieldSlotOptions(); track opt.value) {\n <mat-option [value]=\"opt.value\">{{ opt.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @case ('text_style') {\n <div class=\"text-style-control\">\n <label class=\"text-style-label\">{{ def.label }}</label>\n <div class=\"text-style-row\">\n <mat-form-field appearance=\"outline\" class=\"text-style-number\">\n <mat-label>Size</mat-label>\n <input matInput type=\"number\" [ngModel]=\"styleValue(def.key, 'font_size')\"\n (ngModelChange)=\"onStyleChange(def.key, 'font_size', $event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"text-style-number\">\n <mat-label>Weight</mat-label>\n <input matInput type=\"number\" [ngModel]=\"styleValue(def.key, 'font_weight')\"\n (ngModelChange)=\"onStyleChange(def.key, 'font_weight', $event)\" />\n </mat-form-field>\n <mat-checkbox class=\"text-style-check\" [checked]=\"!!styleValue(def.key, 'italic')\"\n (change)=\"onStyleChange(def.key, 'italic', $event.checked)\">Italic</mat-checkbox>\n </div>\n\n @if (!isSelfColoured()) {\n <div class=\"color-control\">\n <span class=\"color-control-label\">Text colour</span>\n <div class=\"color-control-body\">\n <mat-form-field appearance=\"outline\" class=\"color-token-field\">\n <mat-select [ngModel]=\"colorToken(styleColor(def.key, 'color'))\"\n (ngModelChange)=\"onStyleColorEdit(def.key, 'color', { token: $event })\">\n <mat-option value=\"\">Custom</mat-option>\n @for (t of colorTokens; track t.value) {\n <mat-option [value]=\"t.value\"><span class=\"color-swatch\" [style.background]=\"t.value\"></span>{{ t.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (!isTokenColor(styleColor(def.key, 'color'))) {\n <input type=\"color\" class=\"color-hex\" [value]=\"colorHex(styleColor(def.key, 'color'))\"\n (input)=\"onStyleColorEdit(def.key, 'color', { hex: $any($event.target).value })\" />\n } @else {\n <span class=\"color-preview\" [style.background]=\"styleColor(def.key, 'color')\"></span>\n }\n <input type=\"range\" class=\"color-alpha\" min=\"0\" max=\"100\" step=\"1\"\n [value]=\"colorAlpha(styleColor(def.key, 'color'))\"\n (input)=\"onStyleColorEdit(def.key, 'color', { alpha: +$any($event.target).value })\" />\n <span class=\"color-alpha-value\">{{ colorAlpha(styleColor(def.key, 'color')) }}%</span>\n @if (styleColor(def.key, 'color')) {\n <button type=\"button\" class=\"text-style-clear\" (click)=\"clearStyleColor(def.key, 'color')\">Reset</button>\n } @else {\n <span class=\"text-style-unset-hint\">theme</span>\n }\n </div>\n </div>\n }\n\n <div class=\"color-control\">\n <span class=\"color-control-label\">{{ isSelfColoured() ? 'Cell background' : 'Background' }}</span>\n <div class=\"color-control-body\">\n <mat-form-field appearance=\"outline\" class=\"color-token-field\">\n <mat-select [ngModel]=\"colorToken(styleColor(def.key, 'background'))\"\n (ngModelChange)=\"onStyleColorEdit(def.key, 'background', { token: $event })\">\n <mat-option value=\"\">Custom</mat-option>\n @for (t of colorTokens; track t.value) {\n <mat-option [value]=\"t.value\"><span class=\"color-swatch\" [style.background]=\"t.value\"></span>{{ t.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (!isTokenColor(styleColor(def.key, 'background'))) {\n <input type=\"color\" class=\"color-hex\" [value]=\"colorHex(styleColor(def.key, 'background'))\"\n (input)=\"onStyleColorEdit(def.key, 'background', { hex: $any($event.target).value })\" />\n } @else {\n <span class=\"color-preview\" [style.background]=\"styleColor(def.key, 'background')\"></span>\n }\n <input type=\"range\" class=\"color-alpha\" min=\"0\" max=\"100\" step=\"1\"\n [value]=\"colorAlpha(styleColor(def.key, 'background'))\"\n (input)=\"onStyleColorEdit(def.key, 'background', { alpha: +$any($event.target).value })\" />\n <span class=\"color-alpha-value\">{{ colorAlpha(styleColor(def.key, 'background')) }}%</span>\n @if (styleColor(def.key, 'background')) {\n <button type=\"button\" class=\"text-style-clear\" (click)=\"clearStyleColor(def.key, 'background')\">Reset</button>\n } @else {\n <span class=\"text-style-unset-hint\">none</span>\n }\n </div>\n </div>\n\n @if (styleColor(def.key, 'background') && !isSelfColoured()) {\n <div class=\"text-style-row\">\n <mat-form-field appearance=\"outline\" class=\"fill-select\">\n <mat-label>Fill</mat-label>\n <mat-select [ngModel]=\"styleFill(def.key)\" (ngModelChange)=\"onStyleFill(def.key, $event)\">\n <mat-option value=\"text\">Behind the text (pill)</mat-option>\n <mat-option value=\"cell\">Whole cell</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n }\n </div>\n }\n\n @case ('rule_list') {\n <div class=\"rule-list\">\n <label class=\"text-style-label\">{{ def.label }}</label>\n <p class=\"rule-list-hint\">Checked top to bottom \u2014 the first rule that matches wins.</p>\n @for (rule of ruleItems(def.key); track $index) {\n <div class=\"rule-row\">\n <div class=\"rule-row-head\">\n <mat-form-field appearance=\"outline\" class=\"rule-op\">\n <mat-label>When</mat-label>\n <mat-select [ngModel]=\"rule.op || 'between'\" (ngModelChange)=\"updateRule(def.key, $index, 'op', $event)\">\n @for (o of ruleOperators; track o.value) {\n <mat-option [value]=\"o.value\">{{ o.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <div class=\"rule-actions\">\n <button type=\"button\" class=\"rule-btn\" title=\"Move up\" (click)=\"moveRule(def.key, $index, -1)\">\u2191</button>\n <button type=\"button\" class=\"rule-btn\" title=\"Move down\" (click)=\"moveRule(def.key, $index, 1)\">\u2193</button>\n <button type=\"button\" class=\"rule-btn rule-btn-remove\" title=\"Remove\" (click)=\"removeRule(def.key, $index)\">\u2715</button>\n </div>\n </div>\n @if (operandCount(rule.op) > 0) {\n <div class=\"rule-row-operands\">\n <mat-form-field appearance=\"outline\" class=\"rule-source\">\n <mat-label>{{ operandCount(rule.op) === 2 ? 'From' : 'Value' }}</mat-label>\n <mat-select [ngModel]=\"ruleSource(def.key, $index, 'from')\"\n (ngModelChange)=\"onRuleSource(def.key, $index, 'from', $event)\">\n @for (o of operandSources; track o.value) {\n <mat-option [value]=\"o.value\">{{ o.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (!ruleSource(def.key, $index, 'from')) {\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>Value</mat-label>\n <input matInput [ngModel]=\"rule.from\" (ngModelChange)=\"updateRule(def.key, $index, 'from', $event)\" />\n </mat-form-field>\n } @else {\n @if (ruleSourceArg(def.key, $index, 'from'); as arg) {\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>{{ arg === 'p' ? 'Percentile' : 'N' }}</mat-label>\n <input matInput type=\"number\" [ngModel]=\"ruleSourceArgValue(def.key, $index, 'from')\"\n (ngModelChange)=\"onRuleSourceArg(def.key, $index, 'from', $event)\" />\n </mat-form-field>\n }\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>\u00B1 std dev</mat-label>\n <input matInput type=\"number\" [ngModel]=\"ruleStddevOffset(def.key, $index, 'from')\"\n (ngModelChange)=\"onRuleStddevOffset(def.key, $index, 'from', $event)\" />\n </mat-form-field>\n }\n </div>\n @if (operandCount(rule.op) === 2) {\n <div class=\"rule-row-operands\">\n <mat-form-field appearance=\"outline\" class=\"rule-source\">\n <mat-label>To</mat-label>\n <mat-select [ngModel]=\"ruleSource(def.key, $index, 'to')\"\n (ngModelChange)=\"onRuleSource(def.key, $index, 'to', $event)\">\n @for (o of operandSources; track o.value) {\n <mat-option [value]=\"o.value\">{{ o.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (!ruleSource(def.key, $index, 'to')) {\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>Value</mat-label>\n <input matInput [ngModel]=\"rule.to\" (ngModelChange)=\"updateRule(def.key, $index, 'to', $event)\" />\n </mat-form-field>\n } @else if (ruleSourceArg(def.key, $index, 'to'); as arg) {\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>{{ arg === 'p' ? 'Percentile' : 'N' }}</mat-label>\n <input matInput type=\"number\" [ngModel]=\"ruleSourceArgValue(def.key, $index, 'to')\"\n (ngModelChange)=\"onRuleSourceArg(def.key, $index, 'to', $event)\" />\n </mat-form-field>\n }\n </div>\n }\n }\n <div class=\"rule-row-format\">\n <span class=\"rule-color-group\">\n <span class=\"rule-format-label\">Text</span>\n <!-- Swapped for a read-only preview while a token is in force, as\n the column-level control does: with a token set,\n composeColorValue returns the token and a hex picked here is\n discarded, so an editable swatch invited an edit that\n silently did nothing. -->\n @if (!isTokenColor(rule.color)) {\n <input type=\"color\" class=\"color-hex\" [value]=\"colorHex(rule.color)\"\n (input)=\"onRuleColorEdit(def.key, $index, 'color', { hex: $any($event.target).value })\" />\n } @else {\n <span class=\"color-preview\" [style.background]=\"rule.color\"></span>\n }\n <mat-form-field appearance=\"outline\" class=\"color-token-field rule-token\">\n <mat-select [ngModel]=\"colorToken(rule.color)\"\n (ngModelChange)=\"onRuleColorEdit(def.key, $index, 'color', { token: $event })\">\n <mat-option value=\"\">Custom</mat-option>\n @for (t of colorTokens; track t.value) {\n <mat-option [value]=\"t.value\"><span class=\"color-swatch\" [style.background]=\"t.value\"></span>{{ t.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n </span>\n <!-- Grouped so the row wraps between the two colours rather than\n through one: the percentage broke onto the next line beside\n 'Bold' and read as belonging to it. -->\n <span class=\"rule-color-group\">\n <span class=\"rule-format-label\">Fill</span>\n @if (!isTokenColor(rule.background)) {\n <input type=\"color\" class=\"color-hex\" [value]=\"colorHex(rule.background)\"\n (input)=\"onRuleColorEdit(def.key, $index, 'background', { hex: $any($event.target).value })\" />\n } @else {\n <span class=\"color-preview\" [style.background]=\"rule.background\"></span>\n }\n <!-- A fill is as likely to want a theme colour as the text is, and\n the alpha slider works for either: composeColorValue wraps a\n token in color-mix below 100%. -->\n <mat-form-field appearance=\"outline\" class=\"color-token-field rule-token\">\n <mat-select [ngModel]=\"colorToken(rule.background)\"\n (ngModelChange)=\"onRuleColorEdit(def.key, $index, 'background', { token: $event })\">\n <mat-option value=\"\">Custom</mat-option>\n @for (t of colorTokens; track t.value) {\n <mat-option [value]=\"t.value\"><span class=\"color-swatch\" [style.background]=\"t.value\"></span>{{ t.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <!-- Its own line: the label, swatch and token select already fill\n the panel's width, and a token name as long as 'On Secondary\n Container' pushed the slider off the right edge. -->\n <span class=\"rule-alpha-group\">\n <span class=\"rule-format-label\">Opacity</span>\n <input type=\"range\" class=\"color-alpha\" min=\"0\" max=\"100\" step=\"1\" [value]=\"colorAlpha(rule.background)\"\n (input)=\"onRuleColorEdit(def.key, $index, 'background', { alpha: +$any($event.target).value })\" />\n <span class=\"color-alpha-value\">{{ colorAlpha(rule.background) }}%</span>\n </span>\n </span>\n <mat-checkbox class=\"text-style-check\" [checked]=\"!!rule.bold\"\n (change)=\"updateRule(def.key, $index, 'bold', $event.checked)\">Bold</mat-checkbox>\n <mat-checkbox class=\"text-style-check\" [checked]=\"rule.fill === 'text'\"\n (change)=\"updateRule(def.key, $index, 'fill', $event.checked ? 'text' : 'cell')\">Pill</mat-checkbox>\n <mat-checkbox class=\"text-style-check\" [checked]=\"!!rule.bar\"\n (change)=\"updateRule(def.key, $index, 'bar', $event.checked)\">Data bar</mat-checkbox>\n @if (rule.bar) {\n <mat-checkbox class=\"text-style-check\" [checked]=\"!!rule.bar_auto\"\n (change)=\"updateRule(def.key, $index, 'bar_auto', $event.checked)\">Scale to column</mat-checkbox>\n }\n </div>\n @if (rule.bar && !rule.bar_auto) {\n <div class=\"rule-row-format\">\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>Bar min</mat-label>\n <input matInput type=\"number\" [ngModel]=\"rule.bar_min\" (ngModelChange)=\"updateRule(def.key, $index, 'bar_min', $event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>Bar max</mat-label>\n <input matInput type=\"number\" [ngModel]=\"rule.bar_max\" (ngModelChange)=\"updateRule(def.key, $index, 'bar_max', $event)\" />\n </mat-form-field>\n </div>\n }\n </div>\n }\n <button type=\"button\" class=\"rule-add\" (click)=\"addRule(def.key)\">+ Add rule</button>\n </div>\n }\n\n @case ('checkbox') {\n <mat-checkbox [checked]=\"isChecked(def)\" (change)=\"onCheckbox(def.key, $event.checked)\">\n {{ def.label }}\n </mat-checkbox>\n }\n\n @case ('day_chips') {\n <div class=\"day-chips\">\n <label class=\"day-chips-label\">{{ def.label }}</label>\n <div class=\"day-chips-row\">\n @for (day of weekDays; track day) {\n <button type=\"button\" class=\"day-chip\" [class.selected]=\"isDaySelected(def.key, day)\"\n (click)=\"toggleDay(def.key, day)\">{{ day }}</button>\n }\n </div>\n </div>\n }\n\n @case ('list') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>{{ def.label }}</mat-label>\n <textarea matInput rows=\"4\" [ngModel]=\"listText(def.key)\"\n (ngModelChange)=\"onList(def.key, $event)\"></textarea>\n </mat-form-field>\n }\n\n @case ('color_list') {\n <div class=\"color-list\">\n <label class=\"color-list-label\">{{ def.label }}</label>\n <div class=\"color-list-chips\">\n @for (opt of colorListItems(def.key); track $index) {\n <div class=\"color-list-chip\">\n <input type=\"color\" class=\"color-list-dot\" [value]=\"opt.color || '#9CA3AF'\"\n (input)=\"onColorListColorChange(def.key, $index, $any($event.target).value)\" title=\"Change colour\" />\n <span class=\"color-list-name\">{{ opt.name }}</span>\n <button type=\"button\" class=\"color-list-remove\" (click)=\"removeColorListItem(def.key, $index)\"\n title=\"Remove\">&times;</button>\n </div>\n }\n </div>\n <input class=\"color-list-add\" type=\"text\" placeholder=\"Add option, press Enter\"\n (keydown.enter)=\"addColorListItem(def.key, $any($event.target).value, def.defaultColor || '#9CA3AF'); $any($event.target).value = ''\" />\n </div>\n }\n\n @case ('range_color_list') {\n <div class=\"range-list\">\n <label class=\"range-list-label\">{{ def.label }}</label>\n @for (r of rangeListItems(def.key); track $index) {\n <div class=\"range-list-row\">\n <input type=\"number\" class=\"range-list-num\" min=\"0\" max=\"100\" [value]=\"r.from\"\n (input)=\"onRangeChange(def.key, $index, 'from', $any($event.target).value)\" title=\"From\" />\n <span class=\"range-list-sep\">\u2013</span>\n <input type=\"number\" class=\"range-list-num\" min=\"0\" max=\"100\" [value]=\"r.to\"\n (input)=\"onRangeChange(def.key, $index, 'to', $any($event.target).value)\" title=\"To\" />\n <input type=\"color\" class=\"range-list-color\" [value]=\"r.color || '#22C55E'\"\n (input)=\"onRangeColorChange(def.key, $index, $any($event.target).value)\" title=\"Colour\" />\n <button type=\"button\" class=\"range-list-remove\" (click)=\"removeRangeItem(def.key, $index)\"\n title=\"Remove\">&times;</button>\n </div>\n }\n <button type=\"button\" class=\"range-list-add\" (click)=\"addRangeItem(def.key)\">+ Add range</button>\n </div>\n }\n\n }\n </div>\n }\n </div>\n }\n</aside>\n", styles: [".design-panel-backdrop{position:fixed;inset:0;background:#0000002e;z-index:1000}.column-design-panel{position:fixed;top:0;right:0;bottom:0;width:360px;max-width:90vw;background:var(--grid-surface, #fff);box-shadow:-4px 0 16px #00000029;transform:translate(100%);transition:transform .22s ease;z-index:1001;display:flex;flex-direction:column;font-family:var(--grid-font-family, inherit)}.column-design-panel.open{transform:translate(0)}.design-panel-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--grid-border-color, #e0e0e0)}.design-panel-header .design-panel-title{display:flex;align-items:center;gap:8px;font-weight:600;font-size:15px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.design-panel-body{padding:16px;overflow-y:auto;flex:1}.design-panel-body .design-field{margin-bottom:4px}.design-panel-body .design-field .full-width{width:100%}.design-panel-body .design-field mat-checkbox{display:block;margin:8px 0 16px}.design-panel-body .color-list{margin:4px 0 16px}.design-panel-body .color-list .color-list-label{display:block;font-size:12px;color:var(--grid-on-surface-variant, #49454f);margin-bottom:6px}.design-panel-body .color-list .color-list-chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}.design-panel-body .color-list .color-list-chip{display:inline-flex;align-items:center;gap:6px;padding:3px 8px 3px 4px;border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:16px;background:var(--grid-surface, #fff);font-size:12px}.design-panel-body .color-list .color-list-dot{width:18px;height:18px;padding:0;border:none;background:none;border-radius:50%;cursor:pointer}.design-panel-body .color-list .color-list-remove{border:none;background:none;cursor:pointer;font-size:14px;line-height:1;color:var(--grid-on-surface-variant, #49454f)}.design-panel-body .color-list .color-list-remove:hover{color:var(--grid-error, #b3261e)}.design-panel-body .color-list .color-list-add{width:100%;box-sizing:border-box;padding:8px 10px;border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:6px;font-size:13px;outline:none}.design-panel-body .color-list .color-list-add:focus{border-color:var(--grid-primary, #6750a4)}.range-list{margin:4px 0 16px}.range-list .range-list-label{display:block;font-size:12px;color:var(--grid-on-surface-variant, #49454f);margin-bottom:6px}.range-list .range-list-row{display:flex;align-items:center;gap:6px;margin-bottom:6px}.range-list .range-list-num{width:56px;padding:6px 8px;border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:6px;font-size:13px;outline:none}.range-list .range-list-num:focus{border-color:var(--grid-primary, #6750a4)}.range-list .range-list-sep{color:var(--grid-on-surface-variant, #49454f)}.range-list .range-list-color{width:28px;height:28px;padding:0;border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:6px;background:none;cursor:pointer}.range-list .range-list-remove{margin-left:auto;border:none;background:none;cursor:pointer;font-size:18px;line-height:1;color:var(--grid-on-surface-variant, #49454f)}.range-list .range-list-remove:hover{color:var(--grid-error, #b3261e)}.range-list .range-list-add{margin-top:2px;padding:6px 10px;border:1px dashed var(--grid-outline-variant, #cac4d0);border-radius:6px;background:none;font-size:13px;cursor:pointer;color:var(--grid-primary, #6750a4)}.range-list .range-list-add:hover{background:var(--grid-surface-variant, #f3edf7)}.day-chips{display:flex;flex-direction:column;gap:6px}.day-chips-label{font-size:12px;color:var(--eru-on-surface-variant, #5f6368)}.day-chips-row{display:flex;flex-wrap:wrap;gap:6px}.day-chip{border:1px solid var(--eru-outline, #c4c7c5);background:transparent;border-radius:16px;padding:4px 12px;font-size:12px;cursor:pointer;-webkit-user-select:none;user-select:none;transition:background .15s,color .15s,border-color .15s}.day-chip.selected{background:var(--eru-primary, #1a73e8);border-color:var(--eru-primary, #1a73e8);color:#fff}.design-field-inherited{opacity:.55;pointer-events:none}.design-meta-source{padding-bottom:8px;border-bottom:1px solid var(--grid-outline-variant, #e0e0e0);margin-bottom:12px}.design-meta-hint{margin:4px 0 0;font-size:11px;line-height:1.4;color:var(--grid-on-surface-variant, #49454f)}.text-style-control{display:flex;flex-direction:column;gap:6px}.text-style-label{font-size:12px;color:var(--grid-on-surface-variant, #49454f)}.text-style-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px}.text-style-number{width:96px}.text-style-color{display:flex;align-items:center;gap:6px}.text-style-color input[type=color]{width:32px;height:28px;padding:0;border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:4px;background:none;cursor:pointer}.text-style-color-label{font-size:12px}.text-style-clear{border:none;background:none;padding:0;font-size:11px;color:var(--grid-primary, #6750a4);cursor:pointer}.text-style-color--unset input[type=color]{opacity:.3}.text-style-unset-hint{font-size:11px;color:var(--grid-on-surface-variant, #49454f)}.design-target-hidden{margin-left:6px;padding:1px 6px;border-radius:8px;font-size:10px;text-transform:uppercase;letter-spacing:.4px;background:var(--grid-surface-container-high, #e6e0e9);color:var(--grid-on-surface-variant, #49454f)}.color-control{display:block;margin-top:4px}.color-control-label{display:block;font-size:12px;margin-bottom:2px;color:var(--grid-on-surface-variant, #49454f)}.color-control-body{display:flex;align-items:center;gap:6px;min-width:0}.color-token-field{flex:1 1 auto;min-width:0}.color-swatch{display:inline-block;width:12px;height:12px;margin-right:6px;border-radius:3px;border:1px solid var(--grid-outline-variant, #cac4d0);vertical-align:middle}.color-hex{flex:0 0 auto;width:32px;height:28px;padding:0;border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:4px;background:none;cursor:pointer}.color-preview{width:32px;height:28px;border-radius:4px;border:1px solid var(--grid-outline-variant, #cac4d0)}.color-alpha{flex:0 1 68px;min-width:44px}.color-alpha-value{flex:0 0 auto;font-size:11px;min-width:30px;color:var(--grid-on-surface-variant, #49454f)}.color-preview{flex:0 0 auto}.fill-select{width:220px}.rule-list{display:flex;flex-direction:column;gap:8px}.rule-list-hint{margin:0;font-size:11px;color:var(--grid-on-surface-variant, #49454f)}.rule-row{border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:6px;padding:8px;display:flex;flex-direction:column;gap:6px}.rule-row-head{display:flex;align-items:center;gap:6px;min-width:0}.rule-row-operands,.rule-row-format{display:flex;align-items:center;flex-wrap:wrap;gap:6px}.rule-actions{margin-left:auto;flex:0 0 auto;display:flex;gap:4px}.rule-op{flex:1 1 auto;min-width:0}.rule-operand{flex:1 1 96px;min-width:88px}.rule-source{flex:1 1 140px;min-width:128px}.rule-token{flex:1 1 96px;min-width:88px}.rule-format-label{font-size:11px;color:var(--grid-on-surface-variant, #49454f)}.rule-color-group{display:flex;align-items:center;flex-wrap:wrap;gap:6px;flex:1 1 100%}.rule-alpha-group{display:flex;align-items:center;gap:6px;flex:1 1 100%}.rule-alpha-group .color-alpha{flex:1 1 auto}.rule-btn{border:1px solid var(--grid-outline-variant, #cac4d0);background:none;border-radius:4px;width:26px;height:26px;cursor:pointer;font-size:12px;line-height:1}.rule-btn-remove{color:var(--grid-error, #b3261e)}.rule-add{align-self:flex-start;border:1px dashed var(--grid-outline, #79747e);background:none;border-radius:6px;padding:6px 12px;font-size:12px;cursor:pointer;color:var(--grid-primary, #6750a4)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i3.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i2$3.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i2$3.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i2$3.MatOptgroup, selector: "mat-optgroup", inputs: ["label", "disabled"], exportAs: ["matOptgroup"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i2$1.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1$2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
14159
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: ColumnDesignPanelComponent, isStandalone: true, selector: "eru-column-design-panel", ngImport: i0, template: "@if (isOpen()) {\n<div class=\"design-panel-backdrop\" (click)=\"close()\"></div>\n}\n<aside class=\"column-design-panel\" [class.open]=\"isOpen()\">\n @if (field(); as col) {\n <header class=\"design-panel-header\">\n <div class=\"design-panel-title\">\n <mat-icon>tune</mat-icon>\n <span>{{ col.label || col.name }}</span>\n </div>\n <button mat-icon-button (click)=\"close()\" title=\"Close\">\n <mat-icon>close</mat-icon>\n </button>\n </header>\n\n <div class=\"design-panel-body\">\n <!-- Which field is being designed. A pivot with one measure renders no\n measure-name row, so its single header cell stands for the column\n dimension and the measure at once and can only open one of them; this\n reaches the other. Also the way to a header too narrow to click, or a\n hidden column. Only shown when there is more than one thing to pick. -->\n @if (designTargets().length > 0 && targetCount() > 1) {\n <div class=\"design-field design-target\">\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Editing</mat-label>\n <mat-select [ngModel]=\"gridStore.selectedDesignColumn()\"\n (ngModelChange)=\"gridStore.selectDesignColumn($event)\">\n @for (group of designTargets(); track group.group) {\n <mat-optgroup [label]=\"group.group\">\n @for (target of group.fields; track target.name) {\n <mat-option [value]=\"target.name\">\n {{ target.label || target.name }}\n @if (target.is_hidden) {<span class=\"design-target-hidden\">hidden</span>}\n </mat-option>\n }\n </mat-optgroup>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n\n <!-- Where this column's metadata comes from: typed in here, or inherited\n from a field in the host app's data model so it stays defined once. -->\n <div class=\"design-field design-meta-source\">\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Metadata source</mat-label>\n <mat-select [ngModel]=\"metaSource()\" (ngModelChange)=\"onMetaSourceChange($event)\">\n <mat-option value=\"manual\">Defined here</mat-option>\n <mat-option value=\"entity\">Mapped to data model field</mat-option>\n </mat-select>\n </mat-form-field>\n\n @if (metaSource() === 'entity') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Entity</mat-label>\n <mat-select [ngModel]=\"getValue('mapped_entity')\" (ngModelChange)=\"onEntityChange($event)\"\n (openedChange)=\"$event && requestEntities()\">\n @for (opt of entityOptions(); track opt.value) {\n <mat-option [value]=\"opt.value\">{{ opt.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (getValue('mapped_entity')) {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Field</mat-label>\n <mat-select [ngModel]=\"getValue('mapped_field')\" (ngModelChange)=\"onEntityFieldChange($event)\">\n @for (opt of entityFieldOptions(); track opt.value) {\n <mat-option [value]=\"opt.value\">{{ opt.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @if (isEntityMapped()) {\n <p class=\"design-meta-hint\">\n Colours, options and formatting come from the data model. Edit them there to update\n every grid that maps this field. The label, date format, abbreviation, display scale\n and symbol field start from the model but can be changed here for this grid alone.\n </p>\n }\n }\n </div>\n\n @for (def of metaFields(); track trackMetaField(def)) {\n <div class=\"design-field\" [class.design-field-inherited]=\"isInherited(def.key)\"\n [title]=\"isInherited(def.key) ? 'Inherited from the mapped data model field' : ''\">\n @switch (def.control) {\n\n @case ('text') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>{{ def.label }}</mat-label>\n <input matInput [ngModel]=\"getValue(def.key)\" (ngModelChange)=\"onText(def.key, $event)\" />\n </mat-form-field>\n }\n\n @case ('number') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>{{ def.label }}</mat-label>\n <input matInput type=\"number\" [ngModel]=\"getValue(def.key)\" (ngModelChange)=\"onNumber(def.key, $event)\" />\n </mat-form-field>\n }\n\n @case ('select') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>{{ def.label }}</mat-label>\n <mat-select [ngModel]=\"getValue(def.key) ?? def.defaultValue\" (ngModelChange)=\"onSelect(def.key, $event)\">\n @for (opt of def.options; track opt.value) {\n <mat-option [value]=\"opt.value\">{{ opt.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @case ('field_select') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>{{ def.label }}</mat-label>\n <mat-select [ngModel]=\"getValue(def.key)\" (ngModelChange)=\"onSelect(def.key, $event)\">\n <mat-option [value]=\"null\">None</mat-option>\n @for (opt of fieldSelectOptions(); track opt.value) {\n <mat-option [value]=\"opt.value\">{{ opt.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @case ('field_slot') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>{{ def.label }}</mat-label>\n <mat-select [ngModel]=\"slotValue(def)\" (ngModelChange)=\"onSlotChange(def, $event)\">\n <mat-option [value]=\"null\">None</mat-option>\n @for (opt of fieldSlotOptions(); track opt.value) {\n <mat-option [value]=\"opt.value\">{{ opt.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @case ('text_slot') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>{{ def.label }}</mat-label>\n <input matInput type=\"text\" [ngModel]=\"slotValue(def)\" (ngModelChange)=\"onSlotChange(def, $event)\" />\n </mat-form-field>\n }\n\n @case ('text_style') {\n <div class=\"text-style-control\">\n <label class=\"text-style-label\">{{ def.label }}</label>\n <div class=\"text-style-row\">\n <mat-form-field appearance=\"outline\" class=\"text-style-number\">\n <mat-label>Size</mat-label>\n <input matInput type=\"number\" [ngModel]=\"styleValue(def.key, 'font_size')\"\n (ngModelChange)=\"onStyleChange(def.key, 'font_size', $event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"text-style-number\">\n <mat-label>Weight</mat-label>\n <input matInput type=\"number\" [ngModel]=\"styleValue(def.key, 'font_weight')\"\n (ngModelChange)=\"onStyleChange(def.key, 'font_weight', $event)\" />\n </mat-form-field>\n <mat-checkbox class=\"text-style-check\" [checked]=\"!!styleValue(def.key, 'italic')\"\n (change)=\"onStyleChange(def.key, 'italic', $event.checked)\">Italic</mat-checkbox>\n </div>\n\n @if (!isSelfColoured()) {\n <div class=\"color-control\">\n <span class=\"color-control-label\">Text colour</span>\n <div class=\"color-control-body\">\n <mat-form-field appearance=\"outline\" class=\"color-token-field\">\n <mat-select [ngModel]=\"colorToken(styleColor(def.key, 'color'))\"\n (ngModelChange)=\"onStyleColorEdit(def.key, 'color', { token: $event })\">\n <mat-option value=\"\">Custom</mat-option>\n @for (t of colorTokens; track t.value) {\n <mat-option [value]=\"t.value\"><span class=\"color-swatch\" [style.background]=\"t.value\"></span>{{ t.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (!isTokenColor(styleColor(def.key, 'color'))) {\n <input type=\"color\" class=\"color-hex\" [value]=\"colorHex(styleColor(def.key, 'color'))\"\n (input)=\"onStyleColorEdit(def.key, 'color', { hex: $any($event.target).value })\" />\n } @else {\n <span class=\"color-preview\" [style.background]=\"styleColor(def.key, 'color')\"></span>\n }\n <input type=\"range\" class=\"color-alpha\" min=\"0\" max=\"100\" step=\"1\"\n [value]=\"colorAlpha(styleColor(def.key, 'color'))\"\n (input)=\"onStyleColorEdit(def.key, 'color', { alpha: +$any($event.target).value })\" />\n <span class=\"color-alpha-value\">{{ colorAlpha(styleColor(def.key, 'color')) }}%</span>\n @if (styleColor(def.key, 'color')) {\n <button type=\"button\" class=\"text-style-clear\" (click)=\"clearStyleColor(def.key, 'color')\">Reset</button>\n } @else {\n <span class=\"text-style-unset-hint\">theme</span>\n }\n </div>\n </div>\n }\n\n <div class=\"color-control\">\n <span class=\"color-control-label\">{{ isSelfColoured() ? 'Cell background' : 'Background' }}</span>\n <div class=\"color-control-body\">\n <mat-form-field appearance=\"outline\" class=\"color-token-field\">\n <mat-select [ngModel]=\"colorToken(styleColor(def.key, 'background'))\"\n (ngModelChange)=\"onStyleColorEdit(def.key, 'background', { token: $event })\">\n <mat-option value=\"\">Custom</mat-option>\n @for (t of colorTokens; track t.value) {\n <mat-option [value]=\"t.value\"><span class=\"color-swatch\" [style.background]=\"t.value\"></span>{{ t.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (!isTokenColor(styleColor(def.key, 'background'))) {\n <input type=\"color\" class=\"color-hex\" [value]=\"colorHex(styleColor(def.key, 'background'))\"\n (input)=\"onStyleColorEdit(def.key, 'background', { hex: $any($event.target).value })\" />\n } @else {\n <span class=\"color-preview\" [style.background]=\"styleColor(def.key, 'background')\"></span>\n }\n <input type=\"range\" class=\"color-alpha\" min=\"0\" max=\"100\" step=\"1\"\n [value]=\"colorAlpha(styleColor(def.key, 'background'))\"\n (input)=\"onStyleColorEdit(def.key, 'background', { alpha: +$any($event.target).value })\" />\n <span class=\"color-alpha-value\">{{ colorAlpha(styleColor(def.key, 'background')) }}%</span>\n @if (styleColor(def.key, 'background')) {\n <button type=\"button\" class=\"text-style-clear\" (click)=\"clearStyleColor(def.key, 'background')\">Reset</button>\n } @else {\n <span class=\"text-style-unset-hint\">none</span>\n }\n </div>\n </div>\n\n @if (styleColor(def.key, 'background') && !isSelfColoured()) {\n <div class=\"text-style-row\">\n <mat-form-field appearance=\"outline\" class=\"fill-select\">\n <mat-label>Fill</mat-label>\n <mat-select [ngModel]=\"styleFill(def.key)\" (ngModelChange)=\"onStyleFill(def.key, $event)\">\n <mat-option value=\"text\">Behind the text (pill)</mat-option>\n <mat-option value=\"cell\">Whole cell</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n }\n </div>\n }\n\n @case ('rule_list') {\n <div class=\"rule-list\">\n <label class=\"text-style-label\">{{ def.label }}</label>\n <p class=\"rule-list-hint\">Checked top to bottom \u2014 the first rule that matches wins.</p>\n @for (rule of ruleItems(def.key); track $index) {\n <div class=\"rule-row\">\n <div class=\"rule-row-head\">\n <mat-form-field appearance=\"outline\" class=\"rule-op\">\n <mat-label>When</mat-label>\n <mat-select [ngModel]=\"rule.op || 'between'\" (ngModelChange)=\"updateRule(def.key, $index, 'op', $event)\">\n @for (o of ruleOperators; track o.value) {\n <mat-option [value]=\"o.value\">{{ o.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <div class=\"rule-actions\">\n <button type=\"button\" class=\"rule-btn\" title=\"Move up\" (click)=\"moveRule(def.key, $index, -1)\">\u2191</button>\n <button type=\"button\" class=\"rule-btn\" title=\"Move down\" (click)=\"moveRule(def.key, $index, 1)\">\u2193</button>\n <button type=\"button\" class=\"rule-btn rule-btn-remove\" title=\"Remove\" (click)=\"removeRule(def.key, $index)\">\u2715</button>\n </div>\n </div>\n @if (operandCount(rule.op) > 0) {\n <div class=\"rule-row-operands\">\n <mat-form-field appearance=\"outline\" class=\"rule-source\">\n <mat-label>{{ operandCount(rule.op) === 2 ? 'From' : 'Value' }}</mat-label>\n <mat-select [ngModel]=\"ruleSource(def.key, $index, 'from')\"\n (ngModelChange)=\"onRuleSource(def.key, $index, 'from', $event)\">\n @for (o of operandSources; track o.value) {\n <mat-option [value]=\"o.value\">{{ o.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (!ruleSource(def.key, $index, 'from')) {\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>Value</mat-label>\n <input matInput [ngModel]=\"rule.from\" (ngModelChange)=\"updateRule(def.key, $index, 'from', $event)\" />\n </mat-form-field>\n } @else {\n @if (ruleSourceArg(def.key, $index, 'from'); as arg) {\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>{{ arg === 'p' ? 'Percentile' : 'N' }}</mat-label>\n <input matInput type=\"number\" [ngModel]=\"ruleSourceArgValue(def.key, $index, 'from')\"\n (ngModelChange)=\"onRuleSourceArg(def.key, $index, 'from', $event)\" />\n </mat-form-field>\n }\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>\u00B1 std dev</mat-label>\n <input matInput type=\"number\" [ngModel]=\"ruleStddevOffset(def.key, $index, 'from')\"\n (ngModelChange)=\"onRuleStddevOffset(def.key, $index, 'from', $event)\" />\n </mat-form-field>\n }\n </div>\n @if (operandCount(rule.op) === 2) {\n <div class=\"rule-row-operands\">\n <mat-form-field appearance=\"outline\" class=\"rule-source\">\n <mat-label>To</mat-label>\n <mat-select [ngModel]=\"ruleSource(def.key, $index, 'to')\"\n (ngModelChange)=\"onRuleSource(def.key, $index, 'to', $event)\">\n @for (o of operandSources; track o.value) {\n <mat-option [value]=\"o.value\">{{ o.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (!ruleSource(def.key, $index, 'to')) {\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>Value</mat-label>\n <input matInput [ngModel]=\"rule.to\" (ngModelChange)=\"updateRule(def.key, $index, 'to', $event)\" />\n </mat-form-field>\n } @else if (ruleSourceArg(def.key, $index, 'to'); as arg) {\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>{{ arg === 'p' ? 'Percentile' : 'N' }}</mat-label>\n <input matInput type=\"number\" [ngModel]=\"ruleSourceArgValue(def.key, $index, 'to')\"\n (ngModelChange)=\"onRuleSourceArg(def.key, $index, 'to', $event)\" />\n </mat-form-field>\n }\n </div>\n }\n }\n <div class=\"rule-row-format\">\n <span class=\"rule-color-group\">\n <span class=\"rule-format-label\">Text</span>\n <!-- Swapped for a read-only preview while a token is in force, as\n the column-level control does: with a token set,\n composeColorValue returns the token and a hex picked here is\n discarded, so an editable swatch invited an edit that\n silently did nothing. -->\n @if (!isTokenColor(rule.color)) {\n <input type=\"color\" class=\"color-hex\" [value]=\"colorHex(rule.color)\"\n (input)=\"onRuleColorEdit(def.key, $index, 'color', { hex: $any($event.target).value })\" />\n } @else {\n <span class=\"color-preview\" [style.background]=\"rule.color\"></span>\n }\n <mat-form-field appearance=\"outline\" class=\"color-token-field rule-token\">\n <mat-select [ngModel]=\"colorToken(rule.color)\"\n (ngModelChange)=\"onRuleColorEdit(def.key, $index, 'color', { token: $event })\">\n <mat-option value=\"\">Custom</mat-option>\n @for (t of colorTokens; track t.value) {\n <mat-option [value]=\"t.value\"><span class=\"color-swatch\" [style.background]=\"t.value\"></span>{{ t.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n </span>\n <!-- Grouped so the row wraps between the two colours rather than\n through one: the percentage broke onto the next line beside\n 'Bold' and read as belonging to it. -->\n <span class=\"rule-color-group\">\n <span class=\"rule-format-label\">Fill</span>\n @if (!isTokenColor(rule.background)) {\n <input type=\"color\" class=\"color-hex\" [value]=\"colorHex(rule.background)\"\n (input)=\"onRuleColorEdit(def.key, $index, 'background', { hex: $any($event.target).value })\" />\n } @else {\n <span class=\"color-preview\" [style.background]=\"rule.background\"></span>\n }\n <!-- A fill is as likely to want a theme colour as the text is, and\n the alpha slider works for either: composeColorValue wraps a\n token in color-mix below 100%. -->\n <mat-form-field appearance=\"outline\" class=\"color-token-field rule-token\">\n <mat-select [ngModel]=\"colorToken(rule.background)\"\n (ngModelChange)=\"onRuleColorEdit(def.key, $index, 'background', { token: $event })\">\n <mat-option value=\"\">Custom</mat-option>\n @for (t of colorTokens; track t.value) {\n <mat-option [value]=\"t.value\"><span class=\"color-swatch\" [style.background]=\"t.value\"></span>{{ t.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <!-- Its own line: the label, swatch and token select already fill\n the panel's width, and a token name as long as 'On Secondary\n Container' pushed the slider off the right edge. -->\n <span class=\"rule-alpha-group\">\n <span class=\"rule-format-label\">Opacity</span>\n <input type=\"range\" class=\"color-alpha\" min=\"0\" max=\"100\" step=\"1\" [value]=\"colorAlpha(rule.background)\"\n (input)=\"onRuleColorEdit(def.key, $index, 'background', { alpha: +$any($event.target).value })\" />\n <span class=\"color-alpha-value\">{{ colorAlpha(rule.background) }}%</span>\n </span>\n </span>\n <mat-checkbox class=\"text-style-check\" [checked]=\"!!rule.bold\"\n (change)=\"updateRule(def.key, $index, 'bold', $event.checked)\">Bold</mat-checkbox>\n <mat-checkbox class=\"text-style-check\" [checked]=\"rule.fill === 'text'\"\n (change)=\"updateRule(def.key, $index, 'fill', $event.checked ? 'text' : 'cell')\">Pill</mat-checkbox>\n <mat-checkbox class=\"text-style-check\" [checked]=\"!!rule.bar\"\n (change)=\"updateRule(def.key, $index, 'bar', $event.checked)\">Data bar</mat-checkbox>\n @if (rule.bar) {\n <mat-checkbox class=\"text-style-check\" [checked]=\"!!rule.bar_auto\"\n (change)=\"updateRule(def.key, $index, 'bar_auto', $event.checked)\">Scale to column</mat-checkbox>\n }\n </div>\n @if (rule.bar && !rule.bar_auto) {\n <div class=\"rule-row-format\">\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>Bar min</mat-label>\n <input matInput type=\"number\" [ngModel]=\"rule.bar_min\" (ngModelChange)=\"updateRule(def.key, $index, 'bar_min', $event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>Bar max</mat-label>\n <input matInput type=\"number\" [ngModel]=\"rule.bar_max\" (ngModelChange)=\"updateRule(def.key, $index, 'bar_max', $event)\" />\n </mat-form-field>\n </div>\n }\n </div>\n }\n <button type=\"button\" class=\"rule-add\" (click)=\"addRule(def.key)\">+ Add rule</button>\n </div>\n }\n\n @case ('checkbox') {\n <mat-checkbox [checked]=\"isChecked(def)\" (change)=\"onCheckbox(def.key, $event.checked)\">\n {{ def.label }}\n </mat-checkbox>\n }\n\n @case ('day_chips') {\n <div class=\"day-chips\">\n <label class=\"day-chips-label\">{{ def.label }}</label>\n <div class=\"day-chips-row\">\n @for (day of weekDays; track day) {\n <button type=\"button\" class=\"day-chip\" [class.selected]=\"isDaySelected(def.key, day)\"\n (click)=\"toggleDay(def.key, day)\">{{ day }}</button>\n }\n </div>\n </div>\n }\n\n @case ('list') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>{{ def.label }}</mat-label>\n <textarea matInput rows=\"4\" [ngModel]=\"listText(def.key)\"\n (ngModelChange)=\"onList(def.key, $event)\"></textarea>\n </mat-form-field>\n }\n\n @case ('color_list') {\n <div class=\"color-list\">\n <label class=\"color-list-label\">{{ def.label }}</label>\n <div class=\"color-list-chips\">\n @for (opt of colorListItems(def.key); track $index) {\n <div class=\"color-list-chip\">\n <input type=\"color\" class=\"color-list-dot\" [value]=\"opt.color || '#9CA3AF'\"\n (input)=\"onColorListColorChange(def.key, $index, $any($event.target).value)\" title=\"Change colour\" />\n <span class=\"color-list-name\">{{ opt.name }}</span>\n <button type=\"button\" class=\"color-list-remove\" (click)=\"removeColorListItem(def.key, $index)\"\n title=\"Remove\">&times;</button>\n </div>\n }\n </div>\n <input class=\"color-list-add\" type=\"text\" placeholder=\"Add option, press Enter\"\n (keydown.enter)=\"addColorListItem(def.key, $any($event.target).value, def.defaultColor || '#9CA3AF'); $any($event.target).value = ''\" />\n </div>\n }\n\n @case ('range_color_list') {\n <div class=\"range-list\">\n <label class=\"range-list-label\">{{ def.label }}</label>\n @for (r of rangeListItems(def.key); track $index) {\n <div class=\"range-list-row\">\n <input type=\"number\" class=\"range-list-num\" min=\"0\" max=\"100\" [value]=\"r.from\"\n (input)=\"onRangeChange(def.key, $index, 'from', $any($event.target).value)\" title=\"From\" />\n <span class=\"range-list-sep\">\u2013</span>\n <input type=\"number\" class=\"range-list-num\" min=\"0\" max=\"100\" [value]=\"r.to\"\n (input)=\"onRangeChange(def.key, $index, 'to', $any($event.target).value)\" title=\"To\" />\n <input type=\"color\" class=\"range-list-color\" [value]=\"r.color || '#22C55E'\"\n (input)=\"onRangeColorChange(def.key, $index, $any($event.target).value)\" title=\"Colour\" />\n <button type=\"button\" class=\"range-list-remove\" (click)=\"removeRangeItem(def.key, $index)\"\n title=\"Remove\">&times;</button>\n </div>\n }\n <button type=\"button\" class=\"range-list-add\" (click)=\"addRangeItem(def.key)\">+ Add range</button>\n </div>\n }\n\n }\n </div>\n }\n </div>\n }\n</aside>\n", styles: [".design-panel-backdrop{position:fixed;inset:0;background:#0000002e;z-index:1000}.column-design-panel{position:fixed;top:0;right:0;bottom:0;width:360px;max-width:90vw;background:var(--grid-surface, #fff);box-shadow:-4px 0 16px #00000029;transform:translate(100%);transition:transform .22s ease;z-index:1001;display:flex;flex-direction:column;font-family:var(--grid-font-family, inherit)}.column-design-panel.open{transform:translate(0)}.design-panel-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--grid-border-color, #e0e0e0)}.design-panel-header .design-panel-title{display:flex;align-items:center;gap:8px;font-weight:600;font-size:15px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.design-panel-body{padding:16px;overflow-y:auto;flex:1}.design-panel-body .design-field{margin-bottom:4px}.design-panel-body .design-field .full-width{width:100%}.design-panel-body .design-field mat-checkbox{display:block;margin:8px 0 16px}.design-panel-body .color-list{margin:4px 0 16px}.design-panel-body .color-list .color-list-label{display:block;font-size:12px;color:var(--grid-on-surface-variant, #49454f);margin-bottom:6px}.design-panel-body .color-list .color-list-chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}.design-panel-body .color-list .color-list-chip{display:inline-flex;align-items:center;gap:6px;padding:3px 8px 3px 4px;border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:16px;background:var(--grid-surface, #fff);font-size:12px}.design-panel-body .color-list .color-list-dot{width:18px;height:18px;padding:0;border:none;background:none;border-radius:50%;cursor:pointer}.design-panel-body .color-list .color-list-remove{border:none;background:none;cursor:pointer;font-size:14px;line-height:1;color:var(--grid-on-surface-variant, #49454f)}.design-panel-body .color-list .color-list-remove:hover{color:var(--grid-error, #b3261e)}.design-panel-body .color-list .color-list-add{width:100%;box-sizing:border-box;padding:8px 10px;border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:6px;font-size:13px;outline:none}.design-panel-body .color-list .color-list-add:focus{border-color:var(--grid-primary, #6750a4)}.range-list{margin:4px 0 16px}.range-list .range-list-label{display:block;font-size:12px;color:var(--grid-on-surface-variant, #49454f);margin-bottom:6px}.range-list .range-list-row{display:flex;align-items:center;gap:6px;margin-bottom:6px}.range-list .range-list-num{width:56px;padding:6px 8px;border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:6px;font-size:13px;outline:none}.range-list .range-list-num:focus{border-color:var(--grid-primary, #6750a4)}.range-list .range-list-sep{color:var(--grid-on-surface-variant, #49454f)}.range-list .range-list-color{width:28px;height:28px;padding:0;border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:6px;background:none;cursor:pointer}.range-list .range-list-remove{margin-left:auto;border:none;background:none;cursor:pointer;font-size:18px;line-height:1;color:var(--grid-on-surface-variant, #49454f)}.range-list .range-list-remove:hover{color:var(--grid-error, #b3261e)}.range-list .range-list-add{margin-top:2px;padding:6px 10px;border:1px dashed var(--grid-outline-variant, #cac4d0);border-radius:6px;background:none;font-size:13px;cursor:pointer;color:var(--grid-primary, #6750a4)}.range-list .range-list-add:hover{background:var(--grid-surface-variant, #f3edf7)}.day-chips{display:flex;flex-direction:column;gap:6px}.day-chips-label{font-size:12px;color:var(--eru-on-surface-variant, #5f6368)}.day-chips-row{display:flex;flex-wrap:wrap;gap:6px}.day-chip{border:1px solid var(--eru-outline, #c4c7c5);background:transparent;border-radius:16px;padding:4px 12px;font-size:12px;cursor:pointer;-webkit-user-select:none;user-select:none;transition:background .15s,color .15s,border-color .15s}.day-chip.selected{background:var(--eru-primary, #1a73e8);border-color:var(--eru-primary, #1a73e8);color:#fff}.design-field-inherited{opacity:.55;pointer-events:none}.design-meta-source{padding-bottom:8px;border-bottom:1px solid var(--grid-outline-variant, #e0e0e0);margin-bottom:12px}.design-meta-hint{margin:4px 0 0;font-size:11px;line-height:1.4;color:var(--grid-on-surface-variant, #49454f)}.text-style-control{display:flex;flex-direction:column;gap:6px}.text-style-label{font-size:12px;color:var(--grid-on-surface-variant, #49454f)}.text-style-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px}.text-style-number{width:96px}.text-style-color{display:flex;align-items:center;gap:6px}.text-style-color input[type=color]{width:32px;height:28px;padding:0;border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:4px;background:none;cursor:pointer}.text-style-color-label{font-size:12px}.text-style-clear{border:none;background:none;padding:0;font-size:11px;color:var(--grid-primary, #6750a4);cursor:pointer}.text-style-color--unset input[type=color]{opacity:.3}.text-style-unset-hint{font-size:11px;color:var(--grid-on-surface-variant, #49454f)}.design-target-hidden{margin-left:6px;padding:1px 6px;border-radius:8px;font-size:10px;text-transform:uppercase;letter-spacing:.4px;background:var(--grid-surface-container-high, #e6e0e9);color:var(--grid-on-surface-variant, #49454f)}.color-control{display:block;margin-top:4px}.color-control-label{display:block;font-size:12px;margin-bottom:2px;color:var(--grid-on-surface-variant, #49454f)}.color-control-body{display:flex;align-items:center;gap:6px;min-width:0}.color-token-field{flex:1 1 auto;min-width:0}.color-swatch{display:inline-block;width:12px;height:12px;margin-right:6px;border-radius:3px;border:1px solid var(--grid-outline-variant, #cac4d0);vertical-align:middle}.color-hex{flex:0 0 auto;width:32px;height:28px;padding:0;border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:4px;background:none;cursor:pointer}.color-preview{width:32px;height:28px;border-radius:4px;border:1px solid var(--grid-outline-variant, #cac4d0)}.color-alpha{flex:0 1 68px;min-width:44px}.color-alpha-value{flex:0 0 auto;font-size:11px;min-width:30px;color:var(--grid-on-surface-variant, #49454f)}.color-preview{flex:0 0 auto}.fill-select{width:220px}.rule-list{display:flex;flex-direction:column;gap:8px}.rule-list-hint{margin:0;font-size:11px;color:var(--grid-on-surface-variant, #49454f)}.rule-row{border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:6px;padding:8px;display:flex;flex-direction:column;gap:6px}.rule-row-head{display:flex;align-items:center;gap:6px;min-width:0}.rule-row-operands,.rule-row-format{display:flex;align-items:center;flex-wrap:wrap;gap:6px}.rule-actions{margin-left:auto;flex:0 0 auto;display:flex;gap:4px}.rule-op{flex:1 1 auto;min-width:0}.rule-operand{flex:1 1 96px;min-width:88px}.rule-source{flex:1 1 140px;min-width:128px}.rule-token{flex:1 1 96px;min-width:88px}.rule-format-label{font-size:11px;color:var(--grid-on-surface-variant, #49454f)}.rule-color-group{display:flex;align-items:center;flex-wrap:wrap;gap:6px;flex:1 1 100%}.rule-alpha-group{display:flex;align-items:center;gap:6px;flex:1 1 100%}.rule-alpha-group .color-alpha{flex:1 1 auto}.rule-btn{border:1px solid var(--grid-outline-variant, #cac4d0);background:none;border-radius:4px;width:26px;height:26px;cursor:pointer;font-size:12px;line-height:1}.rule-btn-remove{color:var(--grid-error, #b3261e)}.rule-add{align-self:flex-start;border:1px dashed var(--grid-outline, #79747e);background:none;border-radius:6px;padding:6px 12px;font-size:12px;cursor:pointer;color:var(--grid-primary, #6750a4)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i3.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i2$3.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i2$3.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i2$3.MatOptgroup, selector: "mat-optgroup", inputs: ["label", "disabled"], exportAs: ["matOptgroup"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i2$1.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1$2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
13689
14160
  }
13690
14161
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: ColumnDesignPanelComponent, decorators: [{
13691
14162
  type: Component,
@@ -13698,7 +14169,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
13698
14169
  MatCheckboxModule,
13699
14170
  MatIconModule,
13700
14171
  MatButtonModule,
13701
- ], template: "@if (isOpen()) {\n<div class=\"design-panel-backdrop\" (click)=\"close()\"></div>\n}\n<aside class=\"column-design-panel\" [class.open]=\"isOpen()\">\n @if (field(); as col) {\n <header class=\"design-panel-header\">\n <div class=\"design-panel-title\">\n <mat-icon>tune</mat-icon>\n <span>{{ col.label || col.name }}</span>\n </div>\n <button mat-icon-button (click)=\"close()\" title=\"Close\">\n <mat-icon>close</mat-icon>\n </button>\n </header>\n\n <div class=\"design-panel-body\">\n <!-- Which field is being designed. A pivot with one measure renders no\n measure-name row, so its single header cell stands for the column\n dimension and the measure at once and can only open one of them; this\n reaches the other. Also the way to a header too narrow to click, or a\n hidden column. Only shown when there is more than one thing to pick. -->\n @if (designTargets().length > 0 && targetCount() > 1) {\n <div class=\"design-field design-target\">\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Editing</mat-label>\n <mat-select [ngModel]=\"gridStore.selectedDesignColumn()\"\n (ngModelChange)=\"gridStore.selectDesignColumn($event)\">\n @for (group of designTargets(); track group.group) {\n <mat-optgroup [label]=\"group.group\">\n @for (target of group.fields; track target.name) {\n <mat-option [value]=\"target.name\">\n {{ target.label || target.name }}\n @if (target.is_hidden) {<span class=\"design-target-hidden\">hidden</span>}\n </mat-option>\n }\n </mat-optgroup>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n\n <!-- Where this column's metadata comes from: typed in here, or inherited\n from a field in the host app's data model so it stays defined once. -->\n <div class=\"design-field design-meta-source\">\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Metadata source</mat-label>\n <mat-select [ngModel]=\"metaSource()\" (ngModelChange)=\"onMetaSourceChange($event)\">\n <mat-option value=\"manual\">Defined here</mat-option>\n <mat-option value=\"entity\">Mapped to data model field</mat-option>\n </mat-select>\n </mat-form-field>\n\n @if (metaSource() === 'entity') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Entity</mat-label>\n <mat-select [ngModel]=\"getValue('mapped_entity')\" (ngModelChange)=\"onEntityChange($event)\"\n (openedChange)=\"$event && requestEntities()\">\n @for (opt of entityOptions(); track opt.value) {\n <mat-option [value]=\"opt.value\">{{ opt.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (getValue('mapped_entity')) {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Field</mat-label>\n <mat-select [ngModel]=\"getValue('mapped_field')\" (ngModelChange)=\"onEntityFieldChange($event)\">\n @for (opt of entityFieldOptions(); track opt.value) {\n <mat-option [value]=\"opt.value\">{{ opt.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @if (isEntityMapped()) {\n <p class=\"design-meta-hint\">\n Colours, options and formatting come from the data model. Edit them there to update\n every grid that maps this field. The date format, abbreviation, display scale and\n symbol field start from the model but can be changed here for this grid alone.\n </p>\n }\n }\n </div>\n\n @for (def of metaFields(); track trackMetaField(def)) {\n <div class=\"design-field\" [class.design-field-inherited]=\"isInherited(def.key)\"\n [title]=\"isInherited(def.key) ? 'Inherited from the mapped data model field' : ''\">\n @switch (def.control) {\n\n @case ('text') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>{{ def.label }}</mat-label>\n <input matInput [ngModel]=\"getValue(def.key)\" (ngModelChange)=\"onText(def.key, $event)\" />\n </mat-form-field>\n }\n\n @case ('number') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>{{ def.label }}</mat-label>\n <input matInput type=\"number\" [ngModel]=\"getValue(def.key)\" (ngModelChange)=\"onNumber(def.key, $event)\" />\n </mat-form-field>\n }\n\n @case ('select') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>{{ def.label }}</mat-label>\n <mat-select [ngModel]=\"getValue(def.key) ?? def.defaultValue\" (ngModelChange)=\"onSelect(def.key, $event)\">\n @for (opt of def.options; track opt.value) {\n <mat-option [value]=\"opt.value\">{{ opt.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @case ('field_select') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>{{ def.label }}</mat-label>\n <mat-select [ngModel]=\"getValue(def.key)\" (ngModelChange)=\"onSelect(def.key, $event)\">\n <mat-option [value]=\"null\">None</mat-option>\n @for (opt of fieldSelectOptions(); track opt.value) {\n <mat-option [value]=\"opt.value\">{{ opt.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @case ('field_slot') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>{{ def.label }}</mat-label>\n <mat-select [ngModel]=\"slotValue(def)\" (ngModelChange)=\"onSlotChange(def, $event)\">\n <mat-option [value]=\"null\">None</mat-option>\n @for (opt of fieldSlotOptions(); track opt.value) {\n <mat-option [value]=\"opt.value\">{{ opt.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @case ('text_style') {\n <div class=\"text-style-control\">\n <label class=\"text-style-label\">{{ def.label }}</label>\n <div class=\"text-style-row\">\n <mat-form-field appearance=\"outline\" class=\"text-style-number\">\n <mat-label>Size</mat-label>\n <input matInput type=\"number\" [ngModel]=\"styleValue(def.key, 'font_size')\"\n (ngModelChange)=\"onStyleChange(def.key, 'font_size', $event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"text-style-number\">\n <mat-label>Weight</mat-label>\n <input matInput type=\"number\" [ngModel]=\"styleValue(def.key, 'font_weight')\"\n (ngModelChange)=\"onStyleChange(def.key, 'font_weight', $event)\" />\n </mat-form-field>\n <mat-checkbox class=\"text-style-check\" [checked]=\"!!styleValue(def.key, 'italic')\"\n (change)=\"onStyleChange(def.key, 'italic', $event.checked)\">Italic</mat-checkbox>\n </div>\n\n @if (!isSelfColoured()) {\n <div class=\"color-control\">\n <span class=\"color-control-label\">Text colour</span>\n <div class=\"color-control-body\">\n <mat-form-field appearance=\"outline\" class=\"color-token-field\">\n <mat-select [ngModel]=\"colorToken(styleColor(def.key, 'color'))\"\n (ngModelChange)=\"onStyleColorEdit(def.key, 'color', { token: $event })\">\n <mat-option value=\"\">Custom</mat-option>\n @for (t of colorTokens; track t.value) {\n <mat-option [value]=\"t.value\"><span class=\"color-swatch\" [style.background]=\"t.value\"></span>{{ t.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (!isTokenColor(styleColor(def.key, 'color'))) {\n <input type=\"color\" class=\"color-hex\" [value]=\"colorHex(styleColor(def.key, 'color'))\"\n (input)=\"onStyleColorEdit(def.key, 'color', { hex: $any($event.target).value })\" />\n } @else {\n <span class=\"color-preview\" [style.background]=\"styleColor(def.key, 'color')\"></span>\n }\n <input type=\"range\" class=\"color-alpha\" min=\"0\" max=\"100\" step=\"1\"\n [value]=\"colorAlpha(styleColor(def.key, 'color'))\"\n (input)=\"onStyleColorEdit(def.key, 'color', { alpha: +$any($event.target).value })\" />\n <span class=\"color-alpha-value\">{{ colorAlpha(styleColor(def.key, 'color')) }}%</span>\n @if (styleColor(def.key, 'color')) {\n <button type=\"button\" class=\"text-style-clear\" (click)=\"clearStyleColor(def.key, 'color')\">Reset</button>\n } @else {\n <span class=\"text-style-unset-hint\">theme</span>\n }\n </div>\n </div>\n }\n\n <div class=\"color-control\">\n <span class=\"color-control-label\">{{ isSelfColoured() ? 'Cell background' : 'Background' }}</span>\n <div class=\"color-control-body\">\n <mat-form-field appearance=\"outline\" class=\"color-token-field\">\n <mat-select [ngModel]=\"colorToken(styleColor(def.key, 'background'))\"\n (ngModelChange)=\"onStyleColorEdit(def.key, 'background', { token: $event })\">\n <mat-option value=\"\">Custom</mat-option>\n @for (t of colorTokens; track t.value) {\n <mat-option [value]=\"t.value\"><span class=\"color-swatch\" [style.background]=\"t.value\"></span>{{ t.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (!isTokenColor(styleColor(def.key, 'background'))) {\n <input type=\"color\" class=\"color-hex\" [value]=\"colorHex(styleColor(def.key, 'background'))\"\n (input)=\"onStyleColorEdit(def.key, 'background', { hex: $any($event.target).value })\" />\n } @else {\n <span class=\"color-preview\" [style.background]=\"styleColor(def.key, 'background')\"></span>\n }\n <input type=\"range\" class=\"color-alpha\" min=\"0\" max=\"100\" step=\"1\"\n [value]=\"colorAlpha(styleColor(def.key, 'background'))\"\n (input)=\"onStyleColorEdit(def.key, 'background', { alpha: +$any($event.target).value })\" />\n <span class=\"color-alpha-value\">{{ colorAlpha(styleColor(def.key, 'background')) }}%</span>\n @if (styleColor(def.key, 'background')) {\n <button type=\"button\" class=\"text-style-clear\" (click)=\"clearStyleColor(def.key, 'background')\">Reset</button>\n } @else {\n <span class=\"text-style-unset-hint\">none</span>\n }\n </div>\n </div>\n\n @if (styleColor(def.key, 'background') && !isSelfColoured()) {\n <div class=\"text-style-row\">\n <mat-form-field appearance=\"outline\" class=\"fill-select\">\n <mat-label>Fill</mat-label>\n <mat-select [ngModel]=\"styleFill(def.key)\" (ngModelChange)=\"onStyleFill(def.key, $event)\">\n <mat-option value=\"text\">Behind the text (pill)</mat-option>\n <mat-option value=\"cell\">Whole cell</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n }\n </div>\n }\n\n @case ('rule_list') {\n <div class=\"rule-list\">\n <label class=\"text-style-label\">{{ def.label }}</label>\n <p class=\"rule-list-hint\">Checked top to bottom \u2014 the first rule that matches wins.</p>\n @for (rule of ruleItems(def.key); track $index) {\n <div class=\"rule-row\">\n <div class=\"rule-row-head\">\n <mat-form-field appearance=\"outline\" class=\"rule-op\">\n <mat-label>When</mat-label>\n <mat-select [ngModel]=\"rule.op || 'between'\" (ngModelChange)=\"updateRule(def.key, $index, 'op', $event)\">\n @for (o of ruleOperators; track o.value) {\n <mat-option [value]=\"o.value\">{{ o.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <div class=\"rule-actions\">\n <button type=\"button\" class=\"rule-btn\" title=\"Move up\" (click)=\"moveRule(def.key, $index, -1)\">\u2191</button>\n <button type=\"button\" class=\"rule-btn\" title=\"Move down\" (click)=\"moveRule(def.key, $index, 1)\">\u2193</button>\n <button type=\"button\" class=\"rule-btn rule-btn-remove\" title=\"Remove\" (click)=\"removeRule(def.key, $index)\">\u2715</button>\n </div>\n </div>\n @if (operandCount(rule.op) > 0) {\n <div class=\"rule-row-operands\">\n <mat-form-field appearance=\"outline\" class=\"rule-source\">\n <mat-label>{{ operandCount(rule.op) === 2 ? 'From' : 'Value' }}</mat-label>\n <mat-select [ngModel]=\"ruleSource(def.key, $index, 'from')\"\n (ngModelChange)=\"onRuleSource(def.key, $index, 'from', $event)\">\n @for (o of operandSources; track o.value) {\n <mat-option [value]=\"o.value\">{{ o.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (!ruleSource(def.key, $index, 'from')) {\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>Value</mat-label>\n <input matInput [ngModel]=\"rule.from\" (ngModelChange)=\"updateRule(def.key, $index, 'from', $event)\" />\n </mat-form-field>\n } @else {\n @if (ruleSourceArg(def.key, $index, 'from'); as arg) {\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>{{ arg === 'p' ? 'Percentile' : 'N' }}</mat-label>\n <input matInput type=\"number\" [ngModel]=\"ruleSourceArgValue(def.key, $index, 'from')\"\n (ngModelChange)=\"onRuleSourceArg(def.key, $index, 'from', $event)\" />\n </mat-form-field>\n }\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>\u00B1 std dev</mat-label>\n <input matInput type=\"number\" [ngModel]=\"ruleStddevOffset(def.key, $index, 'from')\"\n (ngModelChange)=\"onRuleStddevOffset(def.key, $index, 'from', $event)\" />\n </mat-form-field>\n }\n </div>\n @if (operandCount(rule.op) === 2) {\n <div class=\"rule-row-operands\">\n <mat-form-field appearance=\"outline\" class=\"rule-source\">\n <mat-label>To</mat-label>\n <mat-select [ngModel]=\"ruleSource(def.key, $index, 'to')\"\n (ngModelChange)=\"onRuleSource(def.key, $index, 'to', $event)\">\n @for (o of operandSources; track o.value) {\n <mat-option [value]=\"o.value\">{{ o.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (!ruleSource(def.key, $index, 'to')) {\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>Value</mat-label>\n <input matInput [ngModel]=\"rule.to\" (ngModelChange)=\"updateRule(def.key, $index, 'to', $event)\" />\n </mat-form-field>\n } @else if (ruleSourceArg(def.key, $index, 'to'); as arg) {\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>{{ arg === 'p' ? 'Percentile' : 'N' }}</mat-label>\n <input matInput type=\"number\" [ngModel]=\"ruleSourceArgValue(def.key, $index, 'to')\"\n (ngModelChange)=\"onRuleSourceArg(def.key, $index, 'to', $event)\" />\n </mat-form-field>\n }\n </div>\n }\n }\n <div class=\"rule-row-format\">\n <span class=\"rule-color-group\">\n <span class=\"rule-format-label\">Text</span>\n <!-- Swapped for a read-only preview while a token is in force, as\n the column-level control does: with a token set,\n composeColorValue returns the token and a hex picked here is\n discarded, so an editable swatch invited an edit that\n silently did nothing. -->\n @if (!isTokenColor(rule.color)) {\n <input type=\"color\" class=\"color-hex\" [value]=\"colorHex(rule.color)\"\n (input)=\"onRuleColorEdit(def.key, $index, 'color', { hex: $any($event.target).value })\" />\n } @else {\n <span class=\"color-preview\" [style.background]=\"rule.color\"></span>\n }\n <mat-form-field appearance=\"outline\" class=\"color-token-field rule-token\">\n <mat-select [ngModel]=\"colorToken(rule.color)\"\n (ngModelChange)=\"onRuleColorEdit(def.key, $index, 'color', { token: $event })\">\n <mat-option value=\"\">Custom</mat-option>\n @for (t of colorTokens; track t.value) {\n <mat-option [value]=\"t.value\"><span class=\"color-swatch\" [style.background]=\"t.value\"></span>{{ t.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n </span>\n <!-- Grouped so the row wraps between the two colours rather than\n through one: the percentage broke onto the next line beside\n 'Bold' and read as belonging to it. -->\n <span class=\"rule-color-group\">\n <span class=\"rule-format-label\">Fill</span>\n @if (!isTokenColor(rule.background)) {\n <input type=\"color\" class=\"color-hex\" [value]=\"colorHex(rule.background)\"\n (input)=\"onRuleColorEdit(def.key, $index, 'background', { hex: $any($event.target).value })\" />\n } @else {\n <span class=\"color-preview\" [style.background]=\"rule.background\"></span>\n }\n <!-- A fill is as likely to want a theme colour as the text is, and\n the alpha slider works for either: composeColorValue wraps a\n token in color-mix below 100%. -->\n <mat-form-field appearance=\"outline\" class=\"color-token-field rule-token\">\n <mat-select [ngModel]=\"colorToken(rule.background)\"\n (ngModelChange)=\"onRuleColorEdit(def.key, $index, 'background', { token: $event })\">\n <mat-option value=\"\">Custom</mat-option>\n @for (t of colorTokens; track t.value) {\n <mat-option [value]=\"t.value\"><span class=\"color-swatch\" [style.background]=\"t.value\"></span>{{ t.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <!-- Its own line: the label, swatch and token select already fill\n the panel's width, and a token name as long as 'On Secondary\n Container' pushed the slider off the right edge. -->\n <span class=\"rule-alpha-group\">\n <span class=\"rule-format-label\">Opacity</span>\n <input type=\"range\" class=\"color-alpha\" min=\"0\" max=\"100\" step=\"1\" [value]=\"colorAlpha(rule.background)\"\n (input)=\"onRuleColorEdit(def.key, $index, 'background', { alpha: +$any($event.target).value })\" />\n <span class=\"color-alpha-value\">{{ colorAlpha(rule.background) }}%</span>\n </span>\n </span>\n <mat-checkbox class=\"text-style-check\" [checked]=\"!!rule.bold\"\n (change)=\"updateRule(def.key, $index, 'bold', $event.checked)\">Bold</mat-checkbox>\n <mat-checkbox class=\"text-style-check\" [checked]=\"rule.fill === 'text'\"\n (change)=\"updateRule(def.key, $index, 'fill', $event.checked ? 'text' : 'cell')\">Pill</mat-checkbox>\n <mat-checkbox class=\"text-style-check\" [checked]=\"!!rule.bar\"\n (change)=\"updateRule(def.key, $index, 'bar', $event.checked)\">Data bar</mat-checkbox>\n @if (rule.bar) {\n <mat-checkbox class=\"text-style-check\" [checked]=\"!!rule.bar_auto\"\n (change)=\"updateRule(def.key, $index, 'bar_auto', $event.checked)\">Scale to column</mat-checkbox>\n }\n </div>\n @if (rule.bar && !rule.bar_auto) {\n <div class=\"rule-row-format\">\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>Bar min</mat-label>\n <input matInput type=\"number\" [ngModel]=\"rule.bar_min\" (ngModelChange)=\"updateRule(def.key, $index, 'bar_min', $event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>Bar max</mat-label>\n <input matInput type=\"number\" [ngModel]=\"rule.bar_max\" (ngModelChange)=\"updateRule(def.key, $index, 'bar_max', $event)\" />\n </mat-form-field>\n </div>\n }\n </div>\n }\n <button type=\"button\" class=\"rule-add\" (click)=\"addRule(def.key)\">+ Add rule</button>\n </div>\n }\n\n @case ('checkbox') {\n <mat-checkbox [checked]=\"isChecked(def)\" (change)=\"onCheckbox(def.key, $event.checked)\">\n {{ def.label }}\n </mat-checkbox>\n }\n\n @case ('day_chips') {\n <div class=\"day-chips\">\n <label class=\"day-chips-label\">{{ def.label }}</label>\n <div class=\"day-chips-row\">\n @for (day of weekDays; track day) {\n <button type=\"button\" class=\"day-chip\" [class.selected]=\"isDaySelected(def.key, day)\"\n (click)=\"toggleDay(def.key, day)\">{{ day }}</button>\n }\n </div>\n </div>\n }\n\n @case ('list') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>{{ def.label }}</mat-label>\n <textarea matInput rows=\"4\" [ngModel]=\"listText(def.key)\"\n (ngModelChange)=\"onList(def.key, $event)\"></textarea>\n </mat-form-field>\n }\n\n @case ('color_list') {\n <div class=\"color-list\">\n <label class=\"color-list-label\">{{ def.label }}</label>\n <div class=\"color-list-chips\">\n @for (opt of colorListItems(def.key); track $index) {\n <div class=\"color-list-chip\">\n <input type=\"color\" class=\"color-list-dot\" [value]=\"opt.color || '#9CA3AF'\"\n (input)=\"onColorListColorChange(def.key, $index, $any($event.target).value)\" title=\"Change colour\" />\n <span class=\"color-list-name\">{{ opt.name }}</span>\n <button type=\"button\" class=\"color-list-remove\" (click)=\"removeColorListItem(def.key, $index)\"\n title=\"Remove\">&times;</button>\n </div>\n }\n </div>\n <input class=\"color-list-add\" type=\"text\" placeholder=\"Add option, press Enter\"\n (keydown.enter)=\"addColorListItem(def.key, $any($event.target).value, def.defaultColor || '#9CA3AF'); $any($event.target).value = ''\" />\n </div>\n }\n\n @case ('range_color_list') {\n <div class=\"range-list\">\n <label class=\"range-list-label\">{{ def.label }}</label>\n @for (r of rangeListItems(def.key); track $index) {\n <div class=\"range-list-row\">\n <input type=\"number\" class=\"range-list-num\" min=\"0\" max=\"100\" [value]=\"r.from\"\n (input)=\"onRangeChange(def.key, $index, 'from', $any($event.target).value)\" title=\"From\" />\n <span class=\"range-list-sep\">\u2013</span>\n <input type=\"number\" class=\"range-list-num\" min=\"0\" max=\"100\" [value]=\"r.to\"\n (input)=\"onRangeChange(def.key, $index, 'to', $any($event.target).value)\" title=\"To\" />\n <input type=\"color\" class=\"range-list-color\" [value]=\"r.color || '#22C55E'\"\n (input)=\"onRangeColorChange(def.key, $index, $any($event.target).value)\" title=\"Colour\" />\n <button type=\"button\" class=\"range-list-remove\" (click)=\"removeRangeItem(def.key, $index)\"\n title=\"Remove\">&times;</button>\n </div>\n }\n <button type=\"button\" class=\"range-list-add\" (click)=\"addRangeItem(def.key)\">+ Add range</button>\n </div>\n }\n\n }\n </div>\n }\n </div>\n }\n</aside>\n", styles: [".design-panel-backdrop{position:fixed;inset:0;background:#0000002e;z-index:1000}.column-design-panel{position:fixed;top:0;right:0;bottom:0;width:360px;max-width:90vw;background:var(--grid-surface, #fff);box-shadow:-4px 0 16px #00000029;transform:translate(100%);transition:transform .22s ease;z-index:1001;display:flex;flex-direction:column;font-family:var(--grid-font-family, inherit)}.column-design-panel.open{transform:translate(0)}.design-panel-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--grid-border-color, #e0e0e0)}.design-panel-header .design-panel-title{display:flex;align-items:center;gap:8px;font-weight:600;font-size:15px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.design-panel-body{padding:16px;overflow-y:auto;flex:1}.design-panel-body .design-field{margin-bottom:4px}.design-panel-body .design-field .full-width{width:100%}.design-panel-body .design-field mat-checkbox{display:block;margin:8px 0 16px}.design-panel-body .color-list{margin:4px 0 16px}.design-panel-body .color-list .color-list-label{display:block;font-size:12px;color:var(--grid-on-surface-variant, #49454f);margin-bottom:6px}.design-panel-body .color-list .color-list-chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}.design-panel-body .color-list .color-list-chip{display:inline-flex;align-items:center;gap:6px;padding:3px 8px 3px 4px;border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:16px;background:var(--grid-surface, #fff);font-size:12px}.design-panel-body .color-list .color-list-dot{width:18px;height:18px;padding:0;border:none;background:none;border-radius:50%;cursor:pointer}.design-panel-body .color-list .color-list-remove{border:none;background:none;cursor:pointer;font-size:14px;line-height:1;color:var(--grid-on-surface-variant, #49454f)}.design-panel-body .color-list .color-list-remove:hover{color:var(--grid-error, #b3261e)}.design-panel-body .color-list .color-list-add{width:100%;box-sizing:border-box;padding:8px 10px;border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:6px;font-size:13px;outline:none}.design-panel-body .color-list .color-list-add:focus{border-color:var(--grid-primary, #6750a4)}.range-list{margin:4px 0 16px}.range-list .range-list-label{display:block;font-size:12px;color:var(--grid-on-surface-variant, #49454f);margin-bottom:6px}.range-list .range-list-row{display:flex;align-items:center;gap:6px;margin-bottom:6px}.range-list .range-list-num{width:56px;padding:6px 8px;border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:6px;font-size:13px;outline:none}.range-list .range-list-num:focus{border-color:var(--grid-primary, #6750a4)}.range-list .range-list-sep{color:var(--grid-on-surface-variant, #49454f)}.range-list .range-list-color{width:28px;height:28px;padding:0;border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:6px;background:none;cursor:pointer}.range-list .range-list-remove{margin-left:auto;border:none;background:none;cursor:pointer;font-size:18px;line-height:1;color:var(--grid-on-surface-variant, #49454f)}.range-list .range-list-remove:hover{color:var(--grid-error, #b3261e)}.range-list .range-list-add{margin-top:2px;padding:6px 10px;border:1px dashed var(--grid-outline-variant, #cac4d0);border-radius:6px;background:none;font-size:13px;cursor:pointer;color:var(--grid-primary, #6750a4)}.range-list .range-list-add:hover{background:var(--grid-surface-variant, #f3edf7)}.day-chips{display:flex;flex-direction:column;gap:6px}.day-chips-label{font-size:12px;color:var(--eru-on-surface-variant, #5f6368)}.day-chips-row{display:flex;flex-wrap:wrap;gap:6px}.day-chip{border:1px solid var(--eru-outline, #c4c7c5);background:transparent;border-radius:16px;padding:4px 12px;font-size:12px;cursor:pointer;-webkit-user-select:none;user-select:none;transition:background .15s,color .15s,border-color .15s}.day-chip.selected{background:var(--eru-primary, #1a73e8);border-color:var(--eru-primary, #1a73e8);color:#fff}.design-field-inherited{opacity:.55;pointer-events:none}.design-meta-source{padding-bottom:8px;border-bottom:1px solid var(--grid-outline-variant, #e0e0e0);margin-bottom:12px}.design-meta-hint{margin:4px 0 0;font-size:11px;line-height:1.4;color:var(--grid-on-surface-variant, #49454f)}.text-style-control{display:flex;flex-direction:column;gap:6px}.text-style-label{font-size:12px;color:var(--grid-on-surface-variant, #49454f)}.text-style-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px}.text-style-number{width:96px}.text-style-color{display:flex;align-items:center;gap:6px}.text-style-color input[type=color]{width:32px;height:28px;padding:0;border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:4px;background:none;cursor:pointer}.text-style-color-label{font-size:12px}.text-style-clear{border:none;background:none;padding:0;font-size:11px;color:var(--grid-primary, #6750a4);cursor:pointer}.text-style-color--unset input[type=color]{opacity:.3}.text-style-unset-hint{font-size:11px;color:var(--grid-on-surface-variant, #49454f)}.design-target-hidden{margin-left:6px;padding:1px 6px;border-radius:8px;font-size:10px;text-transform:uppercase;letter-spacing:.4px;background:var(--grid-surface-container-high, #e6e0e9);color:var(--grid-on-surface-variant, #49454f)}.color-control{display:block;margin-top:4px}.color-control-label{display:block;font-size:12px;margin-bottom:2px;color:var(--grid-on-surface-variant, #49454f)}.color-control-body{display:flex;align-items:center;gap:6px;min-width:0}.color-token-field{flex:1 1 auto;min-width:0}.color-swatch{display:inline-block;width:12px;height:12px;margin-right:6px;border-radius:3px;border:1px solid var(--grid-outline-variant, #cac4d0);vertical-align:middle}.color-hex{flex:0 0 auto;width:32px;height:28px;padding:0;border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:4px;background:none;cursor:pointer}.color-preview{width:32px;height:28px;border-radius:4px;border:1px solid var(--grid-outline-variant, #cac4d0)}.color-alpha{flex:0 1 68px;min-width:44px}.color-alpha-value{flex:0 0 auto;font-size:11px;min-width:30px;color:var(--grid-on-surface-variant, #49454f)}.color-preview{flex:0 0 auto}.fill-select{width:220px}.rule-list{display:flex;flex-direction:column;gap:8px}.rule-list-hint{margin:0;font-size:11px;color:var(--grid-on-surface-variant, #49454f)}.rule-row{border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:6px;padding:8px;display:flex;flex-direction:column;gap:6px}.rule-row-head{display:flex;align-items:center;gap:6px;min-width:0}.rule-row-operands,.rule-row-format{display:flex;align-items:center;flex-wrap:wrap;gap:6px}.rule-actions{margin-left:auto;flex:0 0 auto;display:flex;gap:4px}.rule-op{flex:1 1 auto;min-width:0}.rule-operand{flex:1 1 96px;min-width:88px}.rule-source{flex:1 1 140px;min-width:128px}.rule-token{flex:1 1 96px;min-width:88px}.rule-format-label{font-size:11px;color:var(--grid-on-surface-variant, #49454f)}.rule-color-group{display:flex;align-items:center;flex-wrap:wrap;gap:6px;flex:1 1 100%}.rule-alpha-group{display:flex;align-items:center;gap:6px;flex:1 1 100%}.rule-alpha-group .color-alpha{flex:1 1 auto}.rule-btn{border:1px solid var(--grid-outline-variant, #cac4d0);background:none;border-radius:4px;width:26px;height:26px;cursor:pointer;font-size:12px;line-height:1}.rule-btn-remove{color:var(--grid-error, #b3261e)}.rule-add{align-self:flex-start;border:1px dashed var(--grid-outline, #79747e);background:none;border-radius:6px;padding:6px 12px;font-size:12px;cursor:pointer;color:var(--grid-primary, #6750a4)}\n"] }]
14172
+ ], template: "@if (isOpen()) {\n<div class=\"design-panel-backdrop\" (click)=\"close()\"></div>\n}\n<aside class=\"column-design-panel\" [class.open]=\"isOpen()\">\n @if (field(); as col) {\n <header class=\"design-panel-header\">\n <div class=\"design-panel-title\">\n <mat-icon>tune</mat-icon>\n <span>{{ col.label || col.name }}</span>\n </div>\n <button mat-icon-button (click)=\"close()\" title=\"Close\">\n <mat-icon>close</mat-icon>\n </button>\n </header>\n\n <div class=\"design-panel-body\">\n <!-- Which field is being designed. A pivot with one measure renders no\n measure-name row, so its single header cell stands for the column\n dimension and the measure at once and can only open one of them; this\n reaches the other. Also the way to a header too narrow to click, or a\n hidden column. Only shown when there is more than one thing to pick. -->\n @if (designTargets().length > 0 && targetCount() > 1) {\n <div class=\"design-field design-target\">\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Editing</mat-label>\n <mat-select [ngModel]=\"gridStore.selectedDesignColumn()\"\n (ngModelChange)=\"gridStore.selectDesignColumn($event)\">\n @for (group of designTargets(); track group.group) {\n <mat-optgroup [label]=\"group.group\">\n @for (target of group.fields; track target.name) {\n <mat-option [value]=\"target.name\">\n {{ target.label || target.name }}\n @if (target.is_hidden) {<span class=\"design-target-hidden\">hidden</span>}\n </mat-option>\n }\n </mat-optgroup>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n\n <!-- Where this column's metadata comes from: typed in here, or inherited\n from a field in the host app's data model so it stays defined once. -->\n <div class=\"design-field design-meta-source\">\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Metadata source</mat-label>\n <mat-select [ngModel]=\"metaSource()\" (ngModelChange)=\"onMetaSourceChange($event)\">\n <mat-option value=\"manual\">Defined here</mat-option>\n <mat-option value=\"entity\">Mapped to data model field</mat-option>\n </mat-select>\n </mat-form-field>\n\n @if (metaSource() === 'entity') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Entity</mat-label>\n <mat-select [ngModel]=\"getValue('mapped_entity')\" (ngModelChange)=\"onEntityChange($event)\"\n (openedChange)=\"$event && requestEntities()\">\n @for (opt of entityOptions(); track opt.value) {\n <mat-option [value]=\"opt.value\">{{ opt.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (getValue('mapped_entity')) {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Field</mat-label>\n <mat-select [ngModel]=\"getValue('mapped_field')\" (ngModelChange)=\"onEntityFieldChange($event)\">\n @for (opt of entityFieldOptions(); track opt.value) {\n <mat-option [value]=\"opt.value\">{{ opt.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @if (isEntityMapped()) {\n <p class=\"design-meta-hint\">\n Colours, options and formatting come from the data model. Edit them there to update\n every grid that maps this field. The label, date format, abbreviation, display scale\n and symbol field start from the model but can be changed here for this grid alone.\n </p>\n }\n }\n </div>\n\n @for (def of metaFields(); track trackMetaField(def)) {\n <div class=\"design-field\" [class.design-field-inherited]=\"isInherited(def.key)\"\n [title]=\"isInherited(def.key) ? 'Inherited from the mapped data model field' : ''\">\n @switch (def.control) {\n\n @case ('text') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>{{ def.label }}</mat-label>\n <input matInput [ngModel]=\"getValue(def.key)\" (ngModelChange)=\"onText(def.key, $event)\" />\n </mat-form-field>\n }\n\n @case ('number') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>{{ def.label }}</mat-label>\n <input matInput type=\"number\" [ngModel]=\"getValue(def.key)\" (ngModelChange)=\"onNumber(def.key, $event)\" />\n </mat-form-field>\n }\n\n @case ('select') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>{{ def.label }}</mat-label>\n <mat-select [ngModel]=\"getValue(def.key) ?? def.defaultValue\" (ngModelChange)=\"onSelect(def.key, $event)\">\n @for (opt of def.options; track opt.value) {\n <mat-option [value]=\"opt.value\">{{ opt.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @case ('field_select') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>{{ def.label }}</mat-label>\n <mat-select [ngModel]=\"getValue(def.key)\" (ngModelChange)=\"onSelect(def.key, $event)\">\n <mat-option [value]=\"null\">None</mat-option>\n @for (opt of fieldSelectOptions(); track opt.value) {\n <mat-option [value]=\"opt.value\">{{ opt.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @case ('field_slot') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>{{ def.label }}</mat-label>\n <mat-select [ngModel]=\"slotValue(def)\" (ngModelChange)=\"onSlotChange(def, $event)\">\n <mat-option [value]=\"null\">None</mat-option>\n @for (opt of fieldSlotOptions(); track opt.value) {\n <mat-option [value]=\"opt.value\">{{ opt.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @case ('text_slot') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>{{ def.label }}</mat-label>\n <input matInput type=\"text\" [ngModel]=\"slotValue(def)\" (ngModelChange)=\"onSlotChange(def, $event)\" />\n </mat-form-field>\n }\n\n @case ('text_style') {\n <div class=\"text-style-control\">\n <label class=\"text-style-label\">{{ def.label }}</label>\n <div class=\"text-style-row\">\n <mat-form-field appearance=\"outline\" class=\"text-style-number\">\n <mat-label>Size</mat-label>\n <input matInput type=\"number\" [ngModel]=\"styleValue(def.key, 'font_size')\"\n (ngModelChange)=\"onStyleChange(def.key, 'font_size', $event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"text-style-number\">\n <mat-label>Weight</mat-label>\n <input matInput type=\"number\" [ngModel]=\"styleValue(def.key, 'font_weight')\"\n (ngModelChange)=\"onStyleChange(def.key, 'font_weight', $event)\" />\n </mat-form-field>\n <mat-checkbox class=\"text-style-check\" [checked]=\"!!styleValue(def.key, 'italic')\"\n (change)=\"onStyleChange(def.key, 'italic', $event.checked)\">Italic</mat-checkbox>\n </div>\n\n @if (!isSelfColoured()) {\n <div class=\"color-control\">\n <span class=\"color-control-label\">Text colour</span>\n <div class=\"color-control-body\">\n <mat-form-field appearance=\"outline\" class=\"color-token-field\">\n <mat-select [ngModel]=\"colorToken(styleColor(def.key, 'color'))\"\n (ngModelChange)=\"onStyleColorEdit(def.key, 'color', { token: $event })\">\n <mat-option value=\"\">Custom</mat-option>\n @for (t of colorTokens; track t.value) {\n <mat-option [value]=\"t.value\"><span class=\"color-swatch\" [style.background]=\"t.value\"></span>{{ t.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (!isTokenColor(styleColor(def.key, 'color'))) {\n <input type=\"color\" class=\"color-hex\" [value]=\"colorHex(styleColor(def.key, 'color'))\"\n (input)=\"onStyleColorEdit(def.key, 'color', { hex: $any($event.target).value })\" />\n } @else {\n <span class=\"color-preview\" [style.background]=\"styleColor(def.key, 'color')\"></span>\n }\n <input type=\"range\" class=\"color-alpha\" min=\"0\" max=\"100\" step=\"1\"\n [value]=\"colorAlpha(styleColor(def.key, 'color'))\"\n (input)=\"onStyleColorEdit(def.key, 'color', { alpha: +$any($event.target).value })\" />\n <span class=\"color-alpha-value\">{{ colorAlpha(styleColor(def.key, 'color')) }}%</span>\n @if (styleColor(def.key, 'color')) {\n <button type=\"button\" class=\"text-style-clear\" (click)=\"clearStyleColor(def.key, 'color')\">Reset</button>\n } @else {\n <span class=\"text-style-unset-hint\">theme</span>\n }\n </div>\n </div>\n }\n\n <div class=\"color-control\">\n <span class=\"color-control-label\">{{ isSelfColoured() ? 'Cell background' : 'Background' }}</span>\n <div class=\"color-control-body\">\n <mat-form-field appearance=\"outline\" class=\"color-token-field\">\n <mat-select [ngModel]=\"colorToken(styleColor(def.key, 'background'))\"\n (ngModelChange)=\"onStyleColorEdit(def.key, 'background', { token: $event })\">\n <mat-option value=\"\">Custom</mat-option>\n @for (t of colorTokens; track t.value) {\n <mat-option [value]=\"t.value\"><span class=\"color-swatch\" [style.background]=\"t.value\"></span>{{ t.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (!isTokenColor(styleColor(def.key, 'background'))) {\n <input type=\"color\" class=\"color-hex\" [value]=\"colorHex(styleColor(def.key, 'background'))\"\n (input)=\"onStyleColorEdit(def.key, 'background', { hex: $any($event.target).value })\" />\n } @else {\n <span class=\"color-preview\" [style.background]=\"styleColor(def.key, 'background')\"></span>\n }\n <input type=\"range\" class=\"color-alpha\" min=\"0\" max=\"100\" step=\"1\"\n [value]=\"colorAlpha(styleColor(def.key, 'background'))\"\n (input)=\"onStyleColorEdit(def.key, 'background', { alpha: +$any($event.target).value })\" />\n <span class=\"color-alpha-value\">{{ colorAlpha(styleColor(def.key, 'background')) }}%</span>\n @if (styleColor(def.key, 'background')) {\n <button type=\"button\" class=\"text-style-clear\" (click)=\"clearStyleColor(def.key, 'background')\">Reset</button>\n } @else {\n <span class=\"text-style-unset-hint\">none</span>\n }\n </div>\n </div>\n\n @if (styleColor(def.key, 'background') && !isSelfColoured()) {\n <div class=\"text-style-row\">\n <mat-form-field appearance=\"outline\" class=\"fill-select\">\n <mat-label>Fill</mat-label>\n <mat-select [ngModel]=\"styleFill(def.key)\" (ngModelChange)=\"onStyleFill(def.key, $event)\">\n <mat-option value=\"text\">Behind the text (pill)</mat-option>\n <mat-option value=\"cell\">Whole cell</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n }\n </div>\n }\n\n @case ('rule_list') {\n <div class=\"rule-list\">\n <label class=\"text-style-label\">{{ def.label }}</label>\n <p class=\"rule-list-hint\">Checked top to bottom \u2014 the first rule that matches wins.</p>\n @for (rule of ruleItems(def.key); track $index) {\n <div class=\"rule-row\">\n <div class=\"rule-row-head\">\n <mat-form-field appearance=\"outline\" class=\"rule-op\">\n <mat-label>When</mat-label>\n <mat-select [ngModel]=\"rule.op || 'between'\" (ngModelChange)=\"updateRule(def.key, $index, 'op', $event)\">\n @for (o of ruleOperators; track o.value) {\n <mat-option [value]=\"o.value\">{{ o.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <div class=\"rule-actions\">\n <button type=\"button\" class=\"rule-btn\" title=\"Move up\" (click)=\"moveRule(def.key, $index, -1)\">\u2191</button>\n <button type=\"button\" class=\"rule-btn\" title=\"Move down\" (click)=\"moveRule(def.key, $index, 1)\">\u2193</button>\n <button type=\"button\" class=\"rule-btn rule-btn-remove\" title=\"Remove\" (click)=\"removeRule(def.key, $index)\">\u2715</button>\n </div>\n </div>\n @if (operandCount(rule.op) > 0) {\n <div class=\"rule-row-operands\">\n <mat-form-field appearance=\"outline\" class=\"rule-source\">\n <mat-label>{{ operandCount(rule.op) === 2 ? 'From' : 'Value' }}</mat-label>\n <mat-select [ngModel]=\"ruleSource(def.key, $index, 'from')\"\n (ngModelChange)=\"onRuleSource(def.key, $index, 'from', $event)\">\n @for (o of operandSources; track o.value) {\n <mat-option [value]=\"o.value\">{{ o.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (!ruleSource(def.key, $index, 'from')) {\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>Value</mat-label>\n <input matInput [ngModel]=\"rule.from\" (ngModelChange)=\"updateRule(def.key, $index, 'from', $event)\" />\n </mat-form-field>\n } @else {\n @if (ruleSourceArg(def.key, $index, 'from'); as arg) {\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>{{ arg === 'p' ? 'Percentile' : 'N' }}</mat-label>\n <input matInput type=\"number\" [ngModel]=\"ruleSourceArgValue(def.key, $index, 'from')\"\n (ngModelChange)=\"onRuleSourceArg(def.key, $index, 'from', $event)\" />\n </mat-form-field>\n }\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>\u00B1 std dev</mat-label>\n <input matInput type=\"number\" [ngModel]=\"ruleStddevOffset(def.key, $index, 'from')\"\n (ngModelChange)=\"onRuleStddevOffset(def.key, $index, 'from', $event)\" />\n </mat-form-field>\n }\n </div>\n @if (operandCount(rule.op) === 2) {\n <div class=\"rule-row-operands\">\n <mat-form-field appearance=\"outline\" class=\"rule-source\">\n <mat-label>To</mat-label>\n <mat-select [ngModel]=\"ruleSource(def.key, $index, 'to')\"\n (ngModelChange)=\"onRuleSource(def.key, $index, 'to', $event)\">\n @for (o of operandSources; track o.value) {\n <mat-option [value]=\"o.value\">{{ o.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (!ruleSource(def.key, $index, 'to')) {\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>Value</mat-label>\n <input matInput [ngModel]=\"rule.to\" (ngModelChange)=\"updateRule(def.key, $index, 'to', $event)\" />\n </mat-form-field>\n } @else if (ruleSourceArg(def.key, $index, 'to'); as arg) {\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>{{ arg === 'p' ? 'Percentile' : 'N' }}</mat-label>\n <input matInput type=\"number\" [ngModel]=\"ruleSourceArgValue(def.key, $index, 'to')\"\n (ngModelChange)=\"onRuleSourceArg(def.key, $index, 'to', $event)\" />\n </mat-form-field>\n }\n </div>\n }\n }\n <div class=\"rule-row-format\">\n <span class=\"rule-color-group\">\n <span class=\"rule-format-label\">Text</span>\n <!-- Swapped for a read-only preview while a token is in force, as\n the column-level control does: with a token set,\n composeColorValue returns the token and a hex picked here is\n discarded, so an editable swatch invited an edit that\n silently did nothing. -->\n @if (!isTokenColor(rule.color)) {\n <input type=\"color\" class=\"color-hex\" [value]=\"colorHex(rule.color)\"\n (input)=\"onRuleColorEdit(def.key, $index, 'color', { hex: $any($event.target).value })\" />\n } @else {\n <span class=\"color-preview\" [style.background]=\"rule.color\"></span>\n }\n <mat-form-field appearance=\"outline\" class=\"color-token-field rule-token\">\n <mat-select [ngModel]=\"colorToken(rule.color)\"\n (ngModelChange)=\"onRuleColorEdit(def.key, $index, 'color', { token: $event })\">\n <mat-option value=\"\">Custom</mat-option>\n @for (t of colorTokens; track t.value) {\n <mat-option [value]=\"t.value\"><span class=\"color-swatch\" [style.background]=\"t.value\"></span>{{ t.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n </span>\n <!-- Grouped so the row wraps between the two colours rather than\n through one: the percentage broke onto the next line beside\n 'Bold' and read as belonging to it. -->\n <span class=\"rule-color-group\">\n <span class=\"rule-format-label\">Fill</span>\n @if (!isTokenColor(rule.background)) {\n <input type=\"color\" class=\"color-hex\" [value]=\"colorHex(rule.background)\"\n (input)=\"onRuleColorEdit(def.key, $index, 'background', { hex: $any($event.target).value })\" />\n } @else {\n <span class=\"color-preview\" [style.background]=\"rule.background\"></span>\n }\n <!-- A fill is as likely to want a theme colour as the text is, and\n the alpha slider works for either: composeColorValue wraps a\n token in color-mix below 100%. -->\n <mat-form-field appearance=\"outline\" class=\"color-token-field rule-token\">\n <mat-select [ngModel]=\"colorToken(rule.background)\"\n (ngModelChange)=\"onRuleColorEdit(def.key, $index, 'background', { token: $event })\">\n <mat-option value=\"\">Custom</mat-option>\n @for (t of colorTokens; track t.value) {\n <mat-option [value]=\"t.value\"><span class=\"color-swatch\" [style.background]=\"t.value\"></span>{{ t.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <!-- Its own line: the label, swatch and token select already fill\n the panel's width, and a token name as long as 'On Secondary\n Container' pushed the slider off the right edge. -->\n <span class=\"rule-alpha-group\">\n <span class=\"rule-format-label\">Opacity</span>\n <input type=\"range\" class=\"color-alpha\" min=\"0\" max=\"100\" step=\"1\" [value]=\"colorAlpha(rule.background)\"\n (input)=\"onRuleColorEdit(def.key, $index, 'background', { alpha: +$any($event.target).value })\" />\n <span class=\"color-alpha-value\">{{ colorAlpha(rule.background) }}%</span>\n </span>\n </span>\n <mat-checkbox class=\"text-style-check\" [checked]=\"!!rule.bold\"\n (change)=\"updateRule(def.key, $index, 'bold', $event.checked)\">Bold</mat-checkbox>\n <mat-checkbox class=\"text-style-check\" [checked]=\"rule.fill === 'text'\"\n (change)=\"updateRule(def.key, $index, 'fill', $event.checked ? 'text' : 'cell')\">Pill</mat-checkbox>\n <mat-checkbox class=\"text-style-check\" [checked]=\"!!rule.bar\"\n (change)=\"updateRule(def.key, $index, 'bar', $event.checked)\">Data bar</mat-checkbox>\n @if (rule.bar) {\n <mat-checkbox class=\"text-style-check\" [checked]=\"!!rule.bar_auto\"\n (change)=\"updateRule(def.key, $index, 'bar_auto', $event.checked)\">Scale to column</mat-checkbox>\n }\n </div>\n @if (rule.bar && !rule.bar_auto) {\n <div class=\"rule-row-format\">\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>Bar min</mat-label>\n <input matInput type=\"number\" [ngModel]=\"rule.bar_min\" (ngModelChange)=\"updateRule(def.key, $index, 'bar_min', $event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"rule-operand\">\n <mat-label>Bar max</mat-label>\n <input matInput type=\"number\" [ngModel]=\"rule.bar_max\" (ngModelChange)=\"updateRule(def.key, $index, 'bar_max', $event)\" />\n </mat-form-field>\n </div>\n }\n </div>\n }\n <button type=\"button\" class=\"rule-add\" (click)=\"addRule(def.key)\">+ Add rule</button>\n </div>\n }\n\n @case ('checkbox') {\n <mat-checkbox [checked]=\"isChecked(def)\" (change)=\"onCheckbox(def.key, $event.checked)\">\n {{ def.label }}\n </mat-checkbox>\n }\n\n @case ('day_chips') {\n <div class=\"day-chips\">\n <label class=\"day-chips-label\">{{ def.label }}</label>\n <div class=\"day-chips-row\">\n @for (day of weekDays; track day) {\n <button type=\"button\" class=\"day-chip\" [class.selected]=\"isDaySelected(def.key, day)\"\n (click)=\"toggleDay(def.key, day)\">{{ day }}</button>\n }\n </div>\n </div>\n }\n\n @case ('list') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>{{ def.label }}</mat-label>\n <textarea matInput rows=\"4\" [ngModel]=\"listText(def.key)\"\n (ngModelChange)=\"onList(def.key, $event)\"></textarea>\n </mat-form-field>\n }\n\n @case ('color_list') {\n <div class=\"color-list\">\n <label class=\"color-list-label\">{{ def.label }}</label>\n <div class=\"color-list-chips\">\n @for (opt of colorListItems(def.key); track $index) {\n <div class=\"color-list-chip\">\n <input type=\"color\" class=\"color-list-dot\" [value]=\"opt.color || '#9CA3AF'\"\n (input)=\"onColorListColorChange(def.key, $index, $any($event.target).value)\" title=\"Change colour\" />\n <span class=\"color-list-name\">{{ opt.name }}</span>\n <button type=\"button\" class=\"color-list-remove\" (click)=\"removeColorListItem(def.key, $index)\"\n title=\"Remove\">&times;</button>\n </div>\n }\n </div>\n <input class=\"color-list-add\" type=\"text\" placeholder=\"Add option, press Enter\"\n (keydown.enter)=\"addColorListItem(def.key, $any($event.target).value, def.defaultColor || '#9CA3AF'); $any($event.target).value = ''\" />\n </div>\n }\n\n @case ('range_color_list') {\n <div class=\"range-list\">\n <label class=\"range-list-label\">{{ def.label }}</label>\n @for (r of rangeListItems(def.key); track $index) {\n <div class=\"range-list-row\">\n <input type=\"number\" class=\"range-list-num\" min=\"0\" max=\"100\" [value]=\"r.from\"\n (input)=\"onRangeChange(def.key, $index, 'from', $any($event.target).value)\" title=\"From\" />\n <span class=\"range-list-sep\">\u2013</span>\n <input type=\"number\" class=\"range-list-num\" min=\"0\" max=\"100\" [value]=\"r.to\"\n (input)=\"onRangeChange(def.key, $index, 'to', $any($event.target).value)\" title=\"To\" />\n <input type=\"color\" class=\"range-list-color\" [value]=\"r.color || '#22C55E'\"\n (input)=\"onRangeColorChange(def.key, $index, $any($event.target).value)\" title=\"Colour\" />\n <button type=\"button\" class=\"range-list-remove\" (click)=\"removeRangeItem(def.key, $index)\"\n title=\"Remove\">&times;</button>\n </div>\n }\n <button type=\"button\" class=\"range-list-add\" (click)=\"addRangeItem(def.key)\">+ Add range</button>\n </div>\n }\n\n }\n </div>\n }\n </div>\n }\n</aside>\n", styles: [".design-panel-backdrop{position:fixed;inset:0;background:#0000002e;z-index:1000}.column-design-panel{position:fixed;top:0;right:0;bottom:0;width:360px;max-width:90vw;background:var(--grid-surface, #fff);box-shadow:-4px 0 16px #00000029;transform:translate(100%);transition:transform .22s ease;z-index:1001;display:flex;flex-direction:column;font-family:var(--grid-font-family, inherit)}.column-design-panel.open{transform:translate(0)}.design-panel-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--grid-border-color, #e0e0e0)}.design-panel-header .design-panel-title{display:flex;align-items:center;gap:8px;font-weight:600;font-size:15px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.design-panel-body{padding:16px;overflow-y:auto;flex:1}.design-panel-body .design-field{margin-bottom:4px}.design-panel-body .design-field .full-width{width:100%}.design-panel-body .design-field mat-checkbox{display:block;margin:8px 0 16px}.design-panel-body .color-list{margin:4px 0 16px}.design-panel-body .color-list .color-list-label{display:block;font-size:12px;color:var(--grid-on-surface-variant, #49454f);margin-bottom:6px}.design-panel-body .color-list .color-list-chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}.design-panel-body .color-list .color-list-chip{display:inline-flex;align-items:center;gap:6px;padding:3px 8px 3px 4px;border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:16px;background:var(--grid-surface, #fff);font-size:12px}.design-panel-body .color-list .color-list-dot{width:18px;height:18px;padding:0;border:none;background:none;border-radius:50%;cursor:pointer}.design-panel-body .color-list .color-list-remove{border:none;background:none;cursor:pointer;font-size:14px;line-height:1;color:var(--grid-on-surface-variant, #49454f)}.design-panel-body .color-list .color-list-remove:hover{color:var(--grid-error, #b3261e)}.design-panel-body .color-list .color-list-add{width:100%;box-sizing:border-box;padding:8px 10px;border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:6px;font-size:13px;outline:none}.design-panel-body .color-list .color-list-add:focus{border-color:var(--grid-primary, #6750a4)}.range-list{margin:4px 0 16px}.range-list .range-list-label{display:block;font-size:12px;color:var(--grid-on-surface-variant, #49454f);margin-bottom:6px}.range-list .range-list-row{display:flex;align-items:center;gap:6px;margin-bottom:6px}.range-list .range-list-num{width:56px;padding:6px 8px;border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:6px;font-size:13px;outline:none}.range-list .range-list-num:focus{border-color:var(--grid-primary, #6750a4)}.range-list .range-list-sep{color:var(--grid-on-surface-variant, #49454f)}.range-list .range-list-color{width:28px;height:28px;padding:0;border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:6px;background:none;cursor:pointer}.range-list .range-list-remove{margin-left:auto;border:none;background:none;cursor:pointer;font-size:18px;line-height:1;color:var(--grid-on-surface-variant, #49454f)}.range-list .range-list-remove:hover{color:var(--grid-error, #b3261e)}.range-list .range-list-add{margin-top:2px;padding:6px 10px;border:1px dashed var(--grid-outline-variant, #cac4d0);border-radius:6px;background:none;font-size:13px;cursor:pointer;color:var(--grid-primary, #6750a4)}.range-list .range-list-add:hover{background:var(--grid-surface-variant, #f3edf7)}.day-chips{display:flex;flex-direction:column;gap:6px}.day-chips-label{font-size:12px;color:var(--eru-on-surface-variant, #5f6368)}.day-chips-row{display:flex;flex-wrap:wrap;gap:6px}.day-chip{border:1px solid var(--eru-outline, #c4c7c5);background:transparent;border-radius:16px;padding:4px 12px;font-size:12px;cursor:pointer;-webkit-user-select:none;user-select:none;transition:background .15s,color .15s,border-color .15s}.day-chip.selected{background:var(--eru-primary, #1a73e8);border-color:var(--eru-primary, #1a73e8);color:#fff}.design-field-inherited{opacity:.55;pointer-events:none}.design-meta-source{padding-bottom:8px;border-bottom:1px solid var(--grid-outline-variant, #e0e0e0);margin-bottom:12px}.design-meta-hint{margin:4px 0 0;font-size:11px;line-height:1.4;color:var(--grid-on-surface-variant, #49454f)}.text-style-control{display:flex;flex-direction:column;gap:6px}.text-style-label{font-size:12px;color:var(--grid-on-surface-variant, #49454f)}.text-style-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px}.text-style-number{width:96px}.text-style-color{display:flex;align-items:center;gap:6px}.text-style-color input[type=color]{width:32px;height:28px;padding:0;border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:4px;background:none;cursor:pointer}.text-style-color-label{font-size:12px}.text-style-clear{border:none;background:none;padding:0;font-size:11px;color:var(--grid-primary, #6750a4);cursor:pointer}.text-style-color--unset input[type=color]{opacity:.3}.text-style-unset-hint{font-size:11px;color:var(--grid-on-surface-variant, #49454f)}.design-target-hidden{margin-left:6px;padding:1px 6px;border-radius:8px;font-size:10px;text-transform:uppercase;letter-spacing:.4px;background:var(--grid-surface-container-high, #e6e0e9);color:var(--grid-on-surface-variant, #49454f)}.color-control{display:block;margin-top:4px}.color-control-label{display:block;font-size:12px;margin-bottom:2px;color:var(--grid-on-surface-variant, #49454f)}.color-control-body{display:flex;align-items:center;gap:6px;min-width:0}.color-token-field{flex:1 1 auto;min-width:0}.color-swatch{display:inline-block;width:12px;height:12px;margin-right:6px;border-radius:3px;border:1px solid var(--grid-outline-variant, #cac4d0);vertical-align:middle}.color-hex{flex:0 0 auto;width:32px;height:28px;padding:0;border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:4px;background:none;cursor:pointer}.color-preview{width:32px;height:28px;border-radius:4px;border:1px solid var(--grid-outline-variant, #cac4d0)}.color-alpha{flex:0 1 68px;min-width:44px}.color-alpha-value{flex:0 0 auto;font-size:11px;min-width:30px;color:var(--grid-on-surface-variant, #49454f)}.color-preview{flex:0 0 auto}.fill-select{width:220px}.rule-list{display:flex;flex-direction:column;gap:8px}.rule-list-hint{margin:0;font-size:11px;color:var(--grid-on-surface-variant, #49454f)}.rule-row{border:1px solid var(--grid-outline-variant, #cac4d0);border-radius:6px;padding:8px;display:flex;flex-direction:column;gap:6px}.rule-row-head{display:flex;align-items:center;gap:6px;min-width:0}.rule-row-operands,.rule-row-format{display:flex;align-items:center;flex-wrap:wrap;gap:6px}.rule-actions{margin-left:auto;flex:0 0 auto;display:flex;gap:4px}.rule-op{flex:1 1 auto;min-width:0}.rule-operand{flex:1 1 96px;min-width:88px}.rule-source{flex:1 1 140px;min-width:128px}.rule-token{flex:1 1 96px;min-width:88px}.rule-format-label{font-size:11px;color:var(--grid-on-surface-variant, #49454f)}.rule-color-group{display:flex;align-items:center;flex-wrap:wrap;gap:6px;flex:1 1 100%}.rule-alpha-group{display:flex;align-items:center;gap:6px;flex:1 1 100%}.rule-alpha-group .color-alpha{flex:1 1 auto}.rule-btn{border:1px solid var(--grid-outline-variant, #cac4d0);background:none;border-radius:4px;width:26px;height:26px;cursor:pointer;font-size:12px;line-height:1}.rule-btn-remove{color:var(--grid-error, #b3261e)}.rule-add{align-self:flex-start;border:1px dashed var(--grid-outline, #79747e);background:none;border-radius:6px;padding:6px 12px;font-size:12px;cursor:pointer;color:var(--grid-primary, #6750a4)}\n"] }]
13702
14173
  }], ctorParameters: () => [] });
13703
14174
 
13704
14175
  /**
@@ -13878,6 +14349,11 @@ class EruGridComponent {
13878
14349
  elementRef = inject(ElementRef);
13879
14350
  breakpointObserver = inject(BreakpointObserver);
13880
14351
  isSmViewport = signal(false, ...(ngDevMode ? [{ debugName: "isSmViewport" }] : []));
14352
+ /** Set once the host ResizeObserver has reported a usable width, after which
14353
+ * the window media query stops feeding isSmViewport. */
14354
+ hasMeasuredOwnWidth = false;
14355
+ /** Below `md`, matching the media query this replaced. */
14356
+ static SM_VIEWPORT_MAX_WIDTH = 768;
13881
14357
  containerHeight = signal(400, ...(ngDevMode ? [{ debugName: "containerHeight" }] : [])); // Internal tracking for available height
13882
14358
  resizeObserver = null;
13883
14359
  boardViewportObserver = null;
@@ -13995,6 +14471,15 @@ class EruGridComponent {
13995
14471
  */
13996
14472
  onExcelDownloadClick(event) {
13997
14473
  event.stopPropagation();
14474
+ this.requestExcelDownload();
14475
+ }
14476
+ /**
14477
+ * Enqueue an excel-download request without a click. This is the same path
14478
+ * the icon takes, exposed so a host can trigger the export from its own
14479
+ * button — useful when `config.excel_download_icon` is off because the
14480
+ * built-in icon sits in the wrong place for the page.
14481
+ */
14482
+ requestExcelDownload() {
13998
14483
  const timestamp = Date.now();
13999
14484
  this.gridStore.addExcelDownloadRequest({
14000
14485
  id: timestamp,
@@ -14120,7 +14605,6 @@ class EruGridComponent {
14120
14605
  return !!row && this.gridStore.activeBoardRow() === row;
14121
14606
  }
14122
14607
  initialMinHeight = 400;
14123
- initialTotalWidth = 0;
14124
14608
  viewport;
14125
14609
  groupsViewport;
14126
14610
  groupsScrollContainerEl;
@@ -14131,6 +14615,24 @@ class EruGridComponent {
14131
14615
  groupedRows = computed(() => this.createGroupedRows(), ...(ngDevMode ? [{ debugName: "groupedRows" }] : []));
14132
14616
  columns = computed(() => this.gridStore.displayColumns(), ...(ngDevMode ? [{ debugName: "columns" }] : []));
14133
14617
  selectedRowIds = this.gridStore.selectedRowIds;
14618
+ selectAllMatching = this.gridStore.selectAllMatching;
14619
+ totalMatchingCount = this.gridStore.totalMatchingCount;
14620
+ /**
14621
+ * Show the select-all banner only where it has something to say: either the
14622
+ * user already accepted "all N matching" and needs it reported back, or they
14623
+ * selected everything loaded and there is more behind it to offer. Ticking a
14624
+ * few rows deliberately is not an occasion to offer the whole result set.
14625
+ */
14626
+ showSelectAllBanner = computed(() => {
14627
+ if (this.gridStore.configuration().config?.allowSelection !== true)
14628
+ return false;
14629
+ if (!this.gridStore.hasSelection())
14630
+ return false;
14631
+ if (this.gridStore.selectAllMatching())
14632
+ return true;
14633
+ return this.isAllGroupsSelected() &&
14634
+ this.gridStore.totalMatchingCount() > this.gridStore.totalSelectedCount();
14635
+ }, ...(ngDevMode ? [{ debugName: "showSelectAllBanner" }] : []));
14134
14636
  totalRowCount = computed(() => {
14135
14637
  const groups = this.groups();
14136
14638
  return groups.reduce((sum, group) => sum + group.totalRowCount, 0);
@@ -14261,10 +14763,13 @@ class EruGridComponent {
14261
14763
  const config = this.gridStore.configuration();
14262
14764
  return config?.config?.groupBar ?? true;
14263
14765
  }, ...(ngDevMode ? [{ debugName: "showGroupBar" }] : []));
14264
- // Excel download icon configuration
14766
+ // Excel download icon configuration. `excel_download` enables the export;
14767
+ // `excel_download_icon` (default true) controls whether the built-in icon is
14768
+ // rendered — off means the host triggers requestExcelDownload() itself.
14265
14769
  showExcelDownload = computed(() => {
14266
14770
  const config = this.gridStore.configuration();
14267
- return config?.config?.excel_download ?? false;
14771
+ const enabled = config?.config?.excel_download ?? false;
14772
+ return enabled && (config?.config?.excel_download_icon ?? true);
14268
14773
  }, ...(ngDevMode ? [{ debugName: "showExcelDownload" }] : []));
14269
14774
  // Wrap long header labels onto multiple lines instead of truncating (default: true)
14270
14775
  wrapHeaders = computed(() => {
@@ -14404,10 +14909,21 @@ class EruGridComponent {
14404
14909
  this.viewport?.getRenderedRange();
14405
14910
  this.firstTr = this.viewport?.getRenderedRange()?.start || 0;
14406
14911
  }
14912
+ /**
14913
+ * The sum of the rendered column widths — what every table in the grid is
14914
+ * forced to via `--table-total-width`.
14915
+ *
14916
+ * A signal, not a value captured at init. A resize writes the new width to
14917
+ * the column (or, in pivot mode, to the measure the leaf came from), and the
14918
+ * cells and `<col>`s follow it; a cached total does not, so the table stayed
14919
+ * at its old width while its columns changed underneath — leaving the header
14920
+ * and body tables wider or narrower than the columns they contain, and the
14921
+ * horizontal scroll range wrong. Pivot mode also changes its leaf SET as the
14922
+ * data changes, so even without a resize the total goes stale.
14923
+ */
14924
+ totalTableWidth = computed(() => this.getLeafColumns().reduce((total, column) => total + (column?.field_size || 0), 0), ...(ngDevMode ? [{ debugName: "totalTableWidth" }] : []));
14407
14925
  getTotalTableWidth() {
14408
- const columns = this.getLeafColumns();
14409
- let totalWidth = columns.reduce((total, column) => total + column.field_size, 0);
14410
- return totalWidth;
14926
+ return this.totalTableWidth();
14411
14927
  }
14412
14928
  /* getContainerHeight(): number {
14413
14929
  const element = this.rowContainer?.nativeElement;
@@ -14635,7 +15151,6 @@ class EruGridComponent {
14635
15151
  this.initializeColumnWidths();
14636
15152
  this.firstDataRowIndex.set(this.maxDepth() + 1);
14637
15153
  this.initialMinHeight = this.getMinHeightPx();
14638
- this.initialTotalWidth = this.getTotalTableWidth();
14639
15154
  this.columnsInitialized.set(true);
14640
15155
  }
14641
15156
  });
@@ -14725,10 +15240,19 @@ class EruGridComponent {
14725
15240
  // SCSS fallback in place.
14726
15241
  setVar('--board-column-height', typeof colH === 'number' ? (colH > 0 ? `${colH}px` : 'none') : undefined);
14727
15242
  });
15243
+ // isSmViewport is measured from the grid's own box, not from a window media
15244
+ // query: a grid rendered in a side panel or a narrow column is narrow no
15245
+ // matter how wide the browser is, and the window query kept it on the full
15246
+ // desktop column set there. Fed by the host ResizeObserver in
15247
+ // ngAfterViewInit; the window query only stands in until the first measure.
14728
15248
  this.breakpointObserver
14729
15249
  .observe('(max-width: 767.98px)')
14730
15250
  .pipe(takeUntilDestroyed())
14731
- .subscribe(result => this.isSmViewport.set(result.matches));
15251
+ .subscribe(result => {
15252
+ if (this.hasMeasuredOwnWidth)
15253
+ return;
15254
+ this.isSmViewport.set(result.matches);
15255
+ });
14732
15256
  // Sync preset → data-preset attribute on host. Default 'default' unless the
14733
15257
  // user explicitly sets one. User tokens (via applyTokens) still override.
14734
15258
  effect(() => {
@@ -14828,7 +15352,6 @@ class EruGridComponent {
14828
15352
  // initializeGroups() moved to effect in constructor to handle timing
14829
15353
  this.firstDataRowIndex.set(this.maxDepth() + 1);
14830
15354
  this.initialMinHeight = this.getMinHeightPx();
14831
- this.initialTotalWidth = this.getTotalTableWidth();
14832
15355
  }
14833
15356
  ngAfterViewInit() {
14834
15357
  this.applyTokens();
@@ -14837,6 +15360,16 @@ class EruGridComponent {
14837
15360
  if (typeof ResizeObserver !== 'undefined') {
14838
15361
  this.resizeObserver = new ResizeObserver(entries => {
14839
15362
  for (const entry of entries) {
15363
+ const width = entry.contentRect.width;
15364
+ // 0 is a hidden or not-yet-laid-out host; keep the last real answer
15365
+ // rather than flipping the whole grid to its mobile column set.
15366
+ if (width > 0) {
15367
+ this.hasMeasuredOwnWidth = true;
15368
+ const isSm = width < EruGridComponent.SM_VIEWPORT_MAX_WIDTH;
15369
+ if (isSm !== this.isSmViewport()) {
15370
+ this.ngZone.run(() => this.isSmViewport.set(isSm));
15371
+ }
15372
+ }
14840
15373
  const height = entry.contentRect.height;
14841
15374
  // Cap at gridHeight to prevent feedback loop: larger viewports → taller host → loop
14842
15375
  const capped = Math.min(height, this.layoutGridHeight());
@@ -14915,12 +15448,12 @@ class EruGridComponent {
14915
15448
  return this.initialMinHeight;
14916
15449
  }
14917
15450
  getInitialTotalWidth() {
14918
- return this.initialTotalWidth;
15451
+ return this.totalTableWidth();
14919
15452
  }
14920
15453
  applyCdkWidth() {
14921
15454
  const element = this.rowContainer?.nativeElement;
14922
15455
  if (element) {
14923
- return element.clientWidth > this.initialTotalWidth;
15456
+ return element.clientWidth > this.totalTableWidth();
14924
15457
  }
14925
15458
  return false;
14926
15459
  }
@@ -15324,6 +15857,7 @@ class EruGridComponent {
15324
15857
  const leaves = pivotResult.headerStructure.leafColumns;
15325
15858
  const aggregations = this.gridStore.configuration()?.pivot?.aggregations || [];
15326
15859
  const sourceColumns = this.gridStore.columns() || [];
15860
+ const leafWidths = this.gridStore.pivotLeafWidths();
15327
15861
  // Carry configuration from the field a leaf came from onto the leaf itself.
15328
15862
  // The pivot transform builds leaves with a handful of hard-coded keys — it
15329
15863
  // types every aggregated column as plain 'number' whatever the measure says,
@@ -15337,15 +15871,20 @@ class EruGridComponent {
15337
15871
  const owner = leaf.aggregationFunction
15338
15872
  ? aggregations.find(a => a.name === this.designTargetFor(leaf))
15339
15873
  : sourceColumns.find(c => c.name === leaf.name);
15340
- if (!owner)
15341
- return leaf;
15342
15874
  const patch = {};
15343
- for (const key of EruGridComponent.PIVOT_INHERITED_KEYS) {
15344
- const value = owner[key];
15345
- // `false` is meaningful (enableDrilldown), blank/absent is not.
15346
- if (value !== undefined && value !== null && value !== '')
15347
- patch[key] = value;
15875
+ if (owner) {
15876
+ for (const key of EruGridComponent.PIVOT_INHERITED_KEYS) {
15877
+ const value = owner[key];
15878
+ // `false` is meaningful (enableDrilldown), blank/absent is not.
15879
+ if (value !== undefined && value !== null && value !== '')
15880
+ patch[key] = value;
15881
+ }
15348
15882
  }
15883
+ // A resize of a leaf with no owning field is held against the leaf name
15884
+ // instead — see EruGridStore.updateColumnWidth.
15885
+ const width = leafWidths.get(leaf.name);
15886
+ if (typeof width === 'number' && width > 0)
15887
+ patch.field_size = width;
15349
15888
  return Object.keys(patch).length ? { ...leaf, ...patch } : leaf;
15350
15889
  });
15351
15890
  }
@@ -15417,11 +15956,16 @@ class EruGridComponent {
15417
15956
  toggleRowSelection(event, row) {
15418
15957
  event.stopPropagation();
15419
15958
  const checkbox = event.target;
15420
- const rowId = row.id;
15959
+ const rowId = rowSelectionKey(row);
15960
+ if (!rowId)
15961
+ return;
15421
15962
  if (checkbox.checked) {
15422
15963
  this.gridStore.selectRow(rowId);
15423
15964
  }
15424
15965
  else {
15966
+ // Unticking one row breaks the "all N matching" claim — what is selected
15967
+ // is no longer the whole result set, so it drops back to a plain id list.
15968
+ this.gridStore.setSelectAllMatching(false);
15425
15969
  this.gridStore.deselectRow(rowId);
15426
15970
  }
15427
15971
  }
@@ -15484,7 +16028,20 @@ class EruGridComponent {
15484
16028
  return [...byKey.values()];
15485
16029
  }, ...(ngDevMode ? [{ debugName: "gridActions" }] : []));
15486
16030
  hasConfiguredActions = computed(() => this.gridActions().length > 0, ...(ngDevMode ? [{ debugName: "hasConfiguredActions" }] : []));
15487
- actionDisplayType = computed(() => this.gridStore.configuration()?.config?.action_display_type === 'kebab' ? 'kebab' : 'icons', ...(ngDevMode ? [{ debugName: "actionDisplayType" }] : []));
16031
+ actionDisplayType = computed(() => {
16032
+ const configured = this.gridStore.configuration()?.config?.action_display_type;
16033
+ return configured === 'kebab' || configured === 'icons_outlined' ? configured : 'icons';
16034
+ }, ...(ngDevMode ? [{ debugName: "actionDisplayType" }] : []));
16035
+ /**
16036
+ * Icon font for the row-action icons.
16037
+ *
16038
+ * Filled and outlined are two different Material fonts over the same ligature
16039
+ * names, not two ligatures — so the choice is a `fontSet` swap and every
16040
+ * configured action keeps the icon name it was given. Consumers must have the
16041
+ * outlined font loaded; without it mat-icon falls back to the filled face, so
16042
+ * the worst case is the previous look rather than empty boxes.
16043
+ */
16044
+ actionIconFontSet = computed(() => this.actionDisplayType() === 'icons_outlined' ? 'material-icons-outlined' : 'material-icons', ...(ngDevMode ? [{ debugName: "actionIconFontSet" }] : []));
15488
16045
  /**
15489
16046
  * The actions that apply to one row, after each action's
15490
16047
  * `action_visible_condition` is evaluated against that row's values.
@@ -15590,7 +16147,14 @@ class EruGridComponent {
15590
16147
  // a textbox there. Letting the model's datatype win would undo the choice on
15591
16148
  // every resolve, so a mapped column could never be composed. Everything else
15592
16149
  // about the mapping still inherits.
16150
+ //
16151
+ // The model's datatype is not discarded though: it is what the column
16152
+ // actually holds, and the value still has to be formatted by it. Moving it
16153
+ // aside rather than deleting it is what keeps a composed date column's
16154
+ // pattern working.
15593
16155
  if (PRESENTATION_DATATYPES.has(col?.datatype)) {
16156
+ if (patch.datatype)
16157
+ patch.presentation_base_datatype = patch.datatype;
15594
16158
  delete patch.datatype;
15595
16159
  }
15596
16160
  // The data model uppercases the pattern on save ('DD-MM-YYYY'), while the
@@ -15685,15 +16249,28 @@ class EruGridComponent {
15685
16249
  shouldShowActionColumn(position) {
15686
16250
  return this.isActionColumnEnabled() && this.getActionColumnPosition() === position;
15687
16251
  }
16252
+ /**
16253
+ * The header checkbox. Stage one only: it selects the rows loaded so far, and
16254
+ * the banner then offers "select all N matching" for the rest. It cannot
16255
+ * select unloaded pages itself — the grid never holds them.
16256
+ */
15688
16257
  toggleAllGroups(event) {
15689
16258
  const checkbox = event.target;
15690
16259
  if (checkbox.checked) {
15691
- this.gridStore.rows().forEach(row => this.gridStore.selectRow(row.entity_id || ''));
16260
+ this.gridStore.selectAllLoadedRows();
15692
16261
  }
15693
16262
  else {
15694
16263
  this.gridStore.clearSelection();
15695
16264
  }
15696
16265
  }
16266
+ /** The banner's "select all N matching" — stage two. */
16267
+ selectAllMatchingRows() {
16268
+ this.gridStore.selectAllLoadedRows();
16269
+ this.gridStore.setSelectAllMatching(true);
16270
+ }
16271
+ clearRowSelection() {
16272
+ this.gridStore.clearSelection();
16273
+ }
15697
16274
  isRowSelected(rowId) {
15698
16275
  return rowId ? this.gridStore.isRowSelected(rowId) : false;
15699
16276
  }
@@ -15701,11 +16278,14 @@ class EruGridComponent {
15701
16278
  return this.gridStore.isGroupSelected(groupId);
15702
16279
  }
15703
16280
  isAllGroupsSelected() {
15704
- const allRows = this.gridStore.rows();
16281
+ const allRows = this.gridStore.selectableRows();
15705
16282
  return allRows.length > 0 &&
15706
- allRows.every(row => this.gridStore.isRowSelected(row.entity_id || ''));
16283
+ allRows.every(row => this.gridStore.isRowSelected(rowSelectionKey(row)));
15707
16284
  }
15708
16285
  getSelectedRows() {
16286
+ return this.gridStore.selectedRows();
16287
+ }
16288
+ getSelectedRowIds() {
15709
16289
  return Array.from(this.gridStore.selectedRowIds());
15710
16290
  }
15711
16291
  /* private markGroupAsNoMoreRows(groupId: string) {
@@ -16291,7 +16871,7 @@ class EruGridComponent {
16291
16871
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: EruGridComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
16292
16872
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: EruGridComponent, isStandalone: true, selector: "eru-grid", inputs: { gridConfig: "gridConfig", boardCardTemplate: "boardCardTemplate", personCardTemplate: "personCardTemplate", cellTemplate: "cellTemplate", boardCardHeight: "boardCardHeight", boardCardGap: "boardCardGap", boardCardPadding: "boardCardPadding" }, outputs: { rowSelect: "rowSelect", actionClick: "actionClick" }, providers: [EruGridStore, EruGridService,
16293
16873
  ...MATERIAL_PROVIDERS
16294
- ], viewQueries: [{ propertyName: "rowContainer", first: true, predicate: ["rowContainer"], descendants: true }, { propertyName: "headerScroller", first: true, predicate: ["headerScroller"], descendants: true, read: ElementRef }, { propertyName: "gtScroller", first: true, predicate: ["gtScroller"], descendants: true, read: ElementRef }, { propertyName: "viewport", first: true, predicate: ["vp"], descendants: true }, { propertyName: "groupsViewport", first: true, predicate: ["groupsViewport"], descendants: true }, { propertyName: "groupsScrollContainerEl", first: true, predicate: ["groupsScrollContainer"], descendants: true }, { propertyName: "allViewports", predicate: CdkVirtualScrollViewport, descendants: true }, { propertyName: "headerScrollers", predicate: ["headerScroller"], descendants: true }], ngImport: i0, template: "<!-- <div style=\"background: #f0f0f0; font-size: 12px; border-bottom: 1px solid #ccc;\">\ncurrentPivotScrollIndex {{currentPivotScrollIndex()}} |\nfirstDataRowIndex {{firstDataRowIndex()}} |\nfirstTr {{firstTr}} |\nmaxDepth {{maxDepth()}}\n</div> -->\n<ng-template #excelDownloadIcon>\n <svg class=\"excel-download-icon\" title=\"Download Excel\" (click)=\"onExcelDownloadClick($event)\"\n xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 48 48\" width=\"24px\" height=\"24px\">\n <path fill=\"#169154\" d=\"M29,6H15.744C14.781,6,14,6.781,14,7.744v7.259h15V6z\" />\n <path fill=\"#18482a\" d=\"M14,33.054v7.202C14,41.219,14.781,42,15.743,42H29v-8.946H14z\" />\n <path fill=\"#0c8045\" d=\"M14 15.003H29V24.005000000000003H14z\" />\n <path fill=\"#17472a\" d=\"M14 24.005H29V33.055H14z\" />\n <g>\n <path fill=\"#29c27f\" d=\"M42.256,6H29v9.003h15V7.744C44,6.781,43.219,6,42.256,6z\" />\n <path fill=\"#27663f\" d=\"M29,33.054V42h13.257C43.219,42,44,41.219,44,40.257v-7.202H29z\" />\n <path fill=\"#19ac65\" d=\"M29 15.003H44V24.005000000000003H29z\" />\n <path fill=\"#129652\" d=\"M29 24.005H44V33.055H29z\" />\n </g>\n <path fill=\"#0c7238\"\n d=\"M22.319,34H5.681C4.753,34,4,33.247,4,32.319V15.681C4,14.753,4.753,14,5.681,14h16.638 C23.247,14,24,14.753,24,15.681v16.638C24,33.247,23.247,34,22.319,34z\" />\n <path fill=\"#fff\"\n d=\"M9.807 19L12.193 19 14.129 22.754 16.175 19 18.404 19 15.333 24 18.474 29 16.123 29 14.013 25.07 11.912 29 9.526 29 12.719 23.982z\" />\n </svg>\n</ng-template>\n\n<div class=\"incremental-row-container eru-grid\" #rowContainer [class.pivot-mode]=\"gridStore.isPivotMode()\"\n [class.table-mode]=\"!gridStore.isPivotMode() && !isBoardMode()\" [class.board-mode-host]=\"isBoardMode()\">\n <eru-column-design-panel></eru-column-design-panel>\n <!-- Pivot Mode Template -->\n @if (gridStore.isPivotMode()) {\n <ng-container>\n <div class=\"pivot-container\" style=\"display: flex; flex-direction: column; height: 100%;\"\n [style]=\"'--table-min-height: ' + getInitialMinHeightPx() + 'px; --table-total-width: ' + getInitialTotalWidth() + 'px'\">\n @if(showExcelDownload()) {\n <div class=\"excel-download-bar\">\n <ng-container *ngTemplateOutlet=\"excelDownloadIcon\"></ng-container>\n </div>\n }\n <!-- Debug info for first visible row -->\n\n\n <div class=\"pivot-single-table\"\n style=\"height: 100%; width: 100%; overflow: hidden; display: flex; flex-direction: column;\">\n @if (freezeHeader()) {\n <div #headerScroller class=\"header-shell\">\n <table class=\"eru-grid-table pivot-table\"\n [style]=\"'width: auto; min-width: 100%; --table-total-width: ' + getInitialTotalWidth() + 'px'\"\n [class.show-column-lines]=\"showColumnLines()\" [class.show-row-lines]=\"showRowLines()\">\n <!-- Column Groups for consistent width -->\n <ng-container *ngTemplateOutlet=\"pivotColGroup\"></ng-container>\n\n <ng-container *ngTemplateOutlet=\"pivotTableHead\"></ng-container>\n @if(grandTotalPosition() === 'before' && freezeGrandTotal()) {\n <ng-container *ngTemplateOutlet=\"pivotGrandTotal\"></ng-container>\n }\n </table>\n </div>\n }\n <!-- Virtual Scrolled Table Body -->\n <div>\n <cdk-virtual-scroll-viewport #vp [itemSize]=\"dataRowHeight()\" class=\"viewport pivot-viewport\"\n [class.apply-cdk-width]=\"applyCdkWidth()\" (scrolledIndexChange)=\"onPivotScroll($event)\"\n (scroll)=\"onBodyScroll($event)\" style=\"overflow: auto;\">\n <table class=\"eru-grid-table pivot-table\"\n [style]=\"'width: auto; min-width: 100%; --table-total-width: ' + getInitialTotalWidth() + 'px'\"\n [class.show-column-lines]=\"showColumnLines()\" [class.show-row-lines]=\"showRowLines()\">\n <!-- Column Groups for consistent width -->\n <ng-container *ngTemplateOutlet=\"pivotColGroup\"></ng-container>\n\n @if (!freezeHeader()) {\n <ng-container *ngTemplateOutlet=\"pivotTableHead\"></ng-container>\n }\n <!-- Table Body with Virtual Scrolling -->\n <tbody class=\"pivot-tbody\">\n\n <tr *cdkVirtualFor=\"let pivotRow of gridStore.pivotDisplayData(); \n trackBy: trackByPivotRowFn; \n let i = index\" class=\"pivot-row\" [class.subtotal-row]=\"pivotRow._isSubtotal\"\n [class.grand-total-row]=\"pivotRow._isGrandTotal\"\n [class.subtotal-bold]=\"pivotRow._isSubtotal && subTotalStyle() === 'bold'\"\n [class.subtotal-italic]=\"pivotRow._isSubtotal && subTotalStyle() === 'italic'\"\n [class.subtotal-highlighted]=\"pivotRow._isSubtotal && subTotalStyle() === 'highlighted'\"\n [class.grand-total-bold]=\"pivotRow._isGrandTotal && grandTotalStyle() === 'bold'\"\n [class.grand-total-italic]=\"pivotRow._isGrandTotal && grandTotalStyle() === 'italic'\"\n [class.grand-total-highlighted]=\"pivotRow._isGrandTotal && grandTotalStyle() === 'highlighted'\"\n [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\" [style.cursor]=\"cursorOnHover() || null\" [attr.data-pivot-row]=\"i\">\n @if ((!pivotRow._isGrandTotal && freezeGrandTotal() ) || (!freezeGrandTotal() )) {\n @for (column of getLeafColumns(); track trackByColumnFn($index, column); let colIndex = $index) {\n <td [style.width.px]=\"column.field_size\" [style.minWidth.px]=\"column.field_size\" class=\"pivot-cell\"\n [class.row-dimension-cell]=\"isRowDimensionColumn(column.name)\"\n [class.column-dimension-cell]=\"!isRowDimensionColumn(column.name)\"\n [class.aggregated-value]=\"!isRowDimensionColumn(column.name) && column.datatype === 'number'\"\n [class.pivot-repeated-value]=\"isRepeatedDimensionValue(i, column.name)\"\n [class.pivot-group-start]=\"isPivotGroupStart(i, column.name)\"\n [class.sticky-column]=\"isStickyColumn(column.name, colIndex)\"\n [style.position]=\"isStickyColumn(column.name, colIndex) ? 'sticky' : 'static'\"\n [style.left.px]=\"getStickyColumnLeft(column.name, colIndex)\"\n [style.z-index]=\"isStickyColumn(column.name, colIndex) ? 99 : 1\" [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\">\n <div class=\"cell-content pivot-cell-content\">\n <data-cell [personCardTemplate]=\"personCardTemplate\" [class.aggregation]=\"!!column.aggregationFunction\" [fieldSize]=\"column.field_size\"\n [columnDatatype]=\"column.datatype\" [columnName]=\"column.name\" [value]=\"pivotRow[column.name]\"\n [column]=\"column\" [drillable]=\"column.enableDrilldown || false\" [mode]=\"mode()\"\n [isEditable]=\"isEditable()\" [id]=\"'pivot_' + i + '_' + column.name\" [eruGridStore]=\"gridStore\"\n [row]=\"pivotRow\">\n </data-cell>\n </div>\n </td>\n }\n } @else {\n <td [style.height.px]=\"dataRowHeight()\" [attr.colspan]=\"getLeafColumns().length\"> </td>\n }\n </tr>\n </tbody>\n </table>\n </cdk-virtual-scroll-viewport>\n\n </div>\n @if (freezeGrandTotal() && grandTotalPosition() === 'after') {\n <div #gtScroller class=\"header-shell gt-shell\" [class.adjust-bottom]=\"!applyCdkWidth()\"\n [class.adjust-bottom-vs]=\"adjustScrollWidth()\">\n <table class=\"eru-grid-table pivot-table\"\n [style]=\"'width: auto; min-width: 100%; --table-total-width: ' + getInitialTotalWidth() + 'px'\"\n [class.show-column-lines]=\"showColumnLines()\" [class.show-row-lines]=\"showRowLines()\">\n <ng-container *ngTemplateOutlet=\"pivotColGroup\"></ng-container>\n\n <ng-container *ngTemplateOutlet=\"pivotGrandTotal\"></ng-container>\n\n </table>\n </div>\n }\n\n\n </div>\n </div>\n </ng-container>\n } @else if (isBoardMode()) {\n <!-- Board Mode Template -->\n <div class=\"board-view-container\">\n @if(showExcelDownload()) {\n <div class=\"excel-download-bar\">\n <ng-container *ngTemplateOutlet=\"excelDownloadIcon\"></ng-container>\n </div>\n }\n @if(showSortBar()) {\n <div class=\"board-sort-bar\">\n <span class=\"board-sort-label\">Sort by:</span>\n @for (entry of gridStore.sortColumns(); track getFieldName(entry)) {\n <span class=\"board-sort-chip board-sort-chip-active\">\n <span class=\"board-sort-chip-label\">{{getColumnLabel(entry)}}</span>\n <span class=\"board-sort-chip-arrow\" (click)=\"onBoardSortChipToggle($event, entry)\">\n @if(!entry.startsWith('-')) { \u25B2 } @else { \u25BC }\n </span>\n @if(gridStore.sortColumns().length > 1) {\n <span class=\"board-sort-chip-priority\">{{getSortPriority(getFieldName(entry))}}</span>\n }\n <span class=\"board-sort-chip-remove\" (click)=\"onBoardSortChipRemove($event, entry)\">\u2715</span>\n </span>\n }\n <button class=\"board-sort-add-btn\" [matMenuTriggerFor]=\"sortFieldMenu\">\n <mat-icon class=\"board-sort-add-icon\">add</mat-icon> Add field\n </button>\n <mat-menu #sortFieldMenu=\"matMenu\" class=\"board-sort-menu\">\n @for (column of columns(); track column.name) {\n <button mat-menu-item (click)=\"onBoardSortFieldSelect(column)\"\n [disabled]=\"getSortDirection(column.name) !== null\">\n @if(getSortDirection(column.name) !== null) {\n <mat-icon>check</mat-icon>\n } @else {\n <mat-icon></mat-icon>\n }\n {{column.label}}\n </button>\n }\n </mat-menu>\n @if(gridStore.sortColumns().length > 0) {\n <button class=\"board-sort-clear\" (click)=\"onBoardSortClear()\">\u2715 Clear</button>\n }\n </div>\n }\n <div class=\"board-columns-wrapper\" [class.board-columns-nowrap]=\"!boardWrapColumns()\">\n @for (group of groups(); track group.id) {\n <div class=\"board-column\" [class.board-column-accented]=\"!!boardGroupColor(group)\"\n [style.--board-group-color]=\"boardGroupColor(group)\">\n @if (showBoardColumnHeader()) {\n <div class=\"column-header\">\n <!-- Render the group value through the same read-only cell renderer a\n data cell uses, so the grouped field's datatype formats itself\n (status/tag pills, dates, numbers) instead of printing raw text. -->\n @if (groupByColumn(); as gcol) {\n <span class=\"column-header-title column-header-title-cell\">\n <data-cell\n [eruGridStore]=\"gridStore\"\n [column]=\"gcol\"\n [columnDatatype]=\"gcol.datatype\"\n [columnName]=\"gcol.name\"\n [value]=\"group.title\"\n [id]=\"'board-group-' + group.id\"\n [fieldSize]=\"0\"\n [isEditable]=\"false\"\n [mode]=\"'board-group-header'\">\n </data-cell>\n </span>\n } @else {\n <span class=\"column-header-title\">{{ group.title }}</span>\n }\n <span class=\"column-header-count\">{{ group.currentLoadedRows || 0 }} of {{ group.totalRowCount || 0 }}</span>\n </div>\n }\n <cdk-virtual-scroll-viewport [attr.data-group-id]=\"group.id\" [itemSize]=\"boardCardSlot\" class=\"board-column-body\"\n [style.height.px]=\"boardColumnBodyHeight(group)\"\n (scrolledIndexChange)=\"onBoardScrolledIndexChange($event, group)\">\n <div\n *cdkVirtualFor=\"let row of getRowsForGroupSignal(group.id === null || group.id === undefined ? '__NULL_GROUP__' : group.id)(); templateCacheSize: 0\"\n class=\"board-card-container\"\n [class.selected]=\"isBoardCardActive(row)\"\n [class.show-row-lines]=\"showRowLines()\"\n [style.height.px]=\"boardCardOuterHeight\"\n [style.padding.px]=\"boardCardPadding\"\n [style.marginBottom.px]=\"boardCardGap\"\n [style.cursor]=\"cursorOnHover() || null\"\n (click)=\"emitRowSelect(row, 'board', group)\">\n <!-- Custom template when consumer provides boardCardTemplate; default card otherwise -->\n <ng-container\n *ngTemplateOutlet=\"boardCardTemplate ?? defaultBoardCard;\n context: { $implicit: row, columns: visibleBoardFields(), group: group }\">\n </ng-container>\n </div>\n </cdk-virtual-scroll-viewport>\n @if (group.isLoading) {\n <div class=\"board-ghost-card\">\n <div class=\"board-ghost-line\"></div>\n <div class=\"board-ghost-line board-ghost-line--short\"></div>\n </div>\n }\n </div>\n }\n </div>\n </div>\n } @else {\n\n <!-- Table Mode Template -->\n @if(showExcelDownload() && !showGroupBar()) {\n <div class=\"excel-download-bar\">\n <ng-container *ngTemplateOutlet=\"excelDownloadIcon\"></ng-container>\n </div>\n }\n <!-- Scrollable groups container \u2014 plain iteration avoids CDK fixed-height estimation errors -->\n <div #groupsScrollContainer class=\"groups-scroll-container\" (scroll)=\"onGroupsViewportScroll($event)\">\n\n @for (group of groups(); track trackByGroupFn($index, group); let i = $index) {\n <div class=\"group-container\"\n [attr.data-group-id]=\"group.id === null || group.id === undefined ? '__NULL_GROUP__' : group.id\">\n <!-- Combined sticky header with group info and table -->\n <div style=\"\n background:var(--grid-surface);\n position: sticky;\n top: 0;\n z-index: 115;\n \">\n @if(showGroupBar()) {\n <div class=\"group-header-row\">\n <div class=\"custom-collapse-header\" (click)=\"toggleGroupCollapse(group.id)\">\n <span class=\"collapse-arrow\" [ngClass]=\"{\n 'rotate-arrow': group.isExpanded,\n }\">\u25BC</span>\n <span class=\"f-12\">\n {{ group?.title || \"\" }}\n {{ group?.currentLoadedRows || 0 }} -\n {{ group?.totalRowCount || 0 }} rows...</span>\n @if(groupByField() && isSortable()) {\n <span class=\"group-sort-indicator\">\n <span class=\"sort-triangles\">\n <span class=\"sort-tri sort-tri-up\" [class.sort-tri-active]=\"getSortDirection(groupByField()!) === 'asc'\"\n (click)=\"onGroupSortToggle($event, 'asc')\"></span>\n <span class=\"sort-tri sort-tri-down\"\n [class.sort-tri-active]=\"getSortDirection(groupByField()!) === 'desc'\"\n (click)=\"onGroupSortToggle($event, 'desc')\"></span>\n </span>\n </span>\n }\n </div>\n @if(i === 0 && showExcelDownload()) {\n <ng-container *ngTemplateOutlet=\"excelDownloadIcon\"></ng-container>\n }\n </div>\n }\n\n @if(freezeHeader() && (group.isExpanded || !showGroupBar())) {\n <div #headerScroller class=\"header-shell\" [attr.data-group-id]=\"'header-shell-' + group.id\"\n [style]=\"'--table-total-width: ' + getInitialTotalWidth() + 'px'\">\n <table class=\"eru-grid-table\" [class.freeze-header]=\"freezeHeader()\"\n [class.show-column-lines]=\"showColumnLines()\" [class.show-row-lines]=\"showRowLines()\">\n <ng-container *ngTemplateOutlet=\"tableColGroup\"></ng-container>\n <ng-container *ngTemplateOutlet=\"tableHeader\"></ng-container>\n <!-- Grand Total row after sticky header (position: before) - only for first group -->\n @if(enableGrandTotal() && grandTotalPosition() === 'before' && hasGrandTotalData() && i === 0) {\n <tbody>\n <ng-container *ngTemplateOutlet=\"tableGrandTotal\"></ng-container>\n </tbody>\n }\n <!-- Subtotal row after sticky header (position: before) -->\n @if(enableRowSubtotals() && subtotalPosition() === 'before' && hasSubtotalData(group)) {\n <tbody>\n <ng-container *ngTemplateOutlet=\"tableSubtotal; context: { group: group }\"></ng-container>\n </tbody>\n }\n </table>\n </div>\n }\n </div>\n @if(group.isExpanded || !showGroupBar()) {\n <ng-container>\n <cdk-virtual-scroll-viewport [attr.data-group-id]=\"group.id\" [itemSize]=\"dataRowHeight()\" class=\"viewport table-viewport\"\n (scrolledIndexChange)=\"onScroll($event, group)\" (scroll)=\"onTableBodyScroll($event)\"\n [style]=\"'--table-height: ' + getGroupContentHeight(group.id) + 'px; --table-min-height: ' + getGroupContentHeight(group.id) + 'px; --table-total-width: ' + getInitialTotalWidth() + 'px'\">\n <div class=\"table-wrapper\">\n <table class=\"eru-grid-table\" [class.show-column-lines]=\"showColumnLines()\"\n [class.show-row-lines]=\"showRowLines()\">\n <ng-container *ngTemplateOutlet=\"tableColGroup\"></ng-container>\n @if(!freezeHeader()) {\n <ng-container *ngTemplateOutlet=\"tableHeader\"></ng-container>\n }\n <!-- Grand Total row after normal header (position: before) - only for first group -->\n @if(!freezeHeader() && enableGrandTotal() && grandTotalPosition() === 'before' &&\n hasGrandTotalData() && i === 0) {\n <tbody>\n <ng-container *ngTemplateOutlet=\"tableGrandTotal\"></ng-container>\n </tbody>\n }\n <!-- Subtotal row after normal header (position: before) -->\n @if(!freezeHeader() && enableRowSubtotals() && subtotalPosition() === 'before' &&\n hasSubtotalData(group)) {\n <tbody>\n <ng-container *ngTemplateOutlet=\"tableSubtotal; context: { group: group }\"></ng-container>\n </tbody>\n }\n <tbody>\n @if (columns(); as columnsList) {\n <!-- <tr *ngIf=\"groupItem.type === 'table-header' && groups().length > 1\" style=\"background:#fafafa\">\n @if(gridStore.configuration().config.allowSelection) {\n <th class=\"checkbox-column\" style=\"text-align: center;\">\n <input\n type=\"checkbox\"\n [checked]=\"isGroupSelected(groupItem.group?.id || '')\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleGroupSelection($event, groupItem.group?.id || '')\"\n >\n </th>\n }\n <th *ngFor=\"let column of columns(); trackBy: trackByColumnFn;let i =index\"\n style=\"text-align: center;\"\n [style.width.px]=\"column.field_size\"\n [style.minWidth.px]=\"column.field_size\"\n [resizeColumn]=\"true\"\n [columnConfig]=\"column\"\n [columnDraggable]=\"i\"\n class=\"column-header\">\n <div class=\"column-drag-handle\"></div>\n {{column.label}} {{column.symbol}}\n </th>\n </tr> -->\n <!-- @if(getRowsForGroup(group.id).length > 0 && group.isExpanded) { -->\n <!-- *cdkVirtualFor=\"let row of getRowsForGroupSignal(group.id)(); \n trackBy: trackByRowFn; \n let i = index\" -->\n <!-- @for(row of getRowsForGroupSignal(group.id)(); track trackByRowFn($index, row); let i = $index) { -->\n <ng-container\n *cdkVirtualFor=\"let row of getRowsForGroupSignal(group.id === null || group.id === undefined ? '__NULL_GROUP__' : group.id)(); trackBy: trackByRowFn; let i = index\">\n <tr class=\"row-item\" [attr.data-row-id]=\"i\" [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\" [style.cursor]=\"cursorOnHover() || null\" (click)=\"emitRowSelect(row, 'table', group)\">\n @if(gridStore.configuration().config.allowSelection) {\n <td class=\"checkbox-column\" style=\"text-align: center;\">\n <input type=\"checkbox\" [checked]=\"isRowSelected(row?.entity_id)\"\n (change)=\"toggleRowSelection($event, row)\">\n </td>\n }\n @if(shouldShowActionColumn('before')) {\n <td class=\"action-column\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\">\n <ng-container\n *ngTemplateOutlet=\"actionCell; context: { $implicit: row, mode: 'table', group: group }\"></ng-container>\n </td>\n }\n @if(hasHiddenColumns()) {\n <td class=\"row-expand-toggle\" (click)=\"toggleRowExpand(row, i, $event)\">\n <mat-icon class=\"row-expand-icon\" [class.expanded]=\"isRowExpanded(row, i)\">chevron_right</mat-icon>\n </td>\n }\n @for (column of visibleColumns(); track trackByColumnFn($index, column)) {\n <td #cell [style.width.px]=\"column.field_size\" [style.minWidth.px]=\"column.field_size\"\n class=\"data-cell\" [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\"\n [matTooltipClass]=\"'error-message'\" [matTooltip]=\"datacell.error()?'Error: ' + datacell.error():''\"\n matTooltipPosition=\"below\">\n <div class=\"cell-content\">\n <data-cell #datacell [personCardTemplate]=\"personCardTemplate\" [cellTemplate]=\"cellTemplate\" [td]=cell [fieldSize]=\"column.field_size\" [columnDatatype]=\"column.datatype\"\n [columnName]=\"column.name\" [value]=\"(row?.['entity_data']?.[column.name] ?? row?.[column.name]) || ''\" [column]=\"column\"\n [mode]=\"mode()\" [isEditable]=\"isEditable() && column.editable !== false && column.editable !== 'false'\" [drillable]=\"column.enableDrilldown || false\"\n [id]=\"i + '_' + column.name\" [eruGridStore]=\"gridStore\" [row]=\"row\"></data-cell>\n </div>\n </td>\n }\n @if(shouldShowActionColumn('after')) {\n <td class=\"action-column\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\">\n <ng-container\n *ngTemplateOutlet=\"actionCell; context: { $implicit: row, mode: 'table', group: group }\"></ng-container>\n </td>\n }\n </tr>\n @if(hasHiddenColumns() && isRowExpanded(row, i)) {\n <tr class=\"row-detail\">\n <td class=\"row-detail-cell\" [attr.colspan]=\"rowDetailColspan()\">\n <div class=\"row-detail-grid\">\n @for (hiddenCol of hiddenColumns(); track trackByColumnFn($index, hiddenCol)) {\n <div class=\"row-detail-field\">\n <span class=\"row-detail-label\">{{hiddenCol.label}}</span>\n <div class=\"row-detail-value\">\n <data-cell [cellTemplate]=\"cellTemplate\" [fieldSize]=\"hiddenCol.field_size\" [columnDatatype]=\"hiddenCol.datatype\"\n [columnName]=\"hiddenCol.name\" [value]=\"(row?.['entity_data']?.[hiddenCol.name] ?? row?.[hiddenCol.name]) || ''\"\n [column]=\"hiddenCol\" [mode]=\"mode()\" [isEditable]=\"isEditable()\"\n [drillable]=\"hiddenCol.enableDrilldown || false\"\n [id]=\"'detail_' + i + '_' + hiddenCol.name\" [eruGridStore]=\"gridStore\" [row]=\"row\"></data-cell>\n </div>\n </div>\n }\n </div>\n </td>\n </tr>\n }\n </ng-container>\n <!-- } -->\n <!-- } -->\n @if(group.isLoading && (group.isExpanded || !showGroupBar())) {\n @for(i of [].constructor(ghostRows()); let j = $index; track j) {\n <tr class=\"ghost-loading-row\" [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\">\n @if(gridStore.configuration().config.allowSelection) {\n <td class=\"checkbox-column ghost-cell-container\">\n <div class=\"ghost-cell\"></div>\n </td>\n\n }\n @if(shouldShowActionColumn('before')) {\n <td class=\"action-column ghost-cell-container\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\">\n <div class=\"ghost-cell\"></div>\n </td>\n }\n @if(hasHiddenColumns()) {\n <td class=\"row-expand-toggle ghost-cell-container\">\n <div class=\"ghost-cell\"></div>\n </td>\n }\n @for (column of visibleColumns(); track trackByColumnFn($index, column)) {\n <td [style.width.px]=\"column.field_size\" [style.minWidth.px]=\"column.field_size\"\n class=\"ghost-cell-container\">\n <div class=\"ghost-cell\"></div>\n </td>\n }\n @if(shouldShowActionColumn('after')) {\n <td class=\"action-column ghost-cell-container\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\">\n <div class=\"ghost-cell\"></div>\n </td>\n }\n </tr>\n }\n }\n <!-- <tr\n *ngIf=\"getRowsForGroup(group.id).length === 0 && !group.isExpanded\"\n class=\"group-separator\"\n >\n <td [attr.colspan]=\"groupSeperatorColSpan()\" class=\"separator-cell\"></td>\n </tr> -->\n <!-- Subtotal row at end of group (position: after) -->\n @if(enableRowSubtotals() && subtotalPosition() === 'after' && hasSubtotalData(group)) {\n <ng-container *ngTemplateOutlet=\"tableSubtotal; context: { group: group }\"></ng-container>\n }\n <!-- Grand Total row at end of group (position: after) - only for last group -->\n @if(enableGrandTotal() && grandTotalPosition() === 'after' && hasGrandTotalData() && i ===\n groups().length - 1) {\n <ng-container *ngTemplateOutlet=\"tableGrandTotal\"></ng-container>\n }\n }\n </tbody>\n </table>\n </div>\n </cdk-virtual-scroll-viewport>\n </ng-container>\n }\n </div>\n }\n </div>\n }\n</div>\n\n<!-- Pivot Table Header Template -->\n<ng-template #pivotTableHead>\n <thead [class.eru-wrap-headers]=\"wrapHeaders()\">\n @if (hasNestedHeaders()) {\n <ng-container>\n @for (headerRow of getHeaderRows(); track headerRow; let rowIndex = $index) {\n <tr class=\"pivot-header pivot-header-container\" [class.pivot-header-level]=\"'level-' + rowIndex\">\n @for (header of headerRow; track trackByHeaderFn($index, header); let colIndex = $index) {\n <th [attr.colspan]=\"header.colspan\" [attr.rowspan]=\"header.rowspan\"\n [resizeColumn]=\"isResizablePivotHeader(header)\"\n [columnConfig]=\"getFieldForPivotHeader(header) || $any(header)\"\n class=\"column-header pivot-column-header nested-header\"\n [class.row-dimension-header]=\"isRowDimensionHeader(header)\"\n [class.column-dimension-header]=\"!isRowDimensionHeader(header)\" [class.expanded]=\"header.isExpanded\"\n [class.collapsed]=\"!header.isExpanded\" [class.sticky-column]=\"isStickyColumn(header.name, colIndex)\"\n [class.design-clickable]=\"isDesignMode()\"\n [class.design-selected]=\"isDesignMode() && gridStore.selectedDesignColumn() === designTargetFor($any(header))\"\n [style.position]=\"isStickyColumn(header.name, colIndex) ? 'sticky' : 'static'\"\n [style.left.px]=\"getStickyColumnLeft(header.name, colIndex)\"\n [style.z-index]=\"isStickyColumn(header.name, colIndex) ? 100 : 1\"\n [style.min-height.px]=\"headerRowHeight()\" style=\"height: auto; padding: 8px 6px;\">\n <div class=\"header-content\">\n\n <data-cell [fieldSize]=\"header.field_size\" [columnDatatype]=\"header.dataType\" [columnName]=\"header.name\"\n [value]=\"header.label\" [column]=\"header\" [frozenGrandTotalCell]=\"true\"\n [drillable]=\"header.enableDrilldown || false\" [mode]=\"mode()\" [isEditable]=\"isEditable()\"\n [id]=\"'pivot_' + $index + '_' + header.name\" [eruGridStore]=\"gridStore\" [row]=\"header\">\n </data-cell>\n @if(isDesignMode()) {\n <mat-icon class=\"design-edit-icon\"\n [title]=\"isAggregationHeader($any(header)) ? 'Edit aggregation' : 'Edit column'\"\n (click)=\"onHeaderDesignClick($event, $any(header))\">tune</mat-icon>\n }\n <!-- <span class=\"header-label header-wrap-text\">{{header.label}}</span> -->\n <!-- <button *ngIf=\"!isRowDimensionHeader(header)\"\n class=\"collapse-toggle-btn\"\n [title]=\"header.isExpanded ? 'Collapse group' : 'Expand group'\"\n (click)=\"toggleColumnGroup(header.groupKey)\"\n type=\"button\">\n <span class=\"collapse-icon\">+</span>\n </button> -->\n </div>\n </th>\n }\n </tr>\n }\n </ng-container>\n } @else {\n <!-- Simple header fallback -->\n <ng-container>\n <tr class=\"pivot-header\" [class.freeze-header-enabled]=\"freezeHeader()\">\n @for (column of getLeafColumns(); track trackByColumnFn($index, column); let colIndex = $index) {\n <th [style.width.px]=\"column.field_size\" [style.minWidth.px]=\"column.field_size\"\n [resizeColumn]=\"gridStore.isFeatureEnabled('columnResizable')\" [columnConfig]=\"column\"\n class=\"column-header pivot-column-header\" [class.sticky-column]=\"isStickyColumn(column.name, colIndex)\"\n [class.design-clickable]=\"isDesignMode()\"\n [class.design-selected]=\"isDesignMode() && gridStore.selectedDesignColumn() === designTargetFor(column)\"\n [style.position]=\"isStickyColumn(column.name, colIndex) ? 'sticky' : 'static'\"\n [style.left.px]=\"getStickyColumnLeft(column.name, colIndex)\"\n [style.z-index]=\"isStickyColumn(column.name, colIndex) ? 100 : 1\"\n [style.min-height.px]=\"headerRowHeight()\" style=\"height: auto;padding: 8px 6px\">\n <!-- Label and control laid out as a row: the label truncates, the\n control keeps its place. Left as a bare text node the long\n aggregation labels pushed the icon past the cell edge, where\n `overflow: hidden` clipped it out of sight entirely. -->\n <div class=\"pivot-header-content\">\n <!-- Deliberately not `.column-label`: that class carries the\n wrap-headers rule, which broke these labels onto one word per\n line. This header truncates, as it did before. -->\n <span class=\"pivot-header-label\">{{column.label}}</span>\n @if(isDesignMode()) {\n <mat-icon class=\"design-edit-icon\"\n [title]=\"isAggregationHeader(column) ? 'Edit aggregation' : 'Edit column'\"\n (click)=\"onHeaderDesignClick($event, column)\">tune</mat-icon>\n }\n </div>\n </th>\n }\n </tr>\n </ng-container>\n }\n\n </thead>\n</ng-template>\n\n<!-- Column Group Template for consistent column widths -->\n<ng-template #pivotColGroup>\n <colgroup>\n @for (column of getLeafColumns(); track trackByColumnFn($index, column)) {\n <col\n [style]=\"'width: ' + column.field_size + 'px !important; min-width: ' + column.field_size + 'px !important; max-width: ' + column.field_size + 'px !important; --col-width: ' + column.field_size + 'px'\">\n }\n </colgroup>\n</ng-template>\n\n<ng-template #pivotGrandTotal>\n <tbody class=\"pivot-tbody\">\n @for (pivotRow of gridStore.pivotGrandTotalData(); track trackByPivotRowFn($index, pivotRow); let i = $index) {\n <tr class=\"pivot-row grand-total-row\"\n [class.grand-total-bold]=\"pivotRow._isGrandTotal && grandTotalStyle() === 'bold'\"\n [class.grand-total-italic]=\"pivotRow._isGrandTotal && grandTotalStyle() === 'italic'\"\n [class.grand-total-highlighted]=\"pivotRow._isGrandTotal && grandTotalStyle() === 'highlighted'\"\n [style.height.px]=\"50\" [attr.data-pivot-row]=\"i\">\n <!-- <td colspan=\"20\">{{pivotRow | json}}</td> -->\n @for (column of getLeafColumns(); track trackByColumnFn($index, column); let colIndex = $index) {\n <td [attr.rowspan]=\"getEffectiveRowspan(i, column.name)\" [style.width.px]=\"column.field_size\"\n [style.minWidth.px]=\"column.field_size\" class=\"pivot-cell\"\n [class.row-dimension-cell]=\"isRowDimensionColumn(column.name)\"\n [class.column-dimension-cell]=\"!isRowDimensionColumn(column.name)\"\n [class.aggregated-value]=\"!isRowDimensionColumn(column.name) && column.datatype === 'number'\"\n [class.rowspan-cell]=\"getEffectiveRowspan(i, column.name) || 1 > 1\"\n [class.sticky-column]=\"isStickyColumn(column.name, colIndex)\"\n [style.position]=\"isStickyColumn(column.name, colIndex) ? 'sticky' : 'static'\"\n [style.left.px]=\"getStickyColumnLeft(column.name, colIndex)\"\n [style.z-index]=\"isStickyColumn(column.name, colIndex) ? 99 : 1\" [style.height.px]=\"50\" [attr.xx]=\"i\">\n <div class=\"cell-content pivot-cell-content\">\n <data-cell [fieldSize]=\"column.field_size\" [columnDatatype]=\"column.datatype\" [columnName]=\"column.name\"\n [value]=\"getEffectiveCellValue(i,column.name, pivotRow)\" [column]=\"column\" [frozenGrandTotalCell]=\"true\"\n [drillable]=\"column.enableDrilldown || false\" [mode]=\"mode()\" [isEditable]=\"isEditable()\"\n [id]=\"'pivot_' + i + '_' + column.name\" [eruGridStore]=\"gridStore\" [row]=\"pivotRow\">\n </data-cell>\n </div>\n </td>\n }\n </tr>\n }\n </tbody>\n</ng-template>\n\n<!-- \u2500\u2500\u2500 Action column cell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n One template for both column positions. With no `config.actions` set it\n falls back to the single more_horiz icon the column has always shown, so\n grids that only listen to the store's actionClick signal keep working.\n Context: { $implicit: Row, mode: 'table' | 'board', group?: RowGroup }\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<ng-template #actionCell let-row let-mode=\"mode\" let-group=\"group\">\n @if(!hasConfiguredActions()) {\n <mat-icon (click)=\"onActionClick($event, row, undefined, mode || 'table', group)\">more_horiz</mat-icon>\n } @else if(actionDisplayType() === 'kebab') {\n @if(visibleActionsFor(row).length > 0) {\n <mat-icon class=\"action-kebab\" [matMenuTriggerFor]=\"rowActionMenu\"\n [matMenuTriggerData]=\"{ row: row, mode: mode || 'table', group: group }\"\n (click)=\"$event.stopPropagation()\">more_vert</mat-icon>\n }\n } @else {\n <div class=\"action-icons\">\n @for(action of visibleActionsFor(row); track action.action_name) {\n <mat-icon class=\"action-icon\" [matTooltip]=\"action.action_name\" matTooltipPosition=\"above\"\n (click)=\"onActionClick($event, row, action, mode || 'table', group)\">{{action.action_icon || 'play_arrow'}}</mat-icon>\n }\n </div>\n }\n</ng-template>\n\n<!-- Kebab menu shared by every row; the row is passed through matMenuTriggerData. -->\n<mat-menu #rowActionMenu=\"matMenu\" class=\"eru-grid-action-menu\">\n <ng-template matMenuContent let-row=\"row\" let-mode=\"mode\" let-group=\"group\">\n @for(action of visibleActionsFor(row); track action.action_name) {\n <button mat-menu-item (click)=\"onActionClick($event, row, action, mode || 'table', group)\">\n <mat-icon>{{action.action_icon || 'play_arrow'}}</mat-icon>\n <span>{{action.action_name}}</span>\n </button>\n }\n </ng-template>\n</mat-menu>\n\n<!-- Column Group Template for consistent column widths -->\n<ng-template #tableColGroup>\n <colgroup>\n @if(gridStore.configuration().config.allowSelection) {\n <col style=\"width: 40px; min-width: 40px; max-width: 40px;\">\n }\n @if(shouldShowActionColumn('before')) {\n <col\n [style]=\"'width: ' + actionColumnWidth() + 'px !important; min-width: ' + actionColumnWidth() + 'px !important; max-width: ' + actionColumnWidth() + 'px !important;'\">\n }\n @if(hasHiddenColumns()) {\n <col style=\"width: 40px !important; min-width: 40px !important; max-width: 40px !important;\">\n }\n @for (column of visibleColumns(); track trackByColumnFn($index, column)) {\n <col\n [style]=\"'width: ' + column.field_size + 'px !important; min-width: ' + column.field_size + 'px !important; max-width: ' + column.field_size + 'px !important; --col-width: ' + column.field_size + 'px'\">\n }\n @if(shouldShowActionColumn('after')) {\n <col\n [style]=\"'width: ' + actionColumnWidth() + 'px !important; min-width: ' + actionColumnWidth() + 'px !important; max-width: ' + actionColumnWidth() + 'px !important;'\">\n }\n </colgroup>\n</ng-template>\n\n\n<ng-template #tableHeader>\n\n <thead [class.eru-wrap-headers]=\"wrapHeaders()\">\n <!-- headerRowHeight rides on the row, not the cells: `thead.eru-wrap-headers\n th { height: auto }` outranks any class-level height we could put on a\n th, which is why a configured header height was ignored while data rows\n (inline height on tr.row-item) honoured theirs. On a table row `height`\n is a minimum, so a wrapped two-line header still grows past it. -->\n <tr [style.height.px]=\"headerRowHeight()\" [style.minHeight.px]=\"headerRowHeight()\">\n @if(gridStore.configuration().config.allowSelection) {\n <th class=\"checkbox-column column-header table-column-header\">\n <input type=\"checkbox\" [checked]=\"isAllGroupsSelected()\" (change)=\"toggleAllGroups($event)\">\n </th>\n }\n @if(shouldShowActionColumn('before')) {\n <th class=\"action-column column-header table-column-header\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\">Action</th>\n }\n @if(hasHiddenColumns()) {\n <th class=\"row-expand-toggle column-header table-column-header\"></th>\n }\n @for (column of visibleColumns(); track trackByColumnFn(i, column); let i = $index) {\n <th [style.width.px]=\"column.field_size\" [resizeColumn]=\"gridStore.isFeatureEnabled('columnResizable')\"\n [columnConfig]=\"column\" [index]=\"i\"\n [columnDraggable]=\"gridStore.isFeatureEnabled('columnReorderable') ? i : null\"\n [style.minWidth.px]=\"column.field_size\" class=\"column-header table-column-header\"\n [class.sortable-header]=\"isSortable()\"\n [class.design-clickable]=\"isDesignMode()\"\n [class.design-selected]=\"isDesignMode() && gridStore.selectedDesignColumn() === column.name\"\n [class.sort-asc]=\"isSortable() && getSortDirection(column.name) === 'asc'\"\n [class.sort-desc]=\"isSortable() && getSortDirection(column.name) === 'desc'\">\n @if(gridStore.isFeatureEnabled('columnReorderable')) {\n <div class=\"column-drag-handle\"></div>\n }\n <span class=\"column-label\" [title]=\"column.tool_tip || column.description || ''\">{{column.label}}</span>\n @if(isDesignMode()) {\n <mat-icon class=\"design-edit-icon\" title=\"Edit column\" (click)=\"onHeaderDesignClick($event, column)\">tune</mat-icon>\n }\n @if(isSortable()) {\n <span class=\"sort-indicator\">\n <span class=\"sort-triangles\">\n <span class=\"sort-tri sort-tri-up\" [class.sort-tri-active]=\"getSortDirection(column.name) === 'asc'\"\n (click)=\"onSortColumn($event, column, 'asc')\"></span>\n <span class=\"sort-tri sort-tri-down\" [class.sort-tri-active]=\"getSortDirection(column.name) === 'desc'\"\n (click)=\"onSortColumn($event, column, 'desc')\"></span>\n </span>\n @if(getSortPriority(column.name) !== null && gridStore.sortColumns().length > 1) {\n <span class=\"sort-priority\">{{getSortPriority(column.name)}}</span>\n }\n </span>\n }\n </th>\n }\n @if(shouldShowActionColumn('after')) {\n <th class=\"action-column column-header table-column-header\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\">Action</th>\n }\n </tr>\n </thead>\n</ng-template>\n\n<!-- Table Subtotal Row Template -->\n<ng-template #tableSubtotal let-group=\"group\">\n <tr class=\"subtotal-row\" [class.subtotal-bold]=\"subTotalStyle() === 'bold'\"\n [class.subtotal-italic]=\"subTotalStyle() === 'italic'\"\n [class.subtotal-highlighted]=\"subTotalStyle() === 'highlighted'\" [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\">\n @if(gridStore.configuration().config.allowSelection) {\n <td class=\"checkbox-column\"></td>\n }\n @if(shouldShowActionColumn('before')) {\n <td class=\"action-column\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\"></td>\n }\n @if(hasHiddenColumns()) {\n <td class=\"row-expand-toggle\"></td>\n }\n @for(column of visibleColumns(); track trackByColumnFn($index, column); let colIndex = $index) {\n <td [style.width.px]=\"column.field_size\" [style.minWidth.px]=\"column.field_size\" class=\"subtotal-cell\"\n [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\">\n <div class=\"cell-content\">\n @if(colIndex === 0 && getSubtotalValue(group, column.name) === null) {\n <span class=\"subtotal-label\">{{subtotalLabel()}}</span>\n } @else {\n @if(getSubtotalValue(group, column.name) !== null) {\n <data-cell [fieldSize]=\"column.field_size\" [columnDatatype]=\"column.datatype\" [columnName]=\"column.name\"\n [value]=\"getSubtotalValue(group, column.name)\" [column]=\"column\" [mode]=\"mode()\" [isEditable]=\"false\"\n [id]=\"'subtotal_' + group.id + '_' + column.name\" [eruGridStore]=\"gridStore\" [row]=\"group.subtotal\">\n </data-cell>\n }\n }\n </div>\n </td>\n }\n @if(shouldShowActionColumn('after')) {\n <td class=\"action-column\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\"></td>\n }\n </tr>\n</ng-template>\n\n<!-- Table Grand Total Row Template -->\n<ng-template #tableGrandTotal>\n <tr class=\"grand-total-row\" [class.grand-total-bold]=\"grandTotalStyle() === 'bold'\"\n [class.grand-total-italic]=\"grandTotalStyle() === 'italic'\"\n [class.grand-total-highlighted]=\"grandTotalStyle() === 'highlighted'\" [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\">\n @if(gridStore.configuration().config.allowSelection) {\n <td class=\"checkbox-column\"></td>\n }\n @if(shouldShowActionColumn('before')) {\n <td class=\"action-column\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\"></td>\n }\n @if(hasHiddenColumns()) {\n <td class=\"row-expand-toggle\"></td>\n }\n @for(column of visibleColumns(); track trackByColumnFn($index, column); let colIndex = $index) {\n <td [style.width.px]=\"column.field_size\" [style.minWidth.px]=\"column.field_size\" class=\"grand-total-cell\"\n [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\">\n <div class=\"cell-content\">\n @if(colIndex === 0 && getGrandTotalValue(column.name) === null) {\n <span class=\"grand-total-label\">Grand Total</span>\n } @else {\n @if(getGrandTotalValue(column.name) !== null) {\n <data-cell [fieldSize]=\"column.field_size\" [columnDatatype]=\"column.datatype\" [columnName]=\"column.name\"\n [value]=\"getGrandTotalValue(column.name)\" [column]=\"column\" [mode]=\"mode()\" [isEditable]=\"false\"\n [id]=\"'grandtotal_' + column.name\" [eruGridStore]=\"gridStore\" [row]=\"gridStore.rowGrandTotal()\">\n </data-cell>\n }\n }\n </div>\n </td>\n }\n @if(shouldShowActionColumn('after')) {\n <td class=\"action-column\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\"></td>\n }\n </tr>\n</ng-template>\n\n<!-- \u2500\u2500\u2500 Default board card template \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Used when no boardCardTemplate is passed to <eru-grid>.\n Context: { $implicit: Row, columns: Field[], group: RowGroup }\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<ng-template #defaultBoardCard let-row let-columns=\"columns\" let-group=\"group\">\n <mat-card class=\"board-card\">\n <mat-card-content>\n @for (column of columns; track column.name) {\n @if ((row?.entity_data?.[column.name] ?? row?.[column.name]) !== undefined) {\n <div class=\"board-card-field\">\n <span class=\"board-field-label\">{{ column.label }}</span>\n <data-cell\n [fieldSize]=\"column.field_size\"\n [columnDatatype]=\"column.datatype\"\n [columnName]=\"column.name\"\n [column]=\"column\"\n [value]=\"row?.entity_data?.[column.name] ?? row?.[column.name]\"\n [id]=\"row?.entity_id + '_' + column.name\"\n [eruGridStore]=\"gridStore\"\n [mode]=\"'board'\"\n [row]=\"row\">\n </data-cell>\n </div>\n }\n }\n </mat-card-content>\n <mat-card-actions align=\"end\">\n <button mat-icon-button (click)=\"onActionClick($event, row)\">\n <mat-icon>more_horiz</mat-icon>\n </button>\n </mat-card-actions>\n </mat-card>\n</ng-template>", styles: ["@charset \"UTF-8\";:root{--grid-primary: var(--mat-sys-primary, #6750a4);--grid-on-primary: var(--mat-sys-on-primary, #ffffff);--grid-primary-container: var(--mat-sys-primary-container, #eaddff);--grid-on-primary-container: var(--mat-sys-on-primary-container, #21005d);--grid-secondary: var(--mat-sys-secondary, #625b71);--grid-on-secondary: var(--mat-sys-on-secondary, #ffffff);--grid-secondary-container: var(--mat-sys-secondary-container, #e8def8);--grid-on-secondary-container: var(--mat-sys-on-secondary-container, #1d192b);--grid-tertiary: var(--mat-sys-tertiary, #7d5260);--grid-on-tertiary: var(--mat-sys-on-tertiary, #ffffff);--grid-tertiary-container: var(--mat-sys-tertiary-container, #ffd8e4);--grid-on-tertiary-container: var(--mat-sys-on-tertiary-container, #31111d);--grid-surface: var(--mat-sys-surface, #fef7ff);--grid-surface-variant: var(--mat-sys-surface-variant, #e7e0ec);--grid-surface-container: var(--mat-sys-surface-container, #f3edf7);--grid-surface-container-high: var(--mat-sys-surface-container-high, #ede7f0);--grid-on-surface: var(--mat-sys-on-surface, #1d1b20);--grid-on-surface-variant: var(--mat-sys-on-surface-variant, #49454f);--grid-outline: var(--mat-sys-outline, #79757f);--grid-outline-variant: var(--mat-sys-outline-variant, #cac4d0);--grid-error: var(--mat-sys-error, #ba1a1a);--grid-error-container: var(--mat-sys-error-container, #ffdad6);--grid-base-surface: var(--surface, #ffffff);--grid-base-on-surface: var(--on-surface, #000000);--grid-base-border: var(--border, #e5e7eb);--grid-primary-light: var(--grid-primary-container)}:host,eru-grid{display:block!important;width:100%;height:100%;flex:1 1 0%;max-height:var(--grid-height, none);min-height:var(--grid-min-height, 120px);font-family:var(--grid-font-family);--grid-font-family: \"Poppins\", \"Roboto\", -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;--grid-font-size-body: 12px;--grid-font-size-caption: 12px !important;--grid-line-height-body: 1;--grid-aggregation-text-align: right;--grid-number-text-align: right;--grid-spacing-xxs: 2px;--grid-spacing-xs: 4px;--grid-spacing-sm: 8px;--grid-spacing-md: 16px;--grid-spacing-lg: 24px;--grid-border-radius: 4px;--grid-elevation-1: 0px 1px 2px 0px rgba(0, 0, 0, .3), 0px 1px 3px 1px rgba(0, 0, 0, .15);--grid-elevation-2: 0px 1px 2px 0px rgba(0, 0, 0, .3), 0px 2px 6px 2px rgba(0, 0, 0, .15);--grid-row-hover: var(--grid-surface-variant);--grid-row-selected: var(--grid-surface-container-high);--grid-zebra-odd: transparent;--grid-zebra-even: transparent;--grid-focus-ring: var(--grid-primary);--grid-header-font-weight: 500;--grid-header-text-transform: none;--grid-header-letter-spacing: normal;--grid-header-font-size: var(--grid-font-size-caption);--grid-header-padding-x: 8px;--grid-header-padding-y: 12px;--grid-font-feature-numeric: normal;--grid-cell-padding-x: var(--grid-spacing-xs);--grid-cell-inset-x: 8px;--grid-cell-padding-y: var(--grid-spacing-xxs);--grid-tint-subtle: rgba(0, 0, 0, .025);--grid-tint-soft: rgba(0, 0, 0, .045);--grid-tint-strong: rgba(0, 0, 0, .08);--grid-radius-outer: 0;--grid-shadow-outer: none;--grid-divider-color: var(--grid-outline-variant);--grid-divider-width: 1px;--grid-header-bg: var(--grid-surface-container);--grid-header-color: var(--grid-on-surface);--grid-pill-radius: 999px;--grid-pill-padding-y: 3px;--grid-pill-padding-x: 10px;--grid-pill-font-size: 11px;--grid-pill-font-weight: 500;--grid-priority-dot-size: 8px;--grid-avatar-size: 24px;--grid-avatar-font-size: 10px;--grid-avatar-font-weight: 600;border-radius:var(--grid-radius-outer);box-shadow:var(--grid-shadow-outer)}eru-grid[data-preset=default]{--grid-header-bg: transparent;--grid-header-color: var(--grid-on-surface-variant);--grid-header-font-weight: 500;--grid-header-text-transform: uppercase;--grid-header-letter-spacing: .06em;--grid-header-font-size: 11px;--grid-header-padding-y: 12px;--grid-header-padding-x: 14px;--grid-cell-padding-y: 10px;--grid-cell-padding-x: 14px;--grid-row-hover: var(--grid-tint-subtle);--grid-divider-color: var(--grid-tint-soft);--grid-divider-width: 1px;--grid-font-feature-numeric: \"tnum\"}eru-grid[data-preset=modern]{--grid-header-bg: transparent;--grid-header-color: var(--grid-on-surface-variant);--grid-header-font-weight: 500;--grid-header-text-transform: none;--grid-header-letter-spacing: normal;--grid-header-font-size: 13px;--grid-header-padding-y: 16px;--grid-header-padding-x: 18px;--grid-cell-padding-y: 16px;--grid-cell-padding-x: 18px;--grid-row-hover: var(--grid-tint-soft);--grid-divider-color: var(--grid-tint-subtle);--grid-divider-width: 1px;--grid-radius-outer: 12px;--grid-font-feature-numeric: \"tnum\";--grid-pill-padding-y: 4px;--grid-pill-padding-x: 12px}eru-grid[data-preset=compact]{--grid-header-bg: var(--grid-surface-container);--grid-header-color: var(--grid-on-surface);--grid-header-font-weight: 600;--grid-header-text-transform: none;--grid-header-font-size: 11px;--grid-header-padding-y: 4px;--grid-header-padding-x: 8px;--grid-cell-padding-y: 3px;--grid-cell-padding-x: 8px;--grid-font-size-body: 11px;--grid-row-hover: var(--grid-tint-subtle);--grid-divider-color: var(--grid-tint-subtle);--grid-divider-width: 1px;--grid-font-feature-numeric: \"tnum\";--grid-pill-padding-y: 1px;--grid-pill-padding-x: 6px;--grid-pill-font-size: 10px}eru-grid[data-preset=bold]{--grid-header-bg: var(--grid-surface-container-high);--grid-header-color: var(--grid-on-surface);--grid-header-font-weight: 700;--grid-header-text-transform: none;--grid-header-font-size: 13px;--grid-header-padding-y: 14px;--grid-header-padding-x: 12px;--grid-cell-padding-y: 10px;--grid-cell-padding-x: 12px;--grid-row-hover: var(--grid-tint-soft);--grid-divider-color: var(--grid-tint-strong);--grid-divider-width: 1px;--grid-radius-outer: 2px;--grid-font-feature-numeric: \"tnum\"}eru-grid[data-preset=financial]{--grid-header-bg: var(--grid-surface-container);--grid-header-color: var(--grid-on-surface-variant);--grid-header-font-weight: 500;--grid-header-text-transform: uppercase;--grid-header-letter-spacing: .08em;--grid-header-font-size: 11px;--grid-header-padding-y: 12px;--grid-header-padding-x: 14px;--grid-cell-padding-y: 10px;--grid-cell-padding-x: 14px;--grid-zebra-odd: transparent;--grid-zebra-even: var(--grid-tint-subtle);--grid-row-hover: var(--grid-tint-soft);--grid-divider-color: var(--grid-tint-subtle);--grid-divider-width: 1px;--grid-font-feature-numeric: \"tnum\"}eru-grid[data-preset=elevated]{--grid-header-bg: transparent;--grid-header-color: var(--grid-on-surface-variant);--grid-header-font-weight: 600;--grid-header-text-transform: none;--grid-header-font-size: 12px;--grid-header-padding-y: 16px;--grid-header-padding-x: 18px;--grid-cell-padding-y: 14px;--grid-cell-padding-x: 18px;--grid-row-hover: var(--grid-tint-soft);--grid-divider-color: var(--grid-tint-subtle);--grid-divider-width: 1px;--grid-radius-outer: 16px;--grid-shadow-outer: 0 1px 3px rgba(0, 0, 0, .06), 0 10px 28px rgba(0, 0, 0, .07);--grid-font-feature-numeric: \"tnum\";--grid-pill-padding-y: 4px;--grid-pill-padding-x: 12px;overflow:hidden}.group-container{padding-bottom:8px}.column-header.design-clickable .design-edit-icon{font-size:16px;width:16px;height:16px;margin-left:4px;opacity:.45;vertical-align:middle;cursor:pointer}.column-header.design-clickable:hover .design-edit-icon,.column-header.design-clickable .design-edit-icon:hover{opacity:1}.column-header.design-selected{background-color:var(--grid-primary-container, rgba(63, 81, 181, .12))}.pivot-column-header .pivot-header-content{display:flex;align-items:center;justify-content:center;gap:4px;min-width:0}.pivot-column-header .pivot-header-content .pivot-header-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.pivot-column-header .pivot-header-content .design-edit-icon,.pivot-column-header .header-content .design-edit-icon{flex:0 0 auto}.pivot-column-header .header-content data-cell,.pivot-column-header .header-content data-cell *{color:inherit!important}.incremental-row-container{width:100%;height:100%;min-height:var(--grid-min-height, 120px);max-height:none;overflow:auto;position:relative;background-color:var(--grid-surface);border-radius:var(--grid-border-radius);font-family:var(--grid-font-family)}.viewport{height:100%;min-height:300px;overflow-x:auto;overflow-y:auto;background-color:var(--grid-surface);scrollbar-gutter:stable}.viewport.apply-cdk-width{width:calc(var(--table-total-width) + 10px)!important}.groups-viewport{height:100%;min-height:300px}.groups-scroll-container{max-height:var(--grid-height, 600px);overflow-y:auto;overflow-x:hidden}.table-viewport{background-color:var(--grid-surface);height:var(--table-height, auto);min-height:var(--table-min-height, 100px);overflow-x:auto;overflow-y:auto}.pivot-viewport{min-height:var(--table-min-height, 300px);overflow-x:auto;overflow-y:auto;background-color:var(--grid-surface)}.pivot-viewport .cdk-virtual-scroll-content-wrapper{width:auto;height:auto}.table-wrapper{min-width:100%;overflow-x:visible}.incremental-row-container .eru-grid-table,.eru-grid-table{width:100%!important;border-collapse:separate;border-spacing:0;table-layout:fixed!important;background-color:var(--grid-surface);color:var(--grid-on-surface);font-family:var(--grid-font-family);font-size:var(--grid-font-size-body);line-height:var(--grid-line-height-body)}.eru-grid-table th,.eru-grid-table td{text-align:left;overflow:hidden!important;text-overflow:ellipsis!important;white-space:nowrap!important;color:var(--grid-on-surface);min-width:0;max-width:100%!important;box-sizing:border-box;position:relative}.eru-grid-table th{background-color:var(--grid-header-bg, var(--grid-surface-container))}thead.eru-wrap-headers th{white-space:normal!important;overflow:visible!important;text-overflow:clip!important;height:auto}thead.eru-wrap-headers th .column-label,thead.eru-wrap-headers th .header-label{white-space:normal!important;overflow:visible!important;text-overflow:clip!important;word-break:break-word;overflow-wrap:anywhere}.eru-grid-table tbody td{background-color:transparent}.eru-grid-table thead{background-color:var(--grid-header-bg, var(--grid-surface-container));transform:translateZ(0);will-change:transform;backface-visibility:hidden}.eru-grid-table thead.freeze-header-enabled{position:sticky!important;top:0!important;z-index:100!important}.eru-grid-table thead th{background-color:var(--grid-header-bg, var(--grid-surface-container));color:var(--grid-header-color, var(--grid-on-surface));font-family:var(--grid-font-family);font-weight:var(--grid-header-font-weight);font-size:var(--grid-header-font-size)}.checkbox-column{width:50px;min-width:50px;max-width:50px;text-align:center!important;padding-left:0!important;padding-right:0!important;text-overflow:clip!important;background-color:var(--grid-surface-container)}.checkbox-column input[type=checkbox]{width:16px;height:16px;cursor:pointer;accent-color:var(--grid-primary);border-radius:var(--grid-border-radius)}.checkbox-column input[type=checkbox]:focus{outline:2px solid var(--grid-primary);outline-offset:2px}.action-column{text-align:center!important;padding-left:0!important;padding-right:0!important;text-overflow:clip!important;background-color:var(--grid-surface-container)}.action-column mat-icon{font-size:20px;width:20px;height:20px;line-height:20px;color:var(--grid-on-surface-variant);cursor:pointer}.action-column mat-icon:hover{color:var(--grid-primary)}.action-column .action-icons{display:flex;align-items:center;justify-content:center;gap:6px;overflow-x:auto;scrollbar-width:none}.action-column .action-icons::-webkit-scrollbar{display:none}.action-column .action-icon{flex:0 0 auto}.eru-grid-action-menu .mat-mdc-menu-item mat-icon{margin-right:8px;font-size:18px;width:18px;height:18px;line-height:18px;color:var(--grid-on-surface-variant)}.group-header{background-color:var(--grid-surface-container);color:var(--grid-on-surface);font-size:var(--grid-font-size-caption);font-weight:500;border-bottom:1px solid var(--grid-outline);cursor:pointer;transition:background-color .2s ease}.group-header:hover{background-color:var(--grid-surface-container-high)}.group-header .group-title{font-weight:600;color:var(--grid-primary)}.group-header .group-row-count{color:var(--grid-on-surface-variant);font-size:var(--grid-font-size-caption);margin-left:var(--grid-spacing-sm)}.row-item{background-color:var(--grid-surface);transition:background-color .15s ease}.row-item:nth-child(odd){background-color:var(--grid-zebra-odd, var(--grid-surface))}.row-item:nth-child(2n){background-color:var(--grid-zebra-even, var(--grid-surface))}.row-item:hover{background-color:var(--grid-row-hover)}.required-toggle-row{background-color:var(--grid-surface-container, #f3edf7);border-bottom:1px solid var(--grid-outline-variant, #cac4d0)}.required-toggle-row .required-toggle-cell{padding:4px 8px!important;text-align:center;vertical-align:middle;position:relative}.required-toggle-row .required-toggle-cell .required-label{position:absolute;top:2px;left:4px;font-size:10px;color:var(--grid-on-surface-variant, #49454f);font-weight:400;text-transform:lowercase}.required-toggle-row .required-toggle-cell mat-checkbox{display:flex;justify-content:center;align-items:center}.table-column-header{padding:0 var(--grid-header-padding-x);height:var(--grid-header-row-height, auto)}.column-header{font-weight:var(--grid-header-font-weight);text-transform:var(--grid-header-text-transform);letter-spacing:var(--grid-header-letter-spacing);text-align:center!important;font-size:var(--grid-header-font-size);position:relative;-webkit-user-select:none;user-select:none;--grid-header-affordance-space: 0px;--grid-column-resizer-width: 10px;--grid-header-sort-right: calc(var(--grid-column-resizer-width) + 2px);--grid-header-design-right: calc(var(--grid-column-resizer-width) + 2px);background-color:var(--grid-header-bg, var(--grid-surface-container));color:var(--grid-header-color, var(--grid-on-surface))}.column-header:hover{background-color:var(--grid-header-hover-bg, var(--grid-surface-container-high))}.column-drag-handle{position:absolute;left:0;top:0;bottom:0;width:12px;cursor:grab;opacity:0;transition:opacity .2s ease,background-color .2s ease;z-index:2;display:flex;align-items:center;justify-content:center;border-right:1px solid transparent}.column-drag-handle:after{content:\"\\22ee\\22ee\";font-size:14px;color:var(--grid-on-surface-variant);transform:rotate(90deg)}.column-drag-handle:hover{background-color:var(--grid-surface-container-high);border-right-color:var(--grid-outline)}.column-header:hover .column-drag-handle{opacity:1}.column-drag-handle:active{cursor:grabbing}.table-column-header.sortable-header{--grid-header-affordance-space: 20px}.table-column-header.design-clickable{--grid-header-affordance-space: 28px}.table-column-header.sortable-header.design-clickable{--grid-header-affordance-space: 40px;--grid-header-design-right: calc(var(--grid-column-resizer-width) + 13px)}.table-column-header .column-label{display:block;padding-right:var(--grid-header-affordance-space)}.table-column-header .sort-indicator,.table-column-header .design-edit-icon{position:absolute;top:50%;transform:translateY(-50%);margin-left:0}.table-column-header .sort-indicator{right:var(--grid-header-sort-right)}.table-column-header .design-edit-icon{right:var(--grid-header-design-right)}.sortable-header{cursor:pointer}.sortable-header .sort-indicator{display:inline-flex;align-items:center;gap:2px;cursor:pointer;opacity:0;transition:opacity .15s ease}.sortable-header .sort-indicator .sort-triangles{display:flex;flex-direction:column;align-items:center;gap:2px}.sortable-header .sort-indicator .sort-tri{width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;cursor:pointer;transition:border-color .15s ease}.sortable-header .sort-indicator .sort-tri-up{border-bottom:5px solid var(--grid-on-surface-variant, #49454f);opacity:.3}.sortable-header .sort-indicator .sort-tri-down{border-top:5px solid var(--grid-on-surface-variant, #49454f);opacity:.3}.sortable-header .sort-indicator .sort-tri-active{opacity:1}.sortable-header .sort-indicator .sort-tri-active.sort-tri-up{border-bottom-color:var(--grid-primary, #6750a4)}.sortable-header .sort-indicator .sort-tri-active.sort-tri-down{border-top-color:var(--grid-primary, #6750a4)}.sortable-header .sort-indicator .sort-priority{font-size:9px;font-weight:600;color:var(--grid-primary, #6750a4);line-height:1;min-width:12px;text-align:center}.sortable-header:hover .sort-indicator,.sortable-header.sort-asc .sort-indicator,.sortable-header.sort-desc .sort-indicator{opacity:1}.sortable-header:hover .sort-indicator .sort-tri:not(.sort-tri-active){opacity:.6}.sort-asc,.sort-desc{background-color:var(--grid-surface-container-low, rgba(103, 80, 164, .04))}.dragging{opacity:1;background-color:var(--grid-surface-container);box-shadow:var(--grid-elevation-2)}.drag-over{background-color:var(--grid-surface-container);border-color:var(--grid-primary)}.data-cell{background-color:transparent;color:var(--grid-on-surface);font-family:var(--grid-font-family);font-size:var(--grid-font-size-body);font-feature-settings:var(--grid-font-feature-numeric);padding:0 var(--grid-cell-padding-x)}.cell-content{align-items:center}.cell-content .mdc-text-field{padding:0px var(--grid-spacing-xxs)!important}.cell-display-text{align-items:center;padding:0px var(--grid-spacing-xs)}.ghost-loading-row{background-color:transparent}.ghost-cell-container{padding:var(--grid-spacing-sm)}.ghost-cell{height:20px;width:100%;background-color:var(--grid-surface-container);animation:pulse 1.5s ease-in-out infinite;border-radius:var(--grid-border-radius)}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.column-resizer{position:absolute;right:0;top:0;bottom:0;width:4px;cursor:col-resize;background-color:transparent;transition:background-color .2s ease}.column-resizer:hover{background-color:var(--grid-primary)}.group-separator{height:var(--grid-spacing-sm);background-color:var(--grid-surface-variant)}.group-separator .separator-cell{background-color:var(--grid-surface-variant);border:none;height:var(--grid-spacing-sm)}.error-state{background-color:var(--grid-error-container);color:var(--grid-error);border-color:var(--grid-error)}.error-message{background-color:var(--grid-error);color:#fff;padding:var(--grid-spacing-sm);border-radius:var(--grid-border-radius);font-size:var(--grid-font-size-caption)}.incremental-row-container .eru-grid-table tbody,.incremental-row-container .eru-grid-table{position:relative}.incremental-row-container .eru-grid-table.show-column-lines{border-right:var(--grid-outline-width, 1px) solid var(--grid-outline, #e0e0e0)!important;border-top:var(--grid-outline-width, 1px) solid var(--grid-outline, #e0e0e0)!important}.incremental-row-container .eru-grid-table.show-column-lines:not(.freeze-header){border-bottom:var(--grid-outline-width, 1px) solid var(--grid-outline, #e0e0e0)!important}.incremental-row-container .eru-grid-table:not(.show-column-lines){border:var(--grid-outline-width, 1px) solid var(--grid-outline, #e0e0e0)!important}.incremental-row-container .eru-grid-table thead:after{content:\"\";position:absolute;bottom:0;left:0;right:0;height:calc(var(--grid-divider-width, 1px) * 2);background-color:var(--grid-divider-color, var(--grid-outline, #e0e0e0));pointer-events:none;z-index:10}.incremental-row-container .eru-grid-table.show-column-lines thead th,.incremental-row-container .eru-grid-table.show-column-lines tbody td{border-left:var(--grid-divider-width, 1px) solid var(--grid-divider-color, var(--grid-outline, #e0e0e0))!important}.incremental-row-container .eru-grid-table.show-row-lines thead th,.incremental-row-container .eru-grid-table.show-row-lines tbody td{border-bottom:var(--grid-divider-width, 1px) solid var(--grid-divider-color, var(--grid-outline, #e0e0e0))!important}@media(max-width:768px){.incremental-row-container{height:600px}.eru-grid-table th,.eru-grid-table td{font-size:var(--grid-font-size-caption)}.checkbox-column{width:40px;min-width:40px;max-width:40px}}@media(prefers-contrast:high){.eru-grid-table th,.eru-grid-table td{border-width:2px}.row-item:hover{border-width:2px;border-color:var(--grid-primary)}}@media(prefers-reduced-motion:reduce){.row-item,.column-drag-handle,.ghost-cell{transition:none;animation:none}}.pivot-table .nested-header{text-align:center;font-weight:600;background:var(--grid-surface-container)}.pivot-table .nested-header.row-dimension-header{background:var(--grid-surface-container);font-weight:600}.pivot-table .pivot-header-leafcols{padding:0;margin:0;height:0}.pivot-table .pivot-header-level.level-0 .nested-header{font-size:14px;padding:12px 8px}.pivot-table .pivot-header-level.level-1 .nested-header{font-size:13px;padding:10px 6px}.pivot-table .pivot-header-level.level-2 .nested-header{font-size:12px;padding:8px 4px}.pivot-table .nested-header:hover{background:var(--grid-surface-variant);color:var(--grid-primary);transition:all .2s ease}.pivot-table .pivot-cell.aggregated-value{font-weight:500;font-family:Roboto Mono,monospace}.pivot-table .pivot-cell-content{display:flex;justify-content:center;align-items:center;min-height:38px}.pivot-table .pivot-repeated-value .cell-content,.pivot-table .pivot-repeated-value .pivot-cell-content{visibility:hidden}.pivot-table .pivot-group-start.row-dimension-cell{border-top:1px solid var(--grid-outline, #79757f)}.pivot-mode .incremental-row-container{display:flex;flex-direction:column;height:auto;max-height:85vh;overflow:auto}.pivot-mode .h-shell{position:relative;width:calc(100% - var(--scrollbar-width, 17px))!important;top:0;z-index:1;overflow-x:hidden;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}.pivot-mode .h-shell::-webkit-scrollbar{display:none}.pivot-mode .gt-shell{position:relative;bottom:50px;flex-shrink:0;overflow-x:hidden;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}.pivot-mode .gt-shell::-webkit-scrollbar{display:none}.pivot-mode .gt-shell table{border-bottom:var(--grid-outline-width, 1px) solid var(--grid-outline, #e0e0e0)!important}.pivot-mode .gt-shell.adjust-bottom-vs{bottom:66px!important}.pivot-mode .gt-shell.adjust-bottom:not(.adjust-bottom-vs){bottom:calc(66px - var(--scrollbar-width, 17px))!important}.pivot-mode .header-shell{flex-shrink:0;width:100%;box-sizing:border-box;padding-right:var(--scrollbar-width, 17px);overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}.pivot-mode .header-shell::-webkit-scrollbar{display:none}.pivot-mode .header-shell.apply-cdk-width{width:calc(var(--table-total-width) + 10px)!important}.pivot-mode .header-shell .eru-grid-table{margin-bottom:0;width:100%;table-layout:fixed}.pivot-mode .header-shell .eru-grid-table thead{background:var(--grid-surface-container)}.pivot-mode .header-shell .eru-grid-table thead th{background:var(--grid-surface-container);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important}.pivot-mode .header-shell .eru-grid-table thead th.sticky-column{position:sticky;background:var(--grid-surface-container);z-index:111}.pivot-mode .header-shell .eru-grid-table tbody td{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important}.pivot-mode .pivot-container{display:flex;flex-direction:column;height:100%;width:100%;overflow:hidden}.pivot-mode .pivot-table{width:auto!important;min-width:100%!important;table-layout:fixed!important}.pivot-mode .pivot-table colgroup col{width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important;flex:none!important;flex-shrink:0!important;flex-grow:0!important}.pivot-mode .pivot-table td,.pivot-mode .pivot-table th{box-sizing:border-box!important;flex:none!important;flex-shrink:0!important;flex-grow:0!important;word-wrap:break-word!important;word-break:break-all!important}.pivot-mode .pivot-table{table-layout:fixed!important;width:100%!important}.pivot-mode .pivot-table *{max-width:var(--col-width)!important;box-sizing:border-box!important}.pivot-mode .pivot-table colgroup{width:100%!important}.pivot-mode .pivot-table colgroup col{width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important;flex-basis:var(--col-width)!important;flex:0 0 var(--col-width)!important}.pivot-mode .pivot-table table{width:100%!important;table-layout:fixed!important;border-collapse:collapse!important;border-spacing:0!important}.pivot-mode .pivot-table[style*=--table-total-width]{width:var(--table-total-width)!important;min-width:var(--table-total-width)!important;max-width:var(--table-total-width)!important}.pivot-mode .pivot-table colgroup col{width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important;flex:0 0 var(--col-width)!important;flex-basis:var(--col-width)!important;flex-grow:0!important;flex-shrink:0!important;overflow:hidden!important}.pivot-mode .pivot-table tbody td,.pivot-mode .pivot-table thead th{width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important;overflow:hidden!important;text-overflow:ellipsis!important;white-space:nowrap!important}.pivot-mode .pivot-table .cell-content,.pivot-mode .pivot-table data-cell{width:100%!important;max-width:100%!important;overflow:hidden!important;text-overflow:ellipsis!important;white-space:nowrap!important;display:block!important}.pivot-mode .pivot-table table{width:var(--table-total-width)!important;min-width:var(--table-total-width)!important;max-width:var(--table-total-width)!important;table-layout:fixed!important;border-collapse:collapse!important;border-spacing:0!important;word-wrap:break-word!important;word-break:break-all!important}.pivot-mode .pivot-tbody tr.pivot-row{min-height:var(--grid-data-row-height, 50px)!important;height:var(--grid-data-row-height, 50px)!important}.pivot-mode .pivot-tbody tr.pivot-row:hover{background-color:var(--grid-surface-variant)}.pivot-mode .pivot-tbody tr.pivot-row:nth-child(2n){background-color:#00000005}.pivot-mode .pivot-tbody tr.pivot-row td{min-height:var(--grid-data-row-height, 50px)!important;height:var(--grid-data-row-height, 50px)!important;vertical-align:middle;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important}.pivot-mode .pivot-tbody tr.pivot-row td .cell-content{min-height:calc(var(--grid-data-row-height, 50px) - 2px);display:flex;align-items:center;justify-content:center}.pivot-mode .pivot-tbody tr.pivot-row td .cell-content data-cell{width:100%;min-height:calc(var(--grid-data-row-height, 50px) - 4px);display:flex;align-items:center;justify-content:center;overflow:hidden;flex-shrink:0}.pivot-mode .pivot-cell{vertical-align:middle;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important}.pivot-mode .pivot-cell.aggregated-value{font-weight:500;font-family:Roboto Mono,monospace}.pivot-mode .pivot-cell .cell-content{display:flex;justify-content:center;align-items:center;min-height:var(--grid-header-row-height, 40px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex-shrink:0}.pivot-mode .pivot-table .subtotal-row{background-color:var(--grid-surface-container)!important;font-weight:600}.pivot-mode .pivot-table .subtotal-row td{background-color:var(--grid-surface-container);color:var(--grid-on-surface-variant)}.pivot-mode .pivot-table .subtotal-row td:first-child{color:var(--grid-primary)}.pivot-mode .pivot-table .subtotal-row td.aggregated-value{font-weight:500;color:var(--grid-primary)}.pivot-mode .pivot-table .subtotal-row:hover{background-color:var(--grid-surface-container-high)!important}.pivot-mode .pivot-table .subtotal-row:hover td{background-color:var(--grid-surface-container-high)}.pivot-mode .pivot-table .subtotal-bold td{font-weight:600!important;font-style:normal!important}.pivot-mode .pivot-table .subtotal-bold td.aggregated-value{font-weight:600!important}.pivot-mode .pivot-table .subtotal-italic td{font-style:italic!important}.pivot-mode .pivot-table .subtotal-italic td:first-child{font-weight:600!important}.pivot-mode .pivot-table .subtotal-italic td.aggregated-value{font-style:italic!important;font-weight:500!important}.pivot-mode .pivot-table .subtotal-highlighted{background-color:var(--grid-surface-variant)!important}.pivot-mode .pivot-table .subtotal-highlighted td{background-color:var(--grid-surface-variant)!important;font-weight:700!important;font-style:normal!important;color:var(--grid-primary)!important}.pivot-mode .pivot-table .subtotal-highlighted td.aggregated-value{font-weight:500!important;color:var(--grid-primary)!important}.pivot-mode .pivot-table .subtotal-highlighted:hover,.pivot-mode .pivot-table .subtotal-highlighted:hover td{background-color:var(--grid-surface-container-high)!important}.pivot-mode .pivot-table .grand-total-row{background-color:var(--grid-surface-container-high)!important;font-weight:700;font-size:var(--grid-font-size-body)}.pivot-mode .pivot-table .grand-total-row td{background-color:var(--grid-surface-container-high)!important;color:var(--grid-on-surface)}.pivot-mode .pivot-table .grand-total-row td:first-child{font-style:normal;font-weight:800;color:var(--grid-primary)}.pivot-mode .pivot-table .grand-total-row td.aggregated-value{font-weight:500;color:var(--grid-primary);font-family:Roboto Mono,monospace}.pivot-mode .pivot-table .grand-total-row:hover,.pivot-mode .pivot-table .grand-total-row:hover td{background-color:var(--grid-surface-container-high)!important}.pivot-mode .pivot-table .grand-total-bold td{font-weight:700!important;font-style:normal!important}.pivot-mode .pivot-table .grand-total-bold td.aggregated-value{font-weight:700!important}.pivot-mode .pivot-table .grand-total-italic td,.pivot-mode .pivot-table .grand-total-italic td.aggregated-value{font-style:italic!important;font-weight:500!important}.pivot-mode .pivot-table .grand-total-highlighted{background-color:var(--grid-primary)!important;box-shadow:var(--grid-elevation-2)!important}.pivot-mode .pivot-table .grand-total-highlighted td{background-color:var(--grid-primary)!important;color:var(--grid-on-primary)!important;font-weight:500!important;font-style:normal!important}.pivot-mode .pivot-table .grand-total-highlighted td.aggregated-value{color:var(--grid-on-primary)!important;font-weight:500!important}.pivot-mode .pivot-table .grand-total-highlighted:hover,.pivot-mode .pivot-table .grand-total-highlighted:hover td{background-color:var(--grid-primary)!important}.pivot-mode .pivot-table .collapsible-header{position:relative}.pivot-mode .pivot-table .collapsible-header .header-content{display:flex;align-items:center;justify-content:space-between;gap:var(--grid-spacing-xs);padding:var(--grid-spacing-xs) var(--grid-spacing-sm)}.pivot-mode .pivot-table .collapsible-header .header-label{flex:1;font-weight:600}.pivot-mode .pivot-table .collapsible-header .collapse-toggle-btn{background:none;border:none;cursor:pointer;padding:var(--grid-spacing-xxs);margin:0;display:flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:var(--grid-border-radius);color:var(--grid-on-surface-variant);transition:all .2s ease;font-size:12px;font-weight:600}.pivot-mode .pivot-table .collapsible-header .collapse-toggle-btn:hover{background-color:var(--grid-surface-container);color:var(--grid-primary);transform:scale(1.1)}.pivot-mode .pivot-table .collapsible-header .collapse-toggle-btn:focus{outline:2px solid var(--grid-primary);outline-offset:1px}.pivot-mode .pivot-table .collapsible-header .collapse-toggle-btn .collapse-icon{display:block;line-height:1;font-family:monospace;font-size:14px}.pivot-mode .pivot-table .collapsible-header.expanded .collapse-toggle-btn .collapse-icon{color:var(--grid-primary)}.pivot-mode .pivot-table .collapsible-header.collapsed{background-color:var(--grid-surface-variant)}.pivot-mode .pivot-table .collapsible-header.collapsed .header-label{font-style:italic;color:var(--grid-on-surface-variant)}.pivot-mode .pivot-table .collapsible-header.collapsed .collapse-toggle-btn .collapse-icon{color:var(--grid-outline)}.pivot-mode .pivot-table .collapsible-header:hover{background-color:var(--grid-surface-container)}.pivot-mode .pivot-table .collapsible-header:hover .header-label{color:var(--grid-on-surface)}.pivot-mode .pivot-table .pivot-single-table{display:flex;flex-direction:column;height:100%;width:100%;overflow:hidden;min-height:var(--table-min-height)!important}.pivot-mode .pivot-table .pivot-single-table .pivot-header-container{flex-shrink:0;background:var(--grid-surface)!important;overflow-x:auto;overflow-y:hidden;min-height:100px!important;height:auto!important}.pivot-mode .pivot-table .pivot-single-table .pivot-header-container .pivot-table{width:auto;min-width:100%;height:auto!important;min-height:100px!important}.pivot-mode .pivot-table .pivot-single-table .pivot-header-container .pivot-table th{background:var(--grid-surface-container)!important;padding:8px 6px!important;white-space:nowrap;min-width:50px;min-height:40px!important;height:auto!important;position:relative;visibility:visible!important;color:var(--grid-on-surface)!important}.pivot-mode .pivot-table .pivot-single-table .pivot-header-container .pivot-table th.sticky-column{position:sticky!important;background:var(--grid-surface-container)!important;border-right:2px solid var(--grid-primary)!important;box-shadow:2px 0 4px #0000001a;z-index:101!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container{flex:1;overflow:auto;min-height:300px!important;height:auto!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-viewport{height:100%!important;width:100%!important;overflow-x:auto!important;overflow-y:auto!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-table,.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-data-table{width:auto;min-width:100%;height:auto!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-table td,.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-data-table td{padding:8px 6px!important;white-space:nowrap;min-width:50px;min-height:32px!important;height:auto!important;background:var(--grid-surface)!important;color:var(--grid-on-surface)!important;visibility:visible!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-table td.sticky-column,.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-data-table td.sticky-column{position:sticky!important;background:var(--grid-surface-container)!important;border-right:2px solid var(--grid-primary)!important;box-shadow:2px 0 4px #0000001a;z-index:100!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-table tbody tr,.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-data-table tbody tr{height:auto!important;min-height:50px!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-table tbody tr.pivot-row,.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-data-table tbody tr.pivot-row{visibility:visible!important;display:table-row!important}.pivot-mode .pivot-table .collapsed-column-group{background-color:var(--grid-surface-container);border-left:3px solid var(--grid-primary)}.pivot-mode .pivot-table .collapsed-column-group:hover{background-color:var(--grid-surface-container-high)}.pivot-row.subtotal-row{background-color:var(--grid-surface-variant);font-weight:500}.pivot-row.subtotal-row.subtotal-bold{font-weight:500}.pivot-row.subtotal-row.subtotal-italic{font-style:italic}.pivot-row.subtotal-row.subtotal-highlighted{background-color:var(--grid-primary);color:var(--grid-on-primary)}.pivot-row.grand-total-row{background-color:var(--grid-surface-container);font-weight:600}.pivot-row.grand-total-row.grand-total-bold{font-weight:800}.pivot-row.grand-total-row.grand-total-italic{font-style:italic}.pivot-row.grand-total-row.grand-total-highlighted{background-color:var(--grid-primary);color:var(--grid-on-primary)}.pivot-row.first-visible-row{background-color:#6750a41a!important;position:relative}.pivot-row.first-visible-row:before{content:\"\\1f441\\fe0f First Visible\";position:absolute;top:-20px;left:0;background:var(--grid-primary);color:var(--grid-on-primary);padding:2px 6px;font-size:10px;border-radius:2px;z-index:1000}.header-wrap-text{white-space:pre-wrap;word-break:auto-phrase}.group-header-row{display:flex;align-items:center;justify-content:space-between;width:100%;padding-right:12px}.custom-collapse-header{background-color:var(--grid-surface-variant);padding:8px 20px;border-top-left-radius:12px;border-top-right-radius:12px;cursor:pointer;display:flex;width:fit-content;align-items:center;-webkit-user-select:none;user-select:none;min-width:200px;margin-bottom:10px;position:sticky;left:1px;z-index:116}.custom-collapse-header .collapse-arrow{display:inline-block;margin-right:8px;font-size:12px;color:var(--grid-on-surface-variant);transition:transform .2s ease;transform:rotate(0)}.custom-collapse-header .collapse-arrow.rotate-arrow{transform:rotate(270deg)}.custom-collapse-header .f-12{font-size:12px;color:var(--grid-on-surface)}.custom-collapse-header .group-sort-indicator{display:inline-flex;align-items:center;margin-left:8px}.custom-collapse-header .group-sort-indicator .sort-triangles{display:flex;flex-direction:column;align-items:center;gap:2px}.custom-collapse-header .group-sort-indicator .sort-tri{width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;cursor:pointer;transition:border-color .15s ease}.custom-collapse-header .group-sort-indicator .sort-tri-up{border-bottom:5px solid var(--grid-on-surface-variant, #49454f);opacity:.3}.custom-collapse-header .group-sort-indicator .sort-tri-down{border-top:5px solid var(--grid-on-surface-variant, #49454f);opacity:.3}.custom-collapse-header .group-sort-indicator .sort-tri-active{opacity:1}.custom-collapse-header .group-sort-indicator .sort-tri-active.sort-tri-up{border-bottom-color:var(--grid-primary, #6750a4)}.custom-collapse-header .group-sort-indicator .sort-tri-active.sort-tri-down{border-top-color:var(--grid-primary, #6750a4)}.excel-download-icon{cursor:pointer}.excel-download-icon:hover{opacity:.75}.excel-download-bar{display:flex;justify-content:flex-end;padding:4px 12px;flex-shrink:0}.table-mode .header-shell{flex-shrink:0;width:100%;box-sizing:border-box;padding-right:var(--scrollbar-width, 17px);overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}.table-mode .header-shell::-webkit-scrollbar{display:none}.table-mode .header-shell.apply-cdk-width{width:calc(var(--table-total-width) + 10px)!important}.table-mode .subtotal-row{background-color:var(--grid-surface-container)!important;font-weight:600}.table-mode .subtotal-row td{background-color:var(--grid-surface-container);color:var(--grid-on-surface-variant)}.table-mode .subtotal-row td:first-child{color:var(--grid-primary)}.table-mode .subtotal-row td.subtotal-cell{font-weight:500}.table-mode .subtotal-row td.subtotal-cell .subtotal-label{font-weight:600;color:var(--grid-primary)}.table-mode .subtotal-row:hover{background-color:var(--grid-surface-container-high)!important}.table-mode .subtotal-row:hover td{background-color:var(--grid-surface-container-high)}.table-mode .subtotal-row.subtotal-bold td{font-weight:600!important;font-style:normal!important}.table-mode .subtotal-row.subtotal-italic td{font-style:italic!important}.table-mode .subtotal-row.subtotal-italic td:first-child{font-weight:600!important}.table-mode .subtotal-row.subtotal-highlighted{background-color:var(--grid-surface-variant)!important}.table-mode .subtotal-row.subtotal-highlighted td{background-color:var(--grid-surface-variant)!important;font-weight:700!important;font-style:normal!important;color:var(--grid-primary)!important}.table-mode .subtotal-row.subtotal-highlighted:hover,.table-mode .subtotal-row.subtotal-highlighted:hover td{background-color:var(--grid-surface-container-high)!important}.table-mode .grand-total-row{background-color:var(--grid-surface-container-high)!important;font-weight:700}.table-mode .grand-total-row td{background-color:var(--grid-surface-container-high);color:var(--grid-on-surface)}.table-mode .grand-total-row td:first-child{color:var(--grid-primary)}.table-mode .grand-total-row td.grand-total-cell{font-weight:600}.table-mode .grand-total-row td.grand-total-cell .grand-total-label{font-weight:700;color:var(--grid-primary)}.table-mode .grand-total-row:hover{background-color:var(--grid-surface-container-high)!important}.table-mode .grand-total-row:hover td{background-color:var(--grid-surface-container-high)}.table-mode .grand-total-row.grand-total-bold td{font-weight:700!important;font-style:normal!important}.table-mode .grand-total-row.grand-total-italic td{font-style:italic!important}.table-mode .grand-total-row.grand-total-italic td:first-child{font-weight:700!important}.table-mode .grand-total-row.grand-total-highlighted{background-color:var(--grid-surface-variant)!important}.table-mode .grand-total-row.grand-total-highlighted td{background-color:var(--grid-surface-variant)!important;font-weight:800!important;font-style:normal!important;color:var(--grid-primary)!important}.table-mode .grand-total-row.grand-total-highlighted:hover,.table-mode .grand-total-row.grand-total-highlighted:hover td{background-color:var(--grid-surface-container-high)!important}.table-mode .subtotal-row-shell{width:100%;box-sizing:border-box;padding-right:var(--scrollbar-width, 17px);overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}.table-mode .subtotal-row-shell::-webkit-scrollbar{display:none}.board-mode-host{overflow:hidden;display:flex;flex-direction:column;max-height:var(--grid-height, 600px)}.board-mode-host .board-view-container{display:flex;flex-direction:column;flex:1;min-height:0}.board-mode-host .board-sort-bar{display:flex;align-items:center;gap:6px;padding:8px 16px;flex-shrink:0;border-bottom:1px solid var(--grid-outline-variant, #cac4d0);background:var(--grid-surface, #fffbfe);overflow-x:auto}.board-mode-host .board-sort-bar .board-sort-label{font-size:12px;font-weight:500;color:var(--grid-on-surface-variant, #49454f);white-space:nowrap}.board-mode-host .board-sort-bar .board-sort-chip{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:16px;border:1px solid var(--grid-outline-variant, #cac4d0);background:var(--grid-surface, #fffbfe);color:var(--grid-on-surface, #1d1b20);font-size:12px;white-space:nowrap}.board-mode-host .board-sort-bar .board-sort-chip-active{background:var(--grid-surface-container);border-color:var(--grid-outline, #79757f);color:var(--grid-on-surface, #1d1b20)}.board-mode-host .board-sort-bar .board-sort-chip-label{pointer-events:none}.board-mode-host .board-sort-bar .board-sort-chip-arrow{font-size:10px;line-height:1;cursor:pointer;padding:2px;border-radius:4px}.board-mode-host .board-sort-bar .board-sort-chip-arrow:hover{background:#00000014}.board-mode-host .board-sort-bar .board-sort-chip-priority{font-size:9px;font-weight:700;background:var(--grid-primary, #6750a4);color:var(--grid-on-primary, #ffffff);border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center}.board-mode-host .board-sort-bar .board-sort-chip-remove{font-size:10px;cursor:pointer;padding:2px;border-radius:4px;color:var(--grid-on-surface-variant, #49454f)}.board-mode-host .board-sort-bar .board-sort-chip-remove:hover{background:#00000014;color:var(--grid-error, #b3261e)}.board-mode-host .board-sort-bar .board-sort-add-btn{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:16px;border:1px dashed var(--grid-outline-variant, #cac4d0);background:transparent;color:var(--grid-on-surface-variant, #49454f);font-size:12px;cursor:pointer;white-space:nowrap;transition:background .15s ease,border-color .15s ease}.board-mode-host .board-sort-bar .board-sort-add-btn .board-sort-add-icon{font-size:14px;width:14px;height:14px}.board-mode-host .board-sort-bar .board-sort-add-btn:hover{background:var(--grid-surface-container-low, #f7f2fa);border-color:var(--grid-primary, #6750a4);color:var(--grid-primary, #6750a4)}.board-mode-host .board-sort-bar .board-sort-clear{display:inline-flex;align-items:center;padding:4px 10px;border-radius:16px;border:1px solid var(--grid-error, #b3261e);background:transparent;color:var(--grid-error, #b3261e);font-size:12px;cursor:pointer;white-space:nowrap;transition:background .15s ease}.board-mode-host .board-sort-bar .board-sort-clear:hover{background:#b3261e14}.board-mode-host .board-columns-wrapper{display:grid;grid-template-columns:repeat(auto-fit,minmax(var(--board-col-min-width, 300px),var(--board-col-max-width, 1fr)));grid-auto-rows:auto;align-items:start;gap:16px;flex:1;min-height:0;overflow-x:hidden;overflow-y:auto;align-content:start;justify-content:start}.board-mode-host .board-columns-wrapper.board-columns-nowrap{grid-auto-flow:column;grid-template-columns:none;grid-template-rows:auto;grid-auto-rows:auto;grid-auto-columns:minmax(var(--board-col-min-width, 300px),var(--board-col-max-width, 1fr));overflow-x:auto;overflow-y:hidden}.board-mode-host .board-column{max-height:var(--board-column-height, 420px);min-width:0;display:flex;flex-direction:column;background:var(--grid-surface-container, #f3edf7);border-radius:12px;min-height:0;overflow:hidden}.board-mode-host .board-column.board-column-accented{border-top:3px solid var(--board-group-color, transparent)}.board-mode-host .board-column .column-header{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 14px;font-weight:600;flex-shrink:0;background-color:transparent}.board-mode-host .board-column .column-header:hover{background-color:transparent}.board-mode-host .board-column .column-header .column-header-title{font-size:15px;font-weight:700;letter-spacing:.2px;line-height:1.2;color:var(--grid-on-surface, #1d1b20);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.board-mode-host .board-column .column-header .column-header-title-cell{display:inline-flex;align-items:center;flex:1 1 auto;min-width:0;overflow:visible;text-overflow:clip}.board-mode-host .board-column .column-header .column-header-title-cell data-cell,.board-mode-host .board-column .column-header .column-header-title-cell .data-cell-component,.board-mode-host .board-column .column-header .column-header-title-cell .container{padding:0!important;margin:0!important;border:none!important;background:transparent!important;min-height:0!important;height:auto!important;width:auto!important;max-width:100%!important;overflow:visible!important}.board-mode-host .board-column .column-header .column-header-title-cell .status-display,.board-mode-host .board-column .column-header .column-header-title-cell .status-display-content,.board-mode-host .board-column .column-header .column-header-title-cell .status-text,.board-mode-host .board-column .column-header .column-header-title-cell .tag-display,.board-mode-host .board-column .column-header .column-header-title-cell .tag-text{max-width:none!important;overflow:visible!important;text-overflow:clip!important}.board-mode-host .board-column .column-header .column-header-count{font-size:10px;font-weight:600;color:var(--eru-board-count-color, var(--grid-on-surface-variant, #49454f));background:var(--eru-board-count-bg, var(--grid-surface-variant, #e7e0ec));border-radius:10px;padding:3px 10px;white-space:nowrap;flex-shrink:0}.board-mode-host .board-column-body{flex:0 1 auto;min-height:0}.board-mode-host .board-card-container{box-sizing:border-box;overflow:hidden;border-radius:8px;transition:background-color .15s ease,box-shadow .15s ease}.board-mode-host .board-card-container.show-row-lines{box-shadow:inset 0 0 0 var(--grid-divider-width, 1px) var(--grid-divider-color, var(--grid-outline, #e0e0e0))}.board-mode-host .board-card-container:hover{background-color:var(--eru-board-card-hover-bg, color-mix(in srgb, var(--mat-sys-primary, #1976d2) 8%, transparent))}.board-mode-host .board-card-container.selected{background-color:var(--eru-board-card-selected-bg, color-mix(in srgb, var(--mat-sys-primary, #1976d2) 14%, transparent));box-shadow:inset 0 0 0 2px var(--eru-board-card-selected-outline, var(--mat-sys-primary, #1976d2))}.board-mode-host .board-card{height:calc(100% - 8px);overflow:hidden;cursor:pointer}.board-mode-host .board-card mat-card-title{font-size:13px}.board-mode-host .board-card mat-card-subtitle{font-size:12px}.board-mode-host .board-card-field{display:flex;flex-direction:column;margin-bottom:4px}.board-mode-host .board-field-label{font-size:10px;color:var(--grid-on-surface-variant, #49454f);font-weight:500;text-transform:uppercase;letter-spacing:.5px}.board-mode-host .board-ghost-card{margin:8px;padding:16px;background:var(--grid-surface, #fef7ff);border-radius:8px;animation:board-pulse 1.5s ease-in-out infinite}.board-mode-host .board-ghost-line{height:12px;background:var(--grid-surface-variant, #e7e0ec);border-radius:4px;margin-bottom:8px}.board-mode-host .board-ghost-line--short{width:60%}@keyframes board-pulse{0%,to{opacity:1}50%{opacity:.5}}th.row-expand-toggle,td.row-expand-toggle{width:40px!important;min-width:40px!important;max-width:40px!important;padding:0!important;text-align:center;vertical-align:middle;cursor:pointer;-webkit-user-select:none;user-select:none;box-sizing:border-box}.row-expand-icon{font-size:20px;width:20px;height:20px;line-height:20px;color:var(--grid-on-surface-variant);transition:transform .15s ease-in-out}.row-expand-icon.expanded{transform:rotate(90deg)}.row-detail{background:var(--grid-surface-container)}.row-detail .row-detail-cell{padding:var(--grid-spacing-sm) var(--grid-spacing-md);border-bottom:1px solid var(--grid-outline-variant)}.row-detail .row-detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:var(--grid-spacing-sm) var(--grid-spacing-md)}.row-detail .row-detail-field{display:flex;flex-direction:column;gap:var(--grid-spacing-xxs);min-width:0}.row-detail .row-detail-label{font-size:var(--grid-font-size-caption);color:var(--grid-on-surface-variant);font-weight:500}.row-detail .row-detail-value{min-width:0}.row-detail .row-detail-value data-cell{display:block;width:100%}\n"], dependencies: [{ kind: "component", type: DataCellComponent, selector: "data-cell", inputs: ["eruGridStore", "fieldSize", "columnDatatype", "columnName", "column", "value", "id", "frozenGrandTotalCell", "td", "drillable", "mode", "isEditable", "row", "personCardTemplate", "cellTemplate"], outputs: ["tdChange"] }, { kind: "ngmodule", type: ScrollingModule }, { kind: "directive", type: i1$3.ɵɵCdkFixedSizeVirtualScroll, selector: "cdk-virtual-scroll-viewport[itemSize]", inputs: ["itemSize", "minBufferPx", "maxBufferPx"] }, { kind: "directive", type: i1$3.ɵɵCdkVirtualForOf, selector: "[cdkVirtualFor][cdkVirtualForOf]", inputs: ["cdkVirtualForOf", "cdkVirtualForTrackBy", "cdkVirtualForTemplate", "cdkVirtualForTemplateCacheSize"] }, { kind: "component", type: i1$3.ɵɵCdkVirtualScrollViewport, selector: "cdk-virtual-scroll-viewport", inputs: ["orientation", "appendOnly"], outputs: ["scrolledIndexChange"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$4.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$4.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i5.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i5.MatCardActions, selector: "mat-card-actions", inputs: ["align"], exportAs: ["matCardActions"] }, { kind: "directive", type: i5.MatCardContent, selector: "mat-card-content" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1$2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i7.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i7.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i7.MatMenuContent, selector: "ng-template[matMenuContent]" }, { kind: "directive", type: i7.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "directive", type: ResizeColumnDirective, selector: "[resizeColumn]", inputs: ["resizeColumn", "index", "columnConfig", "gridConfig"] }, { kind: "directive", type: ColumnDragDirective, selector: "[columnDraggable]", inputs: ["columnDraggable"] }, { kind: "component", type: ColumnDesignPanelComponent, selector: "eru-column-design-panel" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
16874
+ ], viewQueries: [{ propertyName: "rowContainer", first: true, predicate: ["rowContainer"], descendants: true }, { propertyName: "headerScroller", first: true, predicate: ["headerScroller"], descendants: true, read: ElementRef }, { propertyName: "gtScroller", first: true, predicate: ["gtScroller"], descendants: true, read: ElementRef }, { propertyName: "viewport", first: true, predicate: ["vp"], descendants: true }, { propertyName: "groupsViewport", first: true, predicate: ["groupsViewport"], descendants: true }, { propertyName: "groupsScrollContainerEl", first: true, predicate: ["groupsScrollContainer"], descendants: true }, { propertyName: "allViewports", predicate: CdkVirtualScrollViewport, descendants: true }, { propertyName: "headerScrollers", predicate: ["headerScroller"], descendants: true }], ngImport: i0, template: "<!-- <div style=\"background: #f0f0f0; font-size: 12px; border-bottom: 1px solid #ccc;\">\ncurrentPivotScrollIndex {{currentPivotScrollIndex()}} |\nfirstDataRowIndex {{firstDataRowIndex()}} |\nfirstTr {{firstTr}} |\nmaxDepth {{maxDepth()}}\n</div> -->\n<ng-template #excelDownloadIcon>\n <svg class=\"excel-download-icon\" title=\"Download Excel\" (click)=\"onExcelDownloadClick($event)\"\n xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 48 48\" width=\"24px\" height=\"24px\">\n <path fill=\"#169154\" d=\"M29,6H15.744C14.781,6,14,6.781,14,7.744v7.259h15V6z\" />\n <path fill=\"#18482a\" d=\"M14,33.054v7.202C14,41.219,14.781,42,15.743,42H29v-8.946H14z\" />\n <path fill=\"#0c8045\" d=\"M14 15.003H29V24.005000000000003H14z\" />\n <path fill=\"#17472a\" d=\"M14 24.005H29V33.055H14z\" />\n <g>\n <path fill=\"#29c27f\" d=\"M42.256,6H29v9.003h15V7.744C44,6.781,43.219,6,42.256,6z\" />\n <path fill=\"#27663f\" d=\"M29,33.054V42h13.257C43.219,42,44,41.219,44,40.257v-7.202H29z\" />\n <path fill=\"#19ac65\" d=\"M29 15.003H44V24.005000000000003H29z\" />\n <path fill=\"#129652\" d=\"M29 24.005H44V33.055H29z\" />\n </g>\n <path fill=\"#0c7238\"\n d=\"M22.319,34H5.681C4.753,34,4,33.247,4,32.319V15.681C4,14.753,4.753,14,5.681,14h16.638 C23.247,14,24,14.753,24,15.681v16.638C24,33.247,23.247,34,22.319,34z\" />\n <path fill=\"#fff\"\n d=\"M9.807 19L12.193 19 14.129 22.754 16.175 19 18.404 19 15.333 24 18.474 29 16.123 29 14.013 25.07 11.912 29 9.526 29 12.719 23.982z\" />\n </svg>\n</ng-template>\n\n<div class=\"incremental-row-container eru-grid\" #rowContainer [class.pivot-mode]=\"gridStore.isPivotMode()\"\n [class.table-mode]=\"!gridStore.isPivotMode() && !isBoardMode()\" [class.board-mode-host]=\"isBoardMode()\">\n <eru-column-design-panel></eru-column-design-panel>\n <!-- Pivot Mode Template -->\n @if (gridStore.isPivotMode()) {\n <ng-container>\n <div class=\"pivot-container\" style=\"display: flex; flex-direction: column; height: 100%;\"\n [style]=\"'--table-min-height: ' + getInitialMinHeightPx() + 'px; --table-total-width: ' + getInitialTotalWidth() + 'px'\">\n @if(showExcelDownload()) {\n <div class=\"excel-download-bar\">\n <ng-container *ngTemplateOutlet=\"excelDownloadIcon\"></ng-container>\n </div>\n }\n <!-- Debug info for first visible row -->\n\n\n <div class=\"pivot-single-table\"\n style=\"height: 100%; width: 100%; overflow: hidden; display: flex; flex-direction: column;\">\n @if (freezeHeader()) {\n <div #headerScroller class=\"header-shell\">\n <table class=\"eru-grid-table pivot-table\"\n [style]=\"'width: auto; min-width: 100%; --table-total-width: ' + getInitialTotalWidth() + 'px'\"\n [class.show-column-lines]=\"showColumnLines()\" [class.show-row-lines]=\"showRowLines()\">\n <!-- Column Groups for consistent width -->\n <ng-container *ngTemplateOutlet=\"pivotColGroup\"></ng-container>\n\n <ng-container *ngTemplateOutlet=\"pivotTableHead\"></ng-container>\n @if(grandTotalPosition() === 'before' && freezeGrandTotal()) {\n <ng-container *ngTemplateOutlet=\"pivotGrandTotal\"></ng-container>\n }\n </table>\n </div>\n }\n <!-- Virtual Scrolled Table Body -->\n <div>\n <cdk-virtual-scroll-viewport #vp [itemSize]=\"dataRowHeight()\" class=\"viewport pivot-viewport\"\n [class.apply-cdk-width]=\"applyCdkWidth()\" (scrolledIndexChange)=\"onPivotScroll($event)\"\n (scroll)=\"onBodyScroll($event)\" style=\"overflow: auto;\">\n <table class=\"eru-grid-table pivot-table\"\n [style]=\"'width: auto; min-width: 100%; --table-total-width: ' + getInitialTotalWidth() + 'px'\"\n [class.show-column-lines]=\"showColumnLines()\" [class.show-row-lines]=\"showRowLines()\">\n <!-- Column Groups for consistent width -->\n <ng-container *ngTemplateOutlet=\"pivotColGroup\"></ng-container>\n\n @if (!freezeHeader()) {\n <ng-container *ngTemplateOutlet=\"pivotTableHead\"></ng-container>\n }\n <!-- Table Body with Virtual Scrolling -->\n <tbody class=\"pivot-tbody\">\n\n <tr *cdkVirtualFor=\"let pivotRow of gridStore.pivotDisplayData(); \n trackBy: trackByPivotRowFn; \n let i = index\" class=\"pivot-row\" [class.subtotal-row]=\"pivotRow._isSubtotal\"\n [class.grand-total-row]=\"pivotRow._isGrandTotal\"\n [class.subtotal-bold]=\"pivotRow._isSubtotal && subTotalStyle() === 'bold'\"\n [class.subtotal-italic]=\"pivotRow._isSubtotal && subTotalStyle() === 'italic'\"\n [class.subtotal-highlighted]=\"pivotRow._isSubtotal && subTotalStyle() === 'highlighted'\"\n [class.grand-total-bold]=\"pivotRow._isGrandTotal && grandTotalStyle() === 'bold'\"\n [class.grand-total-italic]=\"pivotRow._isGrandTotal && grandTotalStyle() === 'italic'\"\n [class.grand-total-highlighted]=\"pivotRow._isGrandTotal && grandTotalStyle() === 'highlighted'\"\n [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\" [style.cursor]=\"cursorOnHover() || null\" [attr.data-pivot-row]=\"i\">\n @if ((!pivotRow._isGrandTotal && freezeGrandTotal() ) || (!freezeGrandTotal() )) {\n @for (column of getLeafColumns(); track trackByColumnFn($index, column); let colIndex = $index) {\n <td [style.width.px]=\"column.field_size\" [style.minWidth.px]=\"column.field_size\" class=\"pivot-cell\"\n [class.row-dimension-cell]=\"isRowDimensionColumn(column.name)\"\n [class.column-dimension-cell]=\"!isRowDimensionColumn(column.name)\"\n [class.aggregated-value]=\"!isRowDimensionColumn(column.name) && column.datatype === 'number'\"\n [class.pivot-repeated-value]=\"isRepeatedDimensionValue(i, column.name)\"\n [class.pivot-group-start]=\"isPivotGroupStart(i, column.name)\"\n [class.sticky-column]=\"isStickyColumn(column.name, colIndex)\"\n [style.position]=\"isStickyColumn(column.name, colIndex) ? 'sticky' : 'static'\"\n [style.left.px]=\"getStickyColumnLeft(column.name, colIndex)\"\n [style.z-index]=\"isStickyColumn(column.name, colIndex) ? 99 : 1\" [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\">\n <div class=\"cell-content pivot-cell-content\">\n <data-cell [personCardTemplate]=\"personCardTemplate\" [class.aggregation]=\"!!column.aggregationFunction\" [fieldSize]=\"column.field_size\"\n [columnDatatype]=\"column.datatype\" [columnName]=\"column.name\" [value]=\"pivotRow[column.name]\"\n [column]=\"column\" [drillable]=\"column.enableDrilldown || false\" [mode]=\"mode()\"\n [isEditable]=\"isEditable()\" [id]=\"'pivot_' + i + '_' + column.name\" [eruGridStore]=\"gridStore\"\n [row]=\"pivotRow\">\n </data-cell>\n </div>\n </td>\n }\n } @else {\n <td [style.height.px]=\"dataRowHeight()\" [attr.colspan]=\"getLeafColumns().length\"> </td>\n }\n </tr>\n </tbody>\n </table>\n </cdk-virtual-scroll-viewport>\n\n </div>\n @if (freezeGrandTotal() && grandTotalPosition() === 'after') {\n <div #gtScroller class=\"header-shell gt-shell\" [class.adjust-bottom]=\"!applyCdkWidth()\"\n [class.adjust-bottom-vs]=\"adjustScrollWidth()\">\n <table class=\"eru-grid-table pivot-table\"\n [style]=\"'width: auto; min-width: 100%; --table-total-width: ' + getInitialTotalWidth() + 'px'\"\n [class.show-column-lines]=\"showColumnLines()\" [class.show-row-lines]=\"showRowLines()\">\n <ng-container *ngTemplateOutlet=\"pivotColGroup\"></ng-container>\n\n <ng-container *ngTemplateOutlet=\"pivotGrandTotal\"></ng-container>\n\n </table>\n </div>\n }\n\n\n </div>\n </div>\n </ng-container>\n } @else if (isBoardMode()) {\n <!-- Board Mode Template -->\n <div class=\"board-view-container\">\n @if(showExcelDownload()) {\n <div class=\"excel-download-bar\">\n <ng-container *ngTemplateOutlet=\"excelDownloadIcon\"></ng-container>\n </div>\n }\n @if(showSortBar()) {\n <div class=\"board-sort-bar\">\n <span class=\"board-sort-label\">Sort by:</span>\n @for (entry of gridStore.sortColumns(); track getFieldName(entry)) {\n <span class=\"board-sort-chip board-sort-chip-active\">\n <span class=\"board-sort-chip-label\">{{getColumnLabel(entry)}}</span>\n <span class=\"board-sort-chip-arrow\" (click)=\"onBoardSortChipToggle($event, entry)\">\n @if(!entry.startsWith('-')) { \u25B2 } @else { \u25BC }\n </span>\n @if(gridStore.sortColumns().length > 1) {\n <span class=\"board-sort-chip-priority\">{{getSortPriority(getFieldName(entry))}}</span>\n }\n <span class=\"board-sort-chip-remove\" (click)=\"onBoardSortChipRemove($event, entry)\">\u2715</span>\n </span>\n }\n <button class=\"board-sort-add-btn\" [matMenuTriggerFor]=\"sortFieldMenu\">\n <mat-icon class=\"board-sort-add-icon\">add</mat-icon> Add field\n </button>\n <mat-menu #sortFieldMenu=\"matMenu\" class=\"board-sort-menu\">\n @for (column of columns(); track column.name) {\n <button mat-menu-item (click)=\"onBoardSortFieldSelect(column)\"\n [disabled]=\"getSortDirection(column.name) !== null\">\n @if(getSortDirection(column.name) !== null) {\n <mat-icon>check</mat-icon>\n } @else {\n <mat-icon></mat-icon>\n }\n {{column.label}}\n </button>\n }\n </mat-menu>\n @if(gridStore.sortColumns().length > 0) {\n <button class=\"board-sort-clear\" (click)=\"onBoardSortClear()\">\u2715 Clear</button>\n }\n </div>\n }\n <div class=\"board-columns-wrapper\" [class.board-columns-nowrap]=\"!boardWrapColumns()\">\n @for (group of groups(); track group.id) {\n <div class=\"board-column\" [class.board-column-accented]=\"!!boardGroupColor(group)\"\n [style.--board-group-color]=\"boardGroupColor(group)\">\n @if (showBoardColumnHeader()) {\n <div class=\"column-header\">\n <!-- Render the group value through the same read-only cell renderer a\n data cell uses, so the grouped field's datatype formats itself\n (status/tag pills, dates, numbers) instead of printing raw text. -->\n @if (groupByColumn(); as gcol) {\n <span class=\"column-header-title column-header-title-cell\">\n <data-cell\n [eruGridStore]=\"gridStore\"\n [column]=\"gcol\"\n [columnDatatype]=\"gcol.datatype\"\n [columnName]=\"gcol.name\"\n [value]=\"group.title\"\n [id]=\"'board-group-' + group.id\"\n [fieldSize]=\"0\"\n [isEditable]=\"false\"\n [mode]=\"'board-group-header'\">\n </data-cell>\n </span>\n } @else {\n <span class=\"column-header-title\">{{ group.title }}</span>\n }\n <span class=\"column-header-count\">{{ group.currentLoadedRows || 0 }} of {{ group.totalRowCount || 0 }}</span>\n </div>\n }\n <cdk-virtual-scroll-viewport [attr.data-group-id]=\"group.id\" [itemSize]=\"boardCardSlot\" class=\"board-column-body\"\n [style.height.px]=\"boardColumnBodyHeight(group)\"\n (scrolledIndexChange)=\"onBoardScrolledIndexChange($event, group)\">\n <div\n *cdkVirtualFor=\"let row of getRowsForGroupSignal(group.id === null || group.id === undefined ? '__NULL_GROUP__' : group.id)(); templateCacheSize: 0\"\n class=\"board-card-container\"\n [class.selected]=\"isBoardCardActive(row)\"\n [class.show-row-lines]=\"showRowLines()\"\n [style.height.px]=\"boardCardOuterHeight\"\n [style.padding.px]=\"boardCardPadding\"\n [style.marginBottom.px]=\"boardCardGap\"\n [style.cursor]=\"cursorOnHover() || null\"\n (click)=\"emitRowSelect(row, 'board', group)\">\n <!-- Custom template when consumer provides boardCardTemplate; default card otherwise -->\n <ng-container\n *ngTemplateOutlet=\"boardCardTemplate ?? defaultBoardCard;\n context: { $implicit: row, columns: visibleBoardFields(), group: group }\">\n </ng-container>\n </div>\n </cdk-virtual-scroll-viewport>\n @if (group.isLoading) {\n <div class=\"board-ghost-card\">\n <div class=\"board-ghost-line\"></div>\n <div class=\"board-ghost-line board-ghost-line--short\"></div>\n </div>\n }\n </div>\n }\n </div>\n </div>\n } @else {\n\n <!-- Table Mode Template -->\n @if(showExcelDownload() && !showGroupBar()) {\n <div class=\"excel-download-bar\">\n <ng-container *ngTemplateOutlet=\"excelDownloadIcon\"></ng-container>\n </div>\n }\n <!-- Select-all banner: the header checkbox can only reach loaded pages, so\n this is where the user says they meant the whole result set. -->\n @if(showSelectAllBanner()) {\n <div class=\"selection-banner\">\n @if(selectAllMatching()) {\n <span class=\"selection-banner-text\">All {{ totalMatchingCount() }} matching records are selected.</span>\n <button type=\"button\" class=\"selection-banner-action\" (click)=\"clearRowSelection()\">Clear selection</button>\n } @else {\n <span class=\"selection-banner-text\">{{ gridStore.totalSelectedCount() }} selected on this view.</span>\n <button type=\"button\" class=\"selection-banner-action\" (click)=\"selectAllMatchingRows()\">Select all {{\n totalMatchingCount() }} matching</button>\n }\n </div>\n }\n <!-- Scrollable groups container \u2014 plain iteration avoids CDK fixed-height estimation errors -->\n <div #groupsScrollContainer class=\"groups-scroll-container\" (scroll)=\"onGroupsViewportScroll($event)\">\n\n @for (group of groups(); track trackByGroupFn($index, group); let i = $index) {\n <div class=\"group-container\"\n [attr.data-group-id]=\"group.id === null || group.id === undefined ? '__NULL_GROUP__' : group.id\">\n <!-- Combined sticky header with group info and table -->\n <div style=\"\n background:var(--grid-surface);\n position: sticky;\n top: 0;\n z-index: 115;\n \">\n @if(showGroupBar()) {\n <div class=\"group-header-row\">\n <div class=\"custom-collapse-header\" (click)=\"toggleGroupCollapse(group.id)\">\n <span class=\"collapse-arrow\" [ngClass]=\"{\n 'rotate-arrow': group.isExpanded,\n }\">\u25BC</span>\n <span class=\"f-12\">\n {{ group?.title || \"\" }}\n {{ group?.currentLoadedRows || 0 }} -\n {{ group?.totalRowCount || 0 }} rows...</span>\n @if(groupByField() && isSortable()) {\n <span class=\"group-sort-indicator\">\n <span class=\"sort-triangles\">\n <span class=\"sort-tri sort-tri-up\" [class.sort-tri-active]=\"getSortDirection(groupByField()!) === 'asc'\"\n (click)=\"onGroupSortToggle($event, 'asc')\"></span>\n <span class=\"sort-tri sort-tri-down\"\n [class.sort-tri-active]=\"getSortDirection(groupByField()!) === 'desc'\"\n (click)=\"onGroupSortToggle($event, 'desc')\"></span>\n </span>\n </span>\n }\n </div>\n @if(i === 0 && showExcelDownload()) {\n <ng-container *ngTemplateOutlet=\"excelDownloadIcon\"></ng-container>\n }\n </div>\n }\n\n @if(freezeHeader() && (group.isExpanded || !showGroupBar())) {\n <div #headerScroller class=\"header-shell\" [attr.data-group-id]=\"'header-shell-' + group.id\"\n [style]=\"'--table-total-width: ' + getInitialTotalWidth() + 'px'\">\n <table class=\"eru-grid-table\" [class.freeze-header]=\"freezeHeader()\"\n [class.show-column-lines]=\"showColumnLines()\" [class.show-row-lines]=\"showRowLines()\">\n <ng-container *ngTemplateOutlet=\"tableColGroup\"></ng-container>\n <ng-container *ngTemplateOutlet=\"tableHeader\"></ng-container>\n <!-- Grand Total row after sticky header (position: before) - only for first group -->\n @if(enableGrandTotal() && grandTotalPosition() === 'before' && hasGrandTotalData() && i === 0) {\n <tbody>\n <ng-container *ngTemplateOutlet=\"tableGrandTotal\"></ng-container>\n </tbody>\n }\n <!-- Subtotal row after sticky header (position: before) -->\n @if(enableRowSubtotals() && subtotalPosition() === 'before' && hasSubtotalData(group)) {\n <tbody>\n <ng-container *ngTemplateOutlet=\"tableSubtotal; context: { group: group }\"></ng-container>\n </tbody>\n }\n </table>\n </div>\n }\n </div>\n @if(group.isExpanded || !showGroupBar()) {\n <ng-container>\n <cdk-virtual-scroll-viewport [attr.data-group-id]=\"group.id\" [itemSize]=\"dataRowHeight()\" class=\"viewport table-viewport\"\n (scrolledIndexChange)=\"onScroll($event, group)\" (scroll)=\"onTableBodyScroll($event)\"\n [style]=\"'--table-height: ' + getGroupContentHeight(group.id) + 'px; --table-min-height: ' + getGroupContentHeight(group.id) + 'px; --table-total-width: ' + getInitialTotalWidth() + 'px'\">\n <div class=\"table-wrapper\">\n <table class=\"eru-grid-table\" [class.show-column-lines]=\"showColumnLines()\"\n [class.show-row-lines]=\"showRowLines()\">\n <ng-container *ngTemplateOutlet=\"tableColGroup\"></ng-container>\n @if(!freezeHeader()) {\n <ng-container *ngTemplateOutlet=\"tableHeader\"></ng-container>\n }\n <!-- Grand Total row after normal header (position: before) - only for first group -->\n @if(!freezeHeader() && enableGrandTotal() && grandTotalPosition() === 'before' &&\n hasGrandTotalData() && i === 0) {\n <tbody>\n <ng-container *ngTemplateOutlet=\"tableGrandTotal\"></ng-container>\n </tbody>\n }\n <!-- Subtotal row after normal header (position: before) -->\n @if(!freezeHeader() && enableRowSubtotals() && subtotalPosition() === 'before' &&\n hasSubtotalData(group)) {\n <tbody>\n <ng-container *ngTemplateOutlet=\"tableSubtotal; context: { group: group }\"></ng-container>\n </tbody>\n }\n <tbody>\n @if (columns(); as columnsList) {\n <!-- <tr *ngIf=\"groupItem.type === 'table-header' && groups().length > 1\" style=\"background:#fafafa\">\n @if(gridStore.configuration().config.allowSelection) {\n <th class=\"checkbox-column\" style=\"text-align: center;\">\n <input\n type=\"checkbox\"\n [checked]=\"isGroupSelected(groupItem.group?.id || '')\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleGroupSelection($event, groupItem.group?.id || '')\"\n >\n </th>\n }\n <th *ngFor=\"let column of columns(); trackBy: trackByColumnFn;let i =index\"\n style=\"text-align: center;\"\n [style.width.px]=\"column.field_size\"\n [style.minWidth.px]=\"column.field_size\"\n [resizeColumn]=\"true\"\n [columnConfig]=\"column\"\n [columnDraggable]=\"i\"\n class=\"column-header\">\n <div class=\"column-drag-handle\"></div>\n {{column.label}} {{column.symbol}}\n </th>\n </tr> -->\n <!-- @if(getRowsForGroup(group.id).length > 0 && group.isExpanded) { -->\n <!-- *cdkVirtualFor=\"let row of getRowsForGroupSignal(group.id)(); \n trackBy: trackByRowFn; \n let i = index\" -->\n <!-- @for(row of getRowsForGroupSignal(group.id)(); track trackByRowFn($index, row); let i = $index) { -->\n <ng-container\n *cdkVirtualFor=\"let row of getRowsForGroupSignal(group.id === null || group.id === undefined ? '__NULL_GROUP__' : group.id)(); trackBy: trackByRowFn; let i = index\">\n <tr class=\"row-item\" [attr.data-row-id]=\"i\" [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\" [style.cursor]=\"cursorOnHover() || null\" (click)=\"emitRowSelect(row, 'table', group)\">\n @if(gridStore.configuration().config.allowSelection) {\n <td class=\"checkbox-column\" style=\"text-align: center;\">\n <input type=\"checkbox\" [checked]=\"isRowSelected(row?.entity_id)\"\n (change)=\"toggleRowSelection($event, row)\">\n </td>\n }\n @if(shouldShowActionColumn('before')) {\n <td class=\"action-column\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\">\n <ng-container\n *ngTemplateOutlet=\"actionCell; context: { $implicit: row, mode: 'table', group: group }\"></ng-container>\n </td>\n }\n @if(hasHiddenColumns()) {\n <td class=\"row-expand-toggle\" (click)=\"toggleRowExpand(row, i, $event)\">\n <mat-icon class=\"row-expand-icon\" [class.expanded]=\"isRowExpanded(row, i)\">chevron_right</mat-icon>\n </td>\n }\n @for (column of visibleColumns(); track trackByColumnFn($index, column)) {\n <td #cell [style.width.px]=\"column.field_size\" [style.minWidth.px]=\"column.field_size\"\n class=\"data-cell\" [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\"\n [matTooltipClass]=\"'error-message'\" [matTooltip]=\"datacell.error()?'Error: ' + datacell.error():''\"\n matTooltipPosition=\"below\">\n <div class=\"cell-content\">\n <data-cell #datacell [personCardTemplate]=\"personCardTemplate\" [cellTemplate]=\"cellTemplate\" [td]=cell [fieldSize]=\"column.field_size\" [columnDatatype]=\"column.datatype\"\n [columnName]=\"column.name\" [value]=\"(row?.['entity_data']?.[column.name] ?? row?.[column.name]) || ''\" [column]=\"column\"\n [mode]=\"mode()\" [isEditable]=\"isEditable() && column.editable !== false && column.editable !== 'false'\" [drillable]=\"column.enableDrilldown || false\"\n [id]=\"i + '_' + column.name\" [eruGridStore]=\"gridStore\" [row]=\"row\"></data-cell>\n </div>\n </td>\n }\n @if(shouldShowActionColumn('after')) {\n <td class=\"action-column\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\">\n <ng-container\n *ngTemplateOutlet=\"actionCell; context: { $implicit: row, mode: 'table', group: group }\"></ng-container>\n </td>\n }\n </tr>\n @if(hasHiddenColumns() && isRowExpanded(row, i)) {\n <tr class=\"row-detail\">\n <td class=\"row-detail-cell\" [attr.colspan]=\"rowDetailColspan()\">\n <div class=\"row-detail-grid\">\n @for (hiddenCol of hiddenColumns(); track trackByColumnFn($index, hiddenCol)) {\n <div class=\"row-detail-field\">\n <span class=\"row-detail-label\">{{hiddenCol.label}}</span>\n <div class=\"row-detail-value\">\n <data-cell [cellTemplate]=\"cellTemplate\" [fieldSize]=\"hiddenCol.field_size\" [columnDatatype]=\"hiddenCol.datatype\"\n [columnName]=\"hiddenCol.name\" [value]=\"(row?.['entity_data']?.[hiddenCol.name] ?? row?.[hiddenCol.name]) || ''\"\n [column]=\"hiddenCol\" [mode]=\"mode()\" [isEditable]=\"isEditable()\"\n [drillable]=\"hiddenCol.enableDrilldown || false\"\n [id]=\"'detail_' + i + '_' + hiddenCol.name\" [eruGridStore]=\"gridStore\" [row]=\"row\"></data-cell>\n </div>\n </div>\n }\n </div>\n </td>\n </tr>\n }\n </ng-container>\n <!-- } -->\n <!-- } -->\n @if(group.isLoading && (group.isExpanded || !showGroupBar())) {\n @for(i of [].constructor(ghostRows()); let j = $index; track j) {\n <tr class=\"ghost-loading-row\" [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\">\n @if(gridStore.configuration().config.allowSelection) {\n <td class=\"checkbox-column ghost-cell-container\">\n <div class=\"ghost-cell\"></div>\n </td>\n\n }\n @if(shouldShowActionColumn('before')) {\n <td class=\"action-column ghost-cell-container\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\">\n <div class=\"ghost-cell\"></div>\n </td>\n }\n @if(hasHiddenColumns()) {\n <td class=\"row-expand-toggle ghost-cell-container\">\n <div class=\"ghost-cell\"></div>\n </td>\n }\n @for (column of visibleColumns(); track trackByColumnFn($index, column)) {\n <td [style.width.px]=\"column.field_size\" [style.minWidth.px]=\"column.field_size\"\n class=\"ghost-cell-container\">\n <div class=\"ghost-cell\"></div>\n </td>\n }\n @if(shouldShowActionColumn('after')) {\n <td class=\"action-column ghost-cell-container\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\">\n <div class=\"ghost-cell\"></div>\n </td>\n }\n </tr>\n }\n }\n <!-- <tr\n *ngIf=\"getRowsForGroup(group.id).length === 0 && !group.isExpanded\"\n class=\"group-separator\"\n >\n <td [attr.colspan]=\"groupSeperatorColSpan()\" class=\"separator-cell\"></td>\n </tr> -->\n <!-- Subtotal row at end of group (position: after) -->\n @if(enableRowSubtotals() && subtotalPosition() === 'after' && hasSubtotalData(group)) {\n <ng-container *ngTemplateOutlet=\"tableSubtotal; context: { group: group }\"></ng-container>\n }\n <!-- Grand Total row at end of group (position: after) - only for last group -->\n @if(enableGrandTotal() && grandTotalPosition() === 'after' && hasGrandTotalData() && i ===\n groups().length - 1) {\n <ng-container *ngTemplateOutlet=\"tableGrandTotal\"></ng-container>\n }\n }\n </tbody>\n </table>\n </div>\n </cdk-virtual-scroll-viewport>\n </ng-container>\n }\n </div>\n }\n </div>\n }\n</div>\n\n<!-- Pivot Table Header Template -->\n<ng-template #pivotTableHead>\n <thead [class.eru-wrap-headers]=\"wrapHeaders()\">\n @if (hasNestedHeaders()) {\n <ng-container>\n @for (headerRow of getHeaderRows(); track headerRow; let rowIndex = $index) {\n <tr class=\"pivot-header pivot-header-container\" [class.pivot-header-level]=\"'level-' + rowIndex\">\n @for (header of headerRow; track trackByHeaderFn($index, header); let colIndex = $index) {\n <th [attr.colspan]=\"header.colspan\" [attr.rowspan]=\"header.rowspan\"\n [resizeColumn]=\"isResizablePivotHeader(header)\"\n [columnConfig]=\"getFieldForPivotHeader(header) || $any(header)\"\n class=\"column-header pivot-column-header nested-header\"\n [class.row-dimension-header]=\"isRowDimensionHeader(header)\"\n [class.column-dimension-header]=\"!isRowDimensionHeader(header)\" [class.expanded]=\"header.isExpanded\"\n [class.collapsed]=\"!header.isExpanded\" [class.sticky-column]=\"isStickyColumn(header.name, colIndex)\"\n [class.design-clickable]=\"isDesignMode()\"\n [class.design-selected]=\"isDesignMode() && gridStore.selectedDesignColumn() === designTargetFor($any(header))\"\n [style.position]=\"isStickyColumn(header.name, colIndex) ? 'sticky' : 'static'\"\n [style.left.px]=\"getStickyColumnLeft(header.name, colIndex)\"\n [style.z-index]=\"isStickyColumn(header.name, colIndex) ? 100 : 1\"\n [style.min-height.px]=\"headerRowHeight()\" style=\"height: auto; padding: 8px 6px;\">\n <div class=\"header-content\">\n\n <data-cell [fieldSize]=\"header.field_size\" [columnDatatype]=\"header.dataType\" [columnName]=\"header.name\"\n [value]=\"header.label\" [column]=\"header\" [frozenGrandTotalCell]=\"true\"\n [drillable]=\"header.enableDrilldown || false\" [mode]=\"mode()\" [isEditable]=\"isEditable()\"\n [id]=\"'pivot_' + $index + '_' + header.name\" [eruGridStore]=\"gridStore\" [row]=\"header\">\n </data-cell>\n @if(isDesignMode()) {\n <mat-icon class=\"design-edit-icon\"\n [title]=\"isAggregationHeader($any(header)) ? 'Edit aggregation' : 'Edit column'\"\n (click)=\"onHeaderDesignClick($event, $any(header))\">tune</mat-icon>\n }\n <!-- <span class=\"header-label header-wrap-text\">{{header.label}}</span> -->\n <!-- <button *ngIf=\"!isRowDimensionHeader(header)\"\n class=\"collapse-toggle-btn\"\n [title]=\"header.isExpanded ? 'Collapse group' : 'Expand group'\"\n (click)=\"toggleColumnGroup(header.groupKey)\"\n type=\"button\">\n <span class=\"collapse-icon\">+</span>\n </button> -->\n </div>\n </th>\n }\n </tr>\n }\n </ng-container>\n } @else {\n <!-- Simple header fallback -->\n <ng-container>\n <tr class=\"pivot-header\" [class.freeze-header-enabled]=\"freezeHeader()\">\n @for (column of getLeafColumns(); track trackByColumnFn($index, column); let colIndex = $index) {\n <th [style.width.px]=\"column.field_size\" [style.minWidth.px]=\"column.field_size\"\n [resizeColumn]=\"gridStore.isFeatureEnabled('columnResizable')\" [columnConfig]=\"column\"\n class=\"column-header pivot-column-header\" [class.sticky-column]=\"isStickyColumn(column.name, colIndex)\"\n [class.design-clickable]=\"isDesignMode()\"\n [class.design-selected]=\"isDesignMode() && gridStore.selectedDesignColumn() === designTargetFor(column)\"\n [style.position]=\"isStickyColumn(column.name, colIndex) ? 'sticky' : 'static'\"\n [style.left.px]=\"getStickyColumnLeft(column.name, colIndex)\"\n [style.z-index]=\"isStickyColumn(column.name, colIndex) ? 100 : 1\"\n [style.min-height.px]=\"headerRowHeight()\" style=\"height: auto;padding: 8px 6px\">\n <!-- Label and control laid out as a row: the label truncates, the\n control keeps its place. Left as a bare text node the long\n aggregation labels pushed the icon past the cell edge, where\n `overflow: hidden` clipped it out of sight entirely. -->\n <div class=\"pivot-header-content\">\n <!-- Deliberately not `.column-label`: that class carries the\n wrap-headers rule, which broke these labels onto one word per\n line. This header truncates, as it did before. -->\n <span class=\"pivot-header-label\">{{column.label}}</span>\n @if(isDesignMode()) {\n <mat-icon class=\"design-edit-icon\"\n [title]=\"isAggregationHeader(column) ? 'Edit aggregation' : 'Edit column'\"\n (click)=\"onHeaderDesignClick($event, column)\">tune</mat-icon>\n }\n </div>\n </th>\n }\n </tr>\n </ng-container>\n }\n\n </thead>\n</ng-template>\n\n<!-- Column Group Template for consistent column widths -->\n<ng-template #pivotColGroup>\n <colgroup>\n @for (column of getLeafColumns(); track trackByColumnFn($index, column)) {\n <col\n [style]=\"'width: ' + column.field_size + 'px !important; min-width: ' + column.field_size + 'px !important; max-width: ' + column.field_size + 'px !important; --col-width: ' + column.field_size + 'px'\">\n }\n </colgroup>\n</ng-template>\n\n<ng-template #pivotGrandTotal>\n <tbody class=\"pivot-tbody\">\n @for (pivotRow of gridStore.pivotGrandTotalData(); track trackByPivotRowFn($index, pivotRow); let i = $index) {\n <tr class=\"pivot-row grand-total-row\"\n [class.grand-total-bold]=\"pivotRow._isGrandTotal && grandTotalStyle() === 'bold'\"\n [class.grand-total-italic]=\"pivotRow._isGrandTotal && grandTotalStyle() === 'italic'\"\n [class.grand-total-highlighted]=\"pivotRow._isGrandTotal && grandTotalStyle() === 'highlighted'\"\n [style.height.px]=\"50\" [attr.data-pivot-row]=\"i\">\n <!-- <td colspan=\"20\">{{pivotRow | json}}</td> -->\n @for (column of getLeafColumns(); track trackByColumnFn($index, column); let colIndex = $index) {\n <td [attr.rowspan]=\"getEffectiveRowspan(i, column.name)\" [style.width.px]=\"column.field_size\"\n [style.minWidth.px]=\"column.field_size\" class=\"pivot-cell\"\n [class.row-dimension-cell]=\"isRowDimensionColumn(column.name)\"\n [class.column-dimension-cell]=\"!isRowDimensionColumn(column.name)\"\n [class.aggregated-value]=\"!isRowDimensionColumn(column.name) && column.datatype === 'number'\"\n [class.rowspan-cell]=\"getEffectiveRowspan(i, column.name) || 1 > 1\"\n [class.sticky-column]=\"isStickyColumn(column.name, colIndex)\"\n [style.position]=\"isStickyColumn(column.name, colIndex) ? 'sticky' : 'static'\"\n [style.left.px]=\"getStickyColumnLeft(column.name, colIndex)\"\n [style.z-index]=\"isStickyColumn(column.name, colIndex) ? 99 : 1\" [style.height.px]=\"50\" [attr.xx]=\"i\">\n <div class=\"cell-content pivot-cell-content\">\n <data-cell [fieldSize]=\"column.field_size\" [columnDatatype]=\"column.datatype\" [columnName]=\"column.name\"\n [value]=\"getEffectiveCellValue(i,column.name, pivotRow)\" [column]=\"column\" [frozenGrandTotalCell]=\"true\"\n [drillable]=\"column.enableDrilldown || false\" [mode]=\"mode()\" [isEditable]=\"isEditable()\"\n [id]=\"'pivot_' + i + '_' + column.name\" [eruGridStore]=\"gridStore\" [row]=\"pivotRow\">\n </data-cell>\n </div>\n </td>\n }\n </tr>\n }\n </tbody>\n</ng-template>\n\n<!-- \u2500\u2500\u2500 Action column cell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n One template for both column positions. With no `config.actions` set it\n falls back to the single more_horiz icon the column has always shown, so\n grids that only listen to the store's actionClick signal keep working.\n Context: { $implicit: Row, mode: 'table' | 'board', group?: RowGroup }\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<ng-template #actionCell let-row let-mode=\"mode\" let-group=\"group\">\n @if(!hasConfiguredActions()) {\n <mat-icon (click)=\"onActionClick($event, row, undefined, mode || 'table', group)\">more_horiz</mat-icon>\n } @else if(actionDisplayType() === 'kebab') {\n @if(visibleActionsFor(row).length > 0) {\n <mat-icon class=\"action-kebab\" [matMenuTriggerFor]=\"rowActionMenu\"\n [matMenuTriggerData]=\"{ row: row, mode: mode || 'table', group: group }\"\n (click)=\"$event.stopPropagation()\">more_vert</mat-icon>\n }\n } @else {\n <div class=\"action-icons\">\n @for(action of visibleActionsFor(row); track action.action_name) {\n <mat-icon class=\"action-icon\" [fontSet]=\"actionIconFontSet()\" [matTooltip]=\"action.action_name\"\n matTooltipPosition=\"above\"\n (click)=\"onActionClick($event, row, action, mode || 'table', group)\">{{action.action_icon || 'play_arrow'}}</mat-icon>\n }\n </div>\n }\n</ng-template>\n\n<!-- Kebab menu shared by every row; the row is passed through matMenuTriggerData. -->\n<mat-menu #rowActionMenu=\"matMenu\" class=\"eru-grid-action-menu\">\n <ng-template matMenuContent let-row=\"row\" let-mode=\"mode\" let-group=\"group\">\n @for(action of visibleActionsFor(row); track action.action_name) {\n <button mat-menu-item (click)=\"onActionClick($event, row, action, mode || 'table', group)\">\n <mat-icon>{{action.action_icon || 'play_arrow'}}</mat-icon>\n <span>{{action.action_name}}</span>\n </button>\n }\n </ng-template>\n</mat-menu>\n\n<!-- Column Group Template for consistent column widths -->\n<ng-template #tableColGroup>\n <colgroup>\n @if(gridStore.configuration().config.allowSelection) {\n <col style=\"width: 40px; min-width: 40px; max-width: 40px;\">\n }\n @if(shouldShowActionColumn('before')) {\n <col\n [style]=\"'width: ' + actionColumnWidth() + 'px !important; min-width: ' + actionColumnWidth() + 'px !important; max-width: ' + actionColumnWidth() + 'px !important;'\">\n }\n @if(hasHiddenColumns()) {\n <col style=\"width: 40px !important; min-width: 40px !important; max-width: 40px !important;\">\n }\n @for (column of visibleColumns(); track trackByColumnFn($index, column)) {\n <col\n [style]=\"'width: ' + column.field_size + 'px !important; min-width: ' + column.field_size + 'px !important; max-width: ' + column.field_size + 'px !important; --col-width: ' + column.field_size + 'px'\">\n }\n @if(shouldShowActionColumn('after')) {\n <col\n [style]=\"'width: ' + actionColumnWidth() + 'px !important; min-width: ' + actionColumnWidth() + 'px !important; max-width: ' + actionColumnWidth() + 'px !important;'\">\n }\n </colgroup>\n</ng-template>\n\n\n<ng-template #tableHeader>\n\n <thead [class.eru-wrap-headers]=\"wrapHeaders()\">\n <!-- headerRowHeight rides on the row, not the cells: `thead.eru-wrap-headers\n th { height: auto }` outranks any class-level height we could put on a\n th, which is why a configured header height was ignored while data rows\n (inline height on tr.row-item) honoured theirs. On a table row `height`\n is a minimum, so a wrapped two-line header still grows past it. -->\n <tr [style.height.px]=\"headerRowHeight()\" [style.minHeight.px]=\"headerRowHeight()\">\n @if(gridStore.configuration().config.allowSelection) {\n <th class=\"checkbox-column column-header table-column-header\">\n <input type=\"checkbox\" [checked]=\"isAllGroupsSelected()\"\n [indeterminate]=\"!isAllGroupsSelected() && gridStore.hasSelection()\" (change)=\"toggleAllGroups($event)\">\n </th>\n }\n @if(shouldShowActionColumn('before')) {\n <th class=\"action-column column-header table-column-header\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\">Action</th>\n }\n @if(hasHiddenColumns()) {\n <th class=\"row-expand-toggle column-header table-column-header\"></th>\n }\n @for (column of visibleColumns(); track trackByColumnFn(i, column); let i = $index) {\n <th [style.width.px]=\"column.field_size\" [resizeColumn]=\"gridStore.isFeatureEnabled('columnResizable')\"\n [columnConfig]=\"column\" [index]=\"i\"\n [columnDraggable]=\"gridStore.isFeatureEnabled('columnReorderable') ? column.name : null\"\n [style.minWidth.px]=\"column.field_size\" class=\"column-header table-column-header\"\n [class.sortable-header]=\"isSortable()\"\n [class.design-clickable]=\"isDesignMode()\"\n [class.design-selected]=\"isDesignMode() && gridStore.selectedDesignColumn() === column.name\"\n [class.sort-asc]=\"isSortable() && getSortDirection(column.name) === 'asc'\"\n [class.sort-desc]=\"isSortable() && getSortDirection(column.name) === 'desc'\">\n @if(gridStore.isFeatureEnabled('columnReorderable')) {\n <div class=\"column-drag-handle\"></div>\n }\n <span class=\"column-label\" [title]=\"column.tool_tip || column.description || ''\">{{column.label}}</span>\n @if(isDesignMode()) {\n <mat-icon class=\"design-edit-icon\" title=\"Edit column\" (click)=\"onHeaderDesignClick($event, column)\">tune</mat-icon>\n }\n @if(isSortable()) {\n <span class=\"sort-indicator\">\n <span class=\"sort-triangles\">\n <span class=\"sort-tri sort-tri-up\" [class.sort-tri-active]=\"getSortDirection(column.name) === 'asc'\"\n (click)=\"onSortColumn($event, column, 'asc')\"></span>\n <span class=\"sort-tri sort-tri-down\" [class.sort-tri-active]=\"getSortDirection(column.name) === 'desc'\"\n (click)=\"onSortColumn($event, column, 'desc')\"></span>\n </span>\n @if(getSortPriority(column.name) !== null && gridStore.sortColumns().length > 1) {\n <span class=\"sort-priority\">{{getSortPriority(column.name)}}</span>\n }\n </span>\n }\n </th>\n }\n @if(shouldShowActionColumn('after')) {\n <th class=\"action-column column-header table-column-header\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\">Action</th>\n }\n </tr>\n </thead>\n</ng-template>\n\n<!-- Table Subtotal Row Template -->\n<ng-template #tableSubtotal let-group=\"group\">\n <tr class=\"subtotal-row\" [class.subtotal-bold]=\"subTotalStyle() === 'bold'\"\n [class.subtotal-italic]=\"subTotalStyle() === 'italic'\"\n [class.subtotal-highlighted]=\"subTotalStyle() === 'highlighted'\" [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\">\n @if(gridStore.configuration().config.allowSelection) {\n <td class=\"checkbox-column\"></td>\n }\n @if(shouldShowActionColumn('before')) {\n <td class=\"action-column\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\"></td>\n }\n @if(hasHiddenColumns()) {\n <td class=\"row-expand-toggle\"></td>\n }\n @for(column of visibleColumns(); track trackByColumnFn($index, column); let colIndex = $index) {\n <td [style.width.px]=\"column.field_size\" [style.minWidth.px]=\"column.field_size\" class=\"subtotal-cell\"\n [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\">\n <div class=\"cell-content\">\n @if(colIndex === 0 && getSubtotalValue(group, column.name) === null) {\n <span class=\"subtotal-label\">{{subtotalLabel()}}</span>\n } @else {\n @if(getSubtotalValue(group, column.name) !== null) {\n <data-cell [fieldSize]=\"column.field_size\" [columnDatatype]=\"column.datatype\" [columnName]=\"column.name\"\n [value]=\"getSubtotalValue(group, column.name)\" [column]=\"column\" [mode]=\"mode()\" [isEditable]=\"false\"\n [id]=\"'subtotal_' + group.id + '_' + column.name\" [eruGridStore]=\"gridStore\" [row]=\"group.subtotal\">\n </data-cell>\n }\n }\n </div>\n </td>\n }\n @if(shouldShowActionColumn('after')) {\n <td class=\"action-column\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\"></td>\n }\n </tr>\n</ng-template>\n\n<!-- Table Grand Total Row Template -->\n<ng-template #tableGrandTotal>\n <tr class=\"grand-total-row\" [class.grand-total-bold]=\"grandTotalStyle() === 'bold'\"\n [class.grand-total-italic]=\"grandTotalStyle() === 'italic'\"\n [class.grand-total-highlighted]=\"grandTotalStyle() === 'highlighted'\" [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\">\n @if(gridStore.configuration().config.allowSelection) {\n <td class=\"checkbox-column\"></td>\n }\n @if(shouldShowActionColumn('before')) {\n <td class=\"action-column\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\"></td>\n }\n @if(hasHiddenColumns()) {\n <td class=\"row-expand-toggle\"></td>\n }\n @for(column of visibleColumns(); track trackByColumnFn($index, column); let colIndex = $index) {\n <td [style.width.px]=\"column.field_size\" [style.minWidth.px]=\"column.field_size\" class=\"grand-total-cell\"\n [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\">\n <div class=\"cell-content\">\n @if(colIndex === 0 && getGrandTotalValue(column.name) === null) {\n <span class=\"grand-total-label\">Grand Total</span>\n } @else {\n @if(getGrandTotalValue(column.name) !== null) {\n <data-cell [fieldSize]=\"column.field_size\" [columnDatatype]=\"column.datatype\" [columnName]=\"column.name\"\n [value]=\"getGrandTotalValue(column.name)\" [column]=\"column\" [mode]=\"mode()\" [isEditable]=\"false\"\n [id]=\"'grandtotal_' + column.name\" [eruGridStore]=\"gridStore\" [row]=\"gridStore.rowGrandTotal()\">\n </data-cell>\n }\n }\n </div>\n </td>\n }\n @if(shouldShowActionColumn('after')) {\n <td class=\"action-column\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\"></td>\n }\n </tr>\n</ng-template>\n\n<!-- \u2500\u2500\u2500 Default board card template \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Used when no boardCardTemplate is passed to <eru-grid>.\n Context: { $implicit: Row, columns: Field[], group: RowGroup }\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<ng-template #defaultBoardCard let-row let-columns=\"columns\" let-group=\"group\">\n <mat-card class=\"board-card\">\n <mat-card-content>\n @for (column of columns; track column.name) {\n @if ((row?.entity_data?.[column.name] ?? row?.[column.name]) !== undefined) {\n <div class=\"board-card-field\">\n <span class=\"board-field-label\">{{ column.label }}</span>\n <data-cell\n [fieldSize]=\"column.field_size\"\n [columnDatatype]=\"column.datatype\"\n [columnName]=\"column.name\"\n [column]=\"column\"\n [value]=\"row?.entity_data?.[column.name] ?? row?.[column.name]\"\n [id]=\"row?.entity_id + '_' + column.name\"\n [eruGridStore]=\"gridStore\"\n [mode]=\"'board'\"\n [row]=\"row\">\n </data-cell>\n </div>\n }\n }\n </mat-card-content>\n <mat-card-actions align=\"end\">\n <button mat-icon-button (click)=\"onActionClick($event, row)\">\n <mat-icon>more_horiz</mat-icon>\n </button>\n </mat-card-actions>\n </mat-card>\n</ng-template>", styles: ["@charset \"UTF-8\";:root{--grid-primary: var(--mat-sys-primary, #6750a4);--grid-on-primary: var(--mat-sys-on-primary, #ffffff);--grid-primary-container: var(--mat-sys-primary-container, #eaddff);--grid-on-primary-container: var(--mat-sys-on-primary-container, #21005d);--grid-secondary: var(--mat-sys-secondary, #625b71);--grid-on-secondary: var(--mat-sys-on-secondary, #ffffff);--grid-secondary-container: var(--mat-sys-secondary-container, #e8def8);--grid-on-secondary-container: var(--mat-sys-on-secondary-container, #1d192b);--grid-tertiary: var(--mat-sys-tertiary, #7d5260);--grid-on-tertiary: var(--mat-sys-on-tertiary, #ffffff);--grid-tertiary-container: var(--mat-sys-tertiary-container, #ffd8e4);--grid-on-tertiary-container: var(--mat-sys-on-tertiary-container, #31111d);--grid-surface: var(--mat-sys-surface, #fef7ff);--grid-surface-variant: var(--mat-sys-surface-variant, #e7e0ec);--grid-surface-container: var(--mat-sys-surface-container, #f3edf7);--grid-surface-container-high: var(--mat-sys-surface-container-high, #ede7f0);--grid-on-surface: var(--mat-sys-on-surface, #1d1b20);--grid-on-surface-variant: var(--mat-sys-on-surface-variant, #49454f);--grid-outline: var(--mat-sys-outline, #79757f);--grid-outline-variant: var(--mat-sys-outline-variant, #cac4d0);--grid-error: var(--mat-sys-error, #ba1a1a);--grid-error-container: var(--mat-sys-error-container, #ffdad6);--grid-base-surface: var(--surface, #ffffff);--grid-base-on-surface: var(--on-surface, #000000);--grid-base-border: var(--border, #e5e7eb);--grid-primary-light: var(--grid-primary-container)}:host,eru-grid{display:block!important;width:100%;height:100%;flex:1 1 0%;max-height:var(--grid-height, none);min-height:var(--grid-min-height, 120px);font-family:var(--grid-font-family);--grid-font-family: \"Poppins\", \"Roboto\", -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;--grid-font-size-body: 12px;--grid-font-size-caption: 12px !important;--grid-line-height-body: 1;--grid-aggregation-text-align: right;--grid-number-text-align: right;--grid-spacing-xxs: 2px;--grid-spacing-xs: 4px;--grid-spacing-sm: 8px;--grid-spacing-md: 16px;--grid-spacing-lg: 24px;--grid-border-radius: 4px;--grid-elevation-1: 0px 1px 2px 0px rgba(0, 0, 0, .3), 0px 1px 3px 1px rgba(0, 0, 0, .15);--grid-elevation-2: 0px 1px 2px 0px rgba(0, 0, 0, .3), 0px 2px 6px 2px rgba(0, 0, 0, .15);--grid-row-hover: var(--grid-surface-variant);--grid-row-selected: var(--grid-surface-container-high);--grid-zebra-odd: transparent;--grid-zebra-even: transparent;--grid-focus-ring: var(--grid-primary);--grid-header-font-weight: 500;--grid-header-text-transform: none;--grid-header-letter-spacing: normal;--grid-header-font-size: var(--grid-font-size-caption);--grid-header-padding-x: 8px;--grid-header-padding-y: 12px;--grid-font-feature-numeric: normal;--grid-cell-padding-x: var(--grid-spacing-xs);--grid-cell-inset-x: 8px;--grid-cell-padding-y: var(--grid-spacing-xxs);--grid-tint-subtle: rgba(0, 0, 0, .025);--grid-tint-soft: rgba(0, 0, 0, .045);--grid-tint-strong: rgba(0, 0, 0, .08);--grid-radius-outer: 0;--grid-shadow-outer: none;--grid-divider-color: var(--grid-outline-variant);--grid-divider-width: 1px;--grid-header-bg: var(--grid-surface-container);--grid-header-color: var(--grid-on-surface);--grid-pill-radius: 999px;--grid-pill-padding-y: 3px;--grid-pill-padding-x: 10px;--grid-pill-font-size: 11px;--grid-pill-font-weight: 500;--grid-priority-dot-size: 8px;--grid-avatar-size: 24px;--grid-avatar-font-size: 10px;--grid-avatar-font-weight: 600;border-radius:var(--grid-radius-outer);box-shadow:var(--grid-shadow-outer)}eru-grid[data-preset=default]{--grid-header-bg: transparent;--grid-header-color: var(--grid-on-surface-variant);--grid-header-font-weight: 500;--grid-header-text-transform: uppercase;--grid-header-letter-spacing: .06em;--grid-header-font-size: 11px;--grid-header-padding-y: 12px;--grid-header-padding-x: 14px;--grid-cell-padding-y: 10px;--grid-cell-padding-x: 14px;--grid-row-hover: var(--grid-tint-subtle);--grid-divider-color: var(--grid-tint-soft);--grid-divider-width: 1px;--grid-font-feature-numeric: \"tnum\"}eru-grid[data-preset=modern]{--grid-header-bg: transparent;--grid-header-color: var(--grid-on-surface-variant);--grid-header-font-weight: 500;--grid-header-text-transform: none;--grid-header-letter-spacing: normal;--grid-header-font-size: 13px;--grid-header-padding-y: 16px;--grid-header-padding-x: 18px;--grid-cell-padding-y: 16px;--grid-cell-padding-x: 18px;--grid-row-hover: var(--grid-tint-soft);--grid-divider-color: var(--grid-tint-subtle);--grid-divider-width: 1px;--grid-radius-outer: 12px;--grid-font-feature-numeric: \"tnum\";--grid-pill-padding-y: 4px;--grid-pill-padding-x: 12px}eru-grid[data-preset=compact]{--grid-header-bg: var(--grid-surface-container);--grid-header-color: var(--grid-on-surface);--grid-header-font-weight: 600;--grid-header-text-transform: none;--grid-header-font-size: 11px;--grid-header-padding-y: 4px;--grid-header-padding-x: 8px;--grid-cell-padding-y: 3px;--grid-cell-padding-x: 8px;--grid-font-size-body: 11px;--grid-row-hover: var(--grid-tint-subtle);--grid-divider-color: var(--grid-tint-subtle);--grid-divider-width: 1px;--grid-font-feature-numeric: \"tnum\";--grid-pill-padding-y: 1px;--grid-pill-padding-x: 6px;--grid-pill-font-size: 10px}eru-grid[data-preset=bold]{--grid-header-bg: var(--grid-surface-container-high);--grid-header-color: var(--grid-on-surface);--grid-header-font-weight: 700;--grid-header-text-transform: none;--grid-header-font-size: 13px;--grid-header-padding-y: 14px;--grid-header-padding-x: 12px;--grid-cell-padding-y: 10px;--grid-cell-padding-x: 12px;--grid-row-hover: var(--grid-tint-soft);--grid-divider-color: var(--grid-tint-strong);--grid-divider-width: 1px;--grid-radius-outer: 2px;--grid-font-feature-numeric: \"tnum\"}eru-grid[data-preset=financial]{--grid-header-bg: var(--grid-surface-container);--grid-header-color: var(--grid-on-surface-variant);--grid-header-font-weight: 500;--grid-header-text-transform: uppercase;--grid-header-letter-spacing: .08em;--grid-header-font-size: 11px;--grid-header-padding-y: 12px;--grid-header-padding-x: 14px;--grid-cell-padding-y: 10px;--grid-cell-padding-x: 14px;--grid-zebra-odd: transparent;--grid-zebra-even: var(--grid-tint-subtle);--grid-row-hover: var(--grid-tint-soft);--grid-divider-color: var(--grid-tint-subtle);--grid-divider-width: 1px;--grid-font-feature-numeric: \"tnum\"}eru-grid[data-preset=elevated]{--grid-header-bg: transparent;--grid-header-color: var(--grid-on-surface-variant);--grid-header-font-weight: 600;--grid-header-text-transform: none;--grid-header-font-size: 12px;--grid-header-padding-y: 16px;--grid-header-padding-x: 18px;--grid-cell-padding-y: 14px;--grid-cell-padding-x: 18px;--grid-row-hover: var(--grid-tint-soft);--grid-divider-color: var(--grid-tint-subtle);--grid-divider-width: 1px;--grid-radius-outer: 16px;--grid-shadow-outer: 0 1px 3px rgba(0, 0, 0, .06), 0 10px 28px rgba(0, 0, 0, .07);--grid-font-feature-numeric: \"tnum\";--grid-pill-padding-y: 4px;--grid-pill-padding-x: 12px;overflow:hidden}.group-container{padding-bottom:8px}.column-header.design-clickable .design-edit-icon{font-size:16px;width:16px;height:16px;margin-left:4px;opacity:.45;vertical-align:middle;cursor:pointer}.column-header.design-clickable:hover .design-edit-icon,.column-header.design-clickable .design-edit-icon:hover{opacity:1}.column-header.design-selected{background-color:var(--grid-primary-container, rgba(63, 81, 181, .12))}.pivot-column-header .pivot-header-content{display:flex;align-items:center;justify-content:center;gap:4px;min-width:0}.pivot-column-header .pivot-header-content .pivot-header-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.pivot-column-header .pivot-header-content .design-edit-icon,.pivot-column-header .header-content .design-edit-icon{flex:0 0 auto}.pivot-column-header .header-content data-cell,.pivot-column-header .header-content data-cell *{color:inherit!important}.incremental-row-container{width:100%;height:100%;min-height:var(--grid-min-height, 120px);max-height:none;overflow:auto;position:relative;background-color:var(--grid-surface);border-radius:var(--grid-border-radius);font-family:var(--grid-font-family)}.viewport{height:100%;min-height:300px;overflow-x:auto;overflow-y:auto;background-color:var(--grid-surface);scrollbar-gutter:stable}.viewport.apply-cdk-width{width:calc(var(--table-total-width) + 10px)!important}.groups-viewport{height:100%;min-height:300px}.groups-scroll-container{max-height:var(--grid-height, 600px);overflow-y:auto;overflow-x:hidden}.table-viewport{background-color:var(--grid-surface);height:var(--table-height, auto);min-height:var(--table-min-height, 100px);overflow-x:auto;overflow-y:auto}.pivot-viewport{min-height:var(--table-min-height, 300px);overflow-x:auto;overflow-y:auto;background-color:var(--grid-surface)}.pivot-viewport .cdk-virtual-scroll-content-wrapper{width:auto;height:auto}.table-wrapper{min-width:100%;overflow-x:visible}.incremental-row-container .eru-grid-table,.eru-grid-table{width:100%!important;border-collapse:separate;border-spacing:0;table-layout:fixed!important;background-color:var(--grid-surface);color:var(--grid-on-surface);font-family:var(--grid-font-family);font-size:var(--grid-font-size-body);line-height:var(--grid-line-height-body)}.eru-grid-table th,.eru-grid-table td{text-align:left;overflow:hidden!important;text-overflow:ellipsis!important;white-space:nowrap!important;color:var(--grid-on-surface);min-width:0;max-width:100%!important;box-sizing:border-box;position:relative}.eru-grid-table th{background-color:var(--grid-header-bg, var(--grid-surface-container))}thead.eru-wrap-headers th{white-space:normal!important;overflow:visible!important;text-overflow:clip!important;height:auto}thead.eru-wrap-headers th .column-label,thead.eru-wrap-headers th .header-label{white-space:normal!important;overflow:visible!important;text-overflow:clip!important;word-break:break-word;overflow-wrap:anywhere}.eru-grid-table tbody td{background-color:transparent}.eru-grid-table thead{background-color:var(--grid-header-bg, var(--grid-surface-container));transform:translateZ(0);will-change:transform;backface-visibility:hidden}.eru-grid-table thead.freeze-header-enabled{position:sticky!important;top:0!important;z-index:100!important}.eru-grid-table thead th{background-color:var(--grid-header-bg, var(--grid-surface-container));color:var(--grid-header-color, var(--grid-on-surface));font-family:var(--grid-font-family);font-weight:var(--grid-header-font-weight);font-size:var(--grid-header-font-size)}.checkbox-column{width:50px;min-width:50px;max-width:50px;text-align:center!important;padding-left:0!important;padding-right:0!important;text-overflow:clip!important;background-color:var(--grid-surface-container)}.checkbox-column input[type=checkbox]{width:16px;height:16px;cursor:pointer;accent-color:var(--grid-primary);border-radius:var(--grid-border-radius)}.checkbox-column input[type=checkbox]:focus{outline:2px solid var(--grid-primary);outline-offset:2px}.action-column{text-align:center!important;padding-left:0!important;padding-right:0!important;text-overflow:clip!important;background-color:var(--grid-surface-container)}.action-column mat-icon{font-size:18px;width:18px;height:18px;line-height:18px;color:var(--grid-outline);cursor:pointer}.action-column mat-icon:hover{color:var(--grid-primary)}.action-column .action-icons{display:flex;align-items:center;justify-content:center;gap:6px;overflow-x:auto;scrollbar-width:none}.action-column .action-icons::-webkit-scrollbar{display:none}.action-column .action-icon{flex:0 0 auto}.eru-grid-action-menu .mat-mdc-menu-item mat-icon{margin-right:8px;font-size:18px;width:18px;height:18px;line-height:18px;color:var(--grid-on-surface-variant)}.group-header{background-color:var(--grid-surface-container);color:var(--grid-on-surface);font-size:var(--grid-font-size-caption);font-weight:500;border-bottom:1px solid var(--grid-outline);cursor:pointer;transition:background-color .2s ease}.group-header:hover{background-color:var(--grid-surface-container-high)}.group-header .group-title{font-weight:600;color:var(--grid-primary)}.group-header .group-row-count{color:var(--grid-on-surface-variant);font-size:var(--grid-font-size-caption);margin-left:var(--grid-spacing-sm)}.row-item{background-color:var(--grid-surface);transition:background-color .15s ease}.row-item:nth-child(odd){background-color:var(--grid-zebra-odd, var(--grid-surface))}.row-item:nth-child(2n){background-color:var(--grid-zebra-even, var(--grid-surface))}.row-item:hover{background-color:var(--grid-row-hover)}.required-toggle-row{background-color:var(--grid-surface-container, #f3edf7);border-bottom:1px solid var(--grid-outline-variant, #cac4d0)}.required-toggle-row .required-toggle-cell{padding:4px 8px!important;text-align:center;vertical-align:middle;position:relative}.required-toggle-row .required-toggle-cell .required-label{position:absolute;top:2px;left:4px;font-size:10px;color:var(--grid-on-surface-variant, #49454f);font-weight:400;text-transform:lowercase}.required-toggle-row .required-toggle-cell mat-checkbox{display:flex;justify-content:center;align-items:center}.table-column-header{padding:0 var(--grid-header-padding-x);height:var(--grid-header-row-height, auto)}.column-header{font-weight:var(--grid-header-font-weight);text-transform:var(--grid-header-text-transform);letter-spacing:var(--grid-header-letter-spacing);text-align:center!important;font-size:var(--grid-header-font-size);position:relative;-webkit-user-select:none;user-select:none;--grid-header-affordance-space: 0px;--grid-column-resizer-width: 10px;--grid-header-sort-right: calc(var(--grid-column-resizer-width) + 2px);--grid-header-design-right: calc(var(--grid-column-resizer-width) + 2px);background-color:var(--grid-header-bg, var(--grid-surface-container));color:var(--grid-header-color, var(--grid-on-surface))}.column-header:hover{background-color:var(--grid-header-hover-bg, var(--grid-surface-container-high))}.column-drag-handle{position:absolute;left:0;top:0;bottom:0;width:12px;cursor:grab;opacity:0;transition:opacity .2s ease,background-color .2s ease;z-index:2;display:flex;align-items:center;justify-content:center;border-right:1px solid transparent}.column-drag-handle:after{content:\"\\22ee\\22ee\";font-size:14px;color:var(--grid-on-surface-variant);transform:rotate(90deg)}.column-drag-handle:hover{background-color:var(--grid-surface-container-high);border-right-color:var(--grid-outline)}.column-header:hover .column-drag-handle{opacity:1}.column-drag-handle:active{cursor:grabbing}.table-column-header.sortable-header{--grid-header-affordance-space: 20px}.table-column-header.design-clickable{--grid-header-affordance-space: 28px}.table-column-header.sortable-header.design-clickable{--grid-header-affordance-space: 40px;--grid-header-design-right: calc(var(--grid-column-resizer-width) + 13px)}.table-column-header .column-label{display:block;padding-right:var(--grid-header-affordance-space)}.table-column-header .sort-indicator,.table-column-header .design-edit-icon{position:absolute;top:50%;transform:translateY(-50%);margin-left:0}.table-column-header .sort-indicator{right:var(--grid-header-sort-right)}.table-column-header .design-edit-icon{right:var(--grid-header-design-right)}.sortable-header{cursor:pointer}.sortable-header .sort-indicator{display:inline-flex;align-items:center;gap:2px;cursor:pointer;opacity:0;transition:opacity .15s ease}.sortable-header .sort-indicator .sort-triangles{display:flex;flex-direction:column;align-items:center;gap:2px}.sortable-header .sort-indicator .sort-tri{width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;cursor:pointer;transition:border-color .15s ease}.sortable-header .sort-indicator .sort-tri-up{border-bottom:5px solid var(--grid-on-surface-variant, #49454f);opacity:.3}.sortable-header .sort-indicator .sort-tri-down{border-top:5px solid var(--grid-on-surface-variant, #49454f);opacity:.3}.sortable-header .sort-indicator .sort-tri-active{opacity:1}.sortable-header .sort-indicator .sort-tri-active.sort-tri-up{border-bottom-color:var(--grid-primary, #6750a4)}.sortable-header .sort-indicator .sort-tri-active.sort-tri-down{border-top-color:var(--grid-primary, #6750a4)}.sortable-header .sort-indicator .sort-priority{font-size:9px;font-weight:600;color:var(--grid-primary, #6750a4);line-height:1;min-width:12px;text-align:center}.sortable-header:hover .sort-indicator,.sortable-header.sort-asc .sort-indicator,.sortable-header.sort-desc .sort-indicator{opacity:1}.sortable-header:hover .sort-indicator .sort-tri:not(.sort-tri-active){opacity:.6}.sort-asc,.sort-desc{background-color:var(--grid-surface-container-low, rgba(103, 80, 164, .04))}.dragging{opacity:1;background-color:var(--grid-surface-container);box-shadow:var(--grid-elevation-2)}.drag-over{background-color:var(--grid-surface-container);border-color:var(--grid-primary)}.data-cell{background-color:transparent;color:var(--grid-on-surface);font-family:var(--grid-font-family);font-size:var(--grid-font-size-body);font-feature-settings:var(--grid-font-feature-numeric);padding:0 var(--grid-cell-padding-x)}.cell-content{align-items:center}.cell-content .mdc-text-field{padding:0px var(--grid-spacing-xxs)!important}.cell-display-text{align-items:center;padding:0px var(--grid-spacing-xs)}.ghost-loading-row{background-color:transparent}.ghost-cell-container{padding:var(--grid-spacing-sm)}.ghost-cell{height:20px;width:100%;background-color:var(--grid-surface-container);animation:pulse 1.5s ease-in-out infinite;border-radius:var(--grid-border-radius)}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.column-resizer{position:absolute;right:0;top:0;bottom:0;width:4px;cursor:col-resize;background-color:transparent;transition:background-color .2s ease}.column-resizer:hover{background-color:var(--grid-primary)}.group-separator{height:var(--grid-spacing-sm);background-color:var(--grid-surface-variant)}.group-separator .separator-cell{background-color:var(--grid-surface-variant);border:none;height:var(--grid-spacing-sm)}.error-state{background-color:var(--grid-error-container);color:var(--grid-error);border-color:var(--grid-error)}.error-message{background-color:var(--grid-error);color:#fff;padding:var(--grid-spacing-sm);border-radius:var(--grid-border-radius);font-size:var(--grid-font-size-caption)}.incremental-row-container .eru-grid-table tbody,.incremental-row-container .eru-grid-table{position:relative}.incremental-row-container .eru-grid-table.show-column-lines{border-right:var(--grid-outline-width, 1px) solid var(--grid-outline, #e0e0e0)!important;border-top:var(--grid-outline-width, 1px) solid var(--grid-outline, #e0e0e0)!important}.incremental-row-container .eru-grid-table.show-column-lines:not(.freeze-header){border-bottom:var(--grid-outline-width, 1px) solid var(--grid-outline, #e0e0e0)!important}.incremental-row-container .eru-grid-table:not(.show-column-lines){border:var(--grid-outline-width, 1px) solid var(--grid-outline, #e0e0e0)!important}.incremental-row-container .eru-grid-table thead:after{content:\"\";position:absolute;bottom:0;left:0;right:0;height:calc(var(--grid-divider-width, 1px) * 2);background-color:var(--grid-divider-color, var(--grid-outline, #e0e0e0));pointer-events:none;z-index:10}.incremental-row-container .eru-grid-table.show-column-lines thead th,.incremental-row-container .eru-grid-table.show-column-lines tbody td{border-left:var(--grid-divider-width, 1px) solid var(--grid-divider-color, var(--grid-outline, #e0e0e0))!important}.incremental-row-container .eru-grid-table.show-row-lines thead th,.incremental-row-container .eru-grid-table.show-row-lines tbody td{border-bottom:var(--grid-divider-width, 1px) solid var(--grid-divider-color, var(--grid-outline, #e0e0e0))!important}@media(max-width:768px){.incremental-row-container{height:600px}.eru-grid-table th,.eru-grid-table td{font-size:var(--grid-font-size-caption)}.checkbox-column{width:40px;min-width:40px;max-width:40px}}@media(prefers-contrast:high){.eru-grid-table th,.eru-grid-table td{border-width:2px}.row-item:hover{border-width:2px;border-color:var(--grid-primary)}}@media(prefers-reduced-motion:reduce){.row-item,.column-drag-handle,.ghost-cell{transition:none;animation:none}}.pivot-table .nested-header{text-align:center;font-weight:600;background:var(--grid-surface-container)}.pivot-table .nested-header.row-dimension-header{background:var(--grid-surface-container);font-weight:600}.pivot-table .pivot-header-leafcols{padding:0;margin:0;height:0}.pivot-table .pivot-header-level.level-0 .nested-header{font-size:14px;padding:12px 8px}.pivot-table .pivot-header-level.level-1 .nested-header{font-size:13px;padding:10px 6px}.pivot-table .pivot-header-level.level-2 .nested-header{font-size:12px;padding:8px 4px}.pivot-table .nested-header:hover{background:var(--grid-surface-variant);color:var(--grid-primary);transition:all .2s ease}.pivot-table .pivot-cell.aggregated-value{font-weight:500;font-family:Roboto Mono,monospace}.pivot-table .pivot-cell-content{display:flex;justify-content:center;align-items:center;min-height:38px}.pivot-table .pivot-repeated-value .cell-content,.pivot-table .pivot-repeated-value .pivot-cell-content{visibility:hidden}.pivot-table .pivot-group-start.row-dimension-cell{border-top:1px solid var(--grid-outline, #79757f)}.pivot-mode .incremental-row-container{display:flex;flex-direction:column;height:auto;max-height:85vh;overflow:auto}.pivot-mode .h-shell{position:relative;width:calc(100% - var(--scrollbar-width, 17px))!important;top:0;z-index:1;overflow-x:hidden;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}.pivot-mode .h-shell::-webkit-scrollbar{display:none}.pivot-mode .gt-shell{position:relative;bottom:50px;flex-shrink:0;overflow-x:hidden;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}.pivot-mode .gt-shell::-webkit-scrollbar{display:none}.pivot-mode .gt-shell table{border-bottom:var(--grid-outline-width, 1px) solid var(--grid-outline, #e0e0e0)!important}.pivot-mode .gt-shell.adjust-bottom-vs{bottom:66px!important}.pivot-mode .gt-shell.adjust-bottom:not(.adjust-bottom-vs){bottom:calc(66px - var(--scrollbar-width, 17px))!important}.pivot-mode .header-shell{flex-shrink:0;width:100%;box-sizing:border-box;padding-right:var(--scrollbar-width, 17px);overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}.pivot-mode .header-shell::-webkit-scrollbar{display:none}.pivot-mode .header-shell.apply-cdk-width{width:calc(var(--table-total-width) + 10px)!important}.pivot-mode .header-shell .eru-grid-table{margin-bottom:0;width:100%;table-layout:fixed}.pivot-mode .header-shell .eru-grid-table thead{background:var(--grid-surface-container)}.pivot-mode .header-shell .eru-grid-table thead th{background:var(--grid-surface-container);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important}.pivot-mode .header-shell .eru-grid-table thead th.sticky-column{position:sticky;background:var(--grid-surface-container);z-index:111}.pivot-mode .header-shell .eru-grid-table tbody td{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important}.pivot-mode .pivot-container{display:flex;flex-direction:column;height:100%;width:100%;overflow:hidden}.pivot-mode .pivot-table{width:auto!important;min-width:100%!important;table-layout:fixed!important}.pivot-mode .pivot-table colgroup col{width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important;flex:none!important;flex-shrink:0!important;flex-grow:0!important}.pivot-mode .pivot-table td,.pivot-mode .pivot-table th{box-sizing:border-box!important;flex:none!important;flex-shrink:0!important;flex-grow:0!important;word-wrap:break-word!important;word-break:break-all!important}.pivot-mode .pivot-table{table-layout:fixed!important;width:100%!important}.pivot-mode .pivot-table *{max-width:var(--col-width)!important;box-sizing:border-box!important}.pivot-mode .pivot-table colgroup{width:100%!important}.pivot-mode .pivot-table colgroup col{width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important;flex-basis:var(--col-width)!important;flex:0 0 var(--col-width)!important}.pivot-mode .pivot-table table{width:100%!important;table-layout:fixed!important;border-collapse:collapse!important;border-spacing:0!important}.pivot-mode .pivot-table[style*=--table-total-width]{width:var(--table-total-width)!important;min-width:var(--table-total-width)!important;max-width:var(--table-total-width)!important}.pivot-mode .pivot-table colgroup col{width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important;flex:0 0 var(--col-width)!important;flex-basis:var(--col-width)!important;flex-grow:0!important;flex-shrink:0!important;overflow:hidden!important}.pivot-mode .pivot-table tbody td,.pivot-mode .pivot-table thead th{width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important;overflow:hidden!important;text-overflow:ellipsis!important;white-space:nowrap!important}.pivot-mode .pivot-table .cell-content,.pivot-mode .pivot-table data-cell{width:100%!important;max-width:100%!important;overflow:hidden!important;text-overflow:ellipsis!important;white-space:nowrap!important;display:block!important}.pivot-mode .pivot-table table{width:var(--table-total-width)!important;min-width:var(--table-total-width)!important;max-width:var(--table-total-width)!important;table-layout:fixed!important;border-collapse:collapse!important;border-spacing:0!important;word-wrap:break-word!important;word-break:break-all!important}.pivot-mode .pivot-tbody tr.pivot-row{min-height:var(--grid-data-row-height, 50px)!important;height:var(--grid-data-row-height, 50px)!important}.pivot-mode .pivot-tbody tr.pivot-row:hover{background-color:var(--grid-surface-variant)}.pivot-mode .pivot-tbody tr.pivot-row:nth-child(2n){background-color:#00000005}.pivot-mode .pivot-tbody tr.pivot-row td{min-height:var(--grid-data-row-height, 50px)!important;height:var(--grid-data-row-height, 50px)!important;vertical-align:middle;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important}.pivot-mode .pivot-tbody tr.pivot-row td .cell-content{min-height:calc(var(--grid-data-row-height, 50px) - 2px);display:flex;align-items:center;justify-content:center}.pivot-mode .pivot-tbody tr.pivot-row td .cell-content data-cell{width:100%;min-height:calc(var(--grid-data-row-height, 50px) - 4px);display:flex;align-items:center;justify-content:center;overflow:hidden;flex-shrink:0}.pivot-mode .pivot-cell{vertical-align:middle;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important}.pivot-mode .pivot-cell.aggregated-value{font-weight:500;font-family:Roboto Mono,monospace}.pivot-mode .pivot-cell .cell-content{display:flex;justify-content:center;align-items:center;min-height:var(--grid-header-row-height, 40px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex-shrink:0}.pivot-mode .pivot-table .subtotal-row{background-color:var(--grid-surface-container)!important;font-weight:600}.pivot-mode .pivot-table .subtotal-row td{background-color:var(--grid-surface-container);color:var(--grid-on-surface-variant)}.pivot-mode .pivot-table .subtotal-row td:first-child{color:var(--grid-primary)}.pivot-mode .pivot-table .subtotal-row td.aggregated-value{font-weight:500;color:var(--grid-primary)}.pivot-mode .pivot-table .subtotal-row:hover{background-color:var(--grid-surface-container-high)!important}.pivot-mode .pivot-table .subtotal-row:hover td{background-color:var(--grid-surface-container-high)}.pivot-mode .pivot-table .subtotal-bold td{font-weight:600!important;font-style:normal!important}.pivot-mode .pivot-table .subtotal-bold td.aggregated-value{font-weight:600!important}.pivot-mode .pivot-table .subtotal-italic td{font-style:italic!important}.pivot-mode .pivot-table .subtotal-italic td:first-child{font-weight:600!important}.pivot-mode .pivot-table .subtotal-italic td.aggregated-value{font-style:italic!important;font-weight:500!important}.pivot-mode .pivot-table .subtotal-highlighted{background-color:var(--grid-surface-variant)!important}.pivot-mode .pivot-table .subtotal-highlighted td{background-color:var(--grid-surface-variant)!important;font-weight:700!important;font-style:normal!important;color:var(--grid-primary)!important}.pivot-mode .pivot-table .subtotal-highlighted td.aggregated-value{font-weight:500!important;color:var(--grid-primary)!important}.pivot-mode .pivot-table .subtotal-highlighted:hover,.pivot-mode .pivot-table .subtotal-highlighted:hover td{background-color:var(--grid-surface-container-high)!important}.pivot-mode .pivot-table .grand-total-row{background-color:var(--grid-surface-container-high)!important;font-weight:700;font-size:var(--grid-font-size-body)}.pivot-mode .pivot-table .grand-total-row td{background-color:var(--grid-surface-container-high)!important;color:var(--grid-on-surface)}.pivot-mode .pivot-table .grand-total-row td:first-child{font-style:normal;font-weight:800;color:var(--grid-primary)}.pivot-mode .pivot-table .grand-total-row td.aggregated-value{font-weight:500;color:var(--grid-primary);font-family:Roboto Mono,monospace}.pivot-mode .pivot-table .grand-total-row:hover,.pivot-mode .pivot-table .grand-total-row:hover td{background-color:var(--grid-surface-container-high)!important}.pivot-mode .pivot-table .grand-total-bold td{font-weight:700!important;font-style:normal!important}.pivot-mode .pivot-table .grand-total-bold td.aggregated-value{font-weight:700!important}.pivot-mode .pivot-table .grand-total-italic td,.pivot-mode .pivot-table .grand-total-italic td.aggregated-value{font-style:italic!important;font-weight:500!important}.pivot-mode .pivot-table .grand-total-highlighted{background-color:var(--grid-primary)!important;box-shadow:var(--grid-elevation-2)!important}.pivot-mode .pivot-table .grand-total-highlighted td{background-color:var(--grid-primary)!important;color:var(--grid-on-primary)!important;font-weight:500!important;font-style:normal!important}.pivot-mode .pivot-table .grand-total-highlighted td.aggregated-value{color:var(--grid-on-primary)!important;font-weight:500!important}.pivot-mode .pivot-table .grand-total-highlighted:hover,.pivot-mode .pivot-table .grand-total-highlighted:hover td{background-color:var(--grid-primary)!important}.pivot-mode .pivot-table .collapsible-header{position:relative}.pivot-mode .pivot-table .collapsible-header .header-content{display:flex;align-items:center;justify-content:space-between;gap:var(--grid-spacing-xs);padding:var(--grid-spacing-xs) var(--grid-spacing-sm)}.pivot-mode .pivot-table .collapsible-header .header-label{flex:1;font-weight:600}.pivot-mode .pivot-table .collapsible-header .collapse-toggle-btn{background:none;border:none;cursor:pointer;padding:var(--grid-spacing-xxs);margin:0;display:flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:var(--grid-border-radius);color:var(--grid-on-surface-variant);transition:all .2s ease;font-size:12px;font-weight:600}.pivot-mode .pivot-table .collapsible-header .collapse-toggle-btn:hover{background-color:var(--grid-surface-container);color:var(--grid-primary);transform:scale(1.1)}.pivot-mode .pivot-table .collapsible-header .collapse-toggle-btn:focus{outline:2px solid var(--grid-primary);outline-offset:1px}.pivot-mode .pivot-table .collapsible-header .collapse-toggle-btn .collapse-icon{display:block;line-height:1;font-family:monospace;font-size:14px}.pivot-mode .pivot-table .collapsible-header.expanded .collapse-toggle-btn .collapse-icon{color:var(--grid-primary)}.pivot-mode .pivot-table .collapsible-header.collapsed{background-color:var(--grid-surface-variant)}.pivot-mode .pivot-table .collapsible-header.collapsed .header-label{font-style:italic;color:var(--grid-on-surface-variant)}.pivot-mode .pivot-table .collapsible-header.collapsed .collapse-toggle-btn .collapse-icon{color:var(--grid-outline)}.pivot-mode .pivot-table .collapsible-header:hover{background-color:var(--grid-surface-container)}.pivot-mode .pivot-table .collapsible-header:hover .header-label{color:var(--grid-on-surface)}.pivot-mode .pivot-table .pivot-single-table{display:flex;flex-direction:column;height:100%;width:100%;overflow:hidden;min-height:var(--table-min-height)!important}.pivot-mode .pivot-table .pivot-single-table .pivot-header-container{flex-shrink:0;background:var(--grid-surface)!important;overflow-x:auto;overflow-y:hidden;min-height:100px!important;height:auto!important}.pivot-mode .pivot-table .pivot-single-table .pivot-header-container .pivot-table{width:auto;min-width:100%;height:auto!important;min-height:100px!important}.pivot-mode .pivot-table .pivot-single-table .pivot-header-container .pivot-table th{background:var(--grid-surface-container)!important;padding:8px 6px!important;white-space:nowrap;min-width:50px;min-height:40px!important;height:auto!important;position:relative;visibility:visible!important;color:var(--grid-on-surface)!important}.pivot-mode .pivot-table .pivot-single-table .pivot-header-container .pivot-table th.sticky-column{position:sticky!important;background:var(--grid-surface-container)!important;border-right:2px solid var(--grid-primary)!important;box-shadow:2px 0 4px #0000001a;z-index:101!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container{flex:1;overflow:auto;min-height:300px!important;height:auto!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-viewport{height:100%!important;width:100%!important;overflow-x:auto!important;overflow-y:auto!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-table,.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-data-table{width:auto;min-width:100%;height:auto!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-table td,.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-data-table td{padding:8px 6px!important;white-space:nowrap;min-width:50px;min-height:32px!important;height:auto!important;background:var(--grid-surface)!important;color:var(--grid-on-surface)!important;visibility:visible!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-table td.sticky-column,.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-data-table td.sticky-column{position:sticky!important;background:var(--grid-surface-container)!important;border-right:2px solid var(--grid-primary)!important;box-shadow:2px 0 4px #0000001a;z-index:100!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-table tbody tr,.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-data-table tbody tr{height:auto!important;min-height:50px!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-table tbody tr.pivot-row,.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-data-table tbody tr.pivot-row{visibility:visible!important;display:table-row!important}.pivot-mode .pivot-table .collapsed-column-group{background-color:var(--grid-surface-container);border-left:3px solid var(--grid-primary)}.pivot-mode .pivot-table .collapsed-column-group:hover{background-color:var(--grid-surface-container-high)}.pivot-row.subtotal-row{background-color:var(--grid-surface-variant);font-weight:500}.pivot-row.subtotal-row.subtotal-bold{font-weight:500}.pivot-row.subtotal-row.subtotal-italic{font-style:italic}.pivot-row.subtotal-row.subtotal-highlighted{background-color:var(--grid-primary);color:var(--grid-on-primary)}.pivot-row.grand-total-row{background-color:var(--grid-surface-container);font-weight:600}.pivot-row.grand-total-row.grand-total-bold{font-weight:800}.pivot-row.grand-total-row.grand-total-italic{font-style:italic}.pivot-row.grand-total-row.grand-total-highlighted{background-color:var(--grid-primary);color:var(--grid-on-primary)}.pivot-row.first-visible-row{background-color:#6750a41a!important;position:relative}.pivot-row.first-visible-row:before{content:\"\\1f441\\fe0f First Visible\";position:absolute;top:-20px;left:0;background:var(--grid-primary);color:var(--grid-on-primary);padding:2px 6px;font-size:10px;border-radius:2px;z-index:1000}.header-wrap-text{white-space:pre-wrap;word-break:auto-phrase}.group-header-row{display:flex;align-items:center;justify-content:space-between;width:100%;padding-right:12px}.custom-collapse-header{background-color:var(--grid-surface-variant);padding:8px 20px;border-top-left-radius:12px;border-top-right-radius:12px;cursor:pointer;display:flex;width:fit-content;align-items:center;-webkit-user-select:none;user-select:none;min-width:200px;margin-bottom:10px;position:sticky;left:1px;z-index:116}.custom-collapse-header .collapse-arrow{display:inline-block;margin-right:8px;font-size:12px;color:var(--grid-on-surface-variant);transition:transform .2s ease;transform:rotate(0)}.custom-collapse-header .collapse-arrow.rotate-arrow{transform:rotate(270deg)}.custom-collapse-header .f-12{font-size:12px;color:var(--grid-on-surface)}.custom-collapse-header .group-sort-indicator{display:inline-flex;align-items:center;margin-left:8px}.custom-collapse-header .group-sort-indicator .sort-triangles{display:flex;flex-direction:column;align-items:center;gap:2px}.custom-collapse-header .group-sort-indicator .sort-tri{width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;cursor:pointer;transition:border-color .15s ease}.custom-collapse-header .group-sort-indicator .sort-tri-up{border-bottom:5px solid var(--grid-on-surface-variant, #49454f);opacity:.3}.custom-collapse-header .group-sort-indicator .sort-tri-down{border-top:5px solid var(--grid-on-surface-variant, #49454f);opacity:.3}.custom-collapse-header .group-sort-indicator .sort-tri-active{opacity:1}.custom-collapse-header .group-sort-indicator .sort-tri-active.sort-tri-up{border-bottom-color:var(--grid-primary, #6750a4)}.custom-collapse-header .group-sort-indicator .sort-tri-active.sort-tri-down{border-top-color:var(--grid-primary, #6750a4)}.excel-download-icon{cursor:pointer}.excel-download-icon:hover{opacity:.75}.excel-download-bar{display:flex;justify-content:flex-end;padding:4px 12px;flex-shrink:0}.table-mode .header-shell{flex-shrink:0;width:100%;box-sizing:border-box;padding-right:var(--scrollbar-width, 17px);overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}.table-mode .header-shell::-webkit-scrollbar{display:none}.table-mode .header-shell.apply-cdk-width{width:calc(var(--table-total-width) + 10px)!important}.table-mode .subtotal-row{background-color:var(--grid-surface-container)!important;font-weight:600}.table-mode .subtotal-row td{background-color:var(--grid-surface-container);color:var(--grid-on-surface-variant)}.table-mode .subtotal-row td:first-child{color:var(--grid-primary)}.table-mode .subtotal-row td.subtotal-cell{font-weight:500}.table-mode .subtotal-row td.subtotal-cell .subtotal-label{font-weight:600;color:var(--grid-primary);padding-left:var(--grid-cell-padding-x)}.table-mode .subtotal-row:hover{background-color:var(--grid-surface-container-high)!important}.table-mode .subtotal-row:hover td{background-color:var(--grid-surface-container-high)}.table-mode .subtotal-row.subtotal-bold td{font-weight:600!important;font-style:normal!important}.table-mode .subtotal-row.subtotal-italic td{font-style:italic!important}.table-mode .subtotal-row.subtotal-italic td:first-child{font-weight:600!important}.table-mode .subtotal-row.subtotal-highlighted{background-color:var(--grid-surface-variant)!important}.table-mode .subtotal-row.subtotal-highlighted td{background-color:var(--grid-surface-variant)!important;font-weight:700!important;font-style:normal!important;color:var(--grid-primary)!important}.table-mode .subtotal-row.subtotal-highlighted:hover,.table-mode .subtotal-row.subtotal-highlighted:hover td{background-color:var(--grid-surface-container-high)!important}.table-mode .grand-total-row{background-color:var(--grid-surface-container-high)!important;font-weight:700}.table-mode .grand-total-row td{background-color:var(--grid-surface-container-high);color:var(--grid-on-surface)}.table-mode .grand-total-row td:first-child{color:var(--grid-primary)}.table-mode .grand-total-row td.grand-total-cell{font-weight:600}.table-mode .grand-total-row td.grand-total-cell .grand-total-label{font-weight:700;color:var(--grid-primary);padding-left:var(--grid-cell-padding-x)}.table-mode .grand-total-row:hover{background-color:var(--grid-surface-container-high)!important}.table-mode .grand-total-row:hover td{background-color:var(--grid-surface-container-high)}.table-mode .grand-total-row.grand-total-bold td{font-weight:700!important;font-style:normal!important}.table-mode .grand-total-row.grand-total-italic td{font-style:italic!important}.table-mode .grand-total-row.grand-total-italic td:first-child{font-weight:700!important}.table-mode .grand-total-row.grand-total-highlighted{background-color:var(--grid-surface-variant)!important}.table-mode .grand-total-row.grand-total-highlighted td{background-color:var(--grid-surface-variant)!important;font-weight:800!important;font-style:normal!important;color:var(--grid-primary)!important}.table-mode .grand-total-row.grand-total-highlighted:hover,.table-mode .grand-total-row.grand-total-highlighted:hover td{background-color:var(--grid-surface-container-high)!important}.table-mode .subtotal-row-shell{width:100%;box-sizing:border-box;padding-right:var(--scrollbar-width, 17px);overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}.table-mode .subtotal-row-shell::-webkit-scrollbar{display:none}.board-mode-host{overflow:hidden;display:flex;flex-direction:column;max-height:var(--grid-height, 600px)}.board-mode-host .board-view-container{display:flex;flex-direction:column;flex:1;min-height:0}.board-mode-host .board-sort-bar{display:flex;align-items:center;gap:6px;padding:8px 16px;flex-shrink:0;border-bottom:1px solid var(--grid-outline-variant, #cac4d0);background:var(--grid-surface, #fffbfe);overflow-x:auto}.board-mode-host .board-sort-bar .board-sort-label{font-size:12px;font-weight:500;color:var(--grid-on-surface-variant, #49454f);white-space:nowrap}.board-mode-host .board-sort-bar .board-sort-chip{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:16px;border:1px solid var(--grid-outline-variant, #cac4d0);background:var(--grid-surface, #fffbfe);color:var(--grid-on-surface, #1d1b20);font-size:12px;white-space:nowrap}.board-mode-host .board-sort-bar .board-sort-chip-active{background:var(--grid-surface-container);border-color:var(--grid-outline, #79757f);color:var(--grid-on-surface, #1d1b20)}.board-mode-host .board-sort-bar .board-sort-chip-label{pointer-events:none}.board-mode-host .board-sort-bar .board-sort-chip-arrow{font-size:10px;line-height:1;cursor:pointer;padding:2px;border-radius:4px}.board-mode-host .board-sort-bar .board-sort-chip-arrow:hover{background:#00000014}.board-mode-host .board-sort-bar .board-sort-chip-priority{font-size:9px;font-weight:700;background:var(--grid-primary, #6750a4);color:var(--grid-on-primary, #ffffff);border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center}.board-mode-host .board-sort-bar .board-sort-chip-remove{font-size:10px;cursor:pointer;padding:2px;border-radius:4px;color:var(--grid-on-surface-variant, #49454f)}.board-mode-host .board-sort-bar .board-sort-chip-remove:hover{background:#00000014;color:var(--grid-error, #b3261e)}.board-mode-host .board-sort-bar .board-sort-add-btn{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:16px;border:1px dashed var(--grid-outline-variant, #cac4d0);background:transparent;color:var(--grid-on-surface-variant, #49454f);font-size:12px;cursor:pointer;white-space:nowrap;transition:background .15s ease,border-color .15s ease}.board-mode-host .board-sort-bar .board-sort-add-btn .board-sort-add-icon{font-size:14px;width:14px;height:14px}.board-mode-host .board-sort-bar .board-sort-add-btn:hover{background:var(--grid-surface-container-low, #f7f2fa);border-color:var(--grid-primary, #6750a4);color:var(--grid-primary, #6750a4)}.board-mode-host .board-sort-bar .board-sort-clear{display:inline-flex;align-items:center;padding:4px 10px;border-radius:16px;border:1px solid var(--grid-error, #b3261e);background:transparent;color:var(--grid-error, #b3261e);font-size:12px;cursor:pointer;white-space:nowrap;transition:background .15s ease}.board-mode-host .board-sort-bar .board-sort-clear:hover{background:#b3261e14}.board-mode-host .board-columns-wrapper{display:grid;grid-template-columns:repeat(auto-fit,minmax(var(--board-col-min-width, 300px),var(--board-col-max-width, 1fr)));grid-auto-rows:auto;align-items:start;gap:16px;flex:1;min-height:0;overflow-x:hidden;overflow-y:auto;align-content:start;justify-content:start}.board-mode-host .board-columns-wrapper.board-columns-nowrap{grid-auto-flow:column;grid-template-columns:none;grid-template-rows:auto;grid-auto-rows:auto;grid-auto-columns:minmax(var(--board-col-min-width, 300px),var(--board-col-max-width, 1fr));overflow-x:auto;overflow-y:hidden}.board-mode-host .board-column{max-height:var(--board-column-height, 420px);min-width:0;display:flex;flex-direction:column;background:var(--grid-surface-container, #f3edf7);border-radius:12px;min-height:0;overflow:hidden}.board-mode-host .board-column.board-column-accented{border-top:3px solid var(--board-group-color, transparent)}.board-mode-host .board-column .column-header{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 14px;font-weight:600;flex-shrink:0;background-color:transparent}.board-mode-host .board-column .column-header:hover{background-color:transparent}.board-mode-host .board-column .column-header .column-header-title{font-size:15px;font-weight:700;letter-spacing:.2px;line-height:1.2;color:var(--grid-on-surface, #1d1b20);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.board-mode-host .board-column .column-header .column-header-title-cell{display:inline-flex;align-items:center;flex:1 1 auto;min-width:0;overflow:visible;text-overflow:clip}.board-mode-host .board-column .column-header .column-header-title-cell data-cell,.board-mode-host .board-column .column-header .column-header-title-cell .data-cell-component,.board-mode-host .board-column .column-header .column-header-title-cell .container{padding:0!important;margin:0!important;border:none!important;background:transparent!important;min-height:0!important;height:auto!important;width:auto!important;max-width:100%!important;overflow:visible!important}.board-mode-host .board-column .column-header .column-header-title-cell .status-display,.board-mode-host .board-column .column-header .column-header-title-cell .status-display-content,.board-mode-host .board-column .column-header .column-header-title-cell .status-text,.board-mode-host .board-column .column-header .column-header-title-cell .tag-display,.board-mode-host .board-column .column-header .column-header-title-cell .tag-text{max-width:none!important;overflow:visible!important;text-overflow:clip!important}.board-mode-host .board-column .column-header .column-header-count{font-size:10px;font-weight:600;color:var(--eru-board-count-color, var(--grid-on-surface-variant, #49454f));background:var(--eru-board-count-bg, var(--grid-surface-variant, #e7e0ec));border-radius:10px;padding:3px 10px;white-space:nowrap;flex-shrink:0}.board-mode-host .board-column-body{flex:0 1 auto;min-height:0}.board-mode-host .board-card-container{box-sizing:border-box;overflow:hidden;border-radius:8px;transition:background-color .15s ease,box-shadow .15s ease}.board-mode-host .board-card-container.show-row-lines{box-shadow:inset 0 0 0 var(--grid-divider-width, 1px) var(--grid-divider-color, var(--grid-outline, #e0e0e0))}.board-mode-host .board-card-container:hover{background-color:var(--eru-board-card-hover-bg, color-mix(in srgb, var(--mat-sys-primary, #1976d2) 8%, transparent))}.board-mode-host .board-card-container.selected{background-color:var(--eru-board-card-selected-bg, color-mix(in srgb, var(--mat-sys-primary, #1976d2) 14%, transparent));box-shadow:inset 0 0 0 2px var(--eru-board-card-selected-outline, var(--mat-sys-primary, #1976d2))}.board-mode-host .board-card{height:calc(100% - 8px);overflow:hidden;cursor:pointer}.board-mode-host .board-card mat-card-title{font-size:13px}.board-mode-host .board-card mat-card-subtitle{font-size:12px}.board-mode-host .board-card-field{display:flex;flex-direction:column;margin-bottom:4px}.board-mode-host .board-field-label{font-size:10px;color:var(--grid-on-surface-variant, #49454f);font-weight:500;text-transform:uppercase;letter-spacing:.5px}.board-mode-host .board-ghost-card{margin:8px;padding:16px;background:var(--grid-surface, #fef7ff);border-radius:8px;animation:board-pulse 1.5s ease-in-out infinite}.board-mode-host .board-ghost-line{height:12px;background:var(--grid-surface-variant, #e7e0ec);border-radius:4px;margin-bottom:8px}.board-mode-host .board-ghost-line--short{width:60%}@keyframes board-pulse{0%,to{opacity:1}50%{opacity:.5}}th.row-expand-toggle,td.row-expand-toggle{width:40px!important;min-width:40px!important;max-width:40px!important;padding:0!important;text-align:center;vertical-align:middle;cursor:pointer;-webkit-user-select:none;user-select:none;box-sizing:border-box}.row-expand-icon{font-size:20px;width:20px;height:20px;line-height:20px;color:var(--grid-on-surface-variant);transition:transform .15s ease-in-out}.row-expand-icon.expanded{transform:rotate(90deg)}.row-detail{background:var(--grid-surface-container)}.row-detail .row-detail-cell{padding:var(--grid-spacing-sm) var(--grid-spacing-md);border-bottom:1px solid var(--grid-outline-variant)}.row-detail .row-detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:var(--grid-spacing-sm) var(--grid-spacing-md)}.row-detail .row-detail-field{display:flex;flex-direction:column;gap:var(--grid-spacing-xxs);min-width:0}.row-detail .row-detail-label{font-size:var(--grid-font-size-caption);color:var(--grid-on-surface-variant);font-weight:500}.row-detail .row-detail-value{min-width:0}.row-detail .row-detail-value data-cell{display:block;width:100%}.selection-banner{display:flex;align-items:center;gap:8px;padding:6px 12px;font-size:12px;background:#eef4ff;border-bottom:1px solid #c7d8f5;color:#1a3a6b}.selection-banner-action{background:none;border:none;padding:0;font:inherit;color:#1558d6;font-weight:600;cursor:pointer;text-decoration:underline}\n"], dependencies: [{ kind: "component", type: DataCellComponent, selector: "data-cell", inputs: ["eruGridStore", "fieldSize", "columnDatatype", "columnName", "column", "value", "id", "frozenGrandTotalCell", "td", "drillable", "mode", "isEditable", "row", "personCardTemplate", "cellTemplate"], outputs: ["tdChange"] }, { kind: "ngmodule", type: ScrollingModule }, { kind: "directive", type: i1$3.ɵɵCdkFixedSizeVirtualScroll, selector: "cdk-virtual-scroll-viewport[itemSize]", inputs: ["itemSize", "minBufferPx", "maxBufferPx"] }, { kind: "directive", type: i1$3.ɵɵCdkVirtualForOf, selector: "[cdkVirtualFor][cdkVirtualForOf]", inputs: ["cdkVirtualForOf", "cdkVirtualForTrackBy", "cdkVirtualForTemplate", "cdkVirtualForTemplateCacheSize"] }, { kind: "component", type: i1$3.ɵɵCdkVirtualScrollViewport, selector: "cdk-virtual-scroll-viewport", inputs: ["orientation", "appendOnly"], outputs: ["scrolledIndexChange"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$4.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$4.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i5.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i5.MatCardActions, selector: "mat-card-actions", inputs: ["align"], exportAs: ["matCardActions"] }, { kind: "directive", type: i5.MatCardContent, selector: "mat-card-content" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1$2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i7.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i7.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i7.MatMenuContent, selector: "ng-template[matMenuContent]" }, { kind: "directive", type: i7.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "directive", type: ResizeColumnDirective, selector: "[resizeColumn]", inputs: ["resizeColumn", "index", "columnConfig", "gridConfig"] }, { kind: "directive", type: ColumnDragDirective, selector: "[columnDraggable]", inputs: ["columnDraggable"] }, { kind: "component", type: ColumnDesignPanelComponent, selector: "eru-column-design-panel" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
16295
16875
  }
16296
16876
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: EruGridComponent, decorators: [{
16297
16877
  type: Component,
@@ -16311,7 +16891,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
16311
16891
  ResizeColumnDirective,
16312
16892
  ColumnDragDirective,
16313
16893
  ColumnDesignPanelComponent
16314
- ], template: "<!-- <div style=\"background: #f0f0f0; font-size: 12px; border-bottom: 1px solid #ccc;\">\ncurrentPivotScrollIndex {{currentPivotScrollIndex()}} |\nfirstDataRowIndex {{firstDataRowIndex()}} |\nfirstTr {{firstTr}} |\nmaxDepth {{maxDepth()}}\n</div> -->\n<ng-template #excelDownloadIcon>\n <svg class=\"excel-download-icon\" title=\"Download Excel\" (click)=\"onExcelDownloadClick($event)\"\n xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 48 48\" width=\"24px\" height=\"24px\">\n <path fill=\"#169154\" d=\"M29,6H15.744C14.781,6,14,6.781,14,7.744v7.259h15V6z\" />\n <path fill=\"#18482a\" d=\"M14,33.054v7.202C14,41.219,14.781,42,15.743,42H29v-8.946H14z\" />\n <path fill=\"#0c8045\" d=\"M14 15.003H29V24.005000000000003H14z\" />\n <path fill=\"#17472a\" d=\"M14 24.005H29V33.055H14z\" />\n <g>\n <path fill=\"#29c27f\" d=\"M42.256,6H29v9.003h15V7.744C44,6.781,43.219,6,42.256,6z\" />\n <path fill=\"#27663f\" d=\"M29,33.054V42h13.257C43.219,42,44,41.219,44,40.257v-7.202H29z\" />\n <path fill=\"#19ac65\" d=\"M29 15.003H44V24.005000000000003H29z\" />\n <path fill=\"#129652\" d=\"M29 24.005H44V33.055H29z\" />\n </g>\n <path fill=\"#0c7238\"\n d=\"M22.319,34H5.681C4.753,34,4,33.247,4,32.319V15.681C4,14.753,4.753,14,5.681,14h16.638 C23.247,14,24,14.753,24,15.681v16.638C24,33.247,23.247,34,22.319,34z\" />\n <path fill=\"#fff\"\n d=\"M9.807 19L12.193 19 14.129 22.754 16.175 19 18.404 19 15.333 24 18.474 29 16.123 29 14.013 25.07 11.912 29 9.526 29 12.719 23.982z\" />\n </svg>\n</ng-template>\n\n<div class=\"incremental-row-container eru-grid\" #rowContainer [class.pivot-mode]=\"gridStore.isPivotMode()\"\n [class.table-mode]=\"!gridStore.isPivotMode() && !isBoardMode()\" [class.board-mode-host]=\"isBoardMode()\">\n <eru-column-design-panel></eru-column-design-panel>\n <!-- Pivot Mode Template -->\n @if (gridStore.isPivotMode()) {\n <ng-container>\n <div class=\"pivot-container\" style=\"display: flex; flex-direction: column; height: 100%;\"\n [style]=\"'--table-min-height: ' + getInitialMinHeightPx() + 'px; --table-total-width: ' + getInitialTotalWidth() + 'px'\">\n @if(showExcelDownload()) {\n <div class=\"excel-download-bar\">\n <ng-container *ngTemplateOutlet=\"excelDownloadIcon\"></ng-container>\n </div>\n }\n <!-- Debug info for first visible row -->\n\n\n <div class=\"pivot-single-table\"\n style=\"height: 100%; width: 100%; overflow: hidden; display: flex; flex-direction: column;\">\n @if (freezeHeader()) {\n <div #headerScroller class=\"header-shell\">\n <table class=\"eru-grid-table pivot-table\"\n [style]=\"'width: auto; min-width: 100%; --table-total-width: ' + getInitialTotalWidth() + 'px'\"\n [class.show-column-lines]=\"showColumnLines()\" [class.show-row-lines]=\"showRowLines()\">\n <!-- Column Groups for consistent width -->\n <ng-container *ngTemplateOutlet=\"pivotColGroup\"></ng-container>\n\n <ng-container *ngTemplateOutlet=\"pivotTableHead\"></ng-container>\n @if(grandTotalPosition() === 'before' && freezeGrandTotal()) {\n <ng-container *ngTemplateOutlet=\"pivotGrandTotal\"></ng-container>\n }\n </table>\n </div>\n }\n <!-- Virtual Scrolled Table Body -->\n <div>\n <cdk-virtual-scroll-viewport #vp [itemSize]=\"dataRowHeight()\" class=\"viewport pivot-viewport\"\n [class.apply-cdk-width]=\"applyCdkWidth()\" (scrolledIndexChange)=\"onPivotScroll($event)\"\n (scroll)=\"onBodyScroll($event)\" style=\"overflow: auto;\">\n <table class=\"eru-grid-table pivot-table\"\n [style]=\"'width: auto; min-width: 100%; --table-total-width: ' + getInitialTotalWidth() + 'px'\"\n [class.show-column-lines]=\"showColumnLines()\" [class.show-row-lines]=\"showRowLines()\">\n <!-- Column Groups for consistent width -->\n <ng-container *ngTemplateOutlet=\"pivotColGroup\"></ng-container>\n\n @if (!freezeHeader()) {\n <ng-container *ngTemplateOutlet=\"pivotTableHead\"></ng-container>\n }\n <!-- Table Body with Virtual Scrolling -->\n <tbody class=\"pivot-tbody\">\n\n <tr *cdkVirtualFor=\"let pivotRow of gridStore.pivotDisplayData(); \n trackBy: trackByPivotRowFn; \n let i = index\" class=\"pivot-row\" [class.subtotal-row]=\"pivotRow._isSubtotal\"\n [class.grand-total-row]=\"pivotRow._isGrandTotal\"\n [class.subtotal-bold]=\"pivotRow._isSubtotal && subTotalStyle() === 'bold'\"\n [class.subtotal-italic]=\"pivotRow._isSubtotal && subTotalStyle() === 'italic'\"\n [class.subtotal-highlighted]=\"pivotRow._isSubtotal && subTotalStyle() === 'highlighted'\"\n [class.grand-total-bold]=\"pivotRow._isGrandTotal && grandTotalStyle() === 'bold'\"\n [class.grand-total-italic]=\"pivotRow._isGrandTotal && grandTotalStyle() === 'italic'\"\n [class.grand-total-highlighted]=\"pivotRow._isGrandTotal && grandTotalStyle() === 'highlighted'\"\n [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\" [style.cursor]=\"cursorOnHover() || null\" [attr.data-pivot-row]=\"i\">\n @if ((!pivotRow._isGrandTotal && freezeGrandTotal() ) || (!freezeGrandTotal() )) {\n @for (column of getLeafColumns(); track trackByColumnFn($index, column); let colIndex = $index) {\n <td [style.width.px]=\"column.field_size\" [style.minWidth.px]=\"column.field_size\" class=\"pivot-cell\"\n [class.row-dimension-cell]=\"isRowDimensionColumn(column.name)\"\n [class.column-dimension-cell]=\"!isRowDimensionColumn(column.name)\"\n [class.aggregated-value]=\"!isRowDimensionColumn(column.name) && column.datatype === 'number'\"\n [class.pivot-repeated-value]=\"isRepeatedDimensionValue(i, column.name)\"\n [class.pivot-group-start]=\"isPivotGroupStart(i, column.name)\"\n [class.sticky-column]=\"isStickyColumn(column.name, colIndex)\"\n [style.position]=\"isStickyColumn(column.name, colIndex) ? 'sticky' : 'static'\"\n [style.left.px]=\"getStickyColumnLeft(column.name, colIndex)\"\n [style.z-index]=\"isStickyColumn(column.name, colIndex) ? 99 : 1\" [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\">\n <div class=\"cell-content pivot-cell-content\">\n <data-cell [personCardTemplate]=\"personCardTemplate\" [class.aggregation]=\"!!column.aggregationFunction\" [fieldSize]=\"column.field_size\"\n [columnDatatype]=\"column.datatype\" [columnName]=\"column.name\" [value]=\"pivotRow[column.name]\"\n [column]=\"column\" [drillable]=\"column.enableDrilldown || false\" [mode]=\"mode()\"\n [isEditable]=\"isEditable()\" [id]=\"'pivot_' + i + '_' + column.name\" [eruGridStore]=\"gridStore\"\n [row]=\"pivotRow\">\n </data-cell>\n </div>\n </td>\n }\n } @else {\n <td [style.height.px]=\"dataRowHeight()\" [attr.colspan]=\"getLeafColumns().length\"> </td>\n }\n </tr>\n </tbody>\n </table>\n </cdk-virtual-scroll-viewport>\n\n </div>\n @if (freezeGrandTotal() && grandTotalPosition() === 'after') {\n <div #gtScroller class=\"header-shell gt-shell\" [class.adjust-bottom]=\"!applyCdkWidth()\"\n [class.adjust-bottom-vs]=\"adjustScrollWidth()\">\n <table class=\"eru-grid-table pivot-table\"\n [style]=\"'width: auto; min-width: 100%; --table-total-width: ' + getInitialTotalWidth() + 'px'\"\n [class.show-column-lines]=\"showColumnLines()\" [class.show-row-lines]=\"showRowLines()\">\n <ng-container *ngTemplateOutlet=\"pivotColGroup\"></ng-container>\n\n <ng-container *ngTemplateOutlet=\"pivotGrandTotal\"></ng-container>\n\n </table>\n </div>\n }\n\n\n </div>\n </div>\n </ng-container>\n } @else if (isBoardMode()) {\n <!-- Board Mode Template -->\n <div class=\"board-view-container\">\n @if(showExcelDownload()) {\n <div class=\"excel-download-bar\">\n <ng-container *ngTemplateOutlet=\"excelDownloadIcon\"></ng-container>\n </div>\n }\n @if(showSortBar()) {\n <div class=\"board-sort-bar\">\n <span class=\"board-sort-label\">Sort by:</span>\n @for (entry of gridStore.sortColumns(); track getFieldName(entry)) {\n <span class=\"board-sort-chip board-sort-chip-active\">\n <span class=\"board-sort-chip-label\">{{getColumnLabel(entry)}}</span>\n <span class=\"board-sort-chip-arrow\" (click)=\"onBoardSortChipToggle($event, entry)\">\n @if(!entry.startsWith('-')) { \u25B2 } @else { \u25BC }\n </span>\n @if(gridStore.sortColumns().length > 1) {\n <span class=\"board-sort-chip-priority\">{{getSortPriority(getFieldName(entry))}}</span>\n }\n <span class=\"board-sort-chip-remove\" (click)=\"onBoardSortChipRemove($event, entry)\">\u2715</span>\n </span>\n }\n <button class=\"board-sort-add-btn\" [matMenuTriggerFor]=\"sortFieldMenu\">\n <mat-icon class=\"board-sort-add-icon\">add</mat-icon> Add field\n </button>\n <mat-menu #sortFieldMenu=\"matMenu\" class=\"board-sort-menu\">\n @for (column of columns(); track column.name) {\n <button mat-menu-item (click)=\"onBoardSortFieldSelect(column)\"\n [disabled]=\"getSortDirection(column.name) !== null\">\n @if(getSortDirection(column.name) !== null) {\n <mat-icon>check</mat-icon>\n } @else {\n <mat-icon></mat-icon>\n }\n {{column.label}}\n </button>\n }\n </mat-menu>\n @if(gridStore.sortColumns().length > 0) {\n <button class=\"board-sort-clear\" (click)=\"onBoardSortClear()\">\u2715 Clear</button>\n }\n </div>\n }\n <div class=\"board-columns-wrapper\" [class.board-columns-nowrap]=\"!boardWrapColumns()\">\n @for (group of groups(); track group.id) {\n <div class=\"board-column\" [class.board-column-accented]=\"!!boardGroupColor(group)\"\n [style.--board-group-color]=\"boardGroupColor(group)\">\n @if (showBoardColumnHeader()) {\n <div class=\"column-header\">\n <!-- Render the group value through the same read-only cell renderer a\n data cell uses, so the grouped field's datatype formats itself\n (status/tag pills, dates, numbers) instead of printing raw text. -->\n @if (groupByColumn(); as gcol) {\n <span class=\"column-header-title column-header-title-cell\">\n <data-cell\n [eruGridStore]=\"gridStore\"\n [column]=\"gcol\"\n [columnDatatype]=\"gcol.datatype\"\n [columnName]=\"gcol.name\"\n [value]=\"group.title\"\n [id]=\"'board-group-' + group.id\"\n [fieldSize]=\"0\"\n [isEditable]=\"false\"\n [mode]=\"'board-group-header'\">\n </data-cell>\n </span>\n } @else {\n <span class=\"column-header-title\">{{ group.title }}</span>\n }\n <span class=\"column-header-count\">{{ group.currentLoadedRows || 0 }} of {{ group.totalRowCount || 0 }}</span>\n </div>\n }\n <cdk-virtual-scroll-viewport [attr.data-group-id]=\"group.id\" [itemSize]=\"boardCardSlot\" class=\"board-column-body\"\n [style.height.px]=\"boardColumnBodyHeight(group)\"\n (scrolledIndexChange)=\"onBoardScrolledIndexChange($event, group)\">\n <div\n *cdkVirtualFor=\"let row of getRowsForGroupSignal(group.id === null || group.id === undefined ? '__NULL_GROUP__' : group.id)(); templateCacheSize: 0\"\n class=\"board-card-container\"\n [class.selected]=\"isBoardCardActive(row)\"\n [class.show-row-lines]=\"showRowLines()\"\n [style.height.px]=\"boardCardOuterHeight\"\n [style.padding.px]=\"boardCardPadding\"\n [style.marginBottom.px]=\"boardCardGap\"\n [style.cursor]=\"cursorOnHover() || null\"\n (click)=\"emitRowSelect(row, 'board', group)\">\n <!-- Custom template when consumer provides boardCardTemplate; default card otherwise -->\n <ng-container\n *ngTemplateOutlet=\"boardCardTemplate ?? defaultBoardCard;\n context: { $implicit: row, columns: visibleBoardFields(), group: group }\">\n </ng-container>\n </div>\n </cdk-virtual-scroll-viewport>\n @if (group.isLoading) {\n <div class=\"board-ghost-card\">\n <div class=\"board-ghost-line\"></div>\n <div class=\"board-ghost-line board-ghost-line--short\"></div>\n </div>\n }\n </div>\n }\n </div>\n </div>\n } @else {\n\n <!-- Table Mode Template -->\n @if(showExcelDownload() && !showGroupBar()) {\n <div class=\"excel-download-bar\">\n <ng-container *ngTemplateOutlet=\"excelDownloadIcon\"></ng-container>\n </div>\n }\n <!-- Scrollable groups container \u2014 plain iteration avoids CDK fixed-height estimation errors -->\n <div #groupsScrollContainer class=\"groups-scroll-container\" (scroll)=\"onGroupsViewportScroll($event)\">\n\n @for (group of groups(); track trackByGroupFn($index, group); let i = $index) {\n <div class=\"group-container\"\n [attr.data-group-id]=\"group.id === null || group.id === undefined ? '__NULL_GROUP__' : group.id\">\n <!-- Combined sticky header with group info and table -->\n <div style=\"\n background:var(--grid-surface);\n position: sticky;\n top: 0;\n z-index: 115;\n \">\n @if(showGroupBar()) {\n <div class=\"group-header-row\">\n <div class=\"custom-collapse-header\" (click)=\"toggleGroupCollapse(group.id)\">\n <span class=\"collapse-arrow\" [ngClass]=\"{\n 'rotate-arrow': group.isExpanded,\n }\">\u25BC</span>\n <span class=\"f-12\">\n {{ group?.title || \"\" }}\n {{ group?.currentLoadedRows || 0 }} -\n {{ group?.totalRowCount || 0 }} rows...</span>\n @if(groupByField() && isSortable()) {\n <span class=\"group-sort-indicator\">\n <span class=\"sort-triangles\">\n <span class=\"sort-tri sort-tri-up\" [class.sort-tri-active]=\"getSortDirection(groupByField()!) === 'asc'\"\n (click)=\"onGroupSortToggle($event, 'asc')\"></span>\n <span class=\"sort-tri sort-tri-down\"\n [class.sort-tri-active]=\"getSortDirection(groupByField()!) === 'desc'\"\n (click)=\"onGroupSortToggle($event, 'desc')\"></span>\n </span>\n </span>\n }\n </div>\n @if(i === 0 && showExcelDownload()) {\n <ng-container *ngTemplateOutlet=\"excelDownloadIcon\"></ng-container>\n }\n </div>\n }\n\n @if(freezeHeader() && (group.isExpanded || !showGroupBar())) {\n <div #headerScroller class=\"header-shell\" [attr.data-group-id]=\"'header-shell-' + group.id\"\n [style]=\"'--table-total-width: ' + getInitialTotalWidth() + 'px'\">\n <table class=\"eru-grid-table\" [class.freeze-header]=\"freezeHeader()\"\n [class.show-column-lines]=\"showColumnLines()\" [class.show-row-lines]=\"showRowLines()\">\n <ng-container *ngTemplateOutlet=\"tableColGroup\"></ng-container>\n <ng-container *ngTemplateOutlet=\"tableHeader\"></ng-container>\n <!-- Grand Total row after sticky header (position: before) - only for first group -->\n @if(enableGrandTotal() && grandTotalPosition() === 'before' && hasGrandTotalData() && i === 0) {\n <tbody>\n <ng-container *ngTemplateOutlet=\"tableGrandTotal\"></ng-container>\n </tbody>\n }\n <!-- Subtotal row after sticky header (position: before) -->\n @if(enableRowSubtotals() && subtotalPosition() === 'before' && hasSubtotalData(group)) {\n <tbody>\n <ng-container *ngTemplateOutlet=\"tableSubtotal; context: { group: group }\"></ng-container>\n </tbody>\n }\n </table>\n </div>\n }\n </div>\n @if(group.isExpanded || !showGroupBar()) {\n <ng-container>\n <cdk-virtual-scroll-viewport [attr.data-group-id]=\"group.id\" [itemSize]=\"dataRowHeight()\" class=\"viewport table-viewport\"\n (scrolledIndexChange)=\"onScroll($event, group)\" (scroll)=\"onTableBodyScroll($event)\"\n [style]=\"'--table-height: ' + getGroupContentHeight(group.id) + 'px; --table-min-height: ' + getGroupContentHeight(group.id) + 'px; --table-total-width: ' + getInitialTotalWidth() + 'px'\">\n <div class=\"table-wrapper\">\n <table class=\"eru-grid-table\" [class.show-column-lines]=\"showColumnLines()\"\n [class.show-row-lines]=\"showRowLines()\">\n <ng-container *ngTemplateOutlet=\"tableColGroup\"></ng-container>\n @if(!freezeHeader()) {\n <ng-container *ngTemplateOutlet=\"tableHeader\"></ng-container>\n }\n <!-- Grand Total row after normal header (position: before) - only for first group -->\n @if(!freezeHeader() && enableGrandTotal() && grandTotalPosition() === 'before' &&\n hasGrandTotalData() && i === 0) {\n <tbody>\n <ng-container *ngTemplateOutlet=\"tableGrandTotal\"></ng-container>\n </tbody>\n }\n <!-- Subtotal row after normal header (position: before) -->\n @if(!freezeHeader() && enableRowSubtotals() && subtotalPosition() === 'before' &&\n hasSubtotalData(group)) {\n <tbody>\n <ng-container *ngTemplateOutlet=\"tableSubtotal; context: { group: group }\"></ng-container>\n </tbody>\n }\n <tbody>\n @if (columns(); as columnsList) {\n <!-- <tr *ngIf=\"groupItem.type === 'table-header' && groups().length > 1\" style=\"background:#fafafa\">\n @if(gridStore.configuration().config.allowSelection) {\n <th class=\"checkbox-column\" style=\"text-align: center;\">\n <input\n type=\"checkbox\"\n [checked]=\"isGroupSelected(groupItem.group?.id || '')\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleGroupSelection($event, groupItem.group?.id || '')\"\n >\n </th>\n }\n <th *ngFor=\"let column of columns(); trackBy: trackByColumnFn;let i =index\"\n style=\"text-align: center;\"\n [style.width.px]=\"column.field_size\"\n [style.minWidth.px]=\"column.field_size\"\n [resizeColumn]=\"true\"\n [columnConfig]=\"column\"\n [columnDraggable]=\"i\"\n class=\"column-header\">\n <div class=\"column-drag-handle\"></div>\n {{column.label}} {{column.symbol}}\n </th>\n </tr> -->\n <!-- @if(getRowsForGroup(group.id).length > 0 && group.isExpanded) { -->\n <!-- *cdkVirtualFor=\"let row of getRowsForGroupSignal(group.id)(); \n trackBy: trackByRowFn; \n let i = index\" -->\n <!-- @for(row of getRowsForGroupSignal(group.id)(); track trackByRowFn($index, row); let i = $index) { -->\n <ng-container\n *cdkVirtualFor=\"let row of getRowsForGroupSignal(group.id === null || group.id === undefined ? '__NULL_GROUP__' : group.id)(); trackBy: trackByRowFn; let i = index\">\n <tr class=\"row-item\" [attr.data-row-id]=\"i\" [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\" [style.cursor]=\"cursorOnHover() || null\" (click)=\"emitRowSelect(row, 'table', group)\">\n @if(gridStore.configuration().config.allowSelection) {\n <td class=\"checkbox-column\" style=\"text-align: center;\">\n <input type=\"checkbox\" [checked]=\"isRowSelected(row?.entity_id)\"\n (change)=\"toggleRowSelection($event, row)\">\n </td>\n }\n @if(shouldShowActionColumn('before')) {\n <td class=\"action-column\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\">\n <ng-container\n *ngTemplateOutlet=\"actionCell; context: { $implicit: row, mode: 'table', group: group }\"></ng-container>\n </td>\n }\n @if(hasHiddenColumns()) {\n <td class=\"row-expand-toggle\" (click)=\"toggleRowExpand(row, i, $event)\">\n <mat-icon class=\"row-expand-icon\" [class.expanded]=\"isRowExpanded(row, i)\">chevron_right</mat-icon>\n </td>\n }\n @for (column of visibleColumns(); track trackByColumnFn($index, column)) {\n <td #cell [style.width.px]=\"column.field_size\" [style.minWidth.px]=\"column.field_size\"\n class=\"data-cell\" [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\"\n [matTooltipClass]=\"'error-message'\" [matTooltip]=\"datacell.error()?'Error: ' + datacell.error():''\"\n matTooltipPosition=\"below\">\n <div class=\"cell-content\">\n <data-cell #datacell [personCardTemplate]=\"personCardTemplate\" [cellTemplate]=\"cellTemplate\" [td]=cell [fieldSize]=\"column.field_size\" [columnDatatype]=\"column.datatype\"\n [columnName]=\"column.name\" [value]=\"(row?.['entity_data']?.[column.name] ?? row?.[column.name]) || ''\" [column]=\"column\"\n [mode]=\"mode()\" [isEditable]=\"isEditable() && column.editable !== false && column.editable !== 'false'\" [drillable]=\"column.enableDrilldown || false\"\n [id]=\"i + '_' + column.name\" [eruGridStore]=\"gridStore\" [row]=\"row\"></data-cell>\n </div>\n </td>\n }\n @if(shouldShowActionColumn('after')) {\n <td class=\"action-column\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\">\n <ng-container\n *ngTemplateOutlet=\"actionCell; context: { $implicit: row, mode: 'table', group: group }\"></ng-container>\n </td>\n }\n </tr>\n @if(hasHiddenColumns() && isRowExpanded(row, i)) {\n <tr class=\"row-detail\">\n <td class=\"row-detail-cell\" [attr.colspan]=\"rowDetailColspan()\">\n <div class=\"row-detail-grid\">\n @for (hiddenCol of hiddenColumns(); track trackByColumnFn($index, hiddenCol)) {\n <div class=\"row-detail-field\">\n <span class=\"row-detail-label\">{{hiddenCol.label}}</span>\n <div class=\"row-detail-value\">\n <data-cell [cellTemplate]=\"cellTemplate\" [fieldSize]=\"hiddenCol.field_size\" [columnDatatype]=\"hiddenCol.datatype\"\n [columnName]=\"hiddenCol.name\" [value]=\"(row?.['entity_data']?.[hiddenCol.name] ?? row?.[hiddenCol.name]) || ''\"\n [column]=\"hiddenCol\" [mode]=\"mode()\" [isEditable]=\"isEditable()\"\n [drillable]=\"hiddenCol.enableDrilldown || false\"\n [id]=\"'detail_' + i + '_' + hiddenCol.name\" [eruGridStore]=\"gridStore\" [row]=\"row\"></data-cell>\n </div>\n </div>\n }\n </div>\n </td>\n </tr>\n }\n </ng-container>\n <!-- } -->\n <!-- } -->\n @if(group.isLoading && (group.isExpanded || !showGroupBar())) {\n @for(i of [].constructor(ghostRows()); let j = $index; track j) {\n <tr class=\"ghost-loading-row\" [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\">\n @if(gridStore.configuration().config.allowSelection) {\n <td class=\"checkbox-column ghost-cell-container\">\n <div class=\"ghost-cell\"></div>\n </td>\n\n }\n @if(shouldShowActionColumn('before')) {\n <td class=\"action-column ghost-cell-container\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\">\n <div class=\"ghost-cell\"></div>\n </td>\n }\n @if(hasHiddenColumns()) {\n <td class=\"row-expand-toggle ghost-cell-container\">\n <div class=\"ghost-cell\"></div>\n </td>\n }\n @for (column of visibleColumns(); track trackByColumnFn($index, column)) {\n <td [style.width.px]=\"column.field_size\" [style.minWidth.px]=\"column.field_size\"\n class=\"ghost-cell-container\">\n <div class=\"ghost-cell\"></div>\n </td>\n }\n @if(shouldShowActionColumn('after')) {\n <td class=\"action-column ghost-cell-container\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\">\n <div class=\"ghost-cell\"></div>\n </td>\n }\n </tr>\n }\n }\n <!-- <tr\n *ngIf=\"getRowsForGroup(group.id).length === 0 && !group.isExpanded\"\n class=\"group-separator\"\n >\n <td [attr.colspan]=\"groupSeperatorColSpan()\" class=\"separator-cell\"></td>\n </tr> -->\n <!-- Subtotal row at end of group (position: after) -->\n @if(enableRowSubtotals() && subtotalPosition() === 'after' && hasSubtotalData(group)) {\n <ng-container *ngTemplateOutlet=\"tableSubtotal; context: { group: group }\"></ng-container>\n }\n <!-- Grand Total row at end of group (position: after) - only for last group -->\n @if(enableGrandTotal() && grandTotalPosition() === 'after' && hasGrandTotalData() && i ===\n groups().length - 1) {\n <ng-container *ngTemplateOutlet=\"tableGrandTotal\"></ng-container>\n }\n }\n </tbody>\n </table>\n </div>\n </cdk-virtual-scroll-viewport>\n </ng-container>\n }\n </div>\n }\n </div>\n }\n</div>\n\n<!-- Pivot Table Header Template -->\n<ng-template #pivotTableHead>\n <thead [class.eru-wrap-headers]=\"wrapHeaders()\">\n @if (hasNestedHeaders()) {\n <ng-container>\n @for (headerRow of getHeaderRows(); track headerRow; let rowIndex = $index) {\n <tr class=\"pivot-header pivot-header-container\" [class.pivot-header-level]=\"'level-' + rowIndex\">\n @for (header of headerRow; track trackByHeaderFn($index, header); let colIndex = $index) {\n <th [attr.colspan]=\"header.colspan\" [attr.rowspan]=\"header.rowspan\"\n [resizeColumn]=\"isResizablePivotHeader(header)\"\n [columnConfig]=\"getFieldForPivotHeader(header) || $any(header)\"\n class=\"column-header pivot-column-header nested-header\"\n [class.row-dimension-header]=\"isRowDimensionHeader(header)\"\n [class.column-dimension-header]=\"!isRowDimensionHeader(header)\" [class.expanded]=\"header.isExpanded\"\n [class.collapsed]=\"!header.isExpanded\" [class.sticky-column]=\"isStickyColumn(header.name, colIndex)\"\n [class.design-clickable]=\"isDesignMode()\"\n [class.design-selected]=\"isDesignMode() && gridStore.selectedDesignColumn() === designTargetFor($any(header))\"\n [style.position]=\"isStickyColumn(header.name, colIndex) ? 'sticky' : 'static'\"\n [style.left.px]=\"getStickyColumnLeft(header.name, colIndex)\"\n [style.z-index]=\"isStickyColumn(header.name, colIndex) ? 100 : 1\"\n [style.min-height.px]=\"headerRowHeight()\" style=\"height: auto; padding: 8px 6px;\">\n <div class=\"header-content\">\n\n <data-cell [fieldSize]=\"header.field_size\" [columnDatatype]=\"header.dataType\" [columnName]=\"header.name\"\n [value]=\"header.label\" [column]=\"header\" [frozenGrandTotalCell]=\"true\"\n [drillable]=\"header.enableDrilldown || false\" [mode]=\"mode()\" [isEditable]=\"isEditable()\"\n [id]=\"'pivot_' + $index + '_' + header.name\" [eruGridStore]=\"gridStore\" [row]=\"header\">\n </data-cell>\n @if(isDesignMode()) {\n <mat-icon class=\"design-edit-icon\"\n [title]=\"isAggregationHeader($any(header)) ? 'Edit aggregation' : 'Edit column'\"\n (click)=\"onHeaderDesignClick($event, $any(header))\">tune</mat-icon>\n }\n <!-- <span class=\"header-label header-wrap-text\">{{header.label}}</span> -->\n <!-- <button *ngIf=\"!isRowDimensionHeader(header)\"\n class=\"collapse-toggle-btn\"\n [title]=\"header.isExpanded ? 'Collapse group' : 'Expand group'\"\n (click)=\"toggleColumnGroup(header.groupKey)\"\n type=\"button\">\n <span class=\"collapse-icon\">+</span>\n </button> -->\n </div>\n </th>\n }\n </tr>\n }\n </ng-container>\n } @else {\n <!-- Simple header fallback -->\n <ng-container>\n <tr class=\"pivot-header\" [class.freeze-header-enabled]=\"freezeHeader()\">\n @for (column of getLeafColumns(); track trackByColumnFn($index, column); let colIndex = $index) {\n <th [style.width.px]=\"column.field_size\" [style.minWidth.px]=\"column.field_size\"\n [resizeColumn]=\"gridStore.isFeatureEnabled('columnResizable')\" [columnConfig]=\"column\"\n class=\"column-header pivot-column-header\" [class.sticky-column]=\"isStickyColumn(column.name, colIndex)\"\n [class.design-clickable]=\"isDesignMode()\"\n [class.design-selected]=\"isDesignMode() && gridStore.selectedDesignColumn() === designTargetFor(column)\"\n [style.position]=\"isStickyColumn(column.name, colIndex) ? 'sticky' : 'static'\"\n [style.left.px]=\"getStickyColumnLeft(column.name, colIndex)\"\n [style.z-index]=\"isStickyColumn(column.name, colIndex) ? 100 : 1\"\n [style.min-height.px]=\"headerRowHeight()\" style=\"height: auto;padding: 8px 6px\">\n <!-- Label and control laid out as a row: the label truncates, the\n control keeps its place. Left as a bare text node the long\n aggregation labels pushed the icon past the cell edge, where\n `overflow: hidden` clipped it out of sight entirely. -->\n <div class=\"pivot-header-content\">\n <!-- Deliberately not `.column-label`: that class carries the\n wrap-headers rule, which broke these labels onto one word per\n line. This header truncates, as it did before. -->\n <span class=\"pivot-header-label\">{{column.label}}</span>\n @if(isDesignMode()) {\n <mat-icon class=\"design-edit-icon\"\n [title]=\"isAggregationHeader(column) ? 'Edit aggregation' : 'Edit column'\"\n (click)=\"onHeaderDesignClick($event, column)\">tune</mat-icon>\n }\n </div>\n </th>\n }\n </tr>\n </ng-container>\n }\n\n </thead>\n</ng-template>\n\n<!-- Column Group Template for consistent column widths -->\n<ng-template #pivotColGroup>\n <colgroup>\n @for (column of getLeafColumns(); track trackByColumnFn($index, column)) {\n <col\n [style]=\"'width: ' + column.field_size + 'px !important; min-width: ' + column.field_size + 'px !important; max-width: ' + column.field_size + 'px !important; --col-width: ' + column.field_size + 'px'\">\n }\n </colgroup>\n</ng-template>\n\n<ng-template #pivotGrandTotal>\n <tbody class=\"pivot-tbody\">\n @for (pivotRow of gridStore.pivotGrandTotalData(); track trackByPivotRowFn($index, pivotRow); let i = $index) {\n <tr class=\"pivot-row grand-total-row\"\n [class.grand-total-bold]=\"pivotRow._isGrandTotal && grandTotalStyle() === 'bold'\"\n [class.grand-total-italic]=\"pivotRow._isGrandTotal && grandTotalStyle() === 'italic'\"\n [class.grand-total-highlighted]=\"pivotRow._isGrandTotal && grandTotalStyle() === 'highlighted'\"\n [style.height.px]=\"50\" [attr.data-pivot-row]=\"i\">\n <!-- <td colspan=\"20\">{{pivotRow | json}}</td> -->\n @for (column of getLeafColumns(); track trackByColumnFn($index, column); let colIndex = $index) {\n <td [attr.rowspan]=\"getEffectiveRowspan(i, column.name)\" [style.width.px]=\"column.field_size\"\n [style.minWidth.px]=\"column.field_size\" class=\"pivot-cell\"\n [class.row-dimension-cell]=\"isRowDimensionColumn(column.name)\"\n [class.column-dimension-cell]=\"!isRowDimensionColumn(column.name)\"\n [class.aggregated-value]=\"!isRowDimensionColumn(column.name) && column.datatype === 'number'\"\n [class.rowspan-cell]=\"getEffectiveRowspan(i, column.name) || 1 > 1\"\n [class.sticky-column]=\"isStickyColumn(column.name, colIndex)\"\n [style.position]=\"isStickyColumn(column.name, colIndex) ? 'sticky' : 'static'\"\n [style.left.px]=\"getStickyColumnLeft(column.name, colIndex)\"\n [style.z-index]=\"isStickyColumn(column.name, colIndex) ? 99 : 1\" [style.height.px]=\"50\" [attr.xx]=\"i\">\n <div class=\"cell-content pivot-cell-content\">\n <data-cell [fieldSize]=\"column.field_size\" [columnDatatype]=\"column.datatype\" [columnName]=\"column.name\"\n [value]=\"getEffectiveCellValue(i,column.name, pivotRow)\" [column]=\"column\" [frozenGrandTotalCell]=\"true\"\n [drillable]=\"column.enableDrilldown || false\" [mode]=\"mode()\" [isEditable]=\"isEditable()\"\n [id]=\"'pivot_' + i + '_' + column.name\" [eruGridStore]=\"gridStore\" [row]=\"pivotRow\">\n </data-cell>\n </div>\n </td>\n }\n </tr>\n }\n </tbody>\n</ng-template>\n\n<!-- \u2500\u2500\u2500 Action column cell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n One template for both column positions. With no `config.actions` set it\n falls back to the single more_horiz icon the column has always shown, so\n grids that only listen to the store's actionClick signal keep working.\n Context: { $implicit: Row, mode: 'table' | 'board', group?: RowGroup }\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<ng-template #actionCell let-row let-mode=\"mode\" let-group=\"group\">\n @if(!hasConfiguredActions()) {\n <mat-icon (click)=\"onActionClick($event, row, undefined, mode || 'table', group)\">more_horiz</mat-icon>\n } @else if(actionDisplayType() === 'kebab') {\n @if(visibleActionsFor(row).length > 0) {\n <mat-icon class=\"action-kebab\" [matMenuTriggerFor]=\"rowActionMenu\"\n [matMenuTriggerData]=\"{ row: row, mode: mode || 'table', group: group }\"\n (click)=\"$event.stopPropagation()\">more_vert</mat-icon>\n }\n } @else {\n <div class=\"action-icons\">\n @for(action of visibleActionsFor(row); track action.action_name) {\n <mat-icon class=\"action-icon\" [matTooltip]=\"action.action_name\" matTooltipPosition=\"above\"\n (click)=\"onActionClick($event, row, action, mode || 'table', group)\">{{action.action_icon || 'play_arrow'}}</mat-icon>\n }\n </div>\n }\n</ng-template>\n\n<!-- Kebab menu shared by every row; the row is passed through matMenuTriggerData. -->\n<mat-menu #rowActionMenu=\"matMenu\" class=\"eru-grid-action-menu\">\n <ng-template matMenuContent let-row=\"row\" let-mode=\"mode\" let-group=\"group\">\n @for(action of visibleActionsFor(row); track action.action_name) {\n <button mat-menu-item (click)=\"onActionClick($event, row, action, mode || 'table', group)\">\n <mat-icon>{{action.action_icon || 'play_arrow'}}</mat-icon>\n <span>{{action.action_name}}</span>\n </button>\n }\n </ng-template>\n</mat-menu>\n\n<!-- Column Group Template for consistent column widths -->\n<ng-template #tableColGroup>\n <colgroup>\n @if(gridStore.configuration().config.allowSelection) {\n <col style=\"width: 40px; min-width: 40px; max-width: 40px;\">\n }\n @if(shouldShowActionColumn('before')) {\n <col\n [style]=\"'width: ' + actionColumnWidth() + 'px !important; min-width: ' + actionColumnWidth() + 'px !important; max-width: ' + actionColumnWidth() + 'px !important;'\">\n }\n @if(hasHiddenColumns()) {\n <col style=\"width: 40px !important; min-width: 40px !important; max-width: 40px !important;\">\n }\n @for (column of visibleColumns(); track trackByColumnFn($index, column)) {\n <col\n [style]=\"'width: ' + column.field_size + 'px !important; min-width: ' + column.field_size + 'px !important; max-width: ' + column.field_size + 'px !important; --col-width: ' + column.field_size + 'px'\">\n }\n @if(shouldShowActionColumn('after')) {\n <col\n [style]=\"'width: ' + actionColumnWidth() + 'px !important; min-width: ' + actionColumnWidth() + 'px !important; max-width: ' + actionColumnWidth() + 'px !important;'\">\n }\n </colgroup>\n</ng-template>\n\n\n<ng-template #tableHeader>\n\n <thead [class.eru-wrap-headers]=\"wrapHeaders()\">\n <!-- headerRowHeight rides on the row, not the cells: `thead.eru-wrap-headers\n th { height: auto }` outranks any class-level height we could put on a\n th, which is why a configured header height was ignored while data rows\n (inline height on tr.row-item) honoured theirs. On a table row `height`\n is a minimum, so a wrapped two-line header still grows past it. -->\n <tr [style.height.px]=\"headerRowHeight()\" [style.minHeight.px]=\"headerRowHeight()\">\n @if(gridStore.configuration().config.allowSelection) {\n <th class=\"checkbox-column column-header table-column-header\">\n <input type=\"checkbox\" [checked]=\"isAllGroupsSelected()\" (change)=\"toggleAllGroups($event)\">\n </th>\n }\n @if(shouldShowActionColumn('before')) {\n <th class=\"action-column column-header table-column-header\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\">Action</th>\n }\n @if(hasHiddenColumns()) {\n <th class=\"row-expand-toggle column-header table-column-header\"></th>\n }\n @for (column of visibleColumns(); track trackByColumnFn(i, column); let i = $index) {\n <th [style.width.px]=\"column.field_size\" [resizeColumn]=\"gridStore.isFeatureEnabled('columnResizable')\"\n [columnConfig]=\"column\" [index]=\"i\"\n [columnDraggable]=\"gridStore.isFeatureEnabled('columnReorderable') ? i : null\"\n [style.minWidth.px]=\"column.field_size\" class=\"column-header table-column-header\"\n [class.sortable-header]=\"isSortable()\"\n [class.design-clickable]=\"isDesignMode()\"\n [class.design-selected]=\"isDesignMode() && gridStore.selectedDesignColumn() === column.name\"\n [class.sort-asc]=\"isSortable() && getSortDirection(column.name) === 'asc'\"\n [class.sort-desc]=\"isSortable() && getSortDirection(column.name) === 'desc'\">\n @if(gridStore.isFeatureEnabled('columnReorderable')) {\n <div class=\"column-drag-handle\"></div>\n }\n <span class=\"column-label\" [title]=\"column.tool_tip || column.description || ''\">{{column.label}}</span>\n @if(isDesignMode()) {\n <mat-icon class=\"design-edit-icon\" title=\"Edit column\" (click)=\"onHeaderDesignClick($event, column)\">tune</mat-icon>\n }\n @if(isSortable()) {\n <span class=\"sort-indicator\">\n <span class=\"sort-triangles\">\n <span class=\"sort-tri sort-tri-up\" [class.sort-tri-active]=\"getSortDirection(column.name) === 'asc'\"\n (click)=\"onSortColumn($event, column, 'asc')\"></span>\n <span class=\"sort-tri sort-tri-down\" [class.sort-tri-active]=\"getSortDirection(column.name) === 'desc'\"\n (click)=\"onSortColumn($event, column, 'desc')\"></span>\n </span>\n @if(getSortPriority(column.name) !== null && gridStore.sortColumns().length > 1) {\n <span class=\"sort-priority\">{{getSortPriority(column.name)}}</span>\n }\n </span>\n }\n </th>\n }\n @if(shouldShowActionColumn('after')) {\n <th class=\"action-column column-header table-column-header\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\">Action</th>\n }\n </tr>\n </thead>\n</ng-template>\n\n<!-- Table Subtotal Row Template -->\n<ng-template #tableSubtotal let-group=\"group\">\n <tr class=\"subtotal-row\" [class.subtotal-bold]=\"subTotalStyle() === 'bold'\"\n [class.subtotal-italic]=\"subTotalStyle() === 'italic'\"\n [class.subtotal-highlighted]=\"subTotalStyle() === 'highlighted'\" [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\">\n @if(gridStore.configuration().config.allowSelection) {\n <td class=\"checkbox-column\"></td>\n }\n @if(shouldShowActionColumn('before')) {\n <td class=\"action-column\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\"></td>\n }\n @if(hasHiddenColumns()) {\n <td class=\"row-expand-toggle\"></td>\n }\n @for(column of visibleColumns(); track trackByColumnFn($index, column); let colIndex = $index) {\n <td [style.width.px]=\"column.field_size\" [style.minWidth.px]=\"column.field_size\" class=\"subtotal-cell\"\n [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\">\n <div class=\"cell-content\">\n @if(colIndex === 0 && getSubtotalValue(group, column.name) === null) {\n <span class=\"subtotal-label\">{{subtotalLabel()}}</span>\n } @else {\n @if(getSubtotalValue(group, column.name) !== null) {\n <data-cell [fieldSize]=\"column.field_size\" [columnDatatype]=\"column.datatype\" [columnName]=\"column.name\"\n [value]=\"getSubtotalValue(group, column.name)\" [column]=\"column\" [mode]=\"mode()\" [isEditable]=\"false\"\n [id]=\"'subtotal_' + group.id + '_' + column.name\" [eruGridStore]=\"gridStore\" [row]=\"group.subtotal\">\n </data-cell>\n }\n }\n </div>\n </td>\n }\n @if(shouldShowActionColumn('after')) {\n <td class=\"action-column\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\"></td>\n }\n </tr>\n</ng-template>\n\n<!-- Table Grand Total Row Template -->\n<ng-template #tableGrandTotal>\n <tr class=\"grand-total-row\" [class.grand-total-bold]=\"grandTotalStyle() === 'bold'\"\n [class.grand-total-italic]=\"grandTotalStyle() === 'italic'\"\n [class.grand-total-highlighted]=\"grandTotalStyle() === 'highlighted'\" [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\">\n @if(gridStore.configuration().config.allowSelection) {\n <td class=\"checkbox-column\"></td>\n }\n @if(shouldShowActionColumn('before')) {\n <td class=\"action-column\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\"></td>\n }\n @if(hasHiddenColumns()) {\n <td class=\"row-expand-toggle\"></td>\n }\n @for(column of visibleColumns(); track trackByColumnFn($index, column); let colIndex = $index) {\n <td [style.width.px]=\"column.field_size\" [style.minWidth.px]=\"column.field_size\" class=\"grand-total-cell\"\n [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\">\n <div class=\"cell-content\">\n @if(colIndex === 0 && getGrandTotalValue(column.name) === null) {\n <span class=\"grand-total-label\">Grand Total</span>\n } @else {\n @if(getGrandTotalValue(column.name) !== null) {\n <data-cell [fieldSize]=\"column.field_size\" [columnDatatype]=\"column.datatype\" [columnName]=\"column.name\"\n [value]=\"getGrandTotalValue(column.name)\" [column]=\"column\" [mode]=\"mode()\" [isEditable]=\"false\"\n [id]=\"'grandtotal_' + column.name\" [eruGridStore]=\"gridStore\" [row]=\"gridStore.rowGrandTotal()\">\n </data-cell>\n }\n }\n </div>\n </td>\n }\n @if(shouldShowActionColumn('after')) {\n <td class=\"action-column\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\"></td>\n }\n </tr>\n</ng-template>\n\n<!-- \u2500\u2500\u2500 Default board card template \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Used when no boardCardTemplate is passed to <eru-grid>.\n Context: { $implicit: Row, columns: Field[], group: RowGroup }\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<ng-template #defaultBoardCard let-row let-columns=\"columns\" let-group=\"group\">\n <mat-card class=\"board-card\">\n <mat-card-content>\n @for (column of columns; track column.name) {\n @if ((row?.entity_data?.[column.name] ?? row?.[column.name]) !== undefined) {\n <div class=\"board-card-field\">\n <span class=\"board-field-label\">{{ column.label }}</span>\n <data-cell\n [fieldSize]=\"column.field_size\"\n [columnDatatype]=\"column.datatype\"\n [columnName]=\"column.name\"\n [column]=\"column\"\n [value]=\"row?.entity_data?.[column.name] ?? row?.[column.name]\"\n [id]=\"row?.entity_id + '_' + column.name\"\n [eruGridStore]=\"gridStore\"\n [mode]=\"'board'\"\n [row]=\"row\">\n </data-cell>\n </div>\n }\n }\n </mat-card-content>\n <mat-card-actions align=\"end\">\n <button mat-icon-button (click)=\"onActionClick($event, row)\">\n <mat-icon>more_horiz</mat-icon>\n </button>\n </mat-card-actions>\n </mat-card>\n</ng-template>", styles: ["@charset \"UTF-8\";:root{--grid-primary: var(--mat-sys-primary, #6750a4);--grid-on-primary: var(--mat-sys-on-primary, #ffffff);--grid-primary-container: var(--mat-sys-primary-container, #eaddff);--grid-on-primary-container: var(--mat-sys-on-primary-container, #21005d);--grid-secondary: var(--mat-sys-secondary, #625b71);--grid-on-secondary: var(--mat-sys-on-secondary, #ffffff);--grid-secondary-container: var(--mat-sys-secondary-container, #e8def8);--grid-on-secondary-container: var(--mat-sys-on-secondary-container, #1d192b);--grid-tertiary: var(--mat-sys-tertiary, #7d5260);--grid-on-tertiary: var(--mat-sys-on-tertiary, #ffffff);--grid-tertiary-container: var(--mat-sys-tertiary-container, #ffd8e4);--grid-on-tertiary-container: var(--mat-sys-on-tertiary-container, #31111d);--grid-surface: var(--mat-sys-surface, #fef7ff);--grid-surface-variant: var(--mat-sys-surface-variant, #e7e0ec);--grid-surface-container: var(--mat-sys-surface-container, #f3edf7);--grid-surface-container-high: var(--mat-sys-surface-container-high, #ede7f0);--grid-on-surface: var(--mat-sys-on-surface, #1d1b20);--grid-on-surface-variant: var(--mat-sys-on-surface-variant, #49454f);--grid-outline: var(--mat-sys-outline, #79757f);--grid-outline-variant: var(--mat-sys-outline-variant, #cac4d0);--grid-error: var(--mat-sys-error, #ba1a1a);--grid-error-container: var(--mat-sys-error-container, #ffdad6);--grid-base-surface: var(--surface, #ffffff);--grid-base-on-surface: var(--on-surface, #000000);--grid-base-border: var(--border, #e5e7eb);--grid-primary-light: var(--grid-primary-container)}:host,eru-grid{display:block!important;width:100%;height:100%;flex:1 1 0%;max-height:var(--grid-height, none);min-height:var(--grid-min-height, 120px);font-family:var(--grid-font-family);--grid-font-family: \"Poppins\", \"Roboto\", -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;--grid-font-size-body: 12px;--grid-font-size-caption: 12px !important;--grid-line-height-body: 1;--grid-aggregation-text-align: right;--grid-number-text-align: right;--grid-spacing-xxs: 2px;--grid-spacing-xs: 4px;--grid-spacing-sm: 8px;--grid-spacing-md: 16px;--grid-spacing-lg: 24px;--grid-border-radius: 4px;--grid-elevation-1: 0px 1px 2px 0px rgba(0, 0, 0, .3), 0px 1px 3px 1px rgba(0, 0, 0, .15);--grid-elevation-2: 0px 1px 2px 0px rgba(0, 0, 0, .3), 0px 2px 6px 2px rgba(0, 0, 0, .15);--grid-row-hover: var(--grid-surface-variant);--grid-row-selected: var(--grid-surface-container-high);--grid-zebra-odd: transparent;--grid-zebra-even: transparent;--grid-focus-ring: var(--grid-primary);--grid-header-font-weight: 500;--grid-header-text-transform: none;--grid-header-letter-spacing: normal;--grid-header-font-size: var(--grid-font-size-caption);--grid-header-padding-x: 8px;--grid-header-padding-y: 12px;--grid-font-feature-numeric: normal;--grid-cell-padding-x: var(--grid-spacing-xs);--grid-cell-inset-x: 8px;--grid-cell-padding-y: var(--grid-spacing-xxs);--grid-tint-subtle: rgba(0, 0, 0, .025);--grid-tint-soft: rgba(0, 0, 0, .045);--grid-tint-strong: rgba(0, 0, 0, .08);--grid-radius-outer: 0;--grid-shadow-outer: none;--grid-divider-color: var(--grid-outline-variant);--grid-divider-width: 1px;--grid-header-bg: var(--grid-surface-container);--grid-header-color: var(--grid-on-surface);--grid-pill-radius: 999px;--grid-pill-padding-y: 3px;--grid-pill-padding-x: 10px;--grid-pill-font-size: 11px;--grid-pill-font-weight: 500;--grid-priority-dot-size: 8px;--grid-avatar-size: 24px;--grid-avatar-font-size: 10px;--grid-avatar-font-weight: 600;border-radius:var(--grid-radius-outer);box-shadow:var(--grid-shadow-outer)}eru-grid[data-preset=default]{--grid-header-bg: transparent;--grid-header-color: var(--grid-on-surface-variant);--grid-header-font-weight: 500;--grid-header-text-transform: uppercase;--grid-header-letter-spacing: .06em;--grid-header-font-size: 11px;--grid-header-padding-y: 12px;--grid-header-padding-x: 14px;--grid-cell-padding-y: 10px;--grid-cell-padding-x: 14px;--grid-row-hover: var(--grid-tint-subtle);--grid-divider-color: var(--grid-tint-soft);--grid-divider-width: 1px;--grid-font-feature-numeric: \"tnum\"}eru-grid[data-preset=modern]{--grid-header-bg: transparent;--grid-header-color: var(--grid-on-surface-variant);--grid-header-font-weight: 500;--grid-header-text-transform: none;--grid-header-letter-spacing: normal;--grid-header-font-size: 13px;--grid-header-padding-y: 16px;--grid-header-padding-x: 18px;--grid-cell-padding-y: 16px;--grid-cell-padding-x: 18px;--grid-row-hover: var(--grid-tint-soft);--grid-divider-color: var(--grid-tint-subtle);--grid-divider-width: 1px;--grid-radius-outer: 12px;--grid-font-feature-numeric: \"tnum\";--grid-pill-padding-y: 4px;--grid-pill-padding-x: 12px}eru-grid[data-preset=compact]{--grid-header-bg: var(--grid-surface-container);--grid-header-color: var(--grid-on-surface);--grid-header-font-weight: 600;--grid-header-text-transform: none;--grid-header-font-size: 11px;--grid-header-padding-y: 4px;--grid-header-padding-x: 8px;--grid-cell-padding-y: 3px;--grid-cell-padding-x: 8px;--grid-font-size-body: 11px;--grid-row-hover: var(--grid-tint-subtle);--grid-divider-color: var(--grid-tint-subtle);--grid-divider-width: 1px;--grid-font-feature-numeric: \"tnum\";--grid-pill-padding-y: 1px;--grid-pill-padding-x: 6px;--grid-pill-font-size: 10px}eru-grid[data-preset=bold]{--grid-header-bg: var(--grid-surface-container-high);--grid-header-color: var(--grid-on-surface);--grid-header-font-weight: 700;--grid-header-text-transform: none;--grid-header-font-size: 13px;--grid-header-padding-y: 14px;--grid-header-padding-x: 12px;--grid-cell-padding-y: 10px;--grid-cell-padding-x: 12px;--grid-row-hover: var(--grid-tint-soft);--grid-divider-color: var(--grid-tint-strong);--grid-divider-width: 1px;--grid-radius-outer: 2px;--grid-font-feature-numeric: \"tnum\"}eru-grid[data-preset=financial]{--grid-header-bg: var(--grid-surface-container);--grid-header-color: var(--grid-on-surface-variant);--grid-header-font-weight: 500;--grid-header-text-transform: uppercase;--grid-header-letter-spacing: .08em;--grid-header-font-size: 11px;--grid-header-padding-y: 12px;--grid-header-padding-x: 14px;--grid-cell-padding-y: 10px;--grid-cell-padding-x: 14px;--grid-zebra-odd: transparent;--grid-zebra-even: var(--grid-tint-subtle);--grid-row-hover: var(--grid-tint-soft);--grid-divider-color: var(--grid-tint-subtle);--grid-divider-width: 1px;--grid-font-feature-numeric: \"tnum\"}eru-grid[data-preset=elevated]{--grid-header-bg: transparent;--grid-header-color: var(--grid-on-surface-variant);--grid-header-font-weight: 600;--grid-header-text-transform: none;--grid-header-font-size: 12px;--grid-header-padding-y: 16px;--grid-header-padding-x: 18px;--grid-cell-padding-y: 14px;--grid-cell-padding-x: 18px;--grid-row-hover: var(--grid-tint-soft);--grid-divider-color: var(--grid-tint-subtle);--grid-divider-width: 1px;--grid-radius-outer: 16px;--grid-shadow-outer: 0 1px 3px rgba(0, 0, 0, .06), 0 10px 28px rgba(0, 0, 0, .07);--grid-font-feature-numeric: \"tnum\";--grid-pill-padding-y: 4px;--grid-pill-padding-x: 12px;overflow:hidden}.group-container{padding-bottom:8px}.column-header.design-clickable .design-edit-icon{font-size:16px;width:16px;height:16px;margin-left:4px;opacity:.45;vertical-align:middle;cursor:pointer}.column-header.design-clickable:hover .design-edit-icon,.column-header.design-clickable .design-edit-icon:hover{opacity:1}.column-header.design-selected{background-color:var(--grid-primary-container, rgba(63, 81, 181, .12))}.pivot-column-header .pivot-header-content{display:flex;align-items:center;justify-content:center;gap:4px;min-width:0}.pivot-column-header .pivot-header-content .pivot-header-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.pivot-column-header .pivot-header-content .design-edit-icon,.pivot-column-header .header-content .design-edit-icon{flex:0 0 auto}.pivot-column-header .header-content data-cell,.pivot-column-header .header-content data-cell *{color:inherit!important}.incremental-row-container{width:100%;height:100%;min-height:var(--grid-min-height, 120px);max-height:none;overflow:auto;position:relative;background-color:var(--grid-surface);border-radius:var(--grid-border-radius);font-family:var(--grid-font-family)}.viewport{height:100%;min-height:300px;overflow-x:auto;overflow-y:auto;background-color:var(--grid-surface);scrollbar-gutter:stable}.viewport.apply-cdk-width{width:calc(var(--table-total-width) + 10px)!important}.groups-viewport{height:100%;min-height:300px}.groups-scroll-container{max-height:var(--grid-height, 600px);overflow-y:auto;overflow-x:hidden}.table-viewport{background-color:var(--grid-surface);height:var(--table-height, auto);min-height:var(--table-min-height, 100px);overflow-x:auto;overflow-y:auto}.pivot-viewport{min-height:var(--table-min-height, 300px);overflow-x:auto;overflow-y:auto;background-color:var(--grid-surface)}.pivot-viewport .cdk-virtual-scroll-content-wrapper{width:auto;height:auto}.table-wrapper{min-width:100%;overflow-x:visible}.incremental-row-container .eru-grid-table,.eru-grid-table{width:100%!important;border-collapse:separate;border-spacing:0;table-layout:fixed!important;background-color:var(--grid-surface);color:var(--grid-on-surface);font-family:var(--grid-font-family);font-size:var(--grid-font-size-body);line-height:var(--grid-line-height-body)}.eru-grid-table th,.eru-grid-table td{text-align:left;overflow:hidden!important;text-overflow:ellipsis!important;white-space:nowrap!important;color:var(--grid-on-surface);min-width:0;max-width:100%!important;box-sizing:border-box;position:relative}.eru-grid-table th{background-color:var(--grid-header-bg, var(--grid-surface-container))}thead.eru-wrap-headers th{white-space:normal!important;overflow:visible!important;text-overflow:clip!important;height:auto}thead.eru-wrap-headers th .column-label,thead.eru-wrap-headers th .header-label{white-space:normal!important;overflow:visible!important;text-overflow:clip!important;word-break:break-word;overflow-wrap:anywhere}.eru-grid-table tbody td{background-color:transparent}.eru-grid-table thead{background-color:var(--grid-header-bg, var(--grid-surface-container));transform:translateZ(0);will-change:transform;backface-visibility:hidden}.eru-grid-table thead.freeze-header-enabled{position:sticky!important;top:0!important;z-index:100!important}.eru-grid-table thead th{background-color:var(--grid-header-bg, var(--grid-surface-container));color:var(--grid-header-color, var(--grid-on-surface));font-family:var(--grid-font-family);font-weight:var(--grid-header-font-weight);font-size:var(--grid-header-font-size)}.checkbox-column{width:50px;min-width:50px;max-width:50px;text-align:center!important;padding-left:0!important;padding-right:0!important;text-overflow:clip!important;background-color:var(--grid-surface-container)}.checkbox-column input[type=checkbox]{width:16px;height:16px;cursor:pointer;accent-color:var(--grid-primary);border-radius:var(--grid-border-radius)}.checkbox-column input[type=checkbox]:focus{outline:2px solid var(--grid-primary);outline-offset:2px}.action-column{text-align:center!important;padding-left:0!important;padding-right:0!important;text-overflow:clip!important;background-color:var(--grid-surface-container)}.action-column mat-icon{font-size:20px;width:20px;height:20px;line-height:20px;color:var(--grid-on-surface-variant);cursor:pointer}.action-column mat-icon:hover{color:var(--grid-primary)}.action-column .action-icons{display:flex;align-items:center;justify-content:center;gap:6px;overflow-x:auto;scrollbar-width:none}.action-column .action-icons::-webkit-scrollbar{display:none}.action-column .action-icon{flex:0 0 auto}.eru-grid-action-menu .mat-mdc-menu-item mat-icon{margin-right:8px;font-size:18px;width:18px;height:18px;line-height:18px;color:var(--grid-on-surface-variant)}.group-header{background-color:var(--grid-surface-container);color:var(--grid-on-surface);font-size:var(--grid-font-size-caption);font-weight:500;border-bottom:1px solid var(--grid-outline);cursor:pointer;transition:background-color .2s ease}.group-header:hover{background-color:var(--grid-surface-container-high)}.group-header .group-title{font-weight:600;color:var(--grid-primary)}.group-header .group-row-count{color:var(--grid-on-surface-variant);font-size:var(--grid-font-size-caption);margin-left:var(--grid-spacing-sm)}.row-item{background-color:var(--grid-surface);transition:background-color .15s ease}.row-item:nth-child(odd){background-color:var(--grid-zebra-odd, var(--grid-surface))}.row-item:nth-child(2n){background-color:var(--grid-zebra-even, var(--grid-surface))}.row-item:hover{background-color:var(--grid-row-hover)}.required-toggle-row{background-color:var(--grid-surface-container, #f3edf7);border-bottom:1px solid var(--grid-outline-variant, #cac4d0)}.required-toggle-row .required-toggle-cell{padding:4px 8px!important;text-align:center;vertical-align:middle;position:relative}.required-toggle-row .required-toggle-cell .required-label{position:absolute;top:2px;left:4px;font-size:10px;color:var(--grid-on-surface-variant, #49454f);font-weight:400;text-transform:lowercase}.required-toggle-row .required-toggle-cell mat-checkbox{display:flex;justify-content:center;align-items:center}.table-column-header{padding:0 var(--grid-header-padding-x);height:var(--grid-header-row-height, auto)}.column-header{font-weight:var(--grid-header-font-weight);text-transform:var(--grid-header-text-transform);letter-spacing:var(--grid-header-letter-spacing);text-align:center!important;font-size:var(--grid-header-font-size);position:relative;-webkit-user-select:none;user-select:none;--grid-header-affordance-space: 0px;--grid-column-resizer-width: 10px;--grid-header-sort-right: calc(var(--grid-column-resizer-width) + 2px);--grid-header-design-right: calc(var(--grid-column-resizer-width) + 2px);background-color:var(--grid-header-bg, var(--grid-surface-container));color:var(--grid-header-color, var(--grid-on-surface))}.column-header:hover{background-color:var(--grid-header-hover-bg, var(--grid-surface-container-high))}.column-drag-handle{position:absolute;left:0;top:0;bottom:0;width:12px;cursor:grab;opacity:0;transition:opacity .2s ease,background-color .2s ease;z-index:2;display:flex;align-items:center;justify-content:center;border-right:1px solid transparent}.column-drag-handle:after{content:\"\\22ee\\22ee\";font-size:14px;color:var(--grid-on-surface-variant);transform:rotate(90deg)}.column-drag-handle:hover{background-color:var(--grid-surface-container-high);border-right-color:var(--grid-outline)}.column-header:hover .column-drag-handle{opacity:1}.column-drag-handle:active{cursor:grabbing}.table-column-header.sortable-header{--grid-header-affordance-space: 20px}.table-column-header.design-clickable{--grid-header-affordance-space: 28px}.table-column-header.sortable-header.design-clickable{--grid-header-affordance-space: 40px;--grid-header-design-right: calc(var(--grid-column-resizer-width) + 13px)}.table-column-header .column-label{display:block;padding-right:var(--grid-header-affordance-space)}.table-column-header .sort-indicator,.table-column-header .design-edit-icon{position:absolute;top:50%;transform:translateY(-50%);margin-left:0}.table-column-header .sort-indicator{right:var(--grid-header-sort-right)}.table-column-header .design-edit-icon{right:var(--grid-header-design-right)}.sortable-header{cursor:pointer}.sortable-header .sort-indicator{display:inline-flex;align-items:center;gap:2px;cursor:pointer;opacity:0;transition:opacity .15s ease}.sortable-header .sort-indicator .sort-triangles{display:flex;flex-direction:column;align-items:center;gap:2px}.sortable-header .sort-indicator .sort-tri{width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;cursor:pointer;transition:border-color .15s ease}.sortable-header .sort-indicator .sort-tri-up{border-bottom:5px solid var(--grid-on-surface-variant, #49454f);opacity:.3}.sortable-header .sort-indicator .sort-tri-down{border-top:5px solid var(--grid-on-surface-variant, #49454f);opacity:.3}.sortable-header .sort-indicator .sort-tri-active{opacity:1}.sortable-header .sort-indicator .sort-tri-active.sort-tri-up{border-bottom-color:var(--grid-primary, #6750a4)}.sortable-header .sort-indicator .sort-tri-active.sort-tri-down{border-top-color:var(--grid-primary, #6750a4)}.sortable-header .sort-indicator .sort-priority{font-size:9px;font-weight:600;color:var(--grid-primary, #6750a4);line-height:1;min-width:12px;text-align:center}.sortable-header:hover .sort-indicator,.sortable-header.sort-asc .sort-indicator,.sortable-header.sort-desc .sort-indicator{opacity:1}.sortable-header:hover .sort-indicator .sort-tri:not(.sort-tri-active){opacity:.6}.sort-asc,.sort-desc{background-color:var(--grid-surface-container-low, rgba(103, 80, 164, .04))}.dragging{opacity:1;background-color:var(--grid-surface-container);box-shadow:var(--grid-elevation-2)}.drag-over{background-color:var(--grid-surface-container);border-color:var(--grid-primary)}.data-cell{background-color:transparent;color:var(--grid-on-surface);font-family:var(--grid-font-family);font-size:var(--grid-font-size-body);font-feature-settings:var(--grid-font-feature-numeric);padding:0 var(--grid-cell-padding-x)}.cell-content{align-items:center}.cell-content .mdc-text-field{padding:0px var(--grid-spacing-xxs)!important}.cell-display-text{align-items:center;padding:0px var(--grid-spacing-xs)}.ghost-loading-row{background-color:transparent}.ghost-cell-container{padding:var(--grid-spacing-sm)}.ghost-cell{height:20px;width:100%;background-color:var(--grid-surface-container);animation:pulse 1.5s ease-in-out infinite;border-radius:var(--grid-border-radius)}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.column-resizer{position:absolute;right:0;top:0;bottom:0;width:4px;cursor:col-resize;background-color:transparent;transition:background-color .2s ease}.column-resizer:hover{background-color:var(--grid-primary)}.group-separator{height:var(--grid-spacing-sm);background-color:var(--grid-surface-variant)}.group-separator .separator-cell{background-color:var(--grid-surface-variant);border:none;height:var(--grid-spacing-sm)}.error-state{background-color:var(--grid-error-container);color:var(--grid-error);border-color:var(--grid-error)}.error-message{background-color:var(--grid-error);color:#fff;padding:var(--grid-spacing-sm);border-radius:var(--grid-border-radius);font-size:var(--grid-font-size-caption)}.incremental-row-container .eru-grid-table tbody,.incremental-row-container .eru-grid-table{position:relative}.incremental-row-container .eru-grid-table.show-column-lines{border-right:var(--grid-outline-width, 1px) solid var(--grid-outline, #e0e0e0)!important;border-top:var(--grid-outline-width, 1px) solid var(--grid-outline, #e0e0e0)!important}.incremental-row-container .eru-grid-table.show-column-lines:not(.freeze-header){border-bottom:var(--grid-outline-width, 1px) solid var(--grid-outline, #e0e0e0)!important}.incremental-row-container .eru-grid-table:not(.show-column-lines){border:var(--grid-outline-width, 1px) solid var(--grid-outline, #e0e0e0)!important}.incremental-row-container .eru-grid-table thead:after{content:\"\";position:absolute;bottom:0;left:0;right:0;height:calc(var(--grid-divider-width, 1px) * 2);background-color:var(--grid-divider-color, var(--grid-outline, #e0e0e0));pointer-events:none;z-index:10}.incremental-row-container .eru-grid-table.show-column-lines thead th,.incremental-row-container .eru-grid-table.show-column-lines tbody td{border-left:var(--grid-divider-width, 1px) solid var(--grid-divider-color, var(--grid-outline, #e0e0e0))!important}.incremental-row-container .eru-grid-table.show-row-lines thead th,.incremental-row-container .eru-grid-table.show-row-lines tbody td{border-bottom:var(--grid-divider-width, 1px) solid var(--grid-divider-color, var(--grid-outline, #e0e0e0))!important}@media(max-width:768px){.incremental-row-container{height:600px}.eru-grid-table th,.eru-grid-table td{font-size:var(--grid-font-size-caption)}.checkbox-column{width:40px;min-width:40px;max-width:40px}}@media(prefers-contrast:high){.eru-grid-table th,.eru-grid-table td{border-width:2px}.row-item:hover{border-width:2px;border-color:var(--grid-primary)}}@media(prefers-reduced-motion:reduce){.row-item,.column-drag-handle,.ghost-cell{transition:none;animation:none}}.pivot-table .nested-header{text-align:center;font-weight:600;background:var(--grid-surface-container)}.pivot-table .nested-header.row-dimension-header{background:var(--grid-surface-container);font-weight:600}.pivot-table .pivot-header-leafcols{padding:0;margin:0;height:0}.pivot-table .pivot-header-level.level-0 .nested-header{font-size:14px;padding:12px 8px}.pivot-table .pivot-header-level.level-1 .nested-header{font-size:13px;padding:10px 6px}.pivot-table .pivot-header-level.level-2 .nested-header{font-size:12px;padding:8px 4px}.pivot-table .nested-header:hover{background:var(--grid-surface-variant);color:var(--grid-primary);transition:all .2s ease}.pivot-table .pivot-cell.aggregated-value{font-weight:500;font-family:Roboto Mono,monospace}.pivot-table .pivot-cell-content{display:flex;justify-content:center;align-items:center;min-height:38px}.pivot-table .pivot-repeated-value .cell-content,.pivot-table .pivot-repeated-value .pivot-cell-content{visibility:hidden}.pivot-table .pivot-group-start.row-dimension-cell{border-top:1px solid var(--grid-outline, #79757f)}.pivot-mode .incremental-row-container{display:flex;flex-direction:column;height:auto;max-height:85vh;overflow:auto}.pivot-mode .h-shell{position:relative;width:calc(100% - var(--scrollbar-width, 17px))!important;top:0;z-index:1;overflow-x:hidden;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}.pivot-mode .h-shell::-webkit-scrollbar{display:none}.pivot-mode .gt-shell{position:relative;bottom:50px;flex-shrink:0;overflow-x:hidden;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}.pivot-mode .gt-shell::-webkit-scrollbar{display:none}.pivot-mode .gt-shell table{border-bottom:var(--grid-outline-width, 1px) solid var(--grid-outline, #e0e0e0)!important}.pivot-mode .gt-shell.adjust-bottom-vs{bottom:66px!important}.pivot-mode .gt-shell.adjust-bottom:not(.adjust-bottom-vs){bottom:calc(66px - var(--scrollbar-width, 17px))!important}.pivot-mode .header-shell{flex-shrink:0;width:100%;box-sizing:border-box;padding-right:var(--scrollbar-width, 17px);overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}.pivot-mode .header-shell::-webkit-scrollbar{display:none}.pivot-mode .header-shell.apply-cdk-width{width:calc(var(--table-total-width) + 10px)!important}.pivot-mode .header-shell .eru-grid-table{margin-bottom:0;width:100%;table-layout:fixed}.pivot-mode .header-shell .eru-grid-table thead{background:var(--grid-surface-container)}.pivot-mode .header-shell .eru-grid-table thead th{background:var(--grid-surface-container);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important}.pivot-mode .header-shell .eru-grid-table thead th.sticky-column{position:sticky;background:var(--grid-surface-container);z-index:111}.pivot-mode .header-shell .eru-grid-table tbody td{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important}.pivot-mode .pivot-container{display:flex;flex-direction:column;height:100%;width:100%;overflow:hidden}.pivot-mode .pivot-table{width:auto!important;min-width:100%!important;table-layout:fixed!important}.pivot-mode .pivot-table colgroup col{width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important;flex:none!important;flex-shrink:0!important;flex-grow:0!important}.pivot-mode .pivot-table td,.pivot-mode .pivot-table th{box-sizing:border-box!important;flex:none!important;flex-shrink:0!important;flex-grow:0!important;word-wrap:break-word!important;word-break:break-all!important}.pivot-mode .pivot-table{table-layout:fixed!important;width:100%!important}.pivot-mode .pivot-table *{max-width:var(--col-width)!important;box-sizing:border-box!important}.pivot-mode .pivot-table colgroup{width:100%!important}.pivot-mode .pivot-table colgroup col{width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important;flex-basis:var(--col-width)!important;flex:0 0 var(--col-width)!important}.pivot-mode .pivot-table table{width:100%!important;table-layout:fixed!important;border-collapse:collapse!important;border-spacing:0!important}.pivot-mode .pivot-table[style*=--table-total-width]{width:var(--table-total-width)!important;min-width:var(--table-total-width)!important;max-width:var(--table-total-width)!important}.pivot-mode .pivot-table colgroup col{width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important;flex:0 0 var(--col-width)!important;flex-basis:var(--col-width)!important;flex-grow:0!important;flex-shrink:0!important;overflow:hidden!important}.pivot-mode .pivot-table tbody td,.pivot-mode .pivot-table thead th{width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important;overflow:hidden!important;text-overflow:ellipsis!important;white-space:nowrap!important}.pivot-mode .pivot-table .cell-content,.pivot-mode .pivot-table data-cell{width:100%!important;max-width:100%!important;overflow:hidden!important;text-overflow:ellipsis!important;white-space:nowrap!important;display:block!important}.pivot-mode .pivot-table table{width:var(--table-total-width)!important;min-width:var(--table-total-width)!important;max-width:var(--table-total-width)!important;table-layout:fixed!important;border-collapse:collapse!important;border-spacing:0!important;word-wrap:break-word!important;word-break:break-all!important}.pivot-mode .pivot-tbody tr.pivot-row{min-height:var(--grid-data-row-height, 50px)!important;height:var(--grid-data-row-height, 50px)!important}.pivot-mode .pivot-tbody tr.pivot-row:hover{background-color:var(--grid-surface-variant)}.pivot-mode .pivot-tbody tr.pivot-row:nth-child(2n){background-color:#00000005}.pivot-mode .pivot-tbody tr.pivot-row td{min-height:var(--grid-data-row-height, 50px)!important;height:var(--grid-data-row-height, 50px)!important;vertical-align:middle;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important}.pivot-mode .pivot-tbody tr.pivot-row td .cell-content{min-height:calc(var(--grid-data-row-height, 50px) - 2px);display:flex;align-items:center;justify-content:center}.pivot-mode .pivot-tbody tr.pivot-row td .cell-content data-cell{width:100%;min-height:calc(var(--grid-data-row-height, 50px) - 4px);display:flex;align-items:center;justify-content:center;overflow:hidden;flex-shrink:0}.pivot-mode .pivot-cell{vertical-align:middle;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important}.pivot-mode .pivot-cell.aggregated-value{font-weight:500;font-family:Roboto Mono,monospace}.pivot-mode .pivot-cell .cell-content{display:flex;justify-content:center;align-items:center;min-height:var(--grid-header-row-height, 40px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex-shrink:0}.pivot-mode .pivot-table .subtotal-row{background-color:var(--grid-surface-container)!important;font-weight:600}.pivot-mode .pivot-table .subtotal-row td{background-color:var(--grid-surface-container);color:var(--grid-on-surface-variant)}.pivot-mode .pivot-table .subtotal-row td:first-child{color:var(--grid-primary)}.pivot-mode .pivot-table .subtotal-row td.aggregated-value{font-weight:500;color:var(--grid-primary)}.pivot-mode .pivot-table .subtotal-row:hover{background-color:var(--grid-surface-container-high)!important}.pivot-mode .pivot-table .subtotal-row:hover td{background-color:var(--grid-surface-container-high)}.pivot-mode .pivot-table .subtotal-bold td{font-weight:600!important;font-style:normal!important}.pivot-mode .pivot-table .subtotal-bold td.aggregated-value{font-weight:600!important}.pivot-mode .pivot-table .subtotal-italic td{font-style:italic!important}.pivot-mode .pivot-table .subtotal-italic td:first-child{font-weight:600!important}.pivot-mode .pivot-table .subtotal-italic td.aggregated-value{font-style:italic!important;font-weight:500!important}.pivot-mode .pivot-table .subtotal-highlighted{background-color:var(--grid-surface-variant)!important}.pivot-mode .pivot-table .subtotal-highlighted td{background-color:var(--grid-surface-variant)!important;font-weight:700!important;font-style:normal!important;color:var(--grid-primary)!important}.pivot-mode .pivot-table .subtotal-highlighted td.aggregated-value{font-weight:500!important;color:var(--grid-primary)!important}.pivot-mode .pivot-table .subtotal-highlighted:hover,.pivot-mode .pivot-table .subtotal-highlighted:hover td{background-color:var(--grid-surface-container-high)!important}.pivot-mode .pivot-table .grand-total-row{background-color:var(--grid-surface-container-high)!important;font-weight:700;font-size:var(--grid-font-size-body)}.pivot-mode .pivot-table .grand-total-row td{background-color:var(--grid-surface-container-high)!important;color:var(--grid-on-surface)}.pivot-mode .pivot-table .grand-total-row td:first-child{font-style:normal;font-weight:800;color:var(--grid-primary)}.pivot-mode .pivot-table .grand-total-row td.aggregated-value{font-weight:500;color:var(--grid-primary);font-family:Roboto Mono,monospace}.pivot-mode .pivot-table .grand-total-row:hover,.pivot-mode .pivot-table .grand-total-row:hover td{background-color:var(--grid-surface-container-high)!important}.pivot-mode .pivot-table .grand-total-bold td{font-weight:700!important;font-style:normal!important}.pivot-mode .pivot-table .grand-total-bold td.aggregated-value{font-weight:700!important}.pivot-mode .pivot-table .grand-total-italic td,.pivot-mode .pivot-table .grand-total-italic td.aggregated-value{font-style:italic!important;font-weight:500!important}.pivot-mode .pivot-table .grand-total-highlighted{background-color:var(--grid-primary)!important;box-shadow:var(--grid-elevation-2)!important}.pivot-mode .pivot-table .grand-total-highlighted td{background-color:var(--grid-primary)!important;color:var(--grid-on-primary)!important;font-weight:500!important;font-style:normal!important}.pivot-mode .pivot-table .grand-total-highlighted td.aggregated-value{color:var(--grid-on-primary)!important;font-weight:500!important}.pivot-mode .pivot-table .grand-total-highlighted:hover,.pivot-mode .pivot-table .grand-total-highlighted:hover td{background-color:var(--grid-primary)!important}.pivot-mode .pivot-table .collapsible-header{position:relative}.pivot-mode .pivot-table .collapsible-header .header-content{display:flex;align-items:center;justify-content:space-between;gap:var(--grid-spacing-xs);padding:var(--grid-spacing-xs) var(--grid-spacing-sm)}.pivot-mode .pivot-table .collapsible-header .header-label{flex:1;font-weight:600}.pivot-mode .pivot-table .collapsible-header .collapse-toggle-btn{background:none;border:none;cursor:pointer;padding:var(--grid-spacing-xxs);margin:0;display:flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:var(--grid-border-radius);color:var(--grid-on-surface-variant);transition:all .2s ease;font-size:12px;font-weight:600}.pivot-mode .pivot-table .collapsible-header .collapse-toggle-btn:hover{background-color:var(--grid-surface-container);color:var(--grid-primary);transform:scale(1.1)}.pivot-mode .pivot-table .collapsible-header .collapse-toggle-btn:focus{outline:2px solid var(--grid-primary);outline-offset:1px}.pivot-mode .pivot-table .collapsible-header .collapse-toggle-btn .collapse-icon{display:block;line-height:1;font-family:monospace;font-size:14px}.pivot-mode .pivot-table .collapsible-header.expanded .collapse-toggle-btn .collapse-icon{color:var(--grid-primary)}.pivot-mode .pivot-table .collapsible-header.collapsed{background-color:var(--grid-surface-variant)}.pivot-mode .pivot-table .collapsible-header.collapsed .header-label{font-style:italic;color:var(--grid-on-surface-variant)}.pivot-mode .pivot-table .collapsible-header.collapsed .collapse-toggle-btn .collapse-icon{color:var(--grid-outline)}.pivot-mode .pivot-table .collapsible-header:hover{background-color:var(--grid-surface-container)}.pivot-mode .pivot-table .collapsible-header:hover .header-label{color:var(--grid-on-surface)}.pivot-mode .pivot-table .pivot-single-table{display:flex;flex-direction:column;height:100%;width:100%;overflow:hidden;min-height:var(--table-min-height)!important}.pivot-mode .pivot-table .pivot-single-table .pivot-header-container{flex-shrink:0;background:var(--grid-surface)!important;overflow-x:auto;overflow-y:hidden;min-height:100px!important;height:auto!important}.pivot-mode .pivot-table .pivot-single-table .pivot-header-container .pivot-table{width:auto;min-width:100%;height:auto!important;min-height:100px!important}.pivot-mode .pivot-table .pivot-single-table .pivot-header-container .pivot-table th{background:var(--grid-surface-container)!important;padding:8px 6px!important;white-space:nowrap;min-width:50px;min-height:40px!important;height:auto!important;position:relative;visibility:visible!important;color:var(--grid-on-surface)!important}.pivot-mode .pivot-table .pivot-single-table .pivot-header-container .pivot-table th.sticky-column{position:sticky!important;background:var(--grid-surface-container)!important;border-right:2px solid var(--grid-primary)!important;box-shadow:2px 0 4px #0000001a;z-index:101!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container{flex:1;overflow:auto;min-height:300px!important;height:auto!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-viewport{height:100%!important;width:100%!important;overflow-x:auto!important;overflow-y:auto!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-table,.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-data-table{width:auto;min-width:100%;height:auto!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-table td,.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-data-table td{padding:8px 6px!important;white-space:nowrap;min-width:50px;min-height:32px!important;height:auto!important;background:var(--grid-surface)!important;color:var(--grid-on-surface)!important;visibility:visible!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-table td.sticky-column,.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-data-table td.sticky-column{position:sticky!important;background:var(--grid-surface-container)!important;border-right:2px solid var(--grid-primary)!important;box-shadow:2px 0 4px #0000001a;z-index:100!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-table tbody tr,.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-data-table tbody tr{height:auto!important;min-height:50px!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-table tbody tr.pivot-row,.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-data-table tbody tr.pivot-row{visibility:visible!important;display:table-row!important}.pivot-mode .pivot-table .collapsed-column-group{background-color:var(--grid-surface-container);border-left:3px solid var(--grid-primary)}.pivot-mode .pivot-table .collapsed-column-group:hover{background-color:var(--grid-surface-container-high)}.pivot-row.subtotal-row{background-color:var(--grid-surface-variant);font-weight:500}.pivot-row.subtotal-row.subtotal-bold{font-weight:500}.pivot-row.subtotal-row.subtotal-italic{font-style:italic}.pivot-row.subtotal-row.subtotal-highlighted{background-color:var(--grid-primary);color:var(--grid-on-primary)}.pivot-row.grand-total-row{background-color:var(--grid-surface-container);font-weight:600}.pivot-row.grand-total-row.grand-total-bold{font-weight:800}.pivot-row.grand-total-row.grand-total-italic{font-style:italic}.pivot-row.grand-total-row.grand-total-highlighted{background-color:var(--grid-primary);color:var(--grid-on-primary)}.pivot-row.first-visible-row{background-color:#6750a41a!important;position:relative}.pivot-row.first-visible-row:before{content:\"\\1f441\\fe0f First Visible\";position:absolute;top:-20px;left:0;background:var(--grid-primary);color:var(--grid-on-primary);padding:2px 6px;font-size:10px;border-radius:2px;z-index:1000}.header-wrap-text{white-space:pre-wrap;word-break:auto-phrase}.group-header-row{display:flex;align-items:center;justify-content:space-between;width:100%;padding-right:12px}.custom-collapse-header{background-color:var(--grid-surface-variant);padding:8px 20px;border-top-left-radius:12px;border-top-right-radius:12px;cursor:pointer;display:flex;width:fit-content;align-items:center;-webkit-user-select:none;user-select:none;min-width:200px;margin-bottom:10px;position:sticky;left:1px;z-index:116}.custom-collapse-header .collapse-arrow{display:inline-block;margin-right:8px;font-size:12px;color:var(--grid-on-surface-variant);transition:transform .2s ease;transform:rotate(0)}.custom-collapse-header .collapse-arrow.rotate-arrow{transform:rotate(270deg)}.custom-collapse-header .f-12{font-size:12px;color:var(--grid-on-surface)}.custom-collapse-header .group-sort-indicator{display:inline-flex;align-items:center;margin-left:8px}.custom-collapse-header .group-sort-indicator .sort-triangles{display:flex;flex-direction:column;align-items:center;gap:2px}.custom-collapse-header .group-sort-indicator .sort-tri{width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;cursor:pointer;transition:border-color .15s ease}.custom-collapse-header .group-sort-indicator .sort-tri-up{border-bottom:5px solid var(--grid-on-surface-variant, #49454f);opacity:.3}.custom-collapse-header .group-sort-indicator .sort-tri-down{border-top:5px solid var(--grid-on-surface-variant, #49454f);opacity:.3}.custom-collapse-header .group-sort-indicator .sort-tri-active{opacity:1}.custom-collapse-header .group-sort-indicator .sort-tri-active.sort-tri-up{border-bottom-color:var(--grid-primary, #6750a4)}.custom-collapse-header .group-sort-indicator .sort-tri-active.sort-tri-down{border-top-color:var(--grid-primary, #6750a4)}.excel-download-icon{cursor:pointer}.excel-download-icon:hover{opacity:.75}.excel-download-bar{display:flex;justify-content:flex-end;padding:4px 12px;flex-shrink:0}.table-mode .header-shell{flex-shrink:0;width:100%;box-sizing:border-box;padding-right:var(--scrollbar-width, 17px);overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}.table-mode .header-shell::-webkit-scrollbar{display:none}.table-mode .header-shell.apply-cdk-width{width:calc(var(--table-total-width) + 10px)!important}.table-mode .subtotal-row{background-color:var(--grid-surface-container)!important;font-weight:600}.table-mode .subtotal-row td{background-color:var(--grid-surface-container);color:var(--grid-on-surface-variant)}.table-mode .subtotal-row td:first-child{color:var(--grid-primary)}.table-mode .subtotal-row td.subtotal-cell{font-weight:500}.table-mode .subtotal-row td.subtotal-cell .subtotal-label{font-weight:600;color:var(--grid-primary)}.table-mode .subtotal-row:hover{background-color:var(--grid-surface-container-high)!important}.table-mode .subtotal-row:hover td{background-color:var(--grid-surface-container-high)}.table-mode .subtotal-row.subtotal-bold td{font-weight:600!important;font-style:normal!important}.table-mode .subtotal-row.subtotal-italic td{font-style:italic!important}.table-mode .subtotal-row.subtotal-italic td:first-child{font-weight:600!important}.table-mode .subtotal-row.subtotal-highlighted{background-color:var(--grid-surface-variant)!important}.table-mode .subtotal-row.subtotal-highlighted td{background-color:var(--grid-surface-variant)!important;font-weight:700!important;font-style:normal!important;color:var(--grid-primary)!important}.table-mode .subtotal-row.subtotal-highlighted:hover,.table-mode .subtotal-row.subtotal-highlighted:hover td{background-color:var(--grid-surface-container-high)!important}.table-mode .grand-total-row{background-color:var(--grid-surface-container-high)!important;font-weight:700}.table-mode .grand-total-row td{background-color:var(--grid-surface-container-high);color:var(--grid-on-surface)}.table-mode .grand-total-row td:first-child{color:var(--grid-primary)}.table-mode .grand-total-row td.grand-total-cell{font-weight:600}.table-mode .grand-total-row td.grand-total-cell .grand-total-label{font-weight:700;color:var(--grid-primary)}.table-mode .grand-total-row:hover{background-color:var(--grid-surface-container-high)!important}.table-mode .grand-total-row:hover td{background-color:var(--grid-surface-container-high)}.table-mode .grand-total-row.grand-total-bold td{font-weight:700!important;font-style:normal!important}.table-mode .grand-total-row.grand-total-italic td{font-style:italic!important}.table-mode .grand-total-row.grand-total-italic td:first-child{font-weight:700!important}.table-mode .grand-total-row.grand-total-highlighted{background-color:var(--grid-surface-variant)!important}.table-mode .grand-total-row.grand-total-highlighted td{background-color:var(--grid-surface-variant)!important;font-weight:800!important;font-style:normal!important;color:var(--grid-primary)!important}.table-mode .grand-total-row.grand-total-highlighted:hover,.table-mode .grand-total-row.grand-total-highlighted:hover td{background-color:var(--grid-surface-container-high)!important}.table-mode .subtotal-row-shell{width:100%;box-sizing:border-box;padding-right:var(--scrollbar-width, 17px);overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}.table-mode .subtotal-row-shell::-webkit-scrollbar{display:none}.board-mode-host{overflow:hidden;display:flex;flex-direction:column;max-height:var(--grid-height, 600px)}.board-mode-host .board-view-container{display:flex;flex-direction:column;flex:1;min-height:0}.board-mode-host .board-sort-bar{display:flex;align-items:center;gap:6px;padding:8px 16px;flex-shrink:0;border-bottom:1px solid var(--grid-outline-variant, #cac4d0);background:var(--grid-surface, #fffbfe);overflow-x:auto}.board-mode-host .board-sort-bar .board-sort-label{font-size:12px;font-weight:500;color:var(--grid-on-surface-variant, #49454f);white-space:nowrap}.board-mode-host .board-sort-bar .board-sort-chip{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:16px;border:1px solid var(--grid-outline-variant, #cac4d0);background:var(--grid-surface, #fffbfe);color:var(--grid-on-surface, #1d1b20);font-size:12px;white-space:nowrap}.board-mode-host .board-sort-bar .board-sort-chip-active{background:var(--grid-surface-container);border-color:var(--grid-outline, #79757f);color:var(--grid-on-surface, #1d1b20)}.board-mode-host .board-sort-bar .board-sort-chip-label{pointer-events:none}.board-mode-host .board-sort-bar .board-sort-chip-arrow{font-size:10px;line-height:1;cursor:pointer;padding:2px;border-radius:4px}.board-mode-host .board-sort-bar .board-sort-chip-arrow:hover{background:#00000014}.board-mode-host .board-sort-bar .board-sort-chip-priority{font-size:9px;font-weight:700;background:var(--grid-primary, #6750a4);color:var(--grid-on-primary, #ffffff);border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center}.board-mode-host .board-sort-bar .board-sort-chip-remove{font-size:10px;cursor:pointer;padding:2px;border-radius:4px;color:var(--grid-on-surface-variant, #49454f)}.board-mode-host .board-sort-bar .board-sort-chip-remove:hover{background:#00000014;color:var(--grid-error, #b3261e)}.board-mode-host .board-sort-bar .board-sort-add-btn{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:16px;border:1px dashed var(--grid-outline-variant, #cac4d0);background:transparent;color:var(--grid-on-surface-variant, #49454f);font-size:12px;cursor:pointer;white-space:nowrap;transition:background .15s ease,border-color .15s ease}.board-mode-host .board-sort-bar .board-sort-add-btn .board-sort-add-icon{font-size:14px;width:14px;height:14px}.board-mode-host .board-sort-bar .board-sort-add-btn:hover{background:var(--grid-surface-container-low, #f7f2fa);border-color:var(--grid-primary, #6750a4);color:var(--grid-primary, #6750a4)}.board-mode-host .board-sort-bar .board-sort-clear{display:inline-flex;align-items:center;padding:4px 10px;border-radius:16px;border:1px solid var(--grid-error, #b3261e);background:transparent;color:var(--grid-error, #b3261e);font-size:12px;cursor:pointer;white-space:nowrap;transition:background .15s ease}.board-mode-host .board-sort-bar .board-sort-clear:hover{background:#b3261e14}.board-mode-host .board-columns-wrapper{display:grid;grid-template-columns:repeat(auto-fit,minmax(var(--board-col-min-width, 300px),var(--board-col-max-width, 1fr)));grid-auto-rows:auto;align-items:start;gap:16px;flex:1;min-height:0;overflow-x:hidden;overflow-y:auto;align-content:start;justify-content:start}.board-mode-host .board-columns-wrapper.board-columns-nowrap{grid-auto-flow:column;grid-template-columns:none;grid-template-rows:auto;grid-auto-rows:auto;grid-auto-columns:minmax(var(--board-col-min-width, 300px),var(--board-col-max-width, 1fr));overflow-x:auto;overflow-y:hidden}.board-mode-host .board-column{max-height:var(--board-column-height, 420px);min-width:0;display:flex;flex-direction:column;background:var(--grid-surface-container, #f3edf7);border-radius:12px;min-height:0;overflow:hidden}.board-mode-host .board-column.board-column-accented{border-top:3px solid var(--board-group-color, transparent)}.board-mode-host .board-column .column-header{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 14px;font-weight:600;flex-shrink:0;background-color:transparent}.board-mode-host .board-column .column-header:hover{background-color:transparent}.board-mode-host .board-column .column-header .column-header-title{font-size:15px;font-weight:700;letter-spacing:.2px;line-height:1.2;color:var(--grid-on-surface, #1d1b20);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.board-mode-host .board-column .column-header .column-header-title-cell{display:inline-flex;align-items:center;flex:1 1 auto;min-width:0;overflow:visible;text-overflow:clip}.board-mode-host .board-column .column-header .column-header-title-cell data-cell,.board-mode-host .board-column .column-header .column-header-title-cell .data-cell-component,.board-mode-host .board-column .column-header .column-header-title-cell .container{padding:0!important;margin:0!important;border:none!important;background:transparent!important;min-height:0!important;height:auto!important;width:auto!important;max-width:100%!important;overflow:visible!important}.board-mode-host .board-column .column-header .column-header-title-cell .status-display,.board-mode-host .board-column .column-header .column-header-title-cell .status-display-content,.board-mode-host .board-column .column-header .column-header-title-cell .status-text,.board-mode-host .board-column .column-header .column-header-title-cell .tag-display,.board-mode-host .board-column .column-header .column-header-title-cell .tag-text{max-width:none!important;overflow:visible!important;text-overflow:clip!important}.board-mode-host .board-column .column-header .column-header-count{font-size:10px;font-weight:600;color:var(--eru-board-count-color, var(--grid-on-surface-variant, #49454f));background:var(--eru-board-count-bg, var(--grid-surface-variant, #e7e0ec));border-radius:10px;padding:3px 10px;white-space:nowrap;flex-shrink:0}.board-mode-host .board-column-body{flex:0 1 auto;min-height:0}.board-mode-host .board-card-container{box-sizing:border-box;overflow:hidden;border-radius:8px;transition:background-color .15s ease,box-shadow .15s ease}.board-mode-host .board-card-container.show-row-lines{box-shadow:inset 0 0 0 var(--grid-divider-width, 1px) var(--grid-divider-color, var(--grid-outline, #e0e0e0))}.board-mode-host .board-card-container:hover{background-color:var(--eru-board-card-hover-bg, color-mix(in srgb, var(--mat-sys-primary, #1976d2) 8%, transparent))}.board-mode-host .board-card-container.selected{background-color:var(--eru-board-card-selected-bg, color-mix(in srgb, var(--mat-sys-primary, #1976d2) 14%, transparent));box-shadow:inset 0 0 0 2px var(--eru-board-card-selected-outline, var(--mat-sys-primary, #1976d2))}.board-mode-host .board-card{height:calc(100% - 8px);overflow:hidden;cursor:pointer}.board-mode-host .board-card mat-card-title{font-size:13px}.board-mode-host .board-card mat-card-subtitle{font-size:12px}.board-mode-host .board-card-field{display:flex;flex-direction:column;margin-bottom:4px}.board-mode-host .board-field-label{font-size:10px;color:var(--grid-on-surface-variant, #49454f);font-weight:500;text-transform:uppercase;letter-spacing:.5px}.board-mode-host .board-ghost-card{margin:8px;padding:16px;background:var(--grid-surface, #fef7ff);border-radius:8px;animation:board-pulse 1.5s ease-in-out infinite}.board-mode-host .board-ghost-line{height:12px;background:var(--grid-surface-variant, #e7e0ec);border-radius:4px;margin-bottom:8px}.board-mode-host .board-ghost-line--short{width:60%}@keyframes board-pulse{0%,to{opacity:1}50%{opacity:.5}}th.row-expand-toggle,td.row-expand-toggle{width:40px!important;min-width:40px!important;max-width:40px!important;padding:0!important;text-align:center;vertical-align:middle;cursor:pointer;-webkit-user-select:none;user-select:none;box-sizing:border-box}.row-expand-icon{font-size:20px;width:20px;height:20px;line-height:20px;color:var(--grid-on-surface-variant);transition:transform .15s ease-in-out}.row-expand-icon.expanded{transform:rotate(90deg)}.row-detail{background:var(--grid-surface-container)}.row-detail .row-detail-cell{padding:var(--grid-spacing-sm) var(--grid-spacing-md);border-bottom:1px solid var(--grid-outline-variant)}.row-detail .row-detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:var(--grid-spacing-sm) var(--grid-spacing-md)}.row-detail .row-detail-field{display:flex;flex-direction:column;gap:var(--grid-spacing-xxs);min-width:0}.row-detail .row-detail-label{font-size:var(--grid-font-size-caption);color:var(--grid-on-surface-variant);font-weight:500}.row-detail .row-detail-value{min-width:0}.row-detail .row-detail-value data-cell{display:block;width:100%}\n"] }]
16894
+ ], template: "<!-- <div style=\"background: #f0f0f0; font-size: 12px; border-bottom: 1px solid #ccc;\">\ncurrentPivotScrollIndex {{currentPivotScrollIndex()}} |\nfirstDataRowIndex {{firstDataRowIndex()}} |\nfirstTr {{firstTr}} |\nmaxDepth {{maxDepth()}}\n</div> -->\n<ng-template #excelDownloadIcon>\n <svg class=\"excel-download-icon\" title=\"Download Excel\" (click)=\"onExcelDownloadClick($event)\"\n xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 48 48\" width=\"24px\" height=\"24px\">\n <path fill=\"#169154\" d=\"M29,6H15.744C14.781,6,14,6.781,14,7.744v7.259h15V6z\" />\n <path fill=\"#18482a\" d=\"M14,33.054v7.202C14,41.219,14.781,42,15.743,42H29v-8.946H14z\" />\n <path fill=\"#0c8045\" d=\"M14 15.003H29V24.005000000000003H14z\" />\n <path fill=\"#17472a\" d=\"M14 24.005H29V33.055H14z\" />\n <g>\n <path fill=\"#29c27f\" d=\"M42.256,6H29v9.003h15V7.744C44,6.781,43.219,6,42.256,6z\" />\n <path fill=\"#27663f\" d=\"M29,33.054V42h13.257C43.219,42,44,41.219,44,40.257v-7.202H29z\" />\n <path fill=\"#19ac65\" d=\"M29 15.003H44V24.005000000000003H29z\" />\n <path fill=\"#129652\" d=\"M29 24.005H44V33.055H29z\" />\n </g>\n <path fill=\"#0c7238\"\n d=\"M22.319,34H5.681C4.753,34,4,33.247,4,32.319V15.681C4,14.753,4.753,14,5.681,14h16.638 C23.247,14,24,14.753,24,15.681v16.638C24,33.247,23.247,34,22.319,34z\" />\n <path fill=\"#fff\"\n d=\"M9.807 19L12.193 19 14.129 22.754 16.175 19 18.404 19 15.333 24 18.474 29 16.123 29 14.013 25.07 11.912 29 9.526 29 12.719 23.982z\" />\n </svg>\n</ng-template>\n\n<div class=\"incremental-row-container eru-grid\" #rowContainer [class.pivot-mode]=\"gridStore.isPivotMode()\"\n [class.table-mode]=\"!gridStore.isPivotMode() && !isBoardMode()\" [class.board-mode-host]=\"isBoardMode()\">\n <eru-column-design-panel></eru-column-design-panel>\n <!-- Pivot Mode Template -->\n @if (gridStore.isPivotMode()) {\n <ng-container>\n <div class=\"pivot-container\" style=\"display: flex; flex-direction: column; height: 100%;\"\n [style]=\"'--table-min-height: ' + getInitialMinHeightPx() + 'px; --table-total-width: ' + getInitialTotalWidth() + 'px'\">\n @if(showExcelDownload()) {\n <div class=\"excel-download-bar\">\n <ng-container *ngTemplateOutlet=\"excelDownloadIcon\"></ng-container>\n </div>\n }\n <!-- Debug info for first visible row -->\n\n\n <div class=\"pivot-single-table\"\n style=\"height: 100%; width: 100%; overflow: hidden; display: flex; flex-direction: column;\">\n @if (freezeHeader()) {\n <div #headerScroller class=\"header-shell\">\n <table class=\"eru-grid-table pivot-table\"\n [style]=\"'width: auto; min-width: 100%; --table-total-width: ' + getInitialTotalWidth() + 'px'\"\n [class.show-column-lines]=\"showColumnLines()\" [class.show-row-lines]=\"showRowLines()\">\n <!-- Column Groups for consistent width -->\n <ng-container *ngTemplateOutlet=\"pivotColGroup\"></ng-container>\n\n <ng-container *ngTemplateOutlet=\"pivotTableHead\"></ng-container>\n @if(grandTotalPosition() === 'before' && freezeGrandTotal()) {\n <ng-container *ngTemplateOutlet=\"pivotGrandTotal\"></ng-container>\n }\n </table>\n </div>\n }\n <!-- Virtual Scrolled Table Body -->\n <div>\n <cdk-virtual-scroll-viewport #vp [itemSize]=\"dataRowHeight()\" class=\"viewport pivot-viewport\"\n [class.apply-cdk-width]=\"applyCdkWidth()\" (scrolledIndexChange)=\"onPivotScroll($event)\"\n (scroll)=\"onBodyScroll($event)\" style=\"overflow: auto;\">\n <table class=\"eru-grid-table pivot-table\"\n [style]=\"'width: auto; min-width: 100%; --table-total-width: ' + getInitialTotalWidth() + 'px'\"\n [class.show-column-lines]=\"showColumnLines()\" [class.show-row-lines]=\"showRowLines()\">\n <!-- Column Groups for consistent width -->\n <ng-container *ngTemplateOutlet=\"pivotColGroup\"></ng-container>\n\n @if (!freezeHeader()) {\n <ng-container *ngTemplateOutlet=\"pivotTableHead\"></ng-container>\n }\n <!-- Table Body with Virtual Scrolling -->\n <tbody class=\"pivot-tbody\">\n\n <tr *cdkVirtualFor=\"let pivotRow of gridStore.pivotDisplayData(); \n trackBy: trackByPivotRowFn; \n let i = index\" class=\"pivot-row\" [class.subtotal-row]=\"pivotRow._isSubtotal\"\n [class.grand-total-row]=\"pivotRow._isGrandTotal\"\n [class.subtotal-bold]=\"pivotRow._isSubtotal && subTotalStyle() === 'bold'\"\n [class.subtotal-italic]=\"pivotRow._isSubtotal && subTotalStyle() === 'italic'\"\n [class.subtotal-highlighted]=\"pivotRow._isSubtotal && subTotalStyle() === 'highlighted'\"\n [class.grand-total-bold]=\"pivotRow._isGrandTotal && grandTotalStyle() === 'bold'\"\n [class.grand-total-italic]=\"pivotRow._isGrandTotal && grandTotalStyle() === 'italic'\"\n [class.grand-total-highlighted]=\"pivotRow._isGrandTotal && grandTotalStyle() === 'highlighted'\"\n [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\" [style.cursor]=\"cursorOnHover() || null\" [attr.data-pivot-row]=\"i\">\n @if ((!pivotRow._isGrandTotal && freezeGrandTotal() ) || (!freezeGrandTotal() )) {\n @for (column of getLeafColumns(); track trackByColumnFn($index, column); let colIndex = $index) {\n <td [style.width.px]=\"column.field_size\" [style.minWidth.px]=\"column.field_size\" class=\"pivot-cell\"\n [class.row-dimension-cell]=\"isRowDimensionColumn(column.name)\"\n [class.column-dimension-cell]=\"!isRowDimensionColumn(column.name)\"\n [class.aggregated-value]=\"!isRowDimensionColumn(column.name) && column.datatype === 'number'\"\n [class.pivot-repeated-value]=\"isRepeatedDimensionValue(i, column.name)\"\n [class.pivot-group-start]=\"isPivotGroupStart(i, column.name)\"\n [class.sticky-column]=\"isStickyColumn(column.name, colIndex)\"\n [style.position]=\"isStickyColumn(column.name, colIndex) ? 'sticky' : 'static'\"\n [style.left.px]=\"getStickyColumnLeft(column.name, colIndex)\"\n [style.z-index]=\"isStickyColumn(column.name, colIndex) ? 99 : 1\" [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\">\n <div class=\"cell-content pivot-cell-content\">\n <data-cell [personCardTemplate]=\"personCardTemplate\" [class.aggregation]=\"!!column.aggregationFunction\" [fieldSize]=\"column.field_size\"\n [columnDatatype]=\"column.datatype\" [columnName]=\"column.name\" [value]=\"pivotRow[column.name]\"\n [column]=\"column\" [drillable]=\"column.enableDrilldown || false\" [mode]=\"mode()\"\n [isEditable]=\"isEditable()\" [id]=\"'pivot_' + i + '_' + column.name\" [eruGridStore]=\"gridStore\"\n [row]=\"pivotRow\">\n </data-cell>\n </div>\n </td>\n }\n } @else {\n <td [style.height.px]=\"dataRowHeight()\" [attr.colspan]=\"getLeafColumns().length\"> </td>\n }\n </tr>\n </tbody>\n </table>\n </cdk-virtual-scroll-viewport>\n\n </div>\n @if (freezeGrandTotal() && grandTotalPosition() === 'after') {\n <div #gtScroller class=\"header-shell gt-shell\" [class.adjust-bottom]=\"!applyCdkWidth()\"\n [class.adjust-bottom-vs]=\"adjustScrollWidth()\">\n <table class=\"eru-grid-table pivot-table\"\n [style]=\"'width: auto; min-width: 100%; --table-total-width: ' + getInitialTotalWidth() + 'px'\"\n [class.show-column-lines]=\"showColumnLines()\" [class.show-row-lines]=\"showRowLines()\">\n <ng-container *ngTemplateOutlet=\"pivotColGroup\"></ng-container>\n\n <ng-container *ngTemplateOutlet=\"pivotGrandTotal\"></ng-container>\n\n </table>\n </div>\n }\n\n\n </div>\n </div>\n </ng-container>\n } @else if (isBoardMode()) {\n <!-- Board Mode Template -->\n <div class=\"board-view-container\">\n @if(showExcelDownload()) {\n <div class=\"excel-download-bar\">\n <ng-container *ngTemplateOutlet=\"excelDownloadIcon\"></ng-container>\n </div>\n }\n @if(showSortBar()) {\n <div class=\"board-sort-bar\">\n <span class=\"board-sort-label\">Sort by:</span>\n @for (entry of gridStore.sortColumns(); track getFieldName(entry)) {\n <span class=\"board-sort-chip board-sort-chip-active\">\n <span class=\"board-sort-chip-label\">{{getColumnLabel(entry)}}</span>\n <span class=\"board-sort-chip-arrow\" (click)=\"onBoardSortChipToggle($event, entry)\">\n @if(!entry.startsWith('-')) { \u25B2 } @else { \u25BC }\n </span>\n @if(gridStore.sortColumns().length > 1) {\n <span class=\"board-sort-chip-priority\">{{getSortPriority(getFieldName(entry))}}</span>\n }\n <span class=\"board-sort-chip-remove\" (click)=\"onBoardSortChipRemove($event, entry)\">\u2715</span>\n </span>\n }\n <button class=\"board-sort-add-btn\" [matMenuTriggerFor]=\"sortFieldMenu\">\n <mat-icon class=\"board-sort-add-icon\">add</mat-icon> Add field\n </button>\n <mat-menu #sortFieldMenu=\"matMenu\" class=\"board-sort-menu\">\n @for (column of columns(); track column.name) {\n <button mat-menu-item (click)=\"onBoardSortFieldSelect(column)\"\n [disabled]=\"getSortDirection(column.name) !== null\">\n @if(getSortDirection(column.name) !== null) {\n <mat-icon>check</mat-icon>\n } @else {\n <mat-icon></mat-icon>\n }\n {{column.label}}\n </button>\n }\n </mat-menu>\n @if(gridStore.sortColumns().length > 0) {\n <button class=\"board-sort-clear\" (click)=\"onBoardSortClear()\">\u2715 Clear</button>\n }\n </div>\n }\n <div class=\"board-columns-wrapper\" [class.board-columns-nowrap]=\"!boardWrapColumns()\">\n @for (group of groups(); track group.id) {\n <div class=\"board-column\" [class.board-column-accented]=\"!!boardGroupColor(group)\"\n [style.--board-group-color]=\"boardGroupColor(group)\">\n @if (showBoardColumnHeader()) {\n <div class=\"column-header\">\n <!-- Render the group value through the same read-only cell renderer a\n data cell uses, so the grouped field's datatype formats itself\n (status/tag pills, dates, numbers) instead of printing raw text. -->\n @if (groupByColumn(); as gcol) {\n <span class=\"column-header-title column-header-title-cell\">\n <data-cell\n [eruGridStore]=\"gridStore\"\n [column]=\"gcol\"\n [columnDatatype]=\"gcol.datatype\"\n [columnName]=\"gcol.name\"\n [value]=\"group.title\"\n [id]=\"'board-group-' + group.id\"\n [fieldSize]=\"0\"\n [isEditable]=\"false\"\n [mode]=\"'board-group-header'\">\n </data-cell>\n </span>\n } @else {\n <span class=\"column-header-title\">{{ group.title }}</span>\n }\n <span class=\"column-header-count\">{{ group.currentLoadedRows || 0 }} of {{ group.totalRowCount || 0 }}</span>\n </div>\n }\n <cdk-virtual-scroll-viewport [attr.data-group-id]=\"group.id\" [itemSize]=\"boardCardSlot\" class=\"board-column-body\"\n [style.height.px]=\"boardColumnBodyHeight(group)\"\n (scrolledIndexChange)=\"onBoardScrolledIndexChange($event, group)\">\n <div\n *cdkVirtualFor=\"let row of getRowsForGroupSignal(group.id === null || group.id === undefined ? '__NULL_GROUP__' : group.id)(); templateCacheSize: 0\"\n class=\"board-card-container\"\n [class.selected]=\"isBoardCardActive(row)\"\n [class.show-row-lines]=\"showRowLines()\"\n [style.height.px]=\"boardCardOuterHeight\"\n [style.padding.px]=\"boardCardPadding\"\n [style.marginBottom.px]=\"boardCardGap\"\n [style.cursor]=\"cursorOnHover() || null\"\n (click)=\"emitRowSelect(row, 'board', group)\">\n <!-- Custom template when consumer provides boardCardTemplate; default card otherwise -->\n <ng-container\n *ngTemplateOutlet=\"boardCardTemplate ?? defaultBoardCard;\n context: { $implicit: row, columns: visibleBoardFields(), group: group }\">\n </ng-container>\n </div>\n </cdk-virtual-scroll-viewport>\n @if (group.isLoading) {\n <div class=\"board-ghost-card\">\n <div class=\"board-ghost-line\"></div>\n <div class=\"board-ghost-line board-ghost-line--short\"></div>\n </div>\n }\n </div>\n }\n </div>\n </div>\n } @else {\n\n <!-- Table Mode Template -->\n @if(showExcelDownload() && !showGroupBar()) {\n <div class=\"excel-download-bar\">\n <ng-container *ngTemplateOutlet=\"excelDownloadIcon\"></ng-container>\n </div>\n }\n <!-- Select-all banner: the header checkbox can only reach loaded pages, so\n this is where the user says they meant the whole result set. -->\n @if(showSelectAllBanner()) {\n <div class=\"selection-banner\">\n @if(selectAllMatching()) {\n <span class=\"selection-banner-text\">All {{ totalMatchingCount() }} matching records are selected.</span>\n <button type=\"button\" class=\"selection-banner-action\" (click)=\"clearRowSelection()\">Clear selection</button>\n } @else {\n <span class=\"selection-banner-text\">{{ gridStore.totalSelectedCount() }} selected on this view.</span>\n <button type=\"button\" class=\"selection-banner-action\" (click)=\"selectAllMatchingRows()\">Select all {{\n totalMatchingCount() }} matching</button>\n }\n </div>\n }\n <!-- Scrollable groups container \u2014 plain iteration avoids CDK fixed-height estimation errors -->\n <div #groupsScrollContainer class=\"groups-scroll-container\" (scroll)=\"onGroupsViewportScroll($event)\">\n\n @for (group of groups(); track trackByGroupFn($index, group); let i = $index) {\n <div class=\"group-container\"\n [attr.data-group-id]=\"group.id === null || group.id === undefined ? '__NULL_GROUP__' : group.id\">\n <!-- Combined sticky header with group info and table -->\n <div style=\"\n background:var(--grid-surface);\n position: sticky;\n top: 0;\n z-index: 115;\n \">\n @if(showGroupBar()) {\n <div class=\"group-header-row\">\n <div class=\"custom-collapse-header\" (click)=\"toggleGroupCollapse(group.id)\">\n <span class=\"collapse-arrow\" [ngClass]=\"{\n 'rotate-arrow': group.isExpanded,\n }\">\u25BC</span>\n <span class=\"f-12\">\n {{ group?.title || \"\" }}\n {{ group?.currentLoadedRows || 0 }} -\n {{ group?.totalRowCount || 0 }} rows...</span>\n @if(groupByField() && isSortable()) {\n <span class=\"group-sort-indicator\">\n <span class=\"sort-triangles\">\n <span class=\"sort-tri sort-tri-up\" [class.sort-tri-active]=\"getSortDirection(groupByField()!) === 'asc'\"\n (click)=\"onGroupSortToggle($event, 'asc')\"></span>\n <span class=\"sort-tri sort-tri-down\"\n [class.sort-tri-active]=\"getSortDirection(groupByField()!) === 'desc'\"\n (click)=\"onGroupSortToggle($event, 'desc')\"></span>\n </span>\n </span>\n }\n </div>\n @if(i === 0 && showExcelDownload()) {\n <ng-container *ngTemplateOutlet=\"excelDownloadIcon\"></ng-container>\n }\n </div>\n }\n\n @if(freezeHeader() && (group.isExpanded || !showGroupBar())) {\n <div #headerScroller class=\"header-shell\" [attr.data-group-id]=\"'header-shell-' + group.id\"\n [style]=\"'--table-total-width: ' + getInitialTotalWidth() + 'px'\">\n <table class=\"eru-grid-table\" [class.freeze-header]=\"freezeHeader()\"\n [class.show-column-lines]=\"showColumnLines()\" [class.show-row-lines]=\"showRowLines()\">\n <ng-container *ngTemplateOutlet=\"tableColGroup\"></ng-container>\n <ng-container *ngTemplateOutlet=\"tableHeader\"></ng-container>\n <!-- Grand Total row after sticky header (position: before) - only for first group -->\n @if(enableGrandTotal() && grandTotalPosition() === 'before' && hasGrandTotalData() && i === 0) {\n <tbody>\n <ng-container *ngTemplateOutlet=\"tableGrandTotal\"></ng-container>\n </tbody>\n }\n <!-- Subtotal row after sticky header (position: before) -->\n @if(enableRowSubtotals() && subtotalPosition() === 'before' && hasSubtotalData(group)) {\n <tbody>\n <ng-container *ngTemplateOutlet=\"tableSubtotal; context: { group: group }\"></ng-container>\n </tbody>\n }\n </table>\n </div>\n }\n </div>\n @if(group.isExpanded || !showGroupBar()) {\n <ng-container>\n <cdk-virtual-scroll-viewport [attr.data-group-id]=\"group.id\" [itemSize]=\"dataRowHeight()\" class=\"viewport table-viewport\"\n (scrolledIndexChange)=\"onScroll($event, group)\" (scroll)=\"onTableBodyScroll($event)\"\n [style]=\"'--table-height: ' + getGroupContentHeight(group.id) + 'px; --table-min-height: ' + getGroupContentHeight(group.id) + 'px; --table-total-width: ' + getInitialTotalWidth() + 'px'\">\n <div class=\"table-wrapper\">\n <table class=\"eru-grid-table\" [class.show-column-lines]=\"showColumnLines()\"\n [class.show-row-lines]=\"showRowLines()\">\n <ng-container *ngTemplateOutlet=\"tableColGroup\"></ng-container>\n @if(!freezeHeader()) {\n <ng-container *ngTemplateOutlet=\"tableHeader\"></ng-container>\n }\n <!-- Grand Total row after normal header (position: before) - only for first group -->\n @if(!freezeHeader() && enableGrandTotal() && grandTotalPosition() === 'before' &&\n hasGrandTotalData() && i === 0) {\n <tbody>\n <ng-container *ngTemplateOutlet=\"tableGrandTotal\"></ng-container>\n </tbody>\n }\n <!-- Subtotal row after normal header (position: before) -->\n @if(!freezeHeader() && enableRowSubtotals() && subtotalPosition() === 'before' &&\n hasSubtotalData(group)) {\n <tbody>\n <ng-container *ngTemplateOutlet=\"tableSubtotal; context: { group: group }\"></ng-container>\n </tbody>\n }\n <tbody>\n @if (columns(); as columnsList) {\n <!-- <tr *ngIf=\"groupItem.type === 'table-header' && groups().length > 1\" style=\"background:#fafafa\">\n @if(gridStore.configuration().config.allowSelection) {\n <th class=\"checkbox-column\" style=\"text-align: center;\">\n <input\n type=\"checkbox\"\n [checked]=\"isGroupSelected(groupItem.group?.id || '')\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleGroupSelection($event, groupItem.group?.id || '')\"\n >\n </th>\n }\n <th *ngFor=\"let column of columns(); trackBy: trackByColumnFn;let i =index\"\n style=\"text-align: center;\"\n [style.width.px]=\"column.field_size\"\n [style.minWidth.px]=\"column.field_size\"\n [resizeColumn]=\"true\"\n [columnConfig]=\"column\"\n [columnDraggable]=\"i\"\n class=\"column-header\">\n <div class=\"column-drag-handle\"></div>\n {{column.label}} {{column.symbol}}\n </th>\n </tr> -->\n <!-- @if(getRowsForGroup(group.id).length > 0 && group.isExpanded) { -->\n <!-- *cdkVirtualFor=\"let row of getRowsForGroupSignal(group.id)(); \n trackBy: trackByRowFn; \n let i = index\" -->\n <!-- @for(row of getRowsForGroupSignal(group.id)(); track trackByRowFn($index, row); let i = $index) { -->\n <ng-container\n *cdkVirtualFor=\"let row of getRowsForGroupSignal(group.id === null || group.id === undefined ? '__NULL_GROUP__' : group.id)(); trackBy: trackByRowFn; let i = index\">\n <tr class=\"row-item\" [attr.data-row-id]=\"i\" [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\" [style.cursor]=\"cursorOnHover() || null\" (click)=\"emitRowSelect(row, 'table', group)\">\n @if(gridStore.configuration().config.allowSelection) {\n <td class=\"checkbox-column\" style=\"text-align: center;\">\n <input type=\"checkbox\" [checked]=\"isRowSelected(row?.entity_id)\"\n (change)=\"toggleRowSelection($event, row)\">\n </td>\n }\n @if(shouldShowActionColumn('before')) {\n <td class=\"action-column\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\">\n <ng-container\n *ngTemplateOutlet=\"actionCell; context: { $implicit: row, mode: 'table', group: group }\"></ng-container>\n </td>\n }\n @if(hasHiddenColumns()) {\n <td class=\"row-expand-toggle\" (click)=\"toggleRowExpand(row, i, $event)\">\n <mat-icon class=\"row-expand-icon\" [class.expanded]=\"isRowExpanded(row, i)\">chevron_right</mat-icon>\n </td>\n }\n @for (column of visibleColumns(); track trackByColumnFn($index, column)) {\n <td #cell [style.width.px]=\"column.field_size\" [style.minWidth.px]=\"column.field_size\"\n class=\"data-cell\" [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\"\n [matTooltipClass]=\"'error-message'\" [matTooltip]=\"datacell.error()?'Error: ' + datacell.error():''\"\n matTooltipPosition=\"below\">\n <div class=\"cell-content\">\n <data-cell #datacell [personCardTemplate]=\"personCardTemplate\" [cellTemplate]=\"cellTemplate\" [td]=cell [fieldSize]=\"column.field_size\" [columnDatatype]=\"column.datatype\"\n [columnName]=\"column.name\" [value]=\"(row?.['entity_data']?.[column.name] ?? row?.[column.name]) || ''\" [column]=\"column\"\n [mode]=\"mode()\" [isEditable]=\"isEditable() && column.editable !== false && column.editable !== 'false'\" [drillable]=\"column.enableDrilldown || false\"\n [id]=\"i + '_' + column.name\" [eruGridStore]=\"gridStore\" [row]=\"row\"></data-cell>\n </div>\n </td>\n }\n @if(shouldShowActionColumn('after')) {\n <td class=\"action-column\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\">\n <ng-container\n *ngTemplateOutlet=\"actionCell; context: { $implicit: row, mode: 'table', group: group }\"></ng-container>\n </td>\n }\n </tr>\n @if(hasHiddenColumns() && isRowExpanded(row, i)) {\n <tr class=\"row-detail\">\n <td class=\"row-detail-cell\" [attr.colspan]=\"rowDetailColspan()\">\n <div class=\"row-detail-grid\">\n @for (hiddenCol of hiddenColumns(); track trackByColumnFn($index, hiddenCol)) {\n <div class=\"row-detail-field\">\n <span class=\"row-detail-label\">{{hiddenCol.label}}</span>\n <div class=\"row-detail-value\">\n <data-cell [cellTemplate]=\"cellTemplate\" [fieldSize]=\"hiddenCol.field_size\" [columnDatatype]=\"hiddenCol.datatype\"\n [columnName]=\"hiddenCol.name\" [value]=\"(row?.['entity_data']?.[hiddenCol.name] ?? row?.[hiddenCol.name]) || ''\"\n [column]=\"hiddenCol\" [mode]=\"mode()\" [isEditable]=\"isEditable()\"\n [drillable]=\"hiddenCol.enableDrilldown || false\"\n [id]=\"'detail_' + i + '_' + hiddenCol.name\" [eruGridStore]=\"gridStore\" [row]=\"row\"></data-cell>\n </div>\n </div>\n }\n </div>\n </td>\n </tr>\n }\n </ng-container>\n <!-- } -->\n <!-- } -->\n @if(group.isLoading && (group.isExpanded || !showGroupBar())) {\n @for(i of [].constructor(ghostRows()); let j = $index; track j) {\n <tr class=\"ghost-loading-row\" [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\">\n @if(gridStore.configuration().config.allowSelection) {\n <td class=\"checkbox-column ghost-cell-container\">\n <div class=\"ghost-cell\"></div>\n </td>\n\n }\n @if(shouldShowActionColumn('before')) {\n <td class=\"action-column ghost-cell-container\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\">\n <div class=\"ghost-cell\"></div>\n </td>\n }\n @if(hasHiddenColumns()) {\n <td class=\"row-expand-toggle ghost-cell-container\">\n <div class=\"ghost-cell\"></div>\n </td>\n }\n @for (column of visibleColumns(); track trackByColumnFn($index, column)) {\n <td [style.width.px]=\"column.field_size\" [style.minWidth.px]=\"column.field_size\"\n class=\"ghost-cell-container\">\n <div class=\"ghost-cell\"></div>\n </td>\n }\n @if(shouldShowActionColumn('after')) {\n <td class=\"action-column ghost-cell-container\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\">\n <div class=\"ghost-cell\"></div>\n </td>\n }\n </tr>\n }\n }\n <!-- <tr\n *ngIf=\"getRowsForGroup(group.id).length === 0 && !group.isExpanded\"\n class=\"group-separator\"\n >\n <td [attr.colspan]=\"groupSeperatorColSpan()\" class=\"separator-cell\"></td>\n </tr> -->\n <!-- Subtotal row at end of group (position: after) -->\n @if(enableRowSubtotals() && subtotalPosition() === 'after' && hasSubtotalData(group)) {\n <ng-container *ngTemplateOutlet=\"tableSubtotal; context: { group: group }\"></ng-container>\n }\n <!-- Grand Total row at end of group (position: after) - only for last group -->\n @if(enableGrandTotal() && grandTotalPosition() === 'after' && hasGrandTotalData() && i ===\n groups().length - 1) {\n <ng-container *ngTemplateOutlet=\"tableGrandTotal\"></ng-container>\n }\n }\n </tbody>\n </table>\n </div>\n </cdk-virtual-scroll-viewport>\n </ng-container>\n }\n </div>\n }\n </div>\n }\n</div>\n\n<!-- Pivot Table Header Template -->\n<ng-template #pivotTableHead>\n <thead [class.eru-wrap-headers]=\"wrapHeaders()\">\n @if (hasNestedHeaders()) {\n <ng-container>\n @for (headerRow of getHeaderRows(); track headerRow; let rowIndex = $index) {\n <tr class=\"pivot-header pivot-header-container\" [class.pivot-header-level]=\"'level-' + rowIndex\">\n @for (header of headerRow; track trackByHeaderFn($index, header); let colIndex = $index) {\n <th [attr.colspan]=\"header.colspan\" [attr.rowspan]=\"header.rowspan\"\n [resizeColumn]=\"isResizablePivotHeader(header)\"\n [columnConfig]=\"getFieldForPivotHeader(header) || $any(header)\"\n class=\"column-header pivot-column-header nested-header\"\n [class.row-dimension-header]=\"isRowDimensionHeader(header)\"\n [class.column-dimension-header]=\"!isRowDimensionHeader(header)\" [class.expanded]=\"header.isExpanded\"\n [class.collapsed]=\"!header.isExpanded\" [class.sticky-column]=\"isStickyColumn(header.name, colIndex)\"\n [class.design-clickable]=\"isDesignMode()\"\n [class.design-selected]=\"isDesignMode() && gridStore.selectedDesignColumn() === designTargetFor($any(header))\"\n [style.position]=\"isStickyColumn(header.name, colIndex) ? 'sticky' : 'static'\"\n [style.left.px]=\"getStickyColumnLeft(header.name, colIndex)\"\n [style.z-index]=\"isStickyColumn(header.name, colIndex) ? 100 : 1\"\n [style.min-height.px]=\"headerRowHeight()\" style=\"height: auto; padding: 8px 6px;\">\n <div class=\"header-content\">\n\n <data-cell [fieldSize]=\"header.field_size\" [columnDatatype]=\"header.dataType\" [columnName]=\"header.name\"\n [value]=\"header.label\" [column]=\"header\" [frozenGrandTotalCell]=\"true\"\n [drillable]=\"header.enableDrilldown || false\" [mode]=\"mode()\" [isEditable]=\"isEditable()\"\n [id]=\"'pivot_' + $index + '_' + header.name\" [eruGridStore]=\"gridStore\" [row]=\"header\">\n </data-cell>\n @if(isDesignMode()) {\n <mat-icon class=\"design-edit-icon\"\n [title]=\"isAggregationHeader($any(header)) ? 'Edit aggregation' : 'Edit column'\"\n (click)=\"onHeaderDesignClick($event, $any(header))\">tune</mat-icon>\n }\n <!-- <span class=\"header-label header-wrap-text\">{{header.label}}</span> -->\n <!-- <button *ngIf=\"!isRowDimensionHeader(header)\"\n class=\"collapse-toggle-btn\"\n [title]=\"header.isExpanded ? 'Collapse group' : 'Expand group'\"\n (click)=\"toggleColumnGroup(header.groupKey)\"\n type=\"button\">\n <span class=\"collapse-icon\">+</span>\n </button> -->\n </div>\n </th>\n }\n </tr>\n }\n </ng-container>\n } @else {\n <!-- Simple header fallback -->\n <ng-container>\n <tr class=\"pivot-header\" [class.freeze-header-enabled]=\"freezeHeader()\">\n @for (column of getLeafColumns(); track trackByColumnFn($index, column); let colIndex = $index) {\n <th [style.width.px]=\"column.field_size\" [style.minWidth.px]=\"column.field_size\"\n [resizeColumn]=\"gridStore.isFeatureEnabled('columnResizable')\" [columnConfig]=\"column\"\n class=\"column-header pivot-column-header\" [class.sticky-column]=\"isStickyColumn(column.name, colIndex)\"\n [class.design-clickable]=\"isDesignMode()\"\n [class.design-selected]=\"isDesignMode() && gridStore.selectedDesignColumn() === designTargetFor(column)\"\n [style.position]=\"isStickyColumn(column.name, colIndex) ? 'sticky' : 'static'\"\n [style.left.px]=\"getStickyColumnLeft(column.name, colIndex)\"\n [style.z-index]=\"isStickyColumn(column.name, colIndex) ? 100 : 1\"\n [style.min-height.px]=\"headerRowHeight()\" style=\"height: auto;padding: 8px 6px\">\n <!-- Label and control laid out as a row: the label truncates, the\n control keeps its place. Left as a bare text node the long\n aggregation labels pushed the icon past the cell edge, where\n `overflow: hidden` clipped it out of sight entirely. -->\n <div class=\"pivot-header-content\">\n <!-- Deliberately not `.column-label`: that class carries the\n wrap-headers rule, which broke these labels onto one word per\n line. This header truncates, as it did before. -->\n <span class=\"pivot-header-label\">{{column.label}}</span>\n @if(isDesignMode()) {\n <mat-icon class=\"design-edit-icon\"\n [title]=\"isAggregationHeader(column) ? 'Edit aggregation' : 'Edit column'\"\n (click)=\"onHeaderDesignClick($event, column)\">tune</mat-icon>\n }\n </div>\n </th>\n }\n </tr>\n </ng-container>\n }\n\n </thead>\n</ng-template>\n\n<!-- Column Group Template for consistent column widths -->\n<ng-template #pivotColGroup>\n <colgroup>\n @for (column of getLeafColumns(); track trackByColumnFn($index, column)) {\n <col\n [style]=\"'width: ' + column.field_size + 'px !important; min-width: ' + column.field_size + 'px !important; max-width: ' + column.field_size + 'px !important; --col-width: ' + column.field_size + 'px'\">\n }\n </colgroup>\n</ng-template>\n\n<ng-template #pivotGrandTotal>\n <tbody class=\"pivot-tbody\">\n @for (pivotRow of gridStore.pivotGrandTotalData(); track trackByPivotRowFn($index, pivotRow); let i = $index) {\n <tr class=\"pivot-row grand-total-row\"\n [class.grand-total-bold]=\"pivotRow._isGrandTotal && grandTotalStyle() === 'bold'\"\n [class.grand-total-italic]=\"pivotRow._isGrandTotal && grandTotalStyle() === 'italic'\"\n [class.grand-total-highlighted]=\"pivotRow._isGrandTotal && grandTotalStyle() === 'highlighted'\"\n [style.height.px]=\"50\" [attr.data-pivot-row]=\"i\">\n <!-- <td colspan=\"20\">{{pivotRow | json}}</td> -->\n @for (column of getLeafColumns(); track trackByColumnFn($index, column); let colIndex = $index) {\n <td [attr.rowspan]=\"getEffectiveRowspan(i, column.name)\" [style.width.px]=\"column.field_size\"\n [style.minWidth.px]=\"column.field_size\" class=\"pivot-cell\"\n [class.row-dimension-cell]=\"isRowDimensionColumn(column.name)\"\n [class.column-dimension-cell]=\"!isRowDimensionColumn(column.name)\"\n [class.aggregated-value]=\"!isRowDimensionColumn(column.name) && column.datatype === 'number'\"\n [class.rowspan-cell]=\"getEffectiveRowspan(i, column.name) || 1 > 1\"\n [class.sticky-column]=\"isStickyColumn(column.name, colIndex)\"\n [style.position]=\"isStickyColumn(column.name, colIndex) ? 'sticky' : 'static'\"\n [style.left.px]=\"getStickyColumnLeft(column.name, colIndex)\"\n [style.z-index]=\"isStickyColumn(column.name, colIndex) ? 99 : 1\" [style.height.px]=\"50\" [attr.xx]=\"i\">\n <div class=\"cell-content pivot-cell-content\">\n <data-cell [fieldSize]=\"column.field_size\" [columnDatatype]=\"column.datatype\" [columnName]=\"column.name\"\n [value]=\"getEffectiveCellValue(i,column.name, pivotRow)\" [column]=\"column\" [frozenGrandTotalCell]=\"true\"\n [drillable]=\"column.enableDrilldown || false\" [mode]=\"mode()\" [isEditable]=\"isEditable()\"\n [id]=\"'pivot_' + i + '_' + column.name\" [eruGridStore]=\"gridStore\" [row]=\"pivotRow\">\n </data-cell>\n </div>\n </td>\n }\n </tr>\n }\n </tbody>\n</ng-template>\n\n<!-- \u2500\u2500\u2500 Action column cell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n One template for both column positions. With no `config.actions` set it\n falls back to the single more_horiz icon the column has always shown, so\n grids that only listen to the store's actionClick signal keep working.\n Context: { $implicit: Row, mode: 'table' | 'board', group?: RowGroup }\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<ng-template #actionCell let-row let-mode=\"mode\" let-group=\"group\">\n @if(!hasConfiguredActions()) {\n <mat-icon (click)=\"onActionClick($event, row, undefined, mode || 'table', group)\">more_horiz</mat-icon>\n } @else if(actionDisplayType() === 'kebab') {\n @if(visibleActionsFor(row).length > 0) {\n <mat-icon class=\"action-kebab\" [matMenuTriggerFor]=\"rowActionMenu\"\n [matMenuTriggerData]=\"{ row: row, mode: mode || 'table', group: group }\"\n (click)=\"$event.stopPropagation()\">more_vert</mat-icon>\n }\n } @else {\n <div class=\"action-icons\">\n @for(action of visibleActionsFor(row); track action.action_name) {\n <mat-icon class=\"action-icon\" [fontSet]=\"actionIconFontSet()\" [matTooltip]=\"action.action_name\"\n matTooltipPosition=\"above\"\n (click)=\"onActionClick($event, row, action, mode || 'table', group)\">{{action.action_icon || 'play_arrow'}}</mat-icon>\n }\n </div>\n }\n</ng-template>\n\n<!-- Kebab menu shared by every row; the row is passed through matMenuTriggerData. -->\n<mat-menu #rowActionMenu=\"matMenu\" class=\"eru-grid-action-menu\">\n <ng-template matMenuContent let-row=\"row\" let-mode=\"mode\" let-group=\"group\">\n @for(action of visibleActionsFor(row); track action.action_name) {\n <button mat-menu-item (click)=\"onActionClick($event, row, action, mode || 'table', group)\">\n <mat-icon>{{action.action_icon || 'play_arrow'}}</mat-icon>\n <span>{{action.action_name}}</span>\n </button>\n }\n </ng-template>\n</mat-menu>\n\n<!-- Column Group Template for consistent column widths -->\n<ng-template #tableColGroup>\n <colgroup>\n @if(gridStore.configuration().config.allowSelection) {\n <col style=\"width: 40px; min-width: 40px; max-width: 40px;\">\n }\n @if(shouldShowActionColumn('before')) {\n <col\n [style]=\"'width: ' + actionColumnWidth() + 'px !important; min-width: ' + actionColumnWidth() + 'px !important; max-width: ' + actionColumnWidth() + 'px !important;'\">\n }\n @if(hasHiddenColumns()) {\n <col style=\"width: 40px !important; min-width: 40px !important; max-width: 40px !important;\">\n }\n @for (column of visibleColumns(); track trackByColumnFn($index, column)) {\n <col\n [style]=\"'width: ' + column.field_size + 'px !important; min-width: ' + column.field_size + 'px !important; max-width: ' + column.field_size + 'px !important; --col-width: ' + column.field_size + 'px'\">\n }\n @if(shouldShowActionColumn('after')) {\n <col\n [style]=\"'width: ' + actionColumnWidth() + 'px !important; min-width: ' + actionColumnWidth() + 'px !important; max-width: ' + actionColumnWidth() + 'px !important;'\">\n }\n </colgroup>\n</ng-template>\n\n\n<ng-template #tableHeader>\n\n <thead [class.eru-wrap-headers]=\"wrapHeaders()\">\n <!-- headerRowHeight rides on the row, not the cells: `thead.eru-wrap-headers\n th { height: auto }` outranks any class-level height we could put on a\n th, which is why a configured header height was ignored while data rows\n (inline height on tr.row-item) honoured theirs. On a table row `height`\n is a minimum, so a wrapped two-line header still grows past it. -->\n <tr [style.height.px]=\"headerRowHeight()\" [style.minHeight.px]=\"headerRowHeight()\">\n @if(gridStore.configuration().config.allowSelection) {\n <th class=\"checkbox-column column-header table-column-header\">\n <input type=\"checkbox\" [checked]=\"isAllGroupsSelected()\"\n [indeterminate]=\"!isAllGroupsSelected() && gridStore.hasSelection()\" (change)=\"toggleAllGroups($event)\">\n </th>\n }\n @if(shouldShowActionColumn('before')) {\n <th class=\"action-column column-header table-column-header\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\">Action</th>\n }\n @if(hasHiddenColumns()) {\n <th class=\"row-expand-toggle column-header table-column-header\"></th>\n }\n @for (column of visibleColumns(); track trackByColumnFn(i, column); let i = $index) {\n <th [style.width.px]=\"column.field_size\" [resizeColumn]=\"gridStore.isFeatureEnabled('columnResizable')\"\n [columnConfig]=\"column\" [index]=\"i\"\n [columnDraggable]=\"gridStore.isFeatureEnabled('columnReorderable') ? column.name : null\"\n [style.minWidth.px]=\"column.field_size\" class=\"column-header table-column-header\"\n [class.sortable-header]=\"isSortable()\"\n [class.design-clickable]=\"isDesignMode()\"\n [class.design-selected]=\"isDesignMode() && gridStore.selectedDesignColumn() === column.name\"\n [class.sort-asc]=\"isSortable() && getSortDirection(column.name) === 'asc'\"\n [class.sort-desc]=\"isSortable() && getSortDirection(column.name) === 'desc'\">\n @if(gridStore.isFeatureEnabled('columnReorderable')) {\n <div class=\"column-drag-handle\"></div>\n }\n <span class=\"column-label\" [title]=\"column.tool_tip || column.description || ''\">{{column.label}}</span>\n @if(isDesignMode()) {\n <mat-icon class=\"design-edit-icon\" title=\"Edit column\" (click)=\"onHeaderDesignClick($event, column)\">tune</mat-icon>\n }\n @if(isSortable()) {\n <span class=\"sort-indicator\">\n <span class=\"sort-triangles\">\n <span class=\"sort-tri sort-tri-up\" [class.sort-tri-active]=\"getSortDirection(column.name) === 'asc'\"\n (click)=\"onSortColumn($event, column, 'asc')\"></span>\n <span class=\"sort-tri sort-tri-down\" [class.sort-tri-active]=\"getSortDirection(column.name) === 'desc'\"\n (click)=\"onSortColumn($event, column, 'desc')\"></span>\n </span>\n @if(getSortPriority(column.name) !== null && gridStore.sortColumns().length > 1) {\n <span class=\"sort-priority\">{{getSortPriority(column.name)}}</span>\n }\n </span>\n }\n </th>\n }\n @if(shouldShowActionColumn('after')) {\n <th class=\"action-column column-header table-column-header\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\">Action</th>\n }\n </tr>\n </thead>\n</ng-template>\n\n<!-- Table Subtotal Row Template -->\n<ng-template #tableSubtotal let-group=\"group\">\n <tr class=\"subtotal-row\" [class.subtotal-bold]=\"subTotalStyle() === 'bold'\"\n [class.subtotal-italic]=\"subTotalStyle() === 'italic'\"\n [class.subtotal-highlighted]=\"subTotalStyle() === 'highlighted'\" [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\">\n @if(gridStore.configuration().config.allowSelection) {\n <td class=\"checkbox-column\"></td>\n }\n @if(shouldShowActionColumn('before')) {\n <td class=\"action-column\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\"></td>\n }\n @if(hasHiddenColumns()) {\n <td class=\"row-expand-toggle\"></td>\n }\n @for(column of visibleColumns(); track trackByColumnFn($index, column); let colIndex = $index) {\n <td [style.width.px]=\"column.field_size\" [style.minWidth.px]=\"column.field_size\" class=\"subtotal-cell\"\n [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\">\n <div class=\"cell-content\">\n @if(colIndex === 0 && getSubtotalValue(group, column.name) === null) {\n <span class=\"subtotal-label\">{{subtotalLabel()}}</span>\n } @else {\n @if(getSubtotalValue(group, column.name) !== null) {\n <data-cell [fieldSize]=\"column.field_size\" [columnDatatype]=\"column.datatype\" [columnName]=\"column.name\"\n [value]=\"getSubtotalValue(group, column.name)\" [column]=\"column\" [mode]=\"mode()\" [isEditable]=\"false\"\n [id]=\"'subtotal_' + group.id + '_' + column.name\" [eruGridStore]=\"gridStore\" [row]=\"group.subtotal\">\n </data-cell>\n }\n }\n </div>\n </td>\n }\n @if(shouldShowActionColumn('after')) {\n <td class=\"action-column\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\"></td>\n }\n </tr>\n</ng-template>\n\n<!-- Table Grand Total Row Template -->\n<ng-template #tableGrandTotal>\n <tr class=\"grand-total-row\" [class.grand-total-bold]=\"grandTotalStyle() === 'bold'\"\n [class.grand-total-italic]=\"grandTotalStyle() === 'italic'\"\n [class.grand-total-highlighted]=\"grandTotalStyle() === 'highlighted'\" [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\">\n @if(gridStore.configuration().config.allowSelection) {\n <td class=\"checkbox-column\"></td>\n }\n @if(shouldShowActionColumn('before')) {\n <td class=\"action-column\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\"></td>\n }\n @if(hasHiddenColumns()) {\n <td class=\"row-expand-toggle\"></td>\n }\n @for(column of visibleColumns(); track trackByColumnFn($index, column); let colIndex = $index) {\n <td [style.width.px]=\"column.field_size\" [style.minWidth.px]=\"column.field_size\" class=\"grand-total-cell\"\n [style.height.px]=\"dataRowHeight()\" [style.minHeight.px]=\"dataRowHeight()\">\n <div class=\"cell-content\">\n @if(colIndex === 0 && getGrandTotalValue(column.name) === null) {\n <span class=\"grand-total-label\">Grand Total</span>\n } @else {\n @if(getGrandTotalValue(column.name) !== null) {\n <data-cell [fieldSize]=\"column.field_size\" [columnDatatype]=\"column.datatype\" [columnName]=\"column.name\"\n [value]=\"getGrandTotalValue(column.name)\" [column]=\"column\" [mode]=\"mode()\" [isEditable]=\"false\"\n [id]=\"'grandtotal_' + column.name\" [eruGridStore]=\"gridStore\" [row]=\"gridStore.rowGrandTotal()\">\n </data-cell>\n }\n }\n </div>\n </td>\n }\n @if(shouldShowActionColumn('after')) {\n <td class=\"action-column\" [style.width.px]=\"actionColumnWidth()\"\n [style.minWidth.px]=\"actionColumnWidth()\" [style.maxWidth.px]=\"actionColumnWidth()\"></td>\n }\n </tr>\n</ng-template>\n\n<!-- \u2500\u2500\u2500 Default board card template \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Used when no boardCardTemplate is passed to <eru-grid>.\n Context: { $implicit: Row, columns: Field[], group: RowGroup }\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<ng-template #defaultBoardCard let-row let-columns=\"columns\" let-group=\"group\">\n <mat-card class=\"board-card\">\n <mat-card-content>\n @for (column of columns; track column.name) {\n @if ((row?.entity_data?.[column.name] ?? row?.[column.name]) !== undefined) {\n <div class=\"board-card-field\">\n <span class=\"board-field-label\">{{ column.label }}</span>\n <data-cell\n [fieldSize]=\"column.field_size\"\n [columnDatatype]=\"column.datatype\"\n [columnName]=\"column.name\"\n [column]=\"column\"\n [value]=\"row?.entity_data?.[column.name] ?? row?.[column.name]\"\n [id]=\"row?.entity_id + '_' + column.name\"\n [eruGridStore]=\"gridStore\"\n [mode]=\"'board'\"\n [row]=\"row\">\n </data-cell>\n </div>\n }\n }\n </mat-card-content>\n <mat-card-actions align=\"end\">\n <button mat-icon-button (click)=\"onActionClick($event, row)\">\n <mat-icon>more_horiz</mat-icon>\n </button>\n </mat-card-actions>\n </mat-card>\n</ng-template>", styles: ["@charset \"UTF-8\";:root{--grid-primary: var(--mat-sys-primary, #6750a4);--grid-on-primary: var(--mat-sys-on-primary, #ffffff);--grid-primary-container: var(--mat-sys-primary-container, #eaddff);--grid-on-primary-container: var(--mat-sys-on-primary-container, #21005d);--grid-secondary: var(--mat-sys-secondary, #625b71);--grid-on-secondary: var(--mat-sys-on-secondary, #ffffff);--grid-secondary-container: var(--mat-sys-secondary-container, #e8def8);--grid-on-secondary-container: var(--mat-sys-on-secondary-container, #1d192b);--grid-tertiary: var(--mat-sys-tertiary, #7d5260);--grid-on-tertiary: var(--mat-sys-on-tertiary, #ffffff);--grid-tertiary-container: var(--mat-sys-tertiary-container, #ffd8e4);--grid-on-tertiary-container: var(--mat-sys-on-tertiary-container, #31111d);--grid-surface: var(--mat-sys-surface, #fef7ff);--grid-surface-variant: var(--mat-sys-surface-variant, #e7e0ec);--grid-surface-container: var(--mat-sys-surface-container, #f3edf7);--grid-surface-container-high: var(--mat-sys-surface-container-high, #ede7f0);--grid-on-surface: var(--mat-sys-on-surface, #1d1b20);--grid-on-surface-variant: var(--mat-sys-on-surface-variant, #49454f);--grid-outline: var(--mat-sys-outline, #79757f);--grid-outline-variant: var(--mat-sys-outline-variant, #cac4d0);--grid-error: var(--mat-sys-error, #ba1a1a);--grid-error-container: var(--mat-sys-error-container, #ffdad6);--grid-base-surface: var(--surface, #ffffff);--grid-base-on-surface: var(--on-surface, #000000);--grid-base-border: var(--border, #e5e7eb);--grid-primary-light: var(--grid-primary-container)}:host,eru-grid{display:block!important;width:100%;height:100%;flex:1 1 0%;max-height:var(--grid-height, none);min-height:var(--grid-min-height, 120px);font-family:var(--grid-font-family);--grid-font-family: \"Poppins\", \"Roboto\", -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;--grid-font-size-body: 12px;--grid-font-size-caption: 12px !important;--grid-line-height-body: 1;--grid-aggregation-text-align: right;--grid-number-text-align: right;--grid-spacing-xxs: 2px;--grid-spacing-xs: 4px;--grid-spacing-sm: 8px;--grid-spacing-md: 16px;--grid-spacing-lg: 24px;--grid-border-radius: 4px;--grid-elevation-1: 0px 1px 2px 0px rgba(0, 0, 0, .3), 0px 1px 3px 1px rgba(0, 0, 0, .15);--grid-elevation-2: 0px 1px 2px 0px rgba(0, 0, 0, .3), 0px 2px 6px 2px rgba(0, 0, 0, .15);--grid-row-hover: var(--grid-surface-variant);--grid-row-selected: var(--grid-surface-container-high);--grid-zebra-odd: transparent;--grid-zebra-even: transparent;--grid-focus-ring: var(--grid-primary);--grid-header-font-weight: 500;--grid-header-text-transform: none;--grid-header-letter-spacing: normal;--grid-header-font-size: var(--grid-font-size-caption);--grid-header-padding-x: 8px;--grid-header-padding-y: 12px;--grid-font-feature-numeric: normal;--grid-cell-padding-x: var(--grid-spacing-xs);--grid-cell-inset-x: 8px;--grid-cell-padding-y: var(--grid-spacing-xxs);--grid-tint-subtle: rgba(0, 0, 0, .025);--grid-tint-soft: rgba(0, 0, 0, .045);--grid-tint-strong: rgba(0, 0, 0, .08);--grid-radius-outer: 0;--grid-shadow-outer: none;--grid-divider-color: var(--grid-outline-variant);--grid-divider-width: 1px;--grid-header-bg: var(--grid-surface-container);--grid-header-color: var(--grid-on-surface);--grid-pill-radius: 999px;--grid-pill-padding-y: 3px;--grid-pill-padding-x: 10px;--grid-pill-font-size: 11px;--grid-pill-font-weight: 500;--grid-priority-dot-size: 8px;--grid-avatar-size: 24px;--grid-avatar-font-size: 10px;--grid-avatar-font-weight: 600;border-radius:var(--grid-radius-outer);box-shadow:var(--grid-shadow-outer)}eru-grid[data-preset=default]{--grid-header-bg: transparent;--grid-header-color: var(--grid-on-surface-variant);--grid-header-font-weight: 500;--grid-header-text-transform: uppercase;--grid-header-letter-spacing: .06em;--grid-header-font-size: 11px;--grid-header-padding-y: 12px;--grid-header-padding-x: 14px;--grid-cell-padding-y: 10px;--grid-cell-padding-x: 14px;--grid-row-hover: var(--grid-tint-subtle);--grid-divider-color: var(--grid-tint-soft);--grid-divider-width: 1px;--grid-font-feature-numeric: \"tnum\"}eru-grid[data-preset=modern]{--grid-header-bg: transparent;--grid-header-color: var(--grid-on-surface-variant);--grid-header-font-weight: 500;--grid-header-text-transform: none;--grid-header-letter-spacing: normal;--grid-header-font-size: 13px;--grid-header-padding-y: 16px;--grid-header-padding-x: 18px;--grid-cell-padding-y: 16px;--grid-cell-padding-x: 18px;--grid-row-hover: var(--grid-tint-soft);--grid-divider-color: var(--grid-tint-subtle);--grid-divider-width: 1px;--grid-radius-outer: 12px;--grid-font-feature-numeric: \"tnum\";--grid-pill-padding-y: 4px;--grid-pill-padding-x: 12px}eru-grid[data-preset=compact]{--grid-header-bg: var(--grid-surface-container);--grid-header-color: var(--grid-on-surface);--grid-header-font-weight: 600;--grid-header-text-transform: none;--grid-header-font-size: 11px;--grid-header-padding-y: 4px;--grid-header-padding-x: 8px;--grid-cell-padding-y: 3px;--grid-cell-padding-x: 8px;--grid-font-size-body: 11px;--grid-row-hover: var(--grid-tint-subtle);--grid-divider-color: var(--grid-tint-subtle);--grid-divider-width: 1px;--grid-font-feature-numeric: \"tnum\";--grid-pill-padding-y: 1px;--grid-pill-padding-x: 6px;--grid-pill-font-size: 10px}eru-grid[data-preset=bold]{--grid-header-bg: var(--grid-surface-container-high);--grid-header-color: var(--grid-on-surface);--grid-header-font-weight: 700;--grid-header-text-transform: none;--grid-header-font-size: 13px;--grid-header-padding-y: 14px;--grid-header-padding-x: 12px;--grid-cell-padding-y: 10px;--grid-cell-padding-x: 12px;--grid-row-hover: var(--grid-tint-soft);--grid-divider-color: var(--grid-tint-strong);--grid-divider-width: 1px;--grid-radius-outer: 2px;--grid-font-feature-numeric: \"tnum\"}eru-grid[data-preset=financial]{--grid-header-bg: var(--grid-surface-container);--grid-header-color: var(--grid-on-surface-variant);--grid-header-font-weight: 500;--grid-header-text-transform: uppercase;--grid-header-letter-spacing: .08em;--grid-header-font-size: 11px;--grid-header-padding-y: 12px;--grid-header-padding-x: 14px;--grid-cell-padding-y: 10px;--grid-cell-padding-x: 14px;--grid-zebra-odd: transparent;--grid-zebra-even: var(--grid-tint-subtle);--grid-row-hover: var(--grid-tint-soft);--grid-divider-color: var(--grid-tint-subtle);--grid-divider-width: 1px;--grid-font-feature-numeric: \"tnum\"}eru-grid[data-preset=elevated]{--grid-header-bg: transparent;--grid-header-color: var(--grid-on-surface-variant);--grid-header-font-weight: 600;--grid-header-text-transform: none;--grid-header-font-size: 12px;--grid-header-padding-y: 16px;--grid-header-padding-x: 18px;--grid-cell-padding-y: 14px;--grid-cell-padding-x: 18px;--grid-row-hover: var(--grid-tint-soft);--grid-divider-color: var(--grid-tint-subtle);--grid-divider-width: 1px;--grid-radius-outer: 16px;--grid-shadow-outer: 0 1px 3px rgba(0, 0, 0, .06), 0 10px 28px rgba(0, 0, 0, .07);--grid-font-feature-numeric: \"tnum\";--grid-pill-padding-y: 4px;--grid-pill-padding-x: 12px;overflow:hidden}.group-container{padding-bottom:8px}.column-header.design-clickable .design-edit-icon{font-size:16px;width:16px;height:16px;margin-left:4px;opacity:.45;vertical-align:middle;cursor:pointer}.column-header.design-clickable:hover .design-edit-icon,.column-header.design-clickable .design-edit-icon:hover{opacity:1}.column-header.design-selected{background-color:var(--grid-primary-container, rgba(63, 81, 181, .12))}.pivot-column-header .pivot-header-content{display:flex;align-items:center;justify-content:center;gap:4px;min-width:0}.pivot-column-header .pivot-header-content .pivot-header-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.pivot-column-header .pivot-header-content .design-edit-icon,.pivot-column-header .header-content .design-edit-icon{flex:0 0 auto}.pivot-column-header .header-content data-cell,.pivot-column-header .header-content data-cell *{color:inherit!important}.incremental-row-container{width:100%;height:100%;min-height:var(--grid-min-height, 120px);max-height:none;overflow:auto;position:relative;background-color:var(--grid-surface);border-radius:var(--grid-border-radius);font-family:var(--grid-font-family)}.viewport{height:100%;min-height:300px;overflow-x:auto;overflow-y:auto;background-color:var(--grid-surface);scrollbar-gutter:stable}.viewport.apply-cdk-width{width:calc(var(--table-total-width) + 10px)!important}.groups-viewport{height:100%;min-height:300px}.groups-scroll-container{max-height:var(--grid-height, 600px);overflow-y:auto;overflow-x:hidden}.table-viewport{background-color:var(--grid-surface);height:var(--table-height, auto);min-height:var(--table-min-height, 100px);overflow-x:auto;overflow-y:auto}.pivot-viewport{min-height:var(--table-min-height, 300px);overflow-x:auto;overflow-y:auto;background-color:var(--grid-surface)}.pivot-viewport .cdk-virtual-scroll-content-wrapper{width:auto;height:auto}.table-wrapper{min-width:100%;overflow-x:visible}.incremental-row-container .eru-grid-table,.eru-grid-table{width:100%!important;border-collapse:separate;border-spacing:0;table-layout:fixed!important;background-color:var(--grid-surface);color:var(--grid-on-surface);font-family:var(--grid-font-family);font-size:var(--grid-font-size-body);line-height:var(--grid-line-height-body)}.eru-grid-table th,.eru-grid-table td{text-align:left;overflow:hidden!important;text-overflow:ellipsis!important;white-space:nowrap!important;color:var(--grid-on-surface);min-width:0;max-width:100%!important;box-sizing:border-box;position:relative}.eru-grid-table th{background-color:var(--grid-header-bg, var(--grid-surface-container))}thead.eru-wrap-headers th{white-space:normal!important;overflow:visible!important;text-overflow:clip!important;height:auto}thead.eru-wrap-headers th .column-label,thead.eru-wrap-headers th .header-label{white-space:normal!important;overflow:visible!important;text-overflow:clip!important;word-break:break-word;overflow-wrap:anywhere}.eru-grid-table tbody td{background-color:transparent}.eru-grid-table thead{background-color:var(--grid-header-bg, var(--grid-surface-container));transform:translateZ(0);will-change:transform;backface-visibility:hidden}.eru-grid-table thead.freeze-header-enabled{position:sticky!important;top:0!important;z-index:100!important}.eru-grid-table thead th{background-color:var(--grid-header-bg, var(--grid-surface-container));color:var(--grid-header-color, var(--grid-on-surface));font-family:var(--grid-font-family);font-weight:var(--grid-header-font-weight);font-size:var(--grid-header-font-size)}.checkbox-column{width:50px;min-width:50px;max-width:50px;text-align:center!important;padding-left:0!important;padding-right:0!important;text-overflow:clip!important;background-color:var(--grid-surface-container)}.checkbox-column input[type=checkbox]{width:16px;height:16px;cursor:pointer;accent-color:var(--grid-primary);border-radius:var(--grid-border-radius)}.checkbox-column input[type=checkbox]:focus{outline:2px solid var(--grid-primary);outline-offset:2px}.action-column{text-align:center!important;padding-left:0!important;padding-right:0!important;text-overflow:clip!important;background-color:var(--grid-surface-container)}.action-column mat-icon{font-size:18px;width:18px;height:18px;line-height:18px;color:var(--grid-outline);cursor:pointer}.action-column mat-icon:hover{color:var(--grid-primary)}.action-column .action-icons{display:flex;align-items:center;justify-content:center;gap:6px;overflow-x:auto;scrollbar-width:none}.action-column .action-icons::-webkit-scrollbar{display:none}.action-column .action-icon{flex:0 0 auto}.eru-grid-action-menu .mat-mdc-menu-item mat-icon{margin-right:8px;font-size:18px;width:18px;height:18px;line-height:18px;color:var(--grid-on-surface-variant)}.group-header{background-color:var(--grid-surface-container);color:var(--grid-on-surface);font-size:var(--grid-font-size-caption);font-weight:500;border-bottom:1px solid var(--grid-outline);cursor:pointer;transition:background-color .2s ease}.group-header:hover{background-color:var(--grid-surface-container-high)}.group-header .group-title{font-weight:600;color:var(--grid-primary)}.group-header .group-row-count{color:var(--grid-on-surface-variant);font-size:var(--grid-font-size-caption);margin-left:var(--grid-spacing-sm)}.row-item{background-color:var(--grid-surface);transition:background-color .15s ease}.row-item:nth-child(odd){background-color:var(--grid-zebra-odd, var(--grid-surface))}.row-item:nth-child(2n){background-color:var(--grid-zebra-even, var(--grid-surface))}.row-item:hover{background-color:var(--grid-row-hover)}.required-toggle-row{background-color:var(--grid-surface-container, #f3edf7);border-bottom:1px solid var(--grid-outline-variant, #cac4d0)}.required-toggle-row .required-toggle-cell{padding:4px 8px!important;text-align:center;vertical-align:middle;position:relative}.required-toggle-row .required-toggle-cell .required-label{position:absolute;top:2px;left:4px;font-size:10px;color:var(--grid-on-surface-variant, #49454f);font-weight:400;text-transform:lowercase}.required-toggle-row .required-toggle-cell mat-checkbox{display:flex;justify-content:center;align-items:center}.table-column-header{padding:0 var(--grid-header-padding-x);height:var(--grid-header-row-height, auto)}.column-header{font-weight:var(--grid-header-font-weight);text-transform:var(--grid-header-text-transform);letter-spacing:var(--grid-header-letter-spacing);text-align:center!important;font-size:var(--grid-header-font-size);position:relative;-webkit-user-select:none;user-select:none;--grid-header-affordance-space: 0px;--grid-column-resizer-width: 10px;--grid-header-sort-right: calc(var(--grid-column-resizer-width) + 2px);--grid-header-design-right: calc(var(--grid-column-resizer-width) + 2px);background-color:var(--grid-header-bg, var(--grid-surface-container));color:var(--grid-header-color, var(--grid-on-surface))}.column-header:hover{background-color:var(--grid-header-hover-bg, var(--grid-surface-container-high))}.column-drag-handle{position:absolute;left:0;top:0;bottom:0;width:12px;cursor:grab;opacity:0;transition:opacity .2s ease,background-color .2s ease;z-index:2;display:flex;align-items:center;justify-content:center;border-right:1px solid transparent}.column-drag-handle:after{content:\"\\22ee\\22ee\";font-size:14px;color:var(--grid-on-surface-variant);transform:rotate(90deg)}.column-drag-handle:hover{background-color:var(--grid-surface-container-high);border-right-color:var(--grid-outline)}.column-header:hover .column-drag-handle{opacity:1}.column-drag-handle:active{cursor:grabbing}.table-column-header.sortable-header{--grid-header-affordance-space: 20px}.table-column-header.design-clickable{--grid-header-affordance-space: 28px}.table-column-header.sortable-header.design-clickable{--grid-header-affordance-space: 40px;--grid-header-design-right: calc(var(--grid-column-resizer-width) + 13px)}.table-column-header .column-label{display:block;padding-right:var(--grid-header-affordance-space)}.table-column-header .sort-indicator,.table-column-header .design-edit-icon{position:absolute;top:50%;transform:translateY(-50%);margin-left:0}.table-column-header .sort-indicator{right:var(--grid-header-sort-right)}.table-column-header .design-edit-icon{right:var(--grid-header-design-right)}.sortable-header{cursor:pointer}.sortable-header .sort-indicator{display:inline-flex;align-items:center;gap:2px;cursor:pointer;opacity:0;transition:opacity .15s ease}.sortable-header .sort-indicator .sort-triangles{display:flex;flex-direction:column;align-items:center;gap:2px}.sortable-header .sort-indicator .sort-tri{width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;cursor:pointer;transition:border-color .15s ease}.sortable-header .sort-indicator .sort-tri-up{border-bottom:5px solid var(--grid-on-surface-variant, #49454f);opacity:.3}.sortable-header .sort-indicator .sort-tri-down{border-top:5px solid var(--grid-on-surface-variant, #49454f);opacity:.3}.sortable-header .sort-indicator .sort-tri-active{opacity:1}.sortable-header .sort-indicator .sort-tri-active.sort-tri-up{border-bottom-color:var(--grid-primary, #6750a4)}.sortable-header .sort-indicator .sort-tri-active.sort-tri-down{border-top-color:var(--grid-primary, #6750a4)}.sortable-header .sort-indicator .sort-priority{font-size:9px;font-weight:600;color:var(--grid-primary, #6750a4);line-height:1;min-width:12px;text-align:center}.sortable-header:hover .sort-indicator,.sortable-header.sort-asc .sort-indicator,.sortable-header.sort-desc .sort-indicator{opacity:1}.sortable-header:hover .sort-indicator .sort-tri:not(.sort-tri-active){opacity:.6}.sort-asc,.sort-desc{background-color:var(--grid-surface-container-low, rgba(103, 80, 164, .04))}.dragging{opacity:1;background-color:var(--grid-surface-container);box-shadow:var(--grid-elevation-2)}.drag-over{background-color:var(--grid-surface-container);border-color:var(--grid-primary)}.data-cell{background-color:transparent;color:var(--grid-on-surface);font-family:var(--grid-font-family);font-size:var(--grid-font-size-body);font-feature-settings:var(--grid-font-feature-numeric);padding:0 var(--grid-cell-padding-x)}.cell-content{align-items:center}.cell-content .mdc-text-field{padding:0px var(--grid-spacing-xxs)!important}.cell-display-text{align-items:center;padding:0px var(--grid-spacing-xs)}.ghost-loading-row{background-color:transparent}.ghost-cell-container{padding:var(--grid-spacing-sm)}.ghost-cell{height:20px;width:100%;background-color:var(--grid-surface-container);animation:pulse 1.5s ease-in-out infinite;border-radius:var(--grid-border-radius)}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.column-resizer{position:absolute;right:0;top:0;bottom:0;width:4px;cursor:col-resize;background-color:transparent;transition:background-color .2s ease}.column-resizer:hover{background-color:var(--grid-primary)}.group-separator{height:var(--grid-spacing-sm);background-color:var(--grid-surface-variant)}.group-separator .separator-cell{background-color:var(--grid-surface-variant);border:none;height:var(--grid-spacing-sm)}.error-state{background-color:var(--grid-error-container);color:var(--grid-error);border-color:var(--grid-error)}.error-message{background-color:var(--grid-error);color:#fff;padding:var(--grid-spacing-sm);border-radius:var(--grid-border-radius);font-size:var(--grid-font-size-caption)}.incremental-row-container .eru-grid-table tbody,.incremental-row-container .eru-grid-table{position:relative}.incremental-row-container .eru-grid-table.show-column-lines{border-right:var(--grid-outline-width, 1px) solid var(--grid-outline, #e0e0e0)!important;border-top:var(--grid-outline-width, 1px) solid var(--grid-outline, #e0e0e0)!important}.incremental-row-container .eru-grid-table.show-column-lines:not(.freeze-header){border-bottom:var(--grid-outline-width, 1px) solid var(--grid-outline, #e0e0e0)!important}.incremental-row-container .eru-grid-table:not(.show-column-lines){border:var(--grid-outline-width, 1px) solid var(--grid-outline, #e0e0e0)!important}.incremental-row-container .eru-grid-table thead:after{content:\"\";position:absolute;bottom:0;left:0;right:0;height:calc(var(--grid-divider-width, 1px) * 2);background-color:var(--grid-divider-color, var(--grid-outline, #e0e0e0));pointer-events:none;z-index:10}.incremental-row-container .eru-grid-table.show-column-lines thead th,.incremental-row-container .eru-grid-table.show-column-lines tbody td{border-left:var(--grid-divider-width, 1px) solid var(--grid-divider-color, var(--grid-outline, #e0e0e0))!important}.incremental-row-container .eru-grid-table.show-row-lines thead th,.incremental-row-container .eru-grid-table.show-row-lines tbody td{border-bottom:var(--grid-divider-width, 1px) solid var(--grid-divider-color, var(--grid-outline, #e0e0e0))!important}@media(max-width:768px){.incremental-row-container{height:600px}.eru-grid-table th,.eru-grid-table td{font-size:var(--grid-font-size-caption)}.checkbox-column{width:40px;min-width:40px;max-width:40px}}@media(prefers-contrast:high){.eru-grid-table th,.eru-grid-table td{border-width:2px}.row-item:hover{border-width:2px;border-color:var(--grid-primary)}}@media(prefers-reduced-motion:reduce){.row-item,.column-drag-handle,.ghost-cell{transition:none;animation:none}}.pivot-table .nested-header{text-align:center;font-weight:600;background:var(--grid-surface-container)}.pivot-table .nested-header.row-dimension-header{background:var(--grid-surface-container);font-weight:600}.pivot-table .pivot-header-leafcols{padding:0;margin:0;height:0}.pivot-table .pivot-header-level.level-0 .nested-header{font-size:14px;padding:12px 8px}.pivot-table .pivot-header-level.level-1 .nested-header{font-size:13px;padding:10px 6px}.pivot-table .pivot-header-level.level-2 .nested-header{font-size:12px;padding:8px 4px}.pivot-table .nested-header:hover{background:var(--grid-surface-variant);color:var(--grid-primary);transition:all .2s ease}.pivot-table .pivot-cell.aggregated-value{font-weight:500;font-family:Roboto Mono,monospace}.pivot-table .pivot-cell-content{display:flex;justify-content:center;align-items:center;min-height:38px}.pivot-table .pivot-repeated-value .cell-content,.pivot-table .pivot-repeated-value .pivot-cell-content{visibility:hidden}.pivot-table .pivot-group-start.row-dimension-cell{border-top:1px solid var(--grid-outline, #79757f)}.pivot-mode .incremental-row-container{display:flex;flex-direction:column;height:auto;max-height:85vh;overflow:auto}.pivot-mode .h-shell{position:relative;width:calc(100% - var(--scrollbar-width, 17px))!important;top:0;z-index:1;overflow-x:hidden;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}.pivot-mode .h-shell::-webkit-scrollbar{display:none}.pivot-mode .gt-shell{position:relative;bottom:50px;flex-shrink:0;overflow-x:hidden;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}.pivot-mode .gt-shell::-webkit-scrollbar{display:none}.pivot-mode .gt-shell table{border-bottom:var(--grid-outline-width, 1px) solid var(--grid-outline, #e0e0e0)!important}.pivot-mode .gt-shell.adjust-bottom-vs{bottom:66px!important}.pivot-mode .gt-shell.adjust-bottom:not(.adjust-bottom-vs){bottom:calc(66px - var(--scrollbar-width, 17px))!important}.pivot-mode .header-shell{flex-shrink:0;width:100%;box-sizing:border-box;padding-right:var(--scrollbar-width, 17px);overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}.pivot-mode .header-shell::-webkit-scrollbar{display:none}.pivot-mode .header-shell.apply-cdk-width{width:calc(var(--table-total-width) + 10px)!important}.pivot-mode .header-shell .eru-grid-table{margin-bottom:0;width:100%;table-layout:fixed}.pivot-mode .header-shell .eru-grid-table thead{background:var(--grid-surface-container)}.pivot-mode .header-shell .eru-grid-table thead th{background:var(--grid-surface-container);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important}.pivot-mode .header-shell .eru-grid-table thead th.sticky-column{position:sticky;background:var(--grid-surface-container);z-index:111}.pivot-mode .header-shell .eru-grid-table tbody td{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important}.pivot-mode .pivot-container{display:flex;flex-direction:column;height:100%;width:100%;overflow:hidden}.pivot-mode .pivot-table{width:auto!important;min-width:100%!important;table-layout:fixed!important}.pivot-mode .pivot-table colgroup col{width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important;flex:none!important;flex-shrink:0!important;flex-grow:0!important}.pivot-mode .pivot-table td,.pivot-mode .pivot-table th{box-sizing:border-box!important;flex:none!important;flex-shrink:0!important;flex-grow:0!important;word-wrap:break-word!important;word-break:break-all!important}.pivot-mode .pivot-table{table-layout:fixed!important;width:100%!important}.pivot-mode .pivot-table *{max-width:var(--col-width)!important;box-sizing:border-box!important}.pivot-mode .pivot-table colgroup{width:100%!important}.pivot-mode .pivot-table colgroup col{width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important;flex-basis:var(--col-width)!important;flex:0 0 var(--col-width)!important}.pivot-mode .pivot-table table{width:100%!important;table-layout:fixed!important;border-collapse:collapse!important;border-spacing:0!important}.pivot-mode .pivot-table[style*=--table-total-width]{width:var(--table-total-width)!important;min-width:var(--table-total-width)!important;max-width:var(--table-total-width)!important}.pivot-mode .pivot-table colgroup col{width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important;flex:0 0 var(--col-width)!important;flex-basis:var(--col-width)!important;flex-grow:0!important;flex-shrink:0!important;overflow:hidden!important}.pivot-mode .pivot-table tbody td,.pivot-mode .pivot-table thead th{width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important;overflow:hidden!important;text-overflow:ellipsis!important;white-space:nowrap!important}.pivot-mode .pivot-table .cell-content,.pivot-mode .pivot-table data-cell{width:100%!important;max-width:100%!important;overflow:hidden!important;text-overflow:ellipsis!important;white-space:nowrap!important;display:block!important}.pivot-mode .pivot-table table{width:var(--table-total-width)!important;min-width:var(--table-total-width)!important;max-width:var(--table-total-width)!important;table-layout:fixed!important;border-collapse:collapse!important;border-spacing:0!important;word-wrap:break-word!important;word-break:break-all!important}.pivot-mode .pivot-tbody tr.pivot-row{min-height:var(--grid-data-row-height, 50px)!important;height:var(--grid-data-row-height, 50px)!important}.pivot-mode .pivot-tbody tr.pivot-row:hover{background-color:var(--grid-surface-variant)}.pivot-mode .pivot-tbody tr.pivot-row:nth-child(2n){background-color:#00000005}.pivot-mode .pivot-tbody tr.pivot-row td{min-height:var(--grid-data-row-height, 50px)!important;height:var(--grid-data-row-height, 50px)!important;vertical-align:middle;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important}.pivot-mode .pivot-tbody tr.pivot-row td .cell-content{min-height:calc(var(--grid-data-row-height, 50px) - 2px);display:flex;align-items:center;justify-content:center}.pivot-mode .pivot-tbody tr.pivot-row td .cell-content data-cell{width:100%;min-height:calc(var(--grid-data-row-height, 50px) - 4px);display:flex;align-items:center;justify-content:center;overflow:hidden;flex-shrink:0}.pivot-mode .pivot-cell{vertical-align:middle;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:var(--col-width)!important;min-width:var(--col-width)!important;max-width:var(--col-width)!important}.pivot-mode .pivot-cell.aggregated-value{font-weight:500;font-family:Roboto Mono,monospace}.pivot-mode .pivot-cell .cell-content{display:flex;justify-content:center;align-items:center;min-height:var(--grid-header-row-height, 40px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex-shrink:0}.pivot-mode .pivot-table .subtotal-row{background-color:var(--grid-surface-container)!important;font-weight:600}.pivot-mode .pivot-table .subtotal-row td{background-color:var(--grid-surface-container);color:var(--grid-on-surface-variant)}.pivot-mode .pivot-table .subtotal-row td:first-child{color:var(--grid-primary)}.pivot-mode .pivot-table .subtotal-row td.aggregated-value{font-weight:500;color:var(--grid-primary)}.pivot-mode .pivot-table .subtotal-row:hover{background-color:var(--grid-surface-container-high)!important}.pivot-mode .pivot-table .subtotal-row:hover td{background-color:var(--grid-surface-container-high)}.pivot-mode .pivot-table .subtotal-bold td{font-weight:600!important;font-style:normal!important}.pivot-mode .pivot-table .subtotal-bold td.aggregated-value{font-weight:600!important}.pivot-mode .pivot-table .subtotal-italic td{font-style:italic!important}.pivot-mode .pivot-table .subtotal-italic td:first-child{font-weight:600!important}.pivot-mode .pivot-table .subtotal-italic td.aggregated-value{font-style:italic!important;font-weight:500!important}.pivot-mode .pivot-table .subtotal-highlighted{background-color:var(--grid-surface-variant)!important}.pivot-mode .pivot-table .subtotal-highlighted td{background-color:var(--grid-surface-variant)!important;font-weight:700!important;font-style:normal!important;color:var(--grid-primary)!important}.pivot-mode .pivot-table .subtotal-highlighted td.aggregated-value{font-weight:500!important;color:var(--grid-primary)!important}.pivot-mode .pivot-table .subtotal-highlighted:hover,.pivot-mode .pivot-table .subtotal-highlighted:hover td{background-color:var(--grid-surface-container-high)!important}.pivot-mode .pivot-table .grand-total-row{background-color:var(--grid-surface-container-high)!important;font-weight:700;font-size:var(--grid-font-size-body)}.pivot-mode .pivot-table .grand-total-row td{background-color:var(--grid-surface-container-high)!important;color:var(--grid-on-surface)}.pivot-mode .pivot-table .grand-total-row td:first-child{font-style:normal;font-weight:800;color:var(--grid-primary)}.pivot-mode .pivot-table .grand-total-row td.aggregated-value{font-weight:500;color:var(--grid-primary);font-family:Roboto Mono,monospace}.pivot-mode .pivot-table .grand-total-row:hover,.pivot-mode .pivot-table .grand-total-row:hover td{background-color:var(--grid-surface-container-high)!important}.pivot-mode .pivot-table .grand-total-bold td{font-weight:700!important;font-style:normal!important}.pivot-mode .pivot-table .grand-total-bold td.aggregated-value{font-weight:700!important}.pivot-mode .pivot-table .grand-total-italic td,.pivot-mode .pivot-table .grand-total-italic td.aggregated-value{font-style:italic!important;font-weight:500!important}.pivot-mode .pivot-table .grand-total-highlighted{background-color:var(--grid-primary)!important;box-shadow:var(--grid-elevation-2)!important}.pivot-mode .pivot-table .grand-total-highlighted td{background-color:var(--grid-primary)!important;color:var(--grid-on-primary)!important;font-weight:500!important;font-style:normal!important}.pivot-mode .pivot-table .grand-total-highlighted td.aggregated-value{color:var(--grid-on-primary)!important;font-weight:500!important}.pivot-mode .pivot-table .grand-total-highlighted:hover,.pivot-mode .pivot-table .grand-total-highlighted:hover td{background-color:var(--grid-primary)!important}.pivot-mode .pivot-table .collapsible-header{position:relative}.pivot-mode .pivot-table .collapsible-header .header-content{display:flex;align-items:center;justify-content:space-between;gap:var(--grid-spacing-xs);padding:var(--grid-spacing-xs) var(--grid-spacing-sm)}.pivot-mode .pivot-table .collapsible-header .header-label{flex:1;font-weight:600}.pivot-mode .pivot-table .collapsible-header .collapse-toggle-btn{background:none;border:none;cursor:pointer;padding:var(--grid-spacing-xxs);margin:0;display:flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:var(--grid-border-radius);color:var(--grid-on-surface-variant);transition:all .2s ease;font-size:12px;font-weight:600}.pivot-mode .pivot-table .collapsible-header .collapse-toggle-btn:hover{background-color:var(--grid-surface-container);color:var(--grid-primary);transform:scale(1.1)}.pivot-mode .pivot-table .collapsible-header .collapse-toggle-btn:focus{outline:2px solid var(--grid-primary);outline-offset:1px}.pivot-mode .pivot-table .collapsible-header .collapse-toggle-btn .collapse-icon{display:block;line-height:1;font-family:monospace;font-size:14px}.pivot-mode .pivot-table .collapsible-header.expanded .collapse-toggle-btn .collapse-icon{color:var(--grid-primary)}.pivot-mode .pivot-table .collapsible-header.collapsed{background-color:var(--grid-surface-variant)}.pivot-mode .pivot-table .collapsible-header.collapsed .header-label{font-style:italic;color:var(--grid-on-surface-variant)}.pivot-mode .pivot-table .collapsible-header.collapsed .collapse-toggle-btn .collapse-icon{color:var(--grid-outline)}.pivot-mode .pivot-table .collapsible-header:hover{background-color:var(--grid-surface-container)}.pivot-mode .pivot-table .collapsible-header:hover .header-label{color:var(--grid-on-surface)}.pivot-mode .pivot-table .pivot-single-table{display:flex;flex-direction:column;height:100%;width:100%;overflow:hidden;min-height:var(--table-min-height)!important}.pivot-mode .pivot-table .pivot-single-table .pivot-header-container{flex-shrink:0;background:var(--grid-surface)!important;overflow-x:auto;overflow-y:hidden;min-height:100px!important;height:auto!important}.pivot-mode .pivot-table .pivot-single-table .pivot-header-container .pivot-table{width:auto;min-width:100%;height:auto!important;min-height:100px!important}.pivot-mode .pivot-table .pivot-single-table .pivot-header-container .pivot-table th{background:var(--grid-surface-container)!important;padding:8px 6px!important;white-space:nowrap;min-width:50px;min-height:40px!important;height:auto!important;position:relative;visibility:visible!important;color:var(--grid-on-surface)!important}.pivot-mode .pivot-table .pivot-single-table .pivot-header-container .pivot-table th.sticky-column{position:sticky!important;background:var(--grid-surface-container)!important;border-right:2px solid var(--grid-primary)!important;box-shadow:2px 0 4px #0000001a;z-index:101!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container{flex:1;overflow:auto;min-height:300px!important;height:auto!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-viewport{height:100%!important;width:100%!important;overflow-x:auto!important;overflow-y:auto!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-table,.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-data-table{width:auto;min-width:100%;height:auto!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-table td,.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-data-table td{padding:8px 6px!important;white-space:nowrap;min-width:50px;min-height:32px!important;height:auto!important;background:var(--grid-surface)!important;color:var(--grid-on-surface)!important;visibility:visible!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-table td.sticky-column,.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-data-table td.sticky-column{position:sticky!important;background:var(--grid-surface-container)!important;border-right:2px solid var(--grid-primary)!important;box-shadow:2px 0 4px #0000001a;z-index:100!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-table tbody tr,.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-data-table tbody tr{height:auto!important;min-height:50px!important}.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-table tbody tr.pivot-row,.pivot-mode .pivot-table .pivot-single-table .pivot-data-container .pivot-data-table tbody tr.pivot-row{visibility:visible!important;display:table-row!important}.pivot-mode .pivot-table .collapsed-column-group{background-color:var(--grid-surface-container);border-left:3px solid var(--grid-primary)}.pivot-mode .pivot-table .collapsed-column-group:hover{background-color:var(--grid-surface-container-high)}.pivot-row.subtotal-row{background-color:var(--grid-surface-variant);font-weight:500}.pivot-row.subtotal-row.subtotal-bold{font-weight:500}.pivot-row.subtotal-row.subtotal-italic{font-style:italic}.pivot-row.subtotal-row.subtotal-highlighted{background-color:var(--grid-primary);color:var(--grid-on-primary)}.pivot-row.grand-total-row{background-color:var(--grid-surface-container);font-weight:600}.pivot-row.grand-total-row.grand-total-bold{font-weight:800}.pivot-row.grand-total-row.grand-total-italic{font-style:italic}.pivot-row.grand-total-row.grand-total-highlighted{background-color:var(--grid-primary);color:var(--grid-on-primary)}.pivot-row.first-visible-row{background-color:#6750a41a!important;position:relative}.pivot-row.first-visible-row:before{content:\"\\1f441\\fe0f First Visible\";position:absolute;top:-20px;left:0;background:var(--grid-primary);color:var(--grid-on-primary);padding:2px 6px;font-size:10px;border-radius:2px;z-index:1000}.header-wrap-text{white-space:pre-wrap;word-break:auto-phrase}.group-header-row{display:flex;align-items:center;justify-content:space-between;width:100%;padding-right:12px}.custom-collapse-header{background-color:var(--grid-surface-variant);padding:8px 20px;border-top-left-radius:12px;border-top-right-radius:12px;cursor:pointer;display:flex;width:fit-content;align-items:center;-webkit-user-select:none;user-select:none;min-width:200px;margin-bottom:10px;position:sticky;left:1px;z-index:116}.custom-collapse-header .collapse-arrow{display:inline-block;margin-right:8px;font-size:12px;color:var(--grid-on-surface-variant);transition:transform .2s ease;transform:rotate(0)}.custom-collapse-header .collapse-arrow.rotate-arrow{transform:rotate(270deg)}.custom-collapse-header .f-12{font-size:12px;color:var(--grid-on-surface)}.custom-collapse-header .group-sort-indicator{display:inline-flex;align-items:center;margin-left:8px}.custom-collapse-header .group-sort-indicator .sort-triangles{display:flex;flex-direction:column;align-items:center;gap:2px}.custom-collapse-header .group-sort-indicator .sort-tri{width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;cursor:pointer;transition:border-color .15s ease}.custom-collapse-header .group-sort-indicator .sort-tri-up{border-bottom:5px solid var(--grid-on-surface-variant, #49454f);opacity:.3}.custom-collapse-header .group-sort-indicator .sort-tri-down{border-top:5px solid var(--grid-on-surface-variant, #49454f);opacity:.3}.custom-collapse-header .group-sort-indicator .sort-tri-active{opacity:1}.custom-collapse-header .group-sort-indicator .sort-tri-active.sort-tri-up{border-bottom-color:var(--grid-primary, #6750a4)}.custom-collapse-header .group-sort-indicator .sort-tri-active.sort-tri-down{border-top-color:var(--grid-primary, #6750a4)}.excel-download-icon{cursor:pointer}.excel-download-icon:hover{opacity:.75}.excel-download-bar{display:flex;justify-content:flex-end;padding:4px 12px;flex-shrink:0}.table-mode .header-shell{flex-shrink:0;width:100%;box-sizing:border-box;padding-right:var(--scrollbar-width, 17px);overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}.table-mode .header-shell::-webkit-scrollbar{display:none}.table-mode .header-shell.apply-cdk-width{width:calc(var(--table-total-width) + 10px)!important}.table-mode .subtotal-row{background-color:var(--grid-surface-container)!important;font-weight:600}.table-mode .subtotal-row td{background-color:var(--grid-surface-container);color:var(--grid-on-surface-variant)}.table-mode .subtotal-row td:first-child{color:var(--grid-primary)}.table-mode .subtotal-row td.subtotal-cell{font-weight:500}.table-mode .subtotal-row td.subtotal-cell .subtotal-label{font-weight:600;color:var(--grid-primary);padding-left:var(--grid-cell-padding-x)}.table-mode .subtotal-row:hover{background-color:var(--grid-surface-container-high)!important}.table-mode .subtotal-row:hover td{background-color:var(--grid-surface-container-high)}.table-mode .subtotal-row.subtotal-bold td{font-weight:600!important;font-style:normal!important}.table-mode .subtotal-row.subtotal-italic td{font-style:italic!important}.table-mode .subtotal-row.subtotal-italic td:first-child{font-weight:600!important}.table-mode .subtotal-row.subtotal-highlighted{background-color:var(--grid-surface-variant)!important}.table-mode .subtotal-row.subtotal-highlighted td{background-color:var(--grid-surface-variant)!important;font-weight:700!important;font-style:normal!important;color:var(--grid-primary)!important}.table-mode .subtotal-row.subtotal-highlighted:hover,.table-mode .subtotal-row.subtotal-highlighted:hover td{background-color:var(--grid-surface-container-high)!important}.table-mode .grand-total-row{background-color:var(--grid-surface-container-high)!important;font-weight:700}.table-mode .grand-total-row td{background-color:var(--grid-surface-container-high);color:var(--grid-on-surface)}.table-mode .grand-total-row td:first-child{color:var(--grid-primary)}.table-mode .grand-total-row td.grand-total-cell{font-weight:600}.table-mode .grand-total-row td.grand-total-cell .grand-total-label{font-weight:700;color:var(--grid-primary);padding-left:var(--grid-cell-padding-x)}.table-mode .grand-total-row:hover{background-color:var(--grid-surface-container-high)!important}.table-mode .grand-total-row:hover td{background-color:var(--grid-surface-container-high)}.table-mode .grand-total-row.grand-total-bold td{font-weight:700!important;font-style:normal!important}.table-mode .grand-total-row.grand-total-italic td{font-style:italic!important}.table-mode .grand-total-row.grand-total-italic td:first-child{font-weight:700!important}.table-mode .grand-total-row.grand-total-highlighted{background-color:var(--grid-surface-variant)!important}.table-mode .grand-total-row.grand-total-highlighted td{background-color:var(--grid-surface-variant)!important;font-weight:800!important;font-style:normal!important;color:var(--grid-primary)!important}.table-mode .grand-total-row.grand-total-highlighted:hover,.table-mode .grand-total-row.grand-total-highlighted:hover td{background-color:var(--grid-surface-container-high)!important}.table-mode .subtotal-row-shell{width:100%;box-sizing:border-box;padding-right:var(--scrollbar-width, 17px);overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}.table-mode .subtotal-row-shell::-webkit-scrollbar{display:none}.board-mode-host{overflow:hidden;display:flex;flex-direction:column;max-height:var(--grid-height, 600px)}.board-mode-host .board-view-container{display:flex;flex-direction:column;flex:1;min-height:0}.board-mode-host .board-sort-bar{display:flex;align-items:center;gap:6px;padding:8px 16px;flex-shrink:0;border-bottom:1px solid var(--grid-outline-variant, #cac4d0);background:var(--grid-surface, #fffbfe);overflow-x:auto}.board-mode-host .board-sort-bar .board-sort-label{font-size:12px;font-weight:500;color:var(--grid-on-surface-variant, #49454f);white-space:nowrap}.board-mode-host .board-sort-bar .board-sort-chip{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:16px;border:1px solid var(--grid-outline-variant, #cac4d0);background:var(--grid-surface, #fffbfe);color:var(--grid-on-surface, #1d1b20);font-size:12px;white-space:nowrap}.board-mode-host .board-sort-bar .board-sort-chip-active{background:var(--grid-surface-container);border-color:var(--grid-outline, #79757f);color:var(--grid-on-surface, #1d1b20)}.board-mode-host .board-sort-bar .board-sort-chip-label{pointer-events:none}.board-mode-host .board-sort-bar .board-sort-chip-arrow{font-size:10px;line-height:1;cursor:pointer;padding:2px;border-radius:4px}.board-mode-host .board-sort-bar .board-sort-chip-arrow:hover{background:#00000014}.board-mode-host .board-sort-bar .board-sort-chip-priority{font-size:9px;font-weight:700;background:var(--grid-primary, #6750a4);color:var(--grid-on-primary, #ffffff);border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center}.board-mode-host .board-sort-bar .board-sort-chip-remove{font-size:10px;cursor:pointer;padding:2px;border-radius:4px;color:var(--grid-on-surface-variant, #49454f)}.board-mode-host .board-sort-bar .board-sort-chip-remove:hover{background:#00000014;color:var(--grid-error, #b3261e)}.board-mode-host .board-sort-bar .board-sort-add-btn{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:16px;border:1px dashed var(--grid-outline-variant, #cac4d0);background:transparent;color:var(--grid-on-surface-variant, #49454f);font-size:12px;cursor:pointer;white-space:nowrap;transition:background .15s ease,border-color .15s ease}.board-mode-host .board-sort-bar .board-sort-add-btn .board-sort-add-icon{font-size:14px;width:14px;height:14px}.board-mode-host .board-sort-bar .board-sort-add-btn:hover{background:var(--grid-surface-container-low, #f7f2fa);border-color:var(--grid-primary, #6750a4);color:var(--grid-primary, #6750a4)}.board-mode-host .board-sort-bar .board-sort-clear{display:inline-flex;align-items:center;padding:4px 10px;border-radius:16px;border:1px solid var(--grid-error, #b3261e);background:transparent;color:var(--grid-error, #b3261e);font-size:12px;cursor:pointer;white-space:nowrap;transition:background .15s ease}.board-mode-host .board-sort-bar .board-sort-clear:hover{background:#b3261e14}.board-mode-host .board-columns-wrapper{display:grid;grid-template-columns:repeat(auto-fit,minmax(var(--board-col-min-width, 300px),var(--board-col-max-width, 1fr)));grid-auto-rows:auto;align-items:start;gap:16px;flex:1;min-height:0;overflow-x:hidden;overflow-y:auto;align-content:start;justify-content:start}.board-mode-host .board-columns-wrapper.board-columns-nowrap{grid-auto-flow:column;grid-template-columns:none;grid-template-rows:auto;grid-auto-rows:auto;grid-auto-columns:minmax(var(--board-col-min-width, 300px),var(--board-col-max-width, 1fr));overflow-x:auto;overflow-y:hidden}.board-mode-host .board-column{max-height:var(--board-column-height, 420px);min-width:0;display:flex;flex-direction:column;background:var(--grid-surface-container, #f3edf7);border-radius:12px;min-height:0;overflow:hidden}.board-mode-host .board-column.board-column-accented{border-top:3px solid var(--board-group-color, transparent)}.board-mode-host .board-column .column-header{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 14px;font-weight:600;flex-shrink:0;background-color:transparent}.board-mode-host .board-column .column-header:hover{background-color:transparent}.board-mode-host .board-column .column-header .column-header-title{font-size:15px;font-weight:700;letter-spacing:.2px;line-height:1.2;color:var(--grid-on-surface, #1d1b20);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.board-mode-host .board-column .column-header .column-header-title-cell{display:inline-flex;align-items:center;flex:1 1 auto;min-width:0;overflow:visible;text-overflow:clip}.board-mode-host .board-column .column-header .column-header-title-cell data-cell,.board-mode-host .board-column .column-header .column-header-title-cell .data-cell-component,.board-mode-host .board-column .column-header .column-header-title-cell .container{padding:0!important;margin:0!important;border:none!important;background:transparent!important;min-height:0!important;height:auto!important;width:auto!important;max-width:100%!important;overflow:visible!important}.board-mode-host .board-column .column-header .column-header-title-cell .status-display,.board-mode-host .board-column .column-header .column-header-title-cell .status-display-content,.board-mode-host .board-column .column-header .column-header-title-cell .status-text,.board-mode-host .board-column .column-header .column-header-title-cell .tag-display,.board-mode-host .board-column .column-header .column-header-title-cell .tag-text{max-width:none!important;overflow:visible!important;text-overflow:clip!important}.board-mode-host .board-column .column-header .column-header-count{font-size:10px;font-weight:600;color:var(--eru-board-count-color, var(--grid-on-surface-variant, #49454f));background:var(--eru-board-count-bg, var(--grid-surface-variant, #e7e0ec));border-radius:10px;padding:3px 10px;white-space:nowrap;flex-shrink:0}.board-mode-host .board-column-body{flex:0 1 auto;min-height:0}.board-mode-host .board-card-container{box-sizing:border-box;overflow:hidden;border-radius:8px;transition:background-color .15s ease,box-shadow .15s ease}.board-mode-host .board-card-container.show-row-lines{box-shadow:inset 0 0 0 var(--grid-divider-width, 1px) var(--grid-divider-color, var(--grid-outline, #e0e0e0))}.board-mode-host .board-card-container:hover{background-color:var(--eru-board-card-hover-bg, color-mix(in srgb, var(--mat-sys-primary, #1976d2) 8%, transparent))}.board-mode-host .board-card-container.selected{background-color:var(--eru-board-card-selected-bg, color-mix(in srgb, var(--mat-sys-primary, #1976d2) 14%, transparent));box-shadow:inset 0 0 0 2px var(--eru-board-card-selected-outline, var(--mat-sys-primary, #1976d2))}.board-mode-host .board-card{height:calc(100% - 8px);overflow:hidden;cursor:pointer}.board-mode-host .board-card mat-card-title{font-size:13px}.board-mode-host .board-card mat-card-subtitle{font-size:12px}.board-mode-host .board-card-field{display:flex;flex-direction:column;margin-bottom:4px}.board-mode-host .board-field-label{font-size:10px;color:var(--grid-on-surface-variant, #49454f);font-weight:500;text-transform:uppercase;letter-spacing:.5px}.board-mode-host .board-ghost-card{margin:8px;padding:16px;background:var(--grid-surface, #fef7ff);border-radius:8px;animation:board-pulse 1.5s ease-in-out infinite}.board-mode-host .board-ghost-line{height:12px;background:var(--grid-surface-variant, #e7e0ec);border-radius:4px;margin-bottom:8px}.board-mode-host .board-ghost-line--short{width:60%}@keyframes board-pulse{0%,to{opacity:1}50%{opacity:.5}}th.row-expand-toggle,td.row-expand-toggle{width:40px!important;min-width:40px!important;max-width:40px!important;padding:0!important;text-align:center;vertical-align:middle;cursor:pointer;-webkit-user-select:none;user-select:none;box-sizing:border-box}.row-expand-icon{font-size:20px;width:20px;height:20px;line-height:20px;color:var(--grid-on-surface-variant);transition:transform .15s ease-in-out}.row-expand-icon.expanded{transform:rotate(90deg)}.row-detail{background:var(--grid-surface-container)}.row-detail .row-detail-cell{padding:var(--grid-spacing-sm) var(--grid-spacing-md);border-bottom:1px solid var(--grid-outline-variant)}.row-detail .row-detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:var(--grid-spacing-sm) var(--grid-spacing-md)}.row-detail .row-detail-field{display:flex;flex-direction:column;gap:var(--grid-spacing-xxs);min-width:0}.row-detail .row-detail-label{font-size:var(--grid-font-size-caption);color:var(--grid-on-surface-variant);font-weight:500}.row-detail .row-detail-value{min-width:0}.row-detail .row-detail-value data-cell{display:block;width:100%}.selection-banner{display:flex;align-items:center;gap:8px;padding:6px 12px;font-size:12px;background:#eef4ff;border-bottom:1px solid #c7d8f5;color:#1a3a6b}.selection-banner-action{background:none;border:none;padding:0;font:inherit;color:#1558d6;font-weight:600;cursor:pointer;text-decoration:underline}\n"] }]
16315
16895
  }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { allViewports: [{
16316
16896
  type: ViewChildren,
16317
16897
  args: [CdkVirtualScrollViewport]
@@ -16453,5 +17033,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
16453
17033
  * Generated bundle index. Do not edit.
16454
17034
  */
16455
17035
 
16456
- export { ACTION_COLUMN_MIN_WIDTH, AttachmentComponent, CELL_RULE_OPERATORS, CheckboxComponent, ChipListComponent, ColumnConstraintsService, CompositeComponent, CurrencyComponent, CustomVirtualScrollStrategy, DATA_TYPES, DATETIME_FORMATS, DATE_FORMATS, DateComponent, DatetimeComponent, DurationComponent, EmailComponent, EruGridComponent, EruGridService, EruGridStore, GRID_COLOR_TOKENS, INHERITED_FIELD_KEYS, LocationComponent, MATERIAL_MODULES, MATERIAL_PROVIDERS, MONTH_SHORT_NAMES, NumberComponent, NumericInputDirective, PRESENTATION_DATATYPES, PRESET_CONFIG_DEFAULTS, PRESET_MANAGED_FIELDS, PeopleComponent, PhoneComponent, PriorityComponent, ProgressComponent, RatingComponent, SEEDED_FIELD_KEYS, SELF_COLOURED_DATATYPES, SelectComponent, StatusComponent, TagComponent, TextareaComponent, TextboxComponent, ThemeService, ThemeToggleComponent, WebsiteComponent, abbreviateNumber, abbreviationScaleFor, cellRuleBarPercent, cellRuleMatches, cellRuleToCss, cellStatAlias, cellTextStyleToCss, collectColumnStatRequests, columnCellRules, composeColorValue, evaluateRowCondition, formatCellValue, formatDateWithPattern, formatNumberValue, hasOwnCellRules, matchCellRule, matchColorRange, normalizeDatatype, normalizeDateFormat, normalizeDateTimeFormat, parseCellDate, parseColorValue, parseDateWithPattern, readRowValue, resolveColumnRules, resolveRowValue, resolveStatValue, statusPillColors, tagPillColors };
17036
+ export { ACTION_COLUMN_MIN_WIDTH, AttachmentComponent, CELL_RULE_OPERATORS, CheckboxComponent, ChipListComponent, ColumnConstraintsService, CompositeComponent, CurrencyComponent, CustomVirtualScrollStrategy, DATA_TYPES, DATETIME_FORMATS, DATE_FORMATS, DateComponent, DatetimeComponent, DurationComponent, EmailComponent, EruGridComponent, EruGridService, EruGridStore, GRID_COLOR_TOKENS, INHERITED_FIELD_KEYS, LocationComponent, MATERIAL_MODULES, MATERIAL_PROVIDERS, MONTH_SHORT_NAMES, NumberComponent, NumericInputDirective, PRESENTATION_DATATYPES, PRESET_CONFIG_DEFAULTS, PRESET_MANAGED_FIELDS, PeopleComponent, PhoneComponent, PriorityComponent, ProgressComponent, RatingComponent, SEEDED_FIELD_KEYS, SELF_COLOURED_DATATYPES, SelectComponent, StatusComponent, TagComponent, TextareaComponent, TextboxComponent, ThemeService, ThemeToggleComponent, WebsiteComponent, abbreviateNumber, abbreviationScaleFor, cellRuleBarPercent, cellRuleMatches, cellRuleToCss, cellStatAlias, cellTextStyleToCss, collectColumnStatRequests, columnCellRules, composeColorValue, effectiveValueDatatype, evaluateRowCondition, formatCellValue, formatDateTimeWithPattern, formatDateWithPattern, formatNumberValue, hasOwnCellRules, matchCellRule, matchColorRange, normalizeCellRender, normalizeDatatype, normalizeDateFormat, normalizeDateTimeFormat, parseCellDate, parseCellDateTime, parseColorValue, parseDateWithPattern, readRowValue, resolveColumnRules, resolveRowValue, resolveStatValue, rowSelectionKey, statusPillColors, tagPillColors, toDateOnlyString, toDateTimeString };
16457
17037
  //# sourceMappingURL=eru-grid.mjs.map