eru-grid 0.0.52 → 0.0.53

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));
@@ -3696,6 +3841,17 @@ const NULL_GROUP_KEY = '__NULL_GROUP__';
3696
3841
  function toGroupKey(groupId) {
3697
3842
  return groupId === null || groupId === undefined ? NULL_GROUP_KEY : groupId;
3698
3843
  }
3844
+ /**
3845
+ * The id a row is selected by. Entity rows carry `entity_id`; array/query rows
3846
+ * that were given a synthetic key carry `id`. Every read and write of the
3847
+ * selection set must go through this — the checkbox used to store `row.id`
3848
+ * while the checked-state read `row.entity_id`, so ticking an entity row put
3849
+ * `undefined` in the set and the box never rendered checked.
3850
+ */
3851
+ function rowSelectionKey(row) {
3852
+ const id = row?.entity_id ?? row?.id;
3853
+ return id === null || id === undefined ? '' : String(id);
3854
+ }
3699
3855
  class EruGridStore {
3700
3856
  // Inject services
3701
3857
  gridConfigService = inject(GridConfigService);
@@ -3707,6 +3863,11 @@ class EruGridStore {
3707
3863
  _rows = signal([], ...(ngDevMode ? [{ debugName: "_rows" }] : []));
3708
3864
  _groupRows = signal(new Map(), ...(ngDevMode ? [{ debugName: "_groupRows" }] : []));
3709
3865
  _selectedRowIds = signal(new Set(), ...(ngDevMode ? [{ debugName: "_selectedRowIds" }] : []));
3866
+ // "All N matching records are selected", the second stage of the header
3867
+ // checkbox. The grid only ever holds the pages it has loaded, so this flag is
3868
+ // the only way to say the user meant the whole result set: consumers send
3869
+ // their filter criteria instead of the id list when it is set.
3870
+ _selectAllMatching = signal(false, ...(ngDevMode ? [{ debugName: "_selectAllMatching" }] : []));
3710
3871
  // The single "active" board card (last clicked) — stored by row-object
3711
3872
  // reference so it works for any data source (entity rows have entity_id,
3712
3873
  // query rows may not). Drives the board-card selected-state highlight;
@@ -3813,6 +3974,7 @@ class EruGridStore {
3813
3974
  return result;
3814
3975
  }, ...(ngDevMode ? [{ debugName: "groupRowsSummary" }] : []));
3815
3976
  selectedRowIds = this._selectedRowIds.asReadonly();
3977
+ selectAllMatching = this._selectAllMatching.asReadonly();
3816
3978
  activeBoardRow = this._activeBoardRow.asReadonly();
3817
3979
  configuration = this._configuration.asReadonly();
3818
3980
  isLoading = this._isLoading.asReadonly();
@@ -3838,7 +4000,35 @@ class EruGridStore {
3838
4000
  // Excel-download request readonly signal (exposed to consumers)
3839
4001
  excelDownloadRequest = this._excelDownloadRequest.asReadonly();
3840
4002
  // Computed properties
3841
- selectedRows = computed(() => this.rows().filter(row => this.selectedRowIds().has(row.entity_id)), ...(ngDevMode ? [{ debugName: "selectedRows" }] : []));
4003
+ /**
4004
+ * Every row currently in memory that can be selected, deduped by selection key.
4005
+ *
4006
+ * Selection has to read both row stores: `_rows` is only populated when the
4007
+ * consumer mirrors data in via setRows()/addRows(), while the grouped,
4008
+ * server-paginated renderer fills `_groupRows` through addRowsForGroup(). The
4009
+ * select-all handler read `rows()` alone, which is empty in the normal paged
4010
+ * path — so select-all was a no-op there.
4011
+ */
4012
+ selectableRows = computed(() => {
4013
+ const seen = new Set();
4014
+ const out = [];
4015
+ const push = (row) => {
4016
+ const id = rowSelectionKey(row);
4017
+ if (!id || seen.has(id))
4018
+ return;
4019
+ seen.add(id);
4020
+ out.push(row);
4021
+ };
4022
+ this.rows().forEach(push);
4023
+ this.groupRows().forEach(rows => rows.forEach(push));
4024
+ return out;
4025
+ }, ...(ngDevMode ? [{ debugName: "selectableRows" }] : []));
4026
+ selectedRows = computed(() => this.selectableRows().filter(row => this.selectedRowIds().has(rowSelectionKey(row))), ...(ngDevMode ? [{ debugName: "selectedRows" }] : []));
4027
+ /**
4028
+ * How many records the current groups say exist server-side, loaded or not.
4029
+ * This is the "all N matching" number the select-all banner offers.
4030
+ */
4031
+ totalMatchingCount = computed(() => this.groups().reduce((sum, group) => sum + (group.totalRowCount || 0), 0), ...(ngDevMode ? [{ debugName: "totalMatchingCount" }] : []));
3842
4032
  totalSelectedCount = computed(() => this.selectedRowIds().size, ...(ngDevMode ? [{ debugName: "totalSelectedCount" }] : []));
3843
4033
  hasSelection = computed(() => this.selectedRowIds().size > 0, ...(ngDevMode ? [{ debugName: "hasSelection" }] : []));
3844
4034
  expandedGroups = computed(() => this.groups().filter(group => group.isExpanded), ...(ngDevMode ? [{ debugName: "expandedGroups" }] : []));
@@ -3926,7 +4116,14 @@ class EruGridStore {
3926
4116
  // Resolve data-model datatype aliases (r_status -> status) once here, so
3927
4117
  // every downstream switch, validator and design-panel lookup sees the
3928
4118
  // canonical value.
3929
- datatype: normalizeDatatype(column.datatype),
4119
+ datatype: normalizeDatatype(
4120
+ // A column saved under the old encoding carries 'composite'/'page' in
4121
+ // the datatype slot. normalizeCellRender has just read the renderer off
4122
+ // it; leaving the value there would keep it out of every datatype
4123
+ // switch, so it falls back to text. A mapped column overwrites this
4124
+ // with its real type on the next resolve.
4125
+ PRESENTATION_DATATYPES.has(String(column.datatype)) ? 'textbox' : column.datatype),
4126
+ cell_render: normalizeCellRender(column),
3930
4127
  field_size: this.columnConstraintsService.getDefaultFieldSize(column, config)
3931
4128
  }));
3932
4129
  // Sort columns by grid_index to ensure proper order in table mode
@@ -4018,6 +4215,28 @@ class EruGridStore {
4018
4215
  getConfiguration() {
4019
4216
  return this.configuration();
4020
4217
  }
4218
+ /**
4219
+ * Reorder by column name rather than by rendered position.
4220
+ *
4221
+ * The header renders `displayColumns` — `columns()` minus the hidden columns
4222
+ * and minus the group-by field — so the `$index` of a header cell is not the
4223
+ * index of that column in `_columns`. Passing the rendered index straight to
4224
+ * `reorderColumns` moved whichever column occupied that slot in the full
4225
+ * list, which is why dragging a column near the end appeared to move its
4226
+ * neighbour instead. Names survive every filter, so resolve them here.
4227
+ */
4228
+ reorderColumnsByName(fromName, toName) {
4229
+ const currentColumns = this.columns();
4230
+ const fromIndex = currentColumns.findIndex(col => col.name === fromName);
4231
+ const toIndex = currentColumns.findIndex(col => col.name === toName);
4232
+ // Pivot mode renders generated column definitions that do not exist in
4233
+ // `_columns`; there is nothing to reorder in that case.
4234
+ if (fromIndex === -1 || toIndex === -1) {
4235
+ console.error('Unknown column in reorder', { fromName, toName });
4236
+ return;
4237
+ }
4238
+ this.reorderColumns(fromIndex, toIndex);
4239
+ }
4021
4240
  reorderColumns(fromIndex, toIndex) {
4022
4241
  const currentColumns = [...this.columns()];
4023
4242
  if (fromIndex < 0 || fromIndex >= currentColumns.length ||
@@ -4289,18 +4508,35 @@ class EruGridStore {
4289
4508
  }
4290
4509
  selectAllRowsInGroup(groupId) {
4291
4510
  const selectedRowIds = new Set(this.selectedRowIds());
4292
- const groupRows = this.getRowsForGroup(groupId);
4293
- groupRows.forEach(row => selectedRowIds.add(row.entity_id));
4511
+ this.getRowsForGroup(groupId).forEach(row => {
4512
+ const id = rowSelectionKey(row);
4513
+ if (id)
4514
+ selectedRowIds.add(id);
4515
+ });
4294
4516
  this._selectedRowIds.set(selectedRowIds);
4295
4517
  }
4296
4518
  deselectAllRowsInGroup(groupId) {
4297
4519
  const selectedRowIds = new Set(this.selectedRowIds());
4298
- const groupRows = this.getRowsForGroup(groupId);
4299
- groupRows.forEach(row => selectedRowIds.delete(row.entity_id));
4520
+ this.getRowsForGroup(groupId).forEach(row => selectedRowIds.delete(rowSelectionKey(row)));
4300
4521
  this._selectedRowIds.set(selectedRowIds);
4301
4522
  }
4523
+ /** Select every row loaded so far, across all groups. Stage one of select-all. */
4524
+ selectAllLoadedRows() {
4525
+ const selectedRowIds = new Set(this.selectedRowIds());
4526
+ this.selectableRows().forEach(row => selectedRowIds.add(rowSelectionKey(row)));
4527
+ this._selectedRowIds.set(selectedRowIds);
4528
+ }
4529
+ /**
4530
+ * Stage two of select-all: the user accepted "select all N matching". The set
4531
+ * of ids is left as-is (it still only holds what is loaded) — the flag is what
4532
+ * tells the consumer to act on the filter rather than on the ids.
4533
+ */
4534
+ setSelectAllMatching(value) {
4535
+ this._selectAllMatching.set(value);
4536
+ }
4302
4537
  clearSelection() {
4303
4538
  this._selectedRowIds.set(new Set());
4539
+ this._selectAllMatching.set(false);
4304
4540
  }
4305
4541
  // Configuration methods
4306
4542
  setConfiguration(configuration) {
@@ -4411,7 +4647,7 @@ class EruGridStore {
4411
4647
  isGroupSelected(groupId) {
4412
4648
  const groupRows = this.getRowsForGroup(groupId);
4413
4649
  return groupRows.length > 0 &&
4414
- groupRows.every(row => this.selectedRowIds().has(row.entity_id));
4650
+ groupRows.every(row => this.selectedRowIds().has(rowSelectionKey(row)));
4415
4651
  }
4416
4652
  getRowsForGroup(groupId) {
4417
4653
  const key = toGroupKey(groupId);
@@ -4483,7 +4719,16 @@ class EruGridStore {
4483
4719
  this._groupRows().forEach(rows => allGroupRows.push(...rows));
4484
4720
  sourceData = allGroupRows;
4485
4721
  }
4486
- if (!pivotConfig || sourceData.length === 0) {
4722
+ if (!pivotConfig)
4723
+ return;
4724
+ // No source rows: publish an EMPTY pivot rather than returning and leaving
4725
+ // the previous result on screen. Bailing here is what kept the last
4726
+ // filter's rows — and their subtotals — rendered after a query came back
4727
+ // with nothing, since the pivot view reads _pivotResult and not the row
4728
+ // stores that had already been cleared.
4729
+ if (sourceData.length === 0) {
4730
+ if (this._pivotResult() !== null)
4731
+ this._pivotResult.set(null);
4487
4732
  return;
4488
4733
  }
4489
4734
  this.setLoading(true);
@@ -4759,6 +5004,15 @@ class EruGridStore {
4759
5004
  }
4760
5005
  resetGroupRows() {
4761
5006
  this._groupRows.set(new Map());
5007
+ // A fresh data set invalidates the selection: the ids may not exist under
5008
+ // the new filter/grouping, and an "all N matching" claim was made against
5009
+ // the criteria that just changed. Paging does NOT come through here — it
5010
+ // appends via addRowsForGroup — so a selection still survives scrolling.
5011
+ this.clearSelection();
5012
+ // In pivot mode the view renders from _pivotResult, not from the row
5013
+ // stores, so clearing the rows alone left the old pivot on screen.
5014
+ if (this.isPivotMode())
5015
+ this.transformToPivot();
4762
5016
  }
4763
5017
  /**
4764
5018
  * Add rows for a specific group (called by consumer with API data)
@@ -5089,6 +5343,22 @@ class EruGridService {
5089
5343
  get hasSelection() {
5090
5344
  return this.eruGridStore.hasSelection;
5091
5345
  }
5346
+ get selectedRows() {
5347
+ return this.eruGridStore.selectedRows;
5348
+ }
5349
+ get totalSelectedCount() {
5350
+ return this.eruGridStore.totalSelectedCount;
5351
+ }
5352
+ /** True once the user accepted "select all N matching" — act on the filter, not the ids. */
5353
+ get selectAllMatching() {
5354
+ return this.eruGridStore.selectAllMatching;
5355
+ }
5356
+ get totalMatchingCount() {
5357
+ return this.eruGridStore.totalMatchingCount;
5358
+ }
5359
+ clearSelection() {
5360
+ this.eruGridStore.clearSelection();
5361
+ }
5092
5362
  // Theme management
5093
5363
  get theme() {
5094
5364
  return this.themeService;
@@ -5563,21 +5833,25 @@ class CurrencyComponent {
5563
5833
  }
5564
5834
  onBlur() {
5565
5835
  const value = this.currentValue();
5566
- const numValue = Number(value);
5567
- const cfg = this.config();
5568
- const validation = this.validateField(numValue);
5836
+ // Number('') and Number(null) are both 0, so treating a blank as a number
5837
+ // would emit 0 out of every empty cell the user tabs through. Validation
5838
+ // still runs on NaN, so a mandatory blank is caught.
5839
+ const isBlank = value === null || value === undefined || value === '';
5840
+ const numValue = isBlank ? NaN : Number(value);
5841
+ // Blank goes to validation as-is: it is an empty cell, not the unparseable
5842
+ // number NaN would report it as.
5843
+ const validation = this.validateField(isBlank ? value : numValue);
5569
5844
  if (!validation.isValid && validation.error) {
5570
5845
  this.error.set(validation.error);
5571
5846
  this.validationError.emit(validation.error);
5847
+ return;
5572
5848
  }
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
- }
5849
+ this.error.set('');
5850
+ // `decimal` is DISPLAY precision — the view branch applies it through
5851
+ // formatNumberSignal. Rounding here put the rounded number back into
5852
+ // currentValue, which is the value the cell SAVES, so 10.5 in a 0-decimal
5853
+ // column became 11 for good after any edit.
5854
+ this.blur.emit(isBlank ? null : numValue);
5581
5855
  }
5582
5856
  onValueChange(event) {
5583
5857
  this.currentValue.set(event);
@@ -5706,21 +5980,25 @@ class NumberComponent {
5706
5980
  }
5707
5981
  onBlur() {
5708
5982
  const value = this.currentValue();
5709
- const numValue = Number(value);
5710
- const cfg = this.config();
5711
- const validation = this.validateField(numValue);
5983
+ // Number('') and Number(null) are both 0, so treating a blank as a number
5984
+ // would emit 0 out of every empty cell the user tabs through. Validation
5985
+ // still runs on NaN, so a mandatory blank is caught.
5986
+ const isBlank = value === null || value === undefined || value === '';
5987
+ const numValue = isBlank ? NaN : Number(value);
5988
+ // Blank goes to validation as-is: it is an empty cell, not the unparseable
5989
+ // number NaN would report it as.
5990
+ const validation = this.validateField(isBlank ? value : numValue);
5712
5991
  if (!validation.isValid && validation.error) {
5713
5992
  this.error.set(validation.error);
5714
5993
  this.validationError.emit(validation.error);
5994
+ return;
5715
5995
  }
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
- }
5996
+ this.error.set('');
5997
+ // `decimal` is DISPLAY precision — the view branch applies it through
5998
+ // formatNumberSignal. Rounding here put the rounded number back into
5999
+ // currentValue, which is the value the cell SAVES, so 10.5 in a 0-decimal
6000
+ // column became 11 for good after any edit.
6001
+ this.blur.emit(isBlank ? null : numValue);
5724
6002
  }
5725
6003
  onValueChange(event) {
5726
6004
  this.currentValue.set(event);
@@ -7101,14 +7379,22 @@ class DateComponent {
7101
7379
  }
7102
7380
  onDateChange(date) {
7103
7381
  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);
7382
+ // The canonical `yyyy-MM-dd`, NOT the column's display pattern. Emitting
7383
+ // the pattern stored '05-Sep-2026' as the value, so the stored shape
7384
+ // changed on every edit and re-reading it depended on the column still
7385
+ // carrying the pattern it was written under. eru-studio's date component
7386
+ // canonicalises the same way (storeValueFor -> toDateOnlyString).
7387
+ const stored = date ? toDateOnlyString(date) : '';
7388
+ this.valueChange.emit(stored);
7389
+ this.dateChange.emit(stored);
7107
7390
  }
7108
7391
  onDrillableClick(event) {
7109
7392
  if (this.isDrillable()) {
7110
7393
  event.stopPropagation();
7111
- this.drilldownClick.emit(this.getDisplayValue());
7394
+ // The stored value, not the formatted text — a consumer turns a drill
7395
+ // into a filter, and '05-Sep-2026' matches nothing server-side.
7396
+ const value = this.currentValue();
7397
+ this.drilldownClick.emit(value instanceof Date ? toDateOnlyString(value) : (value ?? ''));
7112
7398
  }
7113
7399
  }
7114
7400
  /** Render a date in the column's configured format, via the shared token
@@ -7327,12 +7613,15 @@ class DatetimeComponent {
7327
7613
  effect(() => {
7328
7614
  const value = this.value();
7329
7615
  if (typeof value === 'string' && value) {
7330
- // Parse datetime string (dd-MM-yyyy HH:mm format)
7331
- const parsed = this.parseDateTimeString(value);
7616
+ // Any stored shape: the column's own pattern, or an ISO timestamp
7617
+ // straight from the server (`2026-09-05T07:35:40.265175Z`), which the
7618
+ // old whitespace-splitting parser could not read at all — it returned
7619
+ // null and the raw string was rendered verbatim.
7620
+ const parsed = parseCellDateTime(value, this.dtFormat());
7332
7621
  if (parsed) {
7333
- this.selectedDate.set(parsed.date);
7334
- this.selectedTime.set(parsed.time);
7335
- this.currentValue.set(parsed.date);
7622
+ this.selectedDate.set(parsed);
7623
+ this.selectedTime.set(this.formatTime(parsed));
7624
+ this.currentValue.set(parsed);
7336
7625
  }
7337
7626
  else {
7338
7627
  this.currentValue.set(value);
@@ -7405,29 +7694,26 @@ class DatetimeComponent {
7405
7694
  this.drilldownClick.emit(value);
7406
7695
  }
7407
7696
  }
7408
- // Column-configured format, e.g. 'dd-mm-yyyy hh:mm:ss'. `date_format` is the
7697
+ // Column-configured format, e.g. 'dd-MMM-yyyy hh:mm:ss'. `date_format` is the
7409
7698
  // 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.
7699
+ // `dateTimeFormat` is the legacy alias. Normalised rather than lowercased —
7700
+ // the model uppercases on save, and lowercasing collapsed `MMM` into `mmm`,
7701
+ // erasing the month-name/minute distinction the token casing carries.
7412
7702
  dtFormat = computed(() => {
7413
7703
  const cfg = this.config();
7414
- return String(cfg?.date_format || cfg?.dateTimeFormat || 'dd-mm-yyyy hh:mm:ss').toLowerCase();
7704
+ return normalizeDateTimeFormat(cfg?.date_format || cfg?.dateTimeFormat) || 'dd-MM-yyyy hh:mm:ss';
7415
7705
  }, ...(ngDevMode ? [{ debugName: "dtFormat" }] : []));
7706
+ /**
7707
+ * The canonical stored form of an edit: `yyyy-MM-dd HH:mm:ss`.
7708
+ *
7709
+ * It used to be built from the DISPLAY pattern, which made the stored shape
7710
+ * depend on how the column happened to be formatted and dropped the seconds
7711
+ * whenever the pattern had no `ss` — so editing a server timestamp under a
7712
+ * `hh:mm` pattern silently truncated it. Display goes through
7713
+ * formatDateTimeWithPattern; storage is always this one shape.
7714
+ */
7416
7715
  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}`;
7716
+ return toDateTimeString(date);
7431
7717
  }
7432
7718
  formatTime(date) {
7433
7719
  const hours = date.getHours();
@@ -7435,49 +7721,20 @@ class DatetimeComponent {
7435
7721
  const pad = (n) => n < 10 ? '0' + n : n.toString();
7436
7722
  return `${pad(hours)}:${pad(minutes)}`;
7437
7723
  }
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
- }
7724
+ /**
7725
+ * Seconds are carried through when the time string has them. The picker only
7726
+ * offers HH:mm, so an untouched value keeps the seconds it arrived with
7727
+ * rather than being silently zeroed on a date-only change.
7728
+ */
7473
7729
  combineDateAndTime(date, time) {
7474
7730
  const timeParts = time.split(':');
7475
7731
  const hours = parseInt(timeParts[0], 10) || 0;
7476
7732
  const minutes = parseInt(timeParts[1], 10) || 0;
7733
+ const seconds = timeParts.length > 2 ? (parseInt(timeParts[2], 10) || 0) : this.selectedDate()?.getSeconds() ?? 0;
7477
7734
  const combined = new Date(date);
7478
7735
  combined.setHours(hours);
7479
7736
  combined.setMinutes(minutes);
7480
- combined.setSeconds(0);
7737
+ combined.setSeconds(seconds);
7481
7738
  combined.setMilliseconds(0);
7482
7739
  return combined;
7483
7740
  }
@@ -7488,10 +7745,14 @@ class DatetimeComponent {
7488
7745
  const val = this.currentValue();
7489
7746
  if (!val)
7490
7747
  return '';
7491
- if (typeof val === 'string')
7492
- return val;
7493
- if (val instanceof Date) {
7494
- return this.formatDateTime(val);
7748
+ const pattern = this.dtFormat();
7749
+ if (val instanceof Date)
7750
+ return formatDateTimeWithPattern(val, pattern);
7751
+ // A string only reaches here when it could not be parsed; show it as-is
7752
+ // rather than an empty cell, so a bad stored value stays visible.
7753
+ if (typeof val === 'string') {
7754
+ const parsed = parseCellDateTime(val, pattern);
7755
+ return parsed ? formatDateTimeWithPattern(parsed, pattern) : val;
7495
7756
  }
7496
7757
  return '';
7497
7758
  }
@@ -7589,22 +7850,44 @@ class DurationComponent {
7589
7850
  if (this.childOverlayOpen()) {
7590
7851
  return;
7591
7852
  }
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);
7853
+ this.commitAndClose();
7597
7854
  }
7598
7855
  onOverlayClick(event) {
7599
7856
  event.stopPropagation();
7600
7857
  }
7601
7858
  onBlur() {
7859
+ this.commitAndClose();
7860
+ }
7861
+ /**
7862
+ * Close the editor, emitting only when the duration actually changed.
7863
+ *
7864
+ * It used to emit unconditionally, and since the editor works in minutes that
7865
+ * rewrote the stored representation on a no-op: a cell holding '02:30' became
7866
+ * 150 merely by opening the overlay and dismissing it. Opening a cell is not
7867
+ * an edit.
7868
+ */
7869
+ commitAndClose() {
7602
7870
  const value = this.getCurrentValueInMinutes();
7603
- this.currentValue.set(value);
7604
- this.blur.emit(value);
7871
+ if (value !== this.storedMinutes()) {
7872
+ this.currentValue.set(value);
7873
+ this.blur.emit(value);
7874
+ }
7605
7875
  this.isOverlayOpen.set(false);
7606
7876
  this.editModeChange.emit(false);
7607
7877
  }
7878
+ /** The incoming value in minutes, for comparison against an edit. */
7879
+ storedMinutes() {
7880
+ const value = this.value();
7881
+ if (value === null || value === undefined || value === '')
7882
+ return null;
7883
+ if (typeof value === 'number')
7884
+ return value;
7885
+ const text = String(value);
7886
+ if (this.isTimeFormat(text))
7887
+ return this.timeToMinutes(text);
7888
+ const num = parseInt(text, 10);
7889
+ return isNaN(num) ? null : num;
7890
+ }
7608
7891
  onDrillableClick(event) {
7609
7892
  if (this.isDrillable()) {
7610
7893
  event.stopPropagation();
@@ -8097,7 +8380,29 @@ class ProgressComponent {
8097
8380
  editModeChange = new EventEmitter();
8098
8381
  // Internal state
8099
8382
  currentValue = signal(0, ...(ngDevMode ? [{ debugName: "currentValue" }] : []));
8100
- // Computed value that always returns a number (defaults to 0)
8383
+ /** Configured scale, defaulting to the 0-100 percentage scale. */
8384
+ scaleMin = computed(() => {
8385
+ const v = Number(this.config()?.start_value);
8386
+ return Number.isFinite(v) ? v : 0;
8387
+ }, ...(ngDevMode ? [{ debugName: "scaleMin" }] : []));
8388
+ scaleMax = computed(() => {
8389
+ const v = Number(this.config()?.end_value);
8390
+ return Number.isFinite(v) && v > this.scaleMin() ? v : 100;
8391
+ }, ...(ngDevMode ? [{ debugName: "scaleMax" }] : []));
8392
+ /**
8393
+ * The value the slider edits — the raw stored number, only coerced when it is
8394
+ * not a number at all.
8395
+ *
8396
+ * Deliberately NOT displayValue(): that clamps to the scale for the bar, and
8397
+ * seeding the control with it meant a stored 150 opened the slider at 100 and
8398
+ * the first drag wrote the clamp back as the real value.
8399
+ */
8400
+ editValue = computed(() => {
8401
+ const val = this.currentValue();
8402
+ const num = typeof val === 'number' ? val : Number(val);
8403
+ return Number.isFinite(num) ? num : this.scaleMin();
8404
+ }, ...(ngDevMode ? [{ debugName: "editValue" }] : []));
8405
+ // Bar geometry only — clamped to the scale so the fill never overflows.
8101
8406
  displayValue = computed(() => {
8102
8407
  const val = this.currentValue();
8103
8408
  // Handle null, undefined, or invalid numbers - default to 0
@@ -8107,12 +8412,23 @@ class ProgressComponent {
8107
8412
  // Convert to number if needed
8108
8413
  const numVal = typeof val === 'number' ? val : Number(val);
8109
8414
  // Handle NaN or invalid numbers
8110
- if (isNaN(numVal) || numVal < 0) {
8111
- return 0;
8415
+ if (isNaN(numVal)) {
8416
+ return this.scaleMin();
8112
8417
  }
8113
- // Clamp between 0 and 100
8114
- return Math.max(0, Math.min(100, numVal));
8418
+ // Clamp to the configured scale
8419
+ return Math.max(this.scaleMin(), Math.min(this.scaleMax(), numVal));
8115
8420
  }, ...(ngDevMode ? [{ debugName: "displayValue" }] : []));
8421
+ /**
8422
+ * Bar fill as a percentage of the configured scale. The bar's width is always
8423
+ * 0-100% of its track, whatever units the column counts in — a 3 on a 0-5
8424
+ * scale fills 60%, not 3%.
8425
+ */
8426
+ fillPercent = computed(() => {
8427
+ const span = this.scaleMax() - this.scaleMin();
8428
+ if (span <= 0)
8429
+ return 0;
8430
+ return ((this.displayValue() - this.scaleMin()) / span) * 100;
8431
+ }, ...(ngDevMode ? [{ debugName: "fillPercent" }] : []));
8116
8432
  // Bar colour from the column's configured value ranges (e.g. 0-20 green).
8117
8433
  // Applies in both editable and view/disabled modes. Null when no range
8118
8434
  // matches, so the bar falls back to the default theme colour.
@@ -8141,24 +8457,18 @@ class ProgressComponent {
8141
8457
  // Sync signal-based input with internal state
8142
8458
  effect(() => {
8143
8459
  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
- }
8460
+ // Only a non-number is coerced. A stored value outside the scale — a 150
8461
+ // on a 0-100 column, or a negative is kept as it is and merely drawn
8462
+ // clamped, so opening the cell cannot quietly replace it.
8463
+ this.currentValue.set(value === null || value === undefined || isNaN(value) ? this.scaleMin() : value);
8151
8464
  });
8152
8465
  }
8153
8466
  ngOnInit() {
8154
8467
  // Initialize value if provided, otherwise default to 0
8155
8468
  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
- }
8469
+ this.currentValue.set(initialValue === null || initialValue === undefined || isNaN(initialValue)
8470
+ ? this.scaleMin()
8471
+ : initialValue);
8162
8472
  }
8163
8473
  onActivate() {
8164
8474
  if (this.isEditable()) {
@@ -8187,11 +8497,11 @@ class ProgressComponent {
8187
8497
  return cfg[property];
8188
8498
  }
8189
8499
  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 });
8500
+ 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
8501
  }
8192
8502
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: ProgressComponent, decorators: [{
8193
8503
  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"] }]
8504
+ 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
8505
  }], 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
8506
  type: Output
8197
8507
  }], blur: [{
@@ -10453,6 +10763,8 @@ class AttachmentComponent {
10453
10763
  drilldownClick = new EventEmitter();
10454
10764
  editModeChange = new EventEmitter();
10455
10765
  fileAdded = new EventEmitter();
10766
+ /** The file's contents and where it is meant to go, for a host that uploads. */
10767
+ fileUpload = new EventEmitter();
10456
10768
  fileDeleted = new EventEmitter();
10457
10769
  fileViewed = new EventEmitter();
10458
10770
  // Internal state
@@ -10664,8 +10976,14 @@ class AttachmentComponent {
10664
10976
  reader.readAsDataURL(file);
10665
10977
  }
10666
10978
  uploadFile(base64String, fileName) {
10667
- // This can be extended to call an upload service
10668
- // For now, just emit the file data
10979
+ const cfg = this.config();
10980
+ this.fileUpload.emit({
10981
+ fileName,
10982
+ file: base64String,
10983
+ storage_name: cfg.storageName ?? '',
10984
+ folder_name: cfg.folderName ?? '',
10985
+ default_upload: cfg.defaultUpload !== false,
10986
+ });
10669
10987
  }
10670
10988
  viewFile(file, event) {
10671
10989
  if (event) {
@@ -10722,7 +11040,7 @@ class AttachmentComponent {
10722
11040
  return file;
10723
11041
  }
10724
11042
  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 });
11043
+ 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
11044
  }
10727
11045
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AttachmentComponent, decorators: [{
10728
11046
  type: Component,
@@ -10746,6 +11064,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
10746
11064
  type: Output
10747
11065
  }], fileAdded: [{
10748
11066
  type: Output
11067
+ }], fileUpload: [{
11068
+ type: Output
10749
11069
  }], fileDeleted: [{
10750
11070
  type: Output
10751
11071
  }], fileViewed: [{
@@ -10773,6 +11093,11 @@ class CompositeComponent {
10773
11093
  drilldownClick = output();
10774
11094
  inline = computed(() => this.column()?.composite_direction === 'inline', ...(ngDevMode ? [{ debugName: "inline" }] : []));
10775
11095
  separator = computed(() => this.column()?.composite_separator ?? '', ...(ngDevMode ? [{ debugName: "separator" }] : []));
11096
+ /** Which side each part's label sits on, or 'none' for bare values. */
11097
+ labelPosition = computed(() => {
11098
+ const p = this.column()?.composite_label_position;
11099
+ return p === 'left' || p === 'right' ? p : 'none';
11100
+ }, ...(ngDevMode ? [{ debugName: "labelPosition" }] : []));
10776
11101
  primaryCss = computed(() => cellTextStyleToCss(this.column()?.cell_style), ...(ngDevMode ? [{ debugName: "primaryCss" }] : []));
10777
11102
  secondaryCss = computed(() => cellTextStyleToCss(this.column()?.cell_style_secondary ?? DEFAULT_SECONDARY_STYLE), ...(ngDevMode ? [{ debugName: "secondaryCss" }] : []));
10778
11103
  /**
@@ -10787,11 +11112,17 @@ class CompositeComponent {
10787
11112
  const names = (column.composite_fields || []).filter(Boolean);
10788
11113
  const byName = new Map((this.columns() || []).map(field => [field.name, field]));
10789
11114
  const row = this.row();
10790
- const parts = names.map(name => {
11115
+ const labels = column.composite_labels || [];
11116
+ const parts = names.map((name, index) => {
10791
11117
  const field = byName.get(name);
11118
+ // Blank means no label for THIS part, never the field's own label as a
11119
+ // fallback: a composite is usually built from columns whose labels are
11120
+ // what the header already says, so falling back printed "Upload Date"
11121
+ // in front of the upload date. Labelling one part and not another is a
11122
+ // normal thing to want.
10792
11123
  return {
10793
11124
  name,
10794
- label: field?.label || name,
11125
+ label: String(labels[index] ?? '').trim(),
10795
11126
  text: formatCellValue(readRowValue(row, name), field),
10796
11127
  };
10797
11128
  });
@@ -10808,11 +11139,11 @@ class CompositeComponent {
10808
11139
  this.drilldownClick.emit(part.text);
10809
11140
  }
10810
11141
  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 });
11142
+ 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
11143
  }
10813
11144
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: CompositeComponent, decorators: [{
10814
11145
  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"] }]
11146
+ 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
11147
  }], 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
11148
  const DEFAULT_SECONDARY_STYLE = {
10818
11149
  font_size: 11,
@@ -10884,8 +11215,14 @@ class DataCellComponent {
10884
11215
  * applies them per part — setting the primary here as well would leak its
10885
11216
  * weight onto the secondary value, which only overrides size and colour.
10886
11217
  */
11218
+ /**
11219
+ * Which renderer draws this cell. Read off the column rather than taken as an
11220
+ * input so every existing `<data-cell>` call site keeps working — they all
11221
+ * already pass `column`.
11222
+ */
11223
+ cellRender = computed(() => normalizeCellRender(this.column()), ...(ngDevMode ? [{ debugName: "cellRender" }] : []));
10887
11224
  containerStyle = computed(() => {
10888
- if (this.columnDatatype() === 'composite')
11225
+ if (this.cellRender() === 'composite')
10889
11226
  return {};
10890
11227
  const base = cellTextStyleToCss(this.column()?.cell_style);
10891
11228
  return { ...base, ...this.ruleStyle() };
@@ -11365,9 +11702,29 @@ class DataCellComponent {
11365
11702
  maxFiles: config?.max_files,
11366
11703
  allowedFileTypes: config?.allowed_file_types,
11367
11704
  maxFileSize: config?.max_file_size,
11368
- columnWidth: config?.column_width || this.currentColumnWidth()
11705
+ columnWidth: config?.column_width || this.currentColumnWidth(),
11706
+ storageName: config?.storage_name || '',
11707
+ folderName: this.resolvedFolderName(config?.folder_name),
11708
+ defaultUpload: config?.default_upload !== false
11369
11709
  };
11370
11710
  }, ...(ngDevMode ? [{ debugName: "getAttachmentConfig" }] : []));
11711
+ /**
11712
+ * `folder_name` on the column names a SIBLING field; the folder is that
11713
+ * field's value on THIS row, so an attachment column files each record under
11714
+ * its own key. Resolved here rather than in the attachment component so the
11715
+ * component never has to know how a row nests its values.
11716
+ */
11717
+ resolvedFolderName(fieldName) {
11718
+ const name = String(fieldName || '').trim();
11719
+ if (!name)
11720
+ return '';
11721
+ const r = this.row();
11722
+ const fromEntityData = r?.entity_data && typeof r.entity_data === 'object'
11723
+ ? r.entity_data[name]
11724
+ : undefined;
11725
+ const raw = fromEntityData !== undefined ? fromEntityData : r?.[name];
11726
+ return raw === undefined || raw === null ? '' : String(raw).trim();
11727
+ }
11371
11728
  onAttachmentBlur(value) {
11372
11729
  this.currentValue.set(value);
11373
11730
  this.eruGridStore().setActiveCell(null);
@@ -11617,15 +11974,19 @@ class DataCellComponent {
11617
11974
  onNumberBlur() {
11618
11975
  const value = this.currentValue();
11619
11976
  // 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.
11977
+ // coercing unconditionally would treat every blank numeric cell the user
11978
+ // tabs through as 0. Validation still runs, so a mandatory blank is caught.
11622
11979
  const isBlank = value === null || value === undefined || value === '';
11623
11980
  const numValue = isBlank ? NaN : Number(value);
11624
11981
  this.validateField(numValue);
11982
+ // The value is stored as the NUMBER the user typed. `decimal` is display
11983
+ // precision and belongs to the view branch alone: rounding here wrote the
11984
+ // rounded value into currentValue — which is what deactivation persists —
11985
+ // so 10.5 in a 0-decimal column was saved as 11 and the real value was
11986
+ // gone. It also flipped the stored type to string, since toFixed returns
11987
+ // one.
11625
11988
  if (!isBlank && !isNaN(numValue) && !this.error()) {
11626
- const config = this.columnCellConfiguration();
11627
- const decimalPlaces = config?.decimal || 0;
11628
- this.currentValue.set(numValue.toFixed(decimalPlaces));
11989
+ this.currentValue.set(numValue);
11629
11990
  }
11630
11991
  }
11631
11992
  getPriorityIcon = computed(() => {
@@ -12370,7 +12731,7 @@ class DataCellComponent {
12370
12731
  useValue: cellValidators
12371
12732
  },
12372
12733
  ...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 });
12734
+ ], 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
12735
  }
12375
12736
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: DataCellComponent, decorators: [{
12376
12737
  type: Component,
@@ -12419,7 +12780,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
12419
12780
  ...MATERIAL_PROVIDERS
12420
12781
  ], host: {
12421
12782
  '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"] }]
12783
+ }, 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
12784
  }], ctorParameters: () => [], propDecorators: { attachmentTrigger: [{
12424
12785
  type: ViewChild,
12425
12786
  args: ['attachmentTrigger']
@@ -12697,7 +13058,15 @@ class ColumnDragDirective {
12697
13058
  el;
12698
13059
  renderer;
12699
13060
  eruGridStore;
12700
- columnIndex = null;
13061
+ /**
13062
+ * The dragged column's `name`, not its rendered index. The header only ever
13063
+ * renders the *visible* columns (hidden ones and the group-by column are
13064
+ * filtered out of `displayColumns`), so a rendered index does not line up
13065
+ * with the store's full column array — reordering by index moved whichever
13066
+ * column happened to sit at that slot in the unfiltered list. The name is
13067
+ * stable across every filtering layer.
13068
+ */
13069
+ columnKey = null;
12701
13070
  dragEl;
12702
13071
  dragHandle;
12703
13072
  isDragging = false;
@@ -12715,7 +13084,7 @@ class ColumnDragDirective {
12715
13084
  // Find the drag handle element - use setTimeout to ensure it's rendered
12716
13085
  setTimeout(() => {
12717
13086
  this.dragHandle = this.dragEl.querySelector('.column-drag-handle');
12718
- if (this.columnIndex !== null && this.columnIndex !== undefined) {
13087
+ if (this.columnKey !== null && this.columnKey !== undefined) {
12719
13088
  // Set draggable attribute on the entire column header
12720
13089
  this.renderer.setAttribute(this.dragEl, 'draggable', 'true');
12721
13090
  if (this.dragHandle) {
@@ -12740,8 +13109,8 @@ class ColumnDragDirective {
12740
13109
  };
12741
13110
  document.addEventListener('mouseup', this.mouseUpListener);
12742
13111
  }
12743
- else if (this.columnIndex === null || this.columnIndex === undefined) {
12744
- // If columnIndex is null, remove draggable attribute
13112
+ else if (this.columnKey === null || this.columnKey === undefined) {
13113
+ // If columnKey is null, remove draggable attribute
12745
13114
  this.renderer.removeAttribute(this.dragEl, 'draggable');
12746
13115
  }
12747
13116
  }, 0);
@@ -12756,7 +13125,7 @@ class ColumnDragDirective {
12756
13125
  }
12757
13126
  }
12758
13127
  onDragStart(event) {
12759
- // Only allow drag if it was initiated by the handle OR if we have a valid columnIndex
13128
+ // Only allow drag if it was initiated by the handle OR if we have a valid columnKey
12760
13129
  // Check if event originated from drag handle
12761
13130
  const target = event.target;
12762
13131
  const isFromHandle = target.classList.contains('column-drag-handle') ||
@@ -12766,9 +13135,9 @@ class ColumnDragDirective {
12766
13135
  event.preventDefault();
12767
13136
  return;
12768
13137
  }
12769
- // Validate column index
12770
- if (this.columnIndex === null || this.columnIndex === undefined) {
12771
- console.error('Invalid column index for drag');
13138
+ // Validate column key
13139
+ if (this.columnKey === null || this.columnKey === undefined) {
13140
+ console.error('Invalid column key for drag');
12772
13141
  event.preventDefault();
12773
13142
  return;
12774
13143
  }
@@ -12782,7 +13151,7 @@ class ColumnDragDirective {
12782
13151
  // Allow moving
12783
13152
  event.dataTransfer.effectAllowed = 'move';
12784
13153
  // Set drag data
12785
- event.dataTransfer.setData('text/plain', this.columnIndex.toString());
13154
+ event.dataTransfer.setData('text/plain', this.columnKey);
12786
13155
  // Set drag image to the entire column header
12787
13156
  event.dataTransfer.setDragImage(this.dragEl, 0, 0);
12788
13157
  }
@@ -12808,26 +13177,18 @@ class ColumnDragDirective {
12808
13177
  }
12809
13178
  }
12810
13179
  onDrop(event) {
12811
- // Validate column index
12812
- if (this.columnIndex === null || this.columnIndex === undefined) {
12813
- console.error('Invalid column index for drop');
13180
+ // Validate column key
13181
+ if (this.columnKey === null || this.columnKey === undefined) {
13182
+ console.error('Invalid column key for drop');
12814
13183
  event.preventDefault();
12815
13184
  return;
12816
13185
  }
12817
13186
  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;
13187
+ const fromKey = event.dataTransfer?.getData('text/plain') || '';
13188
+ const toKey = this.columnKey;
12828
13189
  // Reorder columns if different
12829
- if (fromIndex !== toIndex) {
12830
- this.eruGridStore.reorderColumns(fromIndex, toIndex);
13190
+ if (fromKey && fromKey !== toKey) {
13191
+ this.eruGridStore.reorderColumnsByName(fromKey, toKey);
12831
13192
  }
12832
13193
  }
12833
13194
  onDragEnd(event) {
@@ -12836,7 +13197,7 @@ class ColumnDragDirective {
12836
13197
  this.isDragging = false;
12837
13198
  }
12838
13199
  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 });
13200
+ 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
13201
  }
12841
13202
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: ColumnDragDirective, decorators: [{
12842
13203
  type: Directive,
@@ -12844,7 +13205,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
12844
13205
  selector: '[columnDraggable]',
12845
13206
  standalone: true
12846
13207
  }]
12847
- }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: EruGridStore }], propDecorators: { columnIndex: [{
13208
+ }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: EruGridStore }], propDecorators: { columnKey: [{
12848
13209
  type: Input,
12849
13210
  args: ['columnDraggable']
12850
13211
  }], onDragStart: [{
@@ -12870,6 +13231,17 @@ const UNIVERSAL_FIELDS = [
12870
13231
  control: 'select',
12871
13232
  options: DATA_TYPES.map(d => ({ value: d, label: d })),
12872
13233
  },
13234
+ {
13235
+ key: 'cell_render',
13236
+ label: 'Cell rendering',
13237
+ control: 'select',
13238
+ defaultValue: 'default',
13239
+ options: [
13240
+ { value: 'default', label: 'By data type' },
13241
+ { value: 'composite', label: 'Composite (several fields in one cell)' },
13242
+ { value: 'page', label: 'Page layout' },
13243
+ ],
13244
+ },
12873
13245
  { key: 'tool_tip', label: 'Tooltip', control: 'text' },
12874
13246
  { key: 'description', label: 'Description', control: 'text' },
12875
13247
  { key: 'default', label: 'Default value', control: 'text' },
@@ -12911,6 +13283,38 @@ const COMPOSITE_FIELDS = [
12911
13283
  control: 'text',
12912
13284
  showWhen: f => f.composite_direction === 'inline',
12913
13285
  },
13286
+ {
13287
+ key: 'composite_label_position',
13288
+ label: 'Part labels',
13289
+ control: 'select',
13290
+ defaultValue: 'none',
13291
+ options: [
13292
+ { value: 'none', label: 'Hidden' },
13293
+ { value: 'left', label: 'Before the value' },
13294
+ { value: 'right', label: 'After the value' },
13295
+ ],
13296
+ },
13297
+ {
13298
+ key: 'composite_labels',
13299
+ label: 'Primary label',
13300
+ control: 'text_slot',
13301
+ slot: 0,
13302
+ showWhen: f => f.composite_label_position === 'left' || f.composite_label_position === 'right',
13303
+ },
13304
+ {
13305
+ key: 'composite_labels',
13306
+ label: 'Secondary label',
13307
+ control: 'text_slot',
13308
+ slot: 1,
13309
+ showWhen: f => f.composite_label_position === 'left' || f.composite_label_position === 'right',
13310
+ },
13311
+ {
13312
+ key: 'composite_labels',
13313
+ label: 'Third label',
13314
+ control: 'text_slot',
13315
+ slot: 2,
13316
+ showWhen: f => f.composite_label_position === 'left' || f.composite_label_position === 'right',
13317
+ },
12914
13318
  { key: 'cell_style_secondary', label: 'Secondary value style', control: 'text_style' },
12915
13319
  ];
12916
13320
  const PAGE_CELL_FIELDS = [
@@ -13117,6 +13521,9 @@ const DATATYPE_FIELDS = {
13117
13521
  { key: 'max_files', label: 'Max files', control: 'number' },
13118
13522
  { key: 'allowed_file_types', label: 'Allowed types (one per line)', control: 'list' },
13119
13523
  { key: 'max_file_size', label: 'Max file size (bytes)', control: 'number' },
13524
+ { key: 'default_upload', label: 'Default upload', control: 'checkbox', defaultValue: true },
13525
+ { key: 'storage_name', label: 'Storage name', control: 'text', showWhen: f => f.default_upload !== false },
13526
+ { key: 'folder_name', label: 'Folder from field', control: 'text', showWhen: f => f.default_upload !== false },
13120
13527
  ],
13121
13528
  phone: [{ key: 'default_country', label: 'Default country (ISO)', control: 'text' }],
13122
13529
  people: [
@@ -13138,8 +13545,6 @@ const DATATYPE_FIELDS = {
13138
13545
  ],
13139
13546
  textbox: TEXTBOX_FIELDS,
13140
13547
  textarea: TEXTBOX_FIELDS,
13141
- composite: COMPOSITE_FIELDS,
13142
- page: PAGE_CELL_FIELDS,
13143
13548
  checkbox: [
13144
13549
  { key: 'value_true', label: 'Stored value when checked', control: 'text' },
13145
13550
  { key: 'value_false', label: 'Stored value when unchecked', control: 'text' },
@@ -13154,25 +13559,36 @@ const DATATYPE_FIELDS = {
13154
13559
  * Datatype choices offered for a column.
13155
13560
  *
13156
13561
  * 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.
13562
+ * offer changes that `buildMappedColumnPatch` overwrites on the next resolve
13563
+ * it is offered its own type and nothing else. Composite and page used to be
13564
+ * the exception here; they are `cell_render` now, a separate axis that a mapped
13565
+ * column is free to set without touching the type it inherits.
13161
13566
  */
13162
13567
  function datatypeOptions(field) {
13163
13568
  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;
13569
+ const types = isMapped ? DATA_TYPES.filter(d => d === field.datatype) : DATA_TYPES;
13167
13570
  return types.map(d => ({ value: d, label: d }));
13168
13571
  }
13572
+ /**
13573
+ * Controls for the chosen renderer, shown alongside — not instead of — the
13574
+ * datatype's own. That is the point of the split: a composite column is still a
13575
+ * currency or a date column underneath, and its symbol, decimals and format stay
13576
+ * editable while the composite is on, and keep their values when it is off.
13577
+ */
13578
+ function cellRenderFields(field) {
13579
+ switch (normalizeCellRender(field)) {
13580
+ case 'composite': return COMPOSITE_FIELDS;
13581
+ case 'page': return PAGE_CELL_FIELDS;
13582
+ default: return [];
13583
+ }
13584
+ }
13169
13585
  function getMetaFields(field) {
13170
13586
  if (!field)
13171
13587
  return [];
13172
13588
  const extra = field.datatype ? DATATYPE_FIELDS[field.datatype] ?? [] : [];
13173
13589
  const excluded = new Set(field.datatype ? GOVERNANCE_EXCLUSIONS[field.datatype] ?? [] : []);
13174
13590
  const governance = GOVERNANCE_FIELDS.filter(def => !excluded.has(def.key));
13175
- return [...UNIVERSAL_FIELDS, ...extra, ...governance]
13591
+ return [...UNIVERSAL_FIELDS, ...cellRenderFields(field), ...extra, ...governance]
13176
13592
  .filter(def => !def.showWhen || def.showWhen(field))
13177
13593
  .map(def => (def.key === 'datatype' ? { ...def, options: datatypeOptions(field) } : def));
13178
13594
  }
@@ -13557,9 +13973,14 @@ class ColumnDesignPanelComponent {
13557
13973
  return arr[def.slot ?? 0] ?? null;
13558
13974
  }
13559
13975
  /**
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.
13976
+ * Writes one position of an ordered list. Trailing empties are dropped so
13977
+ * clearing the third slot leaves a two-part composite rather than a list with
13978
+ * a hole in it, which `parts` would otherwise have to filter.
13979
+ *
13980
+ * Interior holes are only collapsed for a `field_slot`. A `text_slot` list is
13981
+ * read positionally against another one — composite_labels against
13982
+ * composite_fields — so compacting it would slide every later label onto the
13983
+ * wrong part the moment an earlier one was left blank.
13563
13984
  */
13564
13985
  onSlotChange(def, value) {
13565
13986
  const slot = def.slot ?? 0;
@@ -13570,7 +13991,7 @@ class ColumnDesignPanelComponent {
13570
13991
  arr[slot] = value || null;
13571
13992
  while (arr.length > 0 && !arr[arr.length - 1])
13572
13993
  arr.pop();
13573
- this.patch(def.key, arr.filter(Boolean));
13994
+ this.patch(def.key, def.control === 'text_slot' ? arr.map(v => v ?? '') : arr.filter(Boolean));
13574
13995
  }
13575
13996
  // ── Text-style control ─────────────────────────────────────────────────
13576
13997
  styleValue(key, prop) {
@@ -13609,8 +14030,37 @@ class ColumnDesignPanelComponent {
13609
14030
  this.patch(key, value === '' || value === null ? null : Number(value));
13610
14031
  }
13611
14032
  onSelect(key, value) {
14033
+ if (key === 'datatype') {
14034
+ this.changeDatatype(value);
14035
+ return;
14036
+ }
13612
14037
  this.patch(key, value);
13613
14038
  }
14039
+ /**
14040
+ * Changing a column's datatype, with the presentation types handled specially.
14041
+ *
14042
+ * `composite` and `page` say how the column is DRAWN, not what it holds, so
14043
+ * switching to one must remember the real type — otherwise the value has
14044
+ * nothing to format itself by, which is why a composed date column reverted
14045
+ * to its raw stored string. Switching back to a real type clears the memo.
14046
+ */
14047
+ changeDatatype(value) {
14048
+ const current = this.field();
14049
+ const next = String(value ?? '');
14050
+ const wasPresentation = PRESENTATION_DATATYPES.has(String(current?.datatype ?? ''));
14051
+ const isPresentation = PRESENTATION_DATATYPES.has(next);
14052
+ const name = this.gridStore.selectedDesignColumn();
14053
+ if (!name)
14054
+ return;
14055
+ const patch = { datatype: next };
14056
+ if (isPresentation && !wasPresentation) {
14057
+ patch.presentation_base_datatype = current?.datatype;
14058
+ }
14059
+ else if (!isPresentation) {
14060
+ patch.presentation_base_datatype = null;
14061
+ }
14062
+ this.gridStore.updateColumnMeta(name, patch);
14063
+ }
13614
14064
  onCheckbox(key, checked) {
13615
14065
  this.patch(key, checked);
13616
14066
  }
@@ -13685,7 +14135,7 @@ class ColumnDesignPanelComponent {
13685
14135
  this.gridStore.selectDesignColumn(null);
13686
14136
  }
13687
14137
  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 });
14138
+ 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
14139
  }
13690
14140
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: ColumnDesignPanelComponent, decorators: [{
13691
14141
  type: Component,
@@ -13698,7 +14148,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
13698
14148
  MatCheckboxModule,
13699
14149
  MatIconModule,
13700
14150
  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"] }]
14151
+ ], 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
14152
  }], ctorParameters: () => [] });
13703
14153
 
13704
14154
  /**
@@ -13878,6 +14328,11 @@ class EruGridComponent {
13878
14328
  elementRef = inject(ElementRef);
13879
14329
  breakpointObserver = inject(BreakpointObserver);
13880
14330
  isSmViewport = signal(false, ...(ngDevMode ? [{ debugName: "isSmViewport" }] : []));
14331
+ /** Set once the host ResizeObserver has reported a usable width, after which
14332
+ * the window media query stops feeding isSmViewport. */
14333
+ hasMeasuredOwnWidth = false;
14334
+ /** Below `md`, matching the media query this replaced. */
14335
+ static SM_VIEWPORT_MAX_WIDTH = 768;
13881
14336
  containerHeight = signal(400, ...(ngDevMode ? [{ debugName: "containerHeight" }] : [])); // Internal tracking for available height
13882
14337
  resizeObserver = null;
13883
14338
  boardViewportObserver = null;
@@ -13995,6 +14450,15 @@ class EruGridComponent {
13995
14450
  */
13996
14451
  onExcelDownloadClick(event) {
13997
14452
  event.stopPropagation();
14453
+ this.requestExcelDownload();
14454
+ }
14455
+ /**
14456
+ * Enqueue an excel-download request without a click. This is the same path
14457
+ * the icon takes, exposed so a host can trigger the export from its own
14458
+ * button — useful when `config.excel_download_icon` is off because the
14459
+ * built-in icon sits in the wrong place for the page.
14460
+ */
14461
+ requestExcelDownload() {
13998
14462
  const timestamp = Date.now();
13999
14463
  this.gridStore.addExcelDownloadRequest({
14000
14464
  id: timestamp,
@@ -14131,6 +14595,24 @@ class EruGridComponent {
14131
14595
  groupedRows = computed(() => this.createGroupedRows(), ...(ngDevMode ? [{ debugName: "groupedRows" }] : []));
14132
14596
  columns = computed(() => this.gridStore.displayColumns(), ...(ngDevMode ? [{ debugName: "columns" }] : []));
14133
14597
  selectedRowIds = this.gridStore.selectedRowIds;
14598
+ selectAllMatching = this.gridStore.selectAllMatching;
14599
+ totalMatchingCount = this.gridStore.totalMatchingCount;
14600
+ /**
14601
+ * Show the select-all banner only where it has something to say: either the
14602
+ * user already accepted "all N matching" and needs it reported back, or they
14603
+ * selected everything loaded and there is more behind it to offer. Ticking a
14604
+ * few rows deliberately is not an occasion to offer the whole result set.
14605
+ */
14606
+ showSelectAllBanner = computed(() => {
14607
+ if (this.gridStore.configuration().config?.allowSelection !== true)
14608
+ return false;
14609
+ if (!this.gridStore.hasSelection())
14610
+ return false;
14611
+ if (this.gridStore.selectAllMatching())
14612
+ return true;
14613
+ return this.isAllGroupsSelected() &&
14614
+ this.gridStore.totalMatchingCount() > this.gridStore.totalSelectedCount();
14615
+ }, ...(ngDevMode ? [{ debugName: "showSelectAllBanner" }] : []));
14134
14616
  totalRowCount = computed(() => {
14135
14617
  const groups = this.groups();
14136
14618
  return groups.reduce((sum, group) => sum + group.totalRowCount, 0);
@@ -14261,10 +14743,13 @@ class EruGridComponent {
14261
14743
  const config = this.gridStore.configuration();
14262
14744
  return config?.config?.groupBar ?? true;
14263
14745
  }, ...(ngDevMode ? [{ debugName: "showGroupBar" }] : []));
14264
- // Excel download icon configuration
14746
+ // Excel download icon configuration. `excel_download` enables the export;
14747
+ // `excel_download_icon` (default true) controls whether the built-in icon is
14748
+ // rendered — off means the host triggers requestExcelDownload() itself.
14265
14749
  showExcelDownload = computed(() => {
14266
14750
  const config = this.gridStore.configuration();
14267
- return config?.config?.excel_download ?? false;
14751
+ const enabled = config?.config?.excel_download ?? false;
14752
+ return enabled && (config?.config?.excel_download_icon ?? true);
14268
14753
  }, ...(ngDevMode ? [{ debugName: "showExcelDownload" }] : []));
14269
14754
  // Wrap long header labels onto multiple lines instead of truncating (default: true)
14270
14755
  wrapHeaders = computed(() => {
@@ -14725,10 +15210,19 @@ class EruGridComponent {
14725
15210
  // SCSS fallback in place.
14726
15211
  setVar('--board-column-height', typeof colH === 'number' ? (colH > 0 ? `${colH}px` : 'none') : undefined);
14727
15212
  });
15213
+ // isSmViewport is measured from the grid's own box, not from a window media
15214
+ // query: a grid rendered in a side panel or a narrow column is narrow no
15215
+ // matter how wide the browser is, and the window query kept it on the full
15216
+ // desktop column set there. Fed by the host ResizeObserver in
15217
+ // ngAfterViewInit; the window query only stands in until the first measure.
14728
15218
  this.breakpointObserver
14729
15219
  .observe('(max-width: 767.98px)')
14730
15220
  .pipe(takeUntilDestroyed())
14731
- .subscribe(result => this.isSmViewport.set(result.matches));
15221
+ .subscribe(result => {
15222
+ if (this.hasMeasuredOwnWidth)
15223
+ return;
15224
+ this.isSmViewport.set(result.matches);
15225
+ });
14732
15226
  // Sync preset → data-preset attribute on host. Default 'default' unless the
14733
15227
  // user explicitly sets one. User tokens (via applyTokens) still override.
14734
15228
  effect(() => {
@@ -14837,6 +15331,16 @@ class EruGridComponent {
14837
15331
  if (typeof ResizeObserver !== 'undefined') {
14838
15332
  this.resizeObserver = new ResizeObserver(entries => {
14839
15333
  for (const entry of entries) {
15334
+ const width = entry.contentRect.width;
15335
+ // 0 is a hidden or not-yet-laid-out host; keep the last real answer
15336
+ // rather than flipping the whole grid to its mobile column set.
15337
+ if (width > 0) {
15338
+ this.hasMeasuredOwnWidth = true;
15339
+ const isSm = width < EruGridComponent.SM_VIEWPORT_MAX_WIDTH;
15340
+ if (isSm !== this.isSmViewport()) {
15341
+ this.ngZone.run(() => this.isSmViewport.set(isSm));
15342
+ }
15343
+ }
14840
15344
  const height = entry.contentRect.height;
14841
15345
  // Cap at gridHeight to prevent feedback loop: larger viewports → taller host → loop
14842
15346
  const capped = Math.min(height, this.layoutGridHeight());
@@ -15417,11 +15921,16 @@ class EruGridComponent {
15417
15921
  toggleRowSelection(event, row) {
15418
15922
  event.stopPropagation();
15419
15923
  const checkbox = event.target;
15420
- const rowId = row.id;
15924
+ const rowId = rowSelectionKey(row);
15925
+ if (!rowId)
15926
+ return;
15421
15927
  if (checkbox.checked) {
15422
15928
  this.gridStore.selectRow(rowId);
15423
15929
  }
15424
15930
  else {
15931
+ // Unticking one row breaks the "all N matching" claim — what is selected
15932
+ // is no longer the whole result set, so it drops back to a plain id list.
15933
+ this.gridStore.setSelectAllMatching(false);
15425
15934
  this.gridStore.deselectRow(rowId);
15426
15935
  }
15427
15936
  }
@@ -15484,7 +15993,20 @@ class EruGridComponent {
15484
15993
  return [...byKey.values()];
15485
15994
  }, ...(ngDevMode ? [{ debugName: "gridActions" }] : []));
15486
15995
  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" }] : []));
15996
+ actionDisplayType = computed(() => {
15997
+ const configured = this.gridStore.configuration()?.config?.action_display_type;
15998
+ return configured === 'kebab' || configured === 'icons_outlined' ? configured : 'icons';
15999
+ }, ...(ngDevMode ? [{ debugName: "actionDisplayType" }] : []));
16000
+ /**
16001
+ * Icon font for the row-action icons.
16002
+ *
16003
+ * Filled and outlined are two different Material fonts over the same ligature
16004
+ * names, not two ligatures — so the choice is a `fontSet` swap and every
16005
+ * configured action keeps the icon name it was given. Consumers must have the
16006
+ * outlined font loaded; without it mat-icon falls back to the filled face, so
16007
+ * the worst case is the previous look rather than empty boxes.
16008
+ */
16009
+ actionIconFontSet = computed(() => this.actionDisplayType() === 'icons_outlined' ? 'material-icons-outlined' : 'material-icons', ...(ngDevMode ? [{ debugName: "actionIconFontSet" }] : []));
15488
16010
  /**
15489
16011
  * The actions that apply to one row, after each action's
15490
16012
  * `action_visible_condition` is evaluated against that row's values.
@@ -15590,7 +16112,14 @@ class EruGridComponent {
15590
16112
  // a textbox there. Letting the model's datatype win would undo the choice on
15591
16113
  // every resolve, so a mapped column could never be composed. Everything else
15592
16114
  // about the mapping still inherits.
16115
+ //
16116
+ // The model's datatype is not discarded though: it is what the column
16117
+ // actually holds, and the value still has to be formatted by it. Moving it
16118
+ // aside rather than deleting it is what keeps a composed date column's
16119
+ // pattern working.
15593
16120
  if (PRESENTATION_DATATYPES.has(col?.datatype)) {
16121
+ if (patch.datatype)
16122
+ patch.presentation_base_datatype = patch.datatype;
15594
16123
  delete patch.datatype;
15595
16124
  }
15596
16125
  // The data model uppercases the pattern on save ('DD-MM-YYYY'), while the
@@ -15685,15 +16214,28 @@ class EruGridComponent {
15685
16214
  shouldShowActionColumn(position) {
15686
16215
  return this.isActionColumnEnabled() && this.getActionColumnPosition() === position;
15687
16216
  }
16217
+ /**
16218
+ * The header checkbox. Stage one only: it selects the rows loaded so far, and
16219
+ * the banner then offers "select all N matching" for the rest. It cannot
16220
+ * select unloaded pages itself — the grid never holds them.
16221
+ */
15688
16222
  toggleAllGroups(event) {
15689
16223
  const checkbox = event.target;
15690
16224
  if (checkbox.checked) {
15691
- this.gridStore.rows().forEach(row => this.gridStore.selectRow(row.entity_id || ''));
16225
+ this.gridStore.selectAllLoadedRows();
15692
16226
  }
15693
16227
  else {
15694
16228
  this.gridStore.clearSelection();
15695
16229
  }
15696
16230
  }
16231
+ /** The banner's "select all N matching" — stage two. */
16232
+ selectAllMatchingRows() {
16233
+ this.gridStore.selectAllLoadedRows();
16234
+ this.gridStore.setSelectAllMatching(true);
16235
+ }
16236
+ clearRowSelection() {
16237
+ this.gridStore.clearSelection();
16238
+ }
15697
16239
  isRowSelected(rowId) {
15698
16240
  return rowId ? this.gridStore.isRowSelected(rowId) : false;
15699
16241
  }
@@ -15701,11 +16243,14 @@ class EruGridComponent {
15701
16243
  return this.gridStore.isGroupSelected(groupId);
15702
16244
  }
15703
16245
  isAllGroupsSelected() {
15704
- const allRows = this.gridStore.rows();
16246
+ const allRows = this.gridStore.selectableRows();
15705
16247
  return allRows.length > 0 &&
15706
- allRows.every(row => this.gridStore.isRowSelected(row.entity_id || ''));
16248
+ allRows.every(row => this.gridStore.isRowSelected(rowSelectionKey(row)));
15707
16249
  }
15708
16250
  getSelectedRows() {
16251
+ return this.gridStore.selectedRows();
16252
+ }
16253
+ getSelectedRowIds() {
15709
16254
  return Array.from(this.gridStore.selectedRowIds());
15710
16255
  }
15711
16256
  /* private markGroupAsNoMoreRows(groupId: string) {
@@ -16291,7 +16836,7 @@ class EruGridComponent {
16291
16836
  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
16837
  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
16838
  ...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 });
16839
+ ], 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
16840
  }
16296
16841
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: EruGridComponent, decorators: [{
16297
16842
  type: Component,
@@ -16311,7 +16856,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
16311
16856
  ResizeColumnDirective,
16312
16857
  ColumnDragDirective,
16313
16858
  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"] }]
16859
+ ], 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
16860
  }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { allViewports: [{
16316
16861
  type: ViewChildren,
16317
16862
  args: [CdkVirtualScrollViewport]
@@ -16453,5 +16998,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
16453
16998
  * Generated bundle index. Do not edit.
16454
16999
  */
16455
17000
 
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 };
17001
+ 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
17002
  //# sourceMappingURL=eru-grid.mjs.map