@openziti/ziti-console-lib 1.7.2 → 1.8.0

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.
@@ -0,0 +1,3067 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Injectable, EventEmitter, PLATFORM_ID, Output, Inject, Directive, Input, Component, ViewChild, HostBinding } from '@angular/core';
3
+ import { BehaviorSubject, Subject, Subscription } from 'rxjs';
4
+ import _, { isEmpty, forEach } from 'lodash';
5
+ import { HttpParams } from '@angular/common/http';
6
+ import * as i1 from '@angular/common';
7
+ import { isPlatformBrowser, CommonModule } from '@angular/common';
8
+ import * as i2 from '@angular/router';
9
+ import * as i5 from '@angular/forms';
10
+ import { FormsModule } from '@angular/forms';
11
+ import * as i6 from '@angular/cdk/drag-drop';
12
+ import { moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop';
13
+ import moment from 'moment';
14
+ import * as i5$1 from '@angular/material/datepicker';
15
+ import { MatDatepickerModule } from '@angular/material/datepicker';
16
+ import { MatNativeDateModule } from '@angular/material/core';
17
+ import * as i2$1 from '@angular/material/tooltip';
18
+ import { MatTooltipModule } from '@angular/material/tooltip';
19
+ import * as i3 from '@angular/platform-browser';
20
+
21
+ class DataTableFilterService {
22
+ // filter params live under this prefix so unrelated params (e.g. ?tab=) aren't treated as filters
23
+ static { this.URL_FILTER_PREFIX = 'f.'; }
24
+ constructor(location, router, activatedRoute) {
25
+ this.location = location;
26
+ this.router = router;
27
+ this.activatedRoute = activatedRoute;
28
+ this.filters = [];
29
+ this.currentPage = 1;
30
+ /** Rows requested per page. Defaults to the legacy page size; the list-table's
31
+ * rows-per-page control changes it via {@link changePageSize}. */
32
+ this.pageSize = 50;
33
+ this.filtersChanged = new BehaviorSubject(this.filters);
34
+ this.filtering = new BehaviorSubject(false);
35
+ this.pageChanged = new BehaviorSubject(this.currentPage);
36
+ this.pageSizeChanged = new BehaviorSubject(this.pageSize);
37
+ this.currentQueryParams = [];
38
+ }
39
+ updateFilter(filterObj) {
40
+ if (isEmpty(filterObj.value) && (isNaN(filterObj.value) || filterObj.value === ''))
41
+ this.removeFilter(filterObj);
42
+ else {
43
+ let isFound = false;
44
+ for (let idx = 0; idx < this.filters.length; idx++) {
45
+ if (this.filters[idx].columnId === filterObj.columnId) {
46
+ this.filters[idx] = filterObj;
47
+ isFound = true;
48
+ break;
49
+ }
50
+ }
51
+ if (!isFound) {
52
+ this.filters.push(filterObj);
53
+ }
54
+ }
55
+ this.updateUrlParameters();
56
+ this.filtersChanged.next(this.filters);
57
+ }
58
+ removeFilter(filterObj) {
59
+ for (let idx = 0; idx < this.filters.length; idx++) {
60
+ if (this.filters[idx].columnId === filterObj.columnId) {
61
+ this.filters.splice(idx, 1);
62
+ break;
63
+ }
64
+ }
65
+ this.updateUrlParameters([filterObj]);
66
+ this.filtersChanged.next(this.filters);
67
+ }
68
+ getFilterString(key) {
69
+ let value = '';
70
+ for (let idx = 0; idx < this.filters.length; idx++) {
71
+ if (this.filters[idx].columnId === key) {
72
+ value = this.filters[idx].value;
73
+ break;
74
+ }
75
+ }
76
+ return value;
77
+ }
78
+ changePage(page) {
79
+ this.currentPage = page;
80
+ this.pageChanged.next(page);
81
+ }
82
+ changePageSize(size) {
83
+ this.pageSize = size;
84
+ this.currentPage = 1;
85
+ this.pageSizeChanged.next(size);
86
+ }
87
+ clearFilters() {
88
+ this.filters = [];
89
+ this.updateUrlParameters();
90
+ this.filtersChanged.next(this.filters);
91
+ }
92
+ /**
93
+ * Remove every active filter (the toolbar/empty-state "clear filters" action).
94
+ * clearFilters() above re-reads the URL and so is really a load-time sync; this one
95
+ * passes the active filters to updateUrlParameters so they are dropped from the URL
96
+ * and storage instead of being restored, then emits the now-empty list.
97
+ */
98
+ clearAllFilters() {
99
+ const toRemove = Object.values(this.getUrlFiltersMap());
100
+ this.filters = [];
101
+ this.updateUrlParameters(toRemove);
102
+ this.filtersChanged.next(this.filters);
103
+ }
104
+ storeFilters() {
105
+ localStorage.setItem('search_filters', JSON.stringify(this.filters));
106
+ }
107
+ getStoredFilters() {
108
+ const storageItem = localStorage.getItem('search_filters');
109
+ const filters = storageItem ? JSON.parse(storageItem) : [];
110
+ return filters;
111
+ }
112
+ updateUrlParameters(filtersToRemove = []) {
113
+ const prefix = DataTableFilterService.URL_FILTER_PREFIX;
114
+ const urlFiltersMap = this.getUrlFiltersMap();
115
+ this.filters.forEach(filter => {
116
+ urlFiltersMap[filter.columnId] = filter;
117
+ });
118
+ const currentParamString = window.location.href.split("?")[1] || '';
119
+ let params = new HttpParams({ fromString: currentParamString });
120
+ params.keys()
121
+ .filter((key) => key.startsWith(prefix))
122
+ .forEach((key) => {
123
+ params = params.delete(key);
124
+ });
125
+ const newFilters = [];
126
+ forEach(urlFiltersMap, (value, key) => {
127
+ const filterRemoved = filtersToRemove.some((removed) => {
128
+ return removed.columnId === key;
129
+ });
130
+ if (!filterRemoved) {
131
+ params = params.append(prefix + key, value.value);
132
+ newFilters.push(value);
133
+ }
134
+ });
135
+ this.filters = newFilters;
136
+ const path = this.router.url.split("?")[0];
137
+ this.location.replaceState(path, params.toString());
138
+ this.storeFilters();
139
+ }
140
+ getUrlFiltersMap() {
141
+ const prefix = DataTableFilterService.URL_FILTER_PREFIX;
142
+ const paramString = window.location.href.split("?")[1];
143
+ const params = new HttpParams({ fromString: paramString });
144
+ const storedFilters = this.getStoredFilters();
145
+ const appliedFiltersMap = {};
146
+ params.keys().forEach((rawKey) => {
147
+ if (!rawKey.startsWith(prefix)) {
148
+ return;
149
+ }
150
+ const key = rawKey.slice(prefix.length);
151
+ const storedFilter = storedFilters.find((filter) => {
152
+ return filter.columnId === key;
153
+ });
154
+ if (storedFilter) {
155
+ appliedFiltersMap[key] = storedFilter;
156
+ }
157
+ else {
158
+ let val = params.get(rawKey);
159
+ let type = 'TEXTINPUT';
160
+ let label = val;
161
+ if (val?.split(',')?.length > 1) {
162
+ val = val?.split(',');
163
+ label = val[0];
164
+ type = 'ATTRIBUTE';
165
+ }
166
+ appliedFiltersMap[key] = {
167
+ columnId: key,
168
+ value: val,
169
+ label: decodeURIComponent(label),
170
+ filterName: key,
171
+ type: type
172
+ };
173
+ }
174
+ });
175
+ return appliedFiltersMap;
176
+ }
177
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.34", ngImport: i0, type: DataTableFilterService, deps: [{ token: i1.Location }, { token: i2.Router }, { token: i2.ActivatedRoute }], target: i0.ɵɵFactoryTarget.Injectable }); }
178
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.34", ngImport: i0, type: DataTableFilterService, providedIn: 'root' }); }
179
+ }
180
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.34", ngImport: i0, type: DataTableFilterService, decorators: [{
181
+ type: Injectable,
182
+ args: [{
183
+ providedIn: 'root'
184
+ }]
185
+ }], ctorParameters: () => [{ type: i1.Location }, { type: i2.Router }, { type: i2.ActivatedRoute }] });
186
+
187
+ /*
188
+ Copyright NetFoundry Inc.
189
+
190
+ Licensed under the Apache License, Version 2.0 (the "License");
191
+ you may not use this file except in compliance with the License.
192
+ You may obtain a copy of the License at
193
+
194
+ https://www.apache.org/licenses/LICENSE-2.0
195
+
196
+ Unless required by applicable law or agreed to in writing, software
197
+ distributed under the License is distributed on an "AS IS" BASIS,
198
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
199
+ See the License for the specific language governing permissions and
200
+ limitations under the License.
201
+ */
202
+
203
+ /*
204
+ Copyright NetFoundry Inc.
205
+
206
+ Licensed under the Apache License, Version 2.0 (the "License");
207
+ you may not use this file except in compliance with the License.
208
+ You may obtain a copy of the License at
209
+
210
+ https://www.apache.org/licenses/LICENSE-2.0
211
+
212
+ Unless required by applicable law or agreed to in writing, software
213
+ distributed under the License is distributed on an "AS IS" BASIS,
214
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
215
+ See the License for the specific language governing permissions and
216
+ limitations under the License.
217
+ */
218
+ /**
219
+ * Per-instance state + localStorage persistence for {@link ListTableComponent}.
220
+ * Uses its own `lt_<tableId>_*` keys (separate from the legacy ag-grid table) so a
221
+ * first visit starts from the column defs' default widths, not a carried-over ag layout.
222
+ */
223
+ class ListTableService {
224
+ constructor() {
225
+ this.tableId = '';
226
+ /** Coordinates the toolbar dropdowns so only one (filter pill or column chooser)
227
+ * is open at a time. Openers emit themselves; others close when they see an
228
+ * owner that isn't them. */
229
+ this.menuOpenedSubject = new Subject();
230
+ this.menuOpened$ = this.menuOpenedSubject.asObservable();
231
+ }
232
+ notifyMenuOpened(owner) {
233
+ this.menuOpenedSubject.next(owner);
234
+ }
235
+ stateKey() {
236
+ return `lt_${this.tableId}_table_state`;
237
+ }
238
+ widthsKey() {
239
+ return `lt_${this.tableId}_column_widths`;
240
+ }
241
+ pageSizeKey() {
242
+ return `lt_${this.tableId}_page_size`;
243
+ }
244
+ treeVariantKey() {
245
+ return `lt_${this.tableId}_tree_variant`;
246
+ }
247
+ /** Returns the saved tree layout ('card' | 'flat'), or `fallback` if none. */
248
+ restoreTreeVariant(fallback) {
249
+ try {
250
+ const raw = localStorage.getItem(this.treeVariantKey());
251
+ return raw === 'card' || raw === 'flat' ? raw : fallback;
252
+ }
253
+ catch {
254
+ return fallback;
255
+ }
256
+ }
257
+ saveTreeVariant(variant) {
258
+ try {
259
+ localStorage.setItem(this.treeVariantKey(), variant);
260
+ }
261
+ catch {
262
+ // ignore
263
+ }
264
+ }
265
+ static pageSizeKeyFor(tableId) {
266
+ return `lt_${tableId}_page_size`;
267
+ }
268
+ restorePageSize(fallback) {
269
+ return ListTableService.readPersistedPageSize(this.tableId, fallback);
270
+ }
271
+ savePageSize(size) {
272
+ try {
273
+ localStorage.setItem(this.pageSizeKey(), String(size));
274
+ }
275
+ catch {
276
+ // ignore
277
+ }
278
+ }
279
+ /** Reads a table's remembered rows-per-page without an instance (for host pre-seeding). */
280
+ static readPersistedPageSize(tableId, fallback) {
281
+ try {
282
+ const raw = localStorage.getItem(ListTableService.pageSizeKeyFor(tableId));
283
+ const n = raw ? parseInt(raw, 10) : NaN;
284
+ return Number.isFinite(n) && n > 0 ? n : fallback;
285
+ }
286
+ catch {
287
+ return fallback;
288
+ }
289
+ }
290
+ /**
291
+ * Returns a shallow-cloned copy of `columns` with any persisted width, visibility,
292
+ * and order applied. Columns not present in the saved state keep their declared
293
+ * position (appended after the saved ones).
294
+ */
295
+ /** Parses a stored JSON value, or null when missing/unavailable/malformed. */
296
+ readJson(key) {
297
+ try {
298
+ const raw = localStorage.getItem(key);
299
+ return raw ? JSON.parse(raw) : null;
300
+ }
301
+ catch {
302
+ return null;
303
+ }
304
+ }
305
+ /** Applies a widths map then a persisted order/visibility list onto a fresh copy of
306
+ * `columns`. Shared by the top-level and per-depth restore paths. */
307
+ mergePersistedColumns(columns, widths, state) {
308
+ let cols = columns.map((c) => ({ ...c }));
309
+ cols.forEach((c) => {
310
+ if (widths[c.key] != null) {
311
+ c.width = widths[c.key];
312
+ }
313
+ });
314
+ if (state.length) {
315
+ const ordered = [];
316
+ state.forEach((s) => {
317
+ const found = cols.find((c) => c.key === s.colId);
318
+ if (found && !ordered.includes(found)) {
319
+ if (s.hide != null) {
320
+ found.hidden = s.hide;
321
+ }
322
+ if (s.width != null) {
323
+ found.width = s.width;
324
+ }
325
+ ordered.push(found);
326
+ }
327
+ });
328
+ cols.forEach((c) => {
329
+ if (!ordered.includes(c)) {
330
+ ordered.push(c);
331
+ }
332
+ });
333
+ cols = ordered;
334
+ }
335
+ return cols;
336
+ }
337
+ /** Order + visibility + width entries for one set of columns. */
338
+ columnStateEntries(columns) {
339
+ return columns.map((c) => ({ colId: c.key, hide: !!c.hidden, width: c.width }));
340
+ }
341
+ applyPersistedState(columns) {
342
+ const widths = this.readJson(this.widthsKey()) || {};
343
+ const state = this.readJson(this.stateKey()) || [];
344
+ return this.mergePersistedColumns(columns, widths, state);
345
+ }
346
+ /** Persists the current order + visibility (and widths embedded in each entry). */
347
+ saveState(columns) {
348
+ try {
349
+ localStorage.setItem(this.stateKey(), JSON.stringify(this.columnStateEntries(columns)));
350
+ }
351
+ catch {
352
+ // ignore
353
+ }
354
+ }
355
+ // ---- nested (per-depth) child columns: same contract as above, keyed by level ----
356
+ childStateKey() {
357
+ return `lt_${this.tableId}_child_state`;
358
+ }
359
+ childWidthsKey() {
360
+ return `lt_${this.tableId}_child_widths`;
361
+ }
362
+ /** Applies persisted order/visibility/width for one nested depth to its columns. */
363
+ applyPersistedChildState(level, columns) {
364
+ const widths = (this.readJson(this.childWidthsKey()) || {})[level] || {};
365
+ const state = (this.readJson(this.childStateKey()) || {})[level] || [];
366
+ return this.mergePersistedColumns(columns, widths, state);
367
+ }
368
+ saveChildState(level, columns) {
369
+ try {
370
+ const byLevel = this.readJson(this.childStateKey()) || {};
371
+ byLevel[level] = this.columnStateEntries(columns);
372
+ localStorage.setItem(this.childStateKey(), JSON.stringify(byLevel));
373
+ }
374
+ catch {
375
+ // ignore
376
+ }
377
+ }
378
+ saveChildWidth(level, colId, width, columns) {
379
+ try {
380
+ const byLevel = this.readJson(this.childWidthsKey()) || {};
381
+ byLevel[level] = { ...(byLevel[level] || {}), [colId]: width };
382
+ localStorage.setItem(this.childWidthsKey(), JSON.stringify(byLevel));
383
+ }
384
+ catch {
385
+ // ignore
386
+ }
387
+ this.saveChildState(level, columns);
388
+ }
389
+ /** Persists a single column's width, then rewrites the ordered state. */
390
+ saveWidth(colId, width, columns) {
391
+ try {
392
+ const raw = localStorage.getItem(this.widthsKey());
393
+ const widths = raw ? JSON.parse(raw) : {};
394
+ widths[colId] = width;
395
+ localStorage.setItem(this.widthsKey(), JSON.stringify(widths));
396
+ }
397
+ catch {
398
+ // ignore
399
+ }
400
+ this.saveState(columns);
401
+ }
402
+ /** Clears all persisted layout for this table ("Restore Default Table"). */
403
+ reset() {
404
+ try {
405
+ localStorage.removeItem(this.stateKey());
406
+ localStorage.removeItem(this.widthsKey());
407
+ localStorage.removeItem(this.pageSizeKey());
408
+ localStorage.removeItem(this.treeVariantKey());
409
+ localStorage.removeItem(this.childStateKey());
410
+ localStorage.removeItem(this.childWidthsKey());
411
+ }
412
+ catch {
413
+ // ignore
414
+ }
415
+ }
416
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.34", ngImport: i0, type: ListTableService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
417
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.34", ngImport: i0, type: ListTableService }); }
418
+ }
419
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.34", ngImport: i0, type: ListTableService, decorators: [{
420
+ type: Injectable
421
+ }] });
422
+
423
+ /*
424
+ Copyright NetFoundry Inc.
425
+
426
+ Licensed under the Apache License, Version 2.0 (the "License");
427
+ you may not use this file except in compliance with the License.
428
+ You may obtain a copy of the License at
429
+
430
+ https://www.apache.org/licenses/LICENSE-2.0
431
+
432
+ Unless required by applicable law or agreed to in writing, software
433
+ distributed under the License is distributed on an "AS IS" BASIS,
434
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
435
+ See the License for the specific language governing permissions and
436
+ limitations under the License.
437
+ */
438
+ /**
439
+ * Emits `clickOutside` when a click lands outside the host element. A local,
440
+ * dependency-free standalone replacement for `ng-click-outside`, so the table and
441
+ * its cells can be consumed from the package without pulling an undeclared dependency.
442
+ *
443
+ * The document listener runs outside Angular to avoid a change-detection pass on every
444
+ * click anywhere on the page; the emit re-enters the zone so bound handlers run normally.
445
+ */
446
+ class ClickOutsideDirective {
447
+ constructor(el, ngZone, platformId) {
448
+ this.el = el;
449
+ this.ngZone = ngZone;
450
+ this.platformId = platformId;
451
+ this.clickOutside = new EventEmitter();
452
+ this.onDocumentClick = this.onDocumentClick.bind(this);
453
+ }
454
+ ngOnInit() {
455
+ if (!isPlatformBrowser(this.platformId)) {
456
+ return;
457
+ }
458
+ this.ngZone.runOutsideAngular(() => {
459
+ document.addEventListener('click', this.onDocumentClick);
460
+ });
461
+ }
462
+ ngOnDestroy() {
463
+ if (!isPlatformBrowser(this.platformId)) {
464
+ return;
465
+ }
466
+ this.ngZone.runOutsideAngular(() => {
467
+ document.removeEventListener('click', this.onDocumentClick);
468
+ });
469
+ }
470
+ onDocumentClick(event) {
471
+ if (!this.el.nativeElement.contains(event.target)) {
472
+ this.ngZone.run(() => this.clickOutside.emit(event));
473
+ }
474
+ }
475
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.34", ngImport: i0, type: ClickOutsideDirective, deps: [{ token: i0.ElementRef }, { token: i0.NgZone }, { token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Directive }); }
476
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.34", type: ClickOutsideDirective, isStandalone: true, selector: "[clickOutside]", outputs: { clickOutside: "clickOutside" }, ngImport: i0 }); }
477
+ }
478
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.34", ngImport: i0, type: ClickOutsideDirective, decorators: [{
479
+ type: Directive,
480
+ args: [{
481
+ selector: '[clickOutside]',
482
+ standalone: true,
483
+ }]
484
+ }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.NgZone }, { type: Object, decorators: [{
485
+ type: Inject,
486
+ args: [PLATFORM_ID]
487
+ }] }], propDecorators: { clickOutside: [{
488
+ type: Output
489
+ }] } });
490
+
491
+ /**
492
+ * Generic, theme-driven action menu popover. Presentational only: the host owns
493
+ * open state and position and handles `(itemClick)` / `(closed)`.
494
+ */
495
+ class ListMenuComponent {
496
+ constructor() {
497
+ /** Whether the menu is rendered. */
498
+ this.open = false;
499
+ /** Rows to render. */
500
+ this.items = [];
501
+ /** Fixed-position left offset (px). */
502
+ this.left = 0;
503
+ /** Fixed-position top offset (px). */
504
+ this.top = 0;
505
+ /** Emitted when a clickable row is selected. */
506
+ this.itemClick = new EventEmitter();
507
+ /** Emitted when the user clicks outside the open menu. */
508
+ this.closed = new EventEmitter();
509
+ this.trackItem = (_, item) => item.action ?? item.label;
510
+ }
511
+ onItem(item, event) {
512
+ if (item.disabled) {
513
+ return;
514
+ }
515
+ this.itemClick.emit({ item, event });
516
+ }
517
+ onClickOutside(event) {
518
+ this.closed.emit(event);
519
+ }
520
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.34", ngImport: i0, type: ListMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
521
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.34", type: ListMenuComponent, isStandalone: true, selector: "lib-list-menu", inputs: { open: "open", items: "items", left: "left", top: "top", menuId: "menuId" }, outputs: { itemClick: "itemClick", closed: "closed" }, ngImport: i0, template: "@if (open) {\n <div\n class=\"lt-menu\"\n [attr.id]=\"menuId || null\"\n [ngStyle]=\"{ left: left + 'px', top: top + 'px' }\"\n (clickOutside)=\"onClickOutside($event)\"\n >\n @for (item of items; track trackItem($index, item)) {\n <button\n type=\"button\"\n class=\"lt-menu-item\"\n [class.has-icon]=\"!!item.icon || !!item.iconSvg\"\n [class.is-hidden]=\"item.hidden\"\n [disabled]=\"!!item.disabled\"\n [attr.id]=\"item.id || null\"\n (click)=\"onItem(item, $event)\"\n >\n @if (item.iconSvg) {\n <span class=\"lt-menu-icon\" aria-hidden=\"true\" [innerHTML]=\"item.iconSvg\"></span>\n } @else if (item.icon) {\n <span class=\"lt-menu-icon {{ item.icon }}\" aria-hidden=\"true\"></span>\n }\n <span class=\"lt-menu-label\">{{ item.label }}</span>\n </button>\n }\n </div>\n}\n", styles: [":host{display:contents}.lt-menu{position:fixed;z-index:1000;min-width:11rem;padding:.25rem;background:var(--lt-control-bg);border:1px solid var(--lt-border);border-radius:var(--lt-radius);box-shadow:var(--shadowLarge);font-family:var(--lt-font);animation:lt-menu-in .1s ease-out}@keyframes lt-menu-in{0%{opacity:0;transform:translateY(-.25rem)}to{opacity:1;transform:translateY(0)}}.lt-menu-item{display:flex;align-items:center;gap:.5rem;width:100%;padding:.4375rem .5rem;font-family:var(--lt-font);font-size:.8125rem;color:var(--lt-cell-text);text-align:left;background:transparent;border:none;border-radius:var(--lt-radius);cursor:pointer}.lt-menu-item:hover{background:var(--lt-row-hover);color:var(--lt-link)}.lt-menu-item:hover .lt-menu-icon{color:var(--lt-link)}.lt-menu-item.is-hidden{display:none}.lt-menu-item:disabled{opacity:.45;cursor:default}.lt-menu-item:disabled:hover{background:transparent;color:var(--lt-cell-text)}.lt-menu-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1rem;font-size:.9375rem;color:var(--lt-icon)}.lt-menu-icon ::ng-deep svg{display:block;width:.9375rem;height:.9375rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: ClickOutsideDirective, selector: "[clickOutside]", outputs: ["clickOutside"] }] }); }
522
+ }
523
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.34", ngImport: i0, type: ListMenuComponent, decorators: [{
524
+ type: Component,
525
+ args: [{ selector: 'lib-list-menu', standalone: true, imports: [CommonModule, ClickOutsideDirective], template: "@if (open) {\n <div\n class=\"lt-menu\"\n [attr.id]=\"menuId || null\"\n [ngStyle]=\"{ left: left + 'px', top: top + 'px' }\"\n (clickOutside)=\"onClickOutside($event)\"\n >\n @for (item of items; track trackItem($index, item)) {\n <button\n type=\"button\"\n class=\"lt-menu-item\"\n [class.has-icon]=\"!!item.icon || !!item.iconSvg\"\n [class.is-hidden]=\"item.hidden\"\n [disabled]=\"!!item.disabled\"\n [attr.id]=\"item.id || null\"\n (click)=\"onItem(item, $event)\"\n >\n @if (item.iconSvg) {\n <span class=\"lt-menu-icon\" aria-hidden=\"true\" [innerHTML]=\"item.iconSvg\"></span>\n } @else if (item.icon) {\n <span class=\"lt-menu-icon {{ item.icon }}\" aria-hidden=\"true\"></span>\n }\n <span class=\"lt-menu-label\">{{ item.label }}</span>\n </button>\n }\n </div>\n}\n", styles: [":host{display:contents}.lt-menu{position:fixed;z-index:1000;min-width:11rem;padding:.25rem;background:var(--lt-control-bg);border:1px solid var(--lt-border);border-radius:var(--lt-radius);box-shadow:var(--shadowLarge);font-family:var(--lt-font);animation:lt-menu-in .1s ease-out}@keyframes lt-menu-in{0%{opacity:0;transform:translateY(-.25rem)}to{opacity:1;transform:translateY(0)}}.lt-menu-item{display:flex;align-items:center;gap:.5rem;width:100%;padding:.4375rem .5rem;font-family:var(--lt-font);font-size:.8125rem;color:var(--lt-cell-text);text-align:left;background:transparent;border:none;border-radius:var(--lt-radius);cursor:pointer}.lt-menu-item:hover{background:var(--lt-row-hover);color:var(--lt-link)}.lt-menu-item:hover .lt-menu-icon{color:var(--lt-link)}.lt-menu-item.is-hidden{display:none}.lt-menu-item:disabled{opacity:.45;cursor:default}.lt-menu-item:disabled:hover{background:transparent;color:var(--lt-cell-text)}.lt-menu-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1rem;font-size:.9375rem;color:var(--lt-icon)}.lt-menu-icon ::ng-deep svg{display:block;width:.9375rem;height:.9375rem}\n"] }]
526
+ }], propDecorators: { open: [{
527
+ type: Input
528
+ }], items: [{
529
+ type: Input
530
+ }], left: [{
531
+ type: Input
532
+ }], top: [{
533
+ type: Input
534
+ }], menuId: [{
535
+ type: Input
536
+ }], itemClick: [{
537
+ type: Output
538
+ }], closed: [{
539
+ type: Output
540
+ }] } });
541
+
542
+ /*
543
+ Copyright NetFoundry Inc.
544
+
545
+ Licensed under the Apache License, Version 2.0 (the "License");
546
+ you may not use this file except in compliance with the License.
547
+ You may obtain a copy of the License at
548
+
549
+ https://www.apache.org/licenses/LICENSE-2.0
550
+
551
+ Unless required by applicable law or agreed to in writing, software
552
+ distributed under the License is distributed on an "AS IS" BASIS,
553
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
554
+ See the License for the specific language governing permissions and
555
+ limitations under the License.
556
+ */
557
+ const DATE_PRESETS = [
558
+ { label: 'Any time', value: '' },
559
+ { label: 'Last Hour', value: 'hour' },
560
+ { label: 'Last Day', value: 'day' },
561
+ { label: 'Last Week', value: 'week' },
562
+ { label: 'Last Month', value: 'month' },
563
+ ];
564
+ /**
565
+ * A single control in the table's filter row. Renders the right widget for the
566
+ * column's `filterType` (text input / single-select / multi-select / date-range)
567
+ * and pushes changes through {@link DataTableFilterService}, whose value contract the
568
+ * controller query-builder already understands (icontains / = / anyOf() / datetime range).
569
+ */
570
+ class ListFilterCellComponent {
571
+ constructor(filterService, listSvc) {
572
+ this.filterService = filterService;
573
+ this.listSvc = listSvc;
574
+ this.mode = 'text';
575
+ this.options = [];
576
+ this.textValue = '';
577
+ this.selectedValue = '';
578
+ this.selectedValues = [];
579
+ /** In-dropdown search text for the multi-select attribute combo. */
580
+ this.attrSearch = '';
581
+ /** Match any selected attribute (or) vs every one (and); attribute filters only. */
582
+ this.attrSemantic = 'AnyOf';
583
+ this.open = false;
584
+ this.menuLeft = 0;
585
+ this.menuTop = 0;
586
+ this.menuWidth = 0;
587
+ // custom date range (daterange mode)
588
+ this.dateRangeStart = null;
589
+ this.dateRangeEnd = null;
590
+ this.sub = new Subscription();
591
+ this.textDebounced = _.debounce(() => this.emitText(), 400);
592
+ }
593
+ ngOnInit() {
594
+ this.mode = this.resolveMode();
595
+ this.options = this.resolveOptions();
596
+ this.sub.add(this.filterService.filtersChanged.subscribe((filters) => this.syncFromFilters(filters)));
597
+ // close this menu whenever another dropdown opens
598
+ this.sub.add(this.listSvc.menuOpened$.subscribe((owner) => {
599
+ if (owner !== this) {
600
+ this.open = false;
601
+ }
602
+ }));
603
+ }
604
+ ngOnDestroy() {
605
+ this.sub.unsubscribe();
606
+ }
607
+ // ------------------------------------------------------------------ config
608
+ get field() {
609
+ return this.column.filterField || this.column.field || this.column.key;
610
+ }
611
+ get placeholder() {
612
+ return this.column.filterPlaceholder || 'All';
613
+ }
614
+ /** True only for the Ziti role-attribute flavor of multi-select (adds #/@ prefixing). */
615
+ get isAttributeMode() {
616
+ return this.column.filterType === 'ATTRIBUTE';
617
+ }
618
+ resolveMode() {
619
+ switch (this.column.filterType) {
620
+ case 'TEXTINPUT':
621
+ return 'text';
622
+ case 'ATTRIBUTE':
623
+ case 'MULTISELECT':
624
+ return 'multi';
625
+ case 'DATETIME':
626
+ return 'daterange';
627
+ default:
628
+ return 'single';
629
+ }
630
+ }
631
+ resolveOptions() {
632
+ if (this.mode === 'daterange') {
633
+ return DATE_PRESETS;
634
+ }
635
+ if (this.mode === 'multi') {
636
+ let raw;
637
+ if (this.isAttributeMode) {
638
+ const getAttrs = this.column.headerParams?.['getRoleAttributes'];
639
+ raw = (typeof getAttrs === 'function' ? getAttrs() : null) || this.column.filterOptions || [];
640
+ }
641
+ else {
642
+ raw = this.column.getFilterOptions ? this.column.getFilterOptions() : (this.column.filterOptions || []);
643
+ }
644
+ return raw.map((a) => (typeof a === 'string' ? { label: a, value: a } : a));
645
+ }
646
+ const raw = this.column.getFilterOptions ? this.column.getFilterOptions() : this.column.filterOptions || [];
647
+ return raw;
648
+ }
649
+ // ------------------------------------------------------------------ display
650
+ labelForValue(value) {
651
+ const opt = this.options.find((o) => `${o.value}` === `${value}`);
652
+ return opt ? opt.label : `${value}`;
653
+ }
654
+ /** Short display of the current selection, shown after the field label when set. */
655
+ get valueLabel() {
656
+ if (this.mode === 'multi') {
657
+ if (this.selectedValues.length === 1) {
658
+ return this.labelForValue(this.selectedValues[0]);
659
+ }
660
+ return `${this.selectedValues.length}`;
661
+ }
662
+ if (this.mode === 'daterange') {
663
+ if (this.selectedValue === 'custom' && this.dateRangeStart) {
664
+ const start = moment(this.dateRangeStart).format('M/D/YY');
665
+ const end = this.dateRangeEnd ? moment(this.dateRangeEnd).format('M/D/YY') : '';
666
+ return end ? `${start} – ${end}` : start;
667
+ }
668
+ const found = this.options.find((o) => o.value === this.selectedValue);
669
+ return found && found.value ? found.label : '';
670
+ }
671
+ const match = this.options.find((o) => `${o.value}` === `${this.selectedValue}`);
672
+ return match && match.value !== '' && match.value != null ? match.label : '';
673
+ }
674
+ get hasValue() {
675
+ if (this.mode === 'multi') {
676
+ return this.selectedValues.length > 0;
677
+ }
678
+ return this.selectedValue !== '' && this.selectedValue != null;
679
+ }
680
+ isOptionSelected(opt) {
681
+ if (this.mode === 'multi') {
682
+ return this.selectedValues.includes(opt.value);
683
+ }
684
+ return `${opt.value}` === `${this.selectedValue}`;
685
+ }
686
+ /** Attribute options filtered by the in-dropdown search box (multi mode). */
687
+ get filteredOptions() {
688
+ if (this.mode !== 'multi' || !this.attrSearch) {
689
+ return this.options;
690
+ }
691
+ const q = this.attrSearch.toLowerCase();
692
+ return this.options.filter((o) => `${o.label ?? o.value}`.toLowerCase().includes(q));
693
+ }
694
+ /** A named attribute (rendered with @ + secondary color) vs a role attribute (# + primary). */
695
+ isNamedOption(opt) {
696
+ return this.isAttributeMode && String(opt?.value ?? opt?.label ?? '').charAt(0) === '@';
697
+ }
698
+ /** Option label. The attribute flavor prefixes # (role) / keeps @ (named); others render plain. */
699
+ optionLabel(opt) {
700
+ const raw = String(opt?.label ?? opt?.value ?? '');
701
+ if (!this.isAttributeMode) {
702
+ return raw;
703
+ }
704
+ if (raw.charAt(0) === '@' || raw.charAt(0) === '#') {
705
+ return raw;
706
+ }
707
+ return '#' + raw;
708
+ }
709
+ // ------------------------------------------------------------------ open/close
710
+ toggleMenu(event) {
711
+ event.stopPropagation();
712
+ if (this.open) {
713
+ this.open = false;
714
+ return;
715
+ }
716
+ const rect = this.trigger?.nativeElement.getBoundingClientRect();
717
+ if (rect) {
718
+ this.menuLeft = rect.left;
719
+ this.menuTop = rect.bottom + 4;
720
+ this.menuWidth = Math.max(rect.width, this.mode === 'multi' ? 220 : 160);
721
+ }
722
+ // refresh dynamic option sources (e.g. role attributes) each open
723
+ this.options = this.resolveOptions();
724
+ this.attrSearch = '';
725
+ this.open = true;
726
+ this.listSvc.notifyMenuOpened(this);
727
+ }
728
+ closeMenu() {
729
+ this.open = false;
730
+ }
731
+ // ------------------------------------------------------------------ text
732
+ onTextInput() {
733
+ this.textDebounced();
734
+ }
735
+ emitText() {
736
+ this.filterService.updateFilter({
737
+ filterName: this.column.label,
738
+ columnId: this.field,
739
+ value: this.textValue,
740
+ label: this.textValue,
741
+ type: 'TEXTINPUT',
742
+ });
743
+ }
744
+ // ------------------------------------------------------------------ single
745
+ selectOption(opt) {
746
+ this.selectedValue = opt.value;
747
+ this.open = false;
748
+ if (opt.value === '' || opt.value == null) {
749
+ this.removeThis();
750
+ return;
751
+ }
752
+ this.filterService.updateFilter({
753
+ filterName: this.column.label,
754
+ columnId: this.field,
755
+ value: opt.value,
756
+ label: opt.label,
757
+ type: this.column.filterType || 'SELECT',
758
+ });
759
+ }
760
+ // ------------------------------------------------------------------ multi
761
+ toggleOption(opt) {
762
+ const idx = this.selectedValues.indexOf(opt.value);
763
+ if (idx >= 0) {
764
+ this.selectedValues.splice(idx, 1);
765
+ }
766
+ else {
767
+ this.selectedValues.push(opt.value);
768
+ }
769
+ this.emitMulti();
770
+ }
771
+ emitMulti() {
772
+ if (!this.selectedValues.length) {
773
+ this.removeThis();
774
+ return;
775
+ }
776
+ const filter = {
777
+ filterName: this.column.label,
778
+ columnId: this.field,
779
+ value: [...this.selectedValues],
780
+ label: this.selectedValues.map((v) => this.labelForValue(v)).join(', '),
781
+ type: this.isAttributeMode ? 'ATTRIBUTE' : 'MULTISELECT',
782
+ semantic: this.isAttributeMode ? this.attrSemantic : 'AnyOf',
783
+ };
784
+ this.filterService.updateFilter(filter);
785
+ }
786
+ setSemantic(semantic) {
787
+ if (this.attrSemantic === semantic) {
788
+ return;
789
+ }
790
+ this.attrSemantic = semantic;
791
+ if (this.selectedValues.length) {
792
+ this.emitMulti();
793
+ }
794
+ }
795
+ // ------------------------------------------------------------------ date range
796
+ selectDatePreset(opt) {
797
+ this.selectedValue = opt.value;
798
+ this.dateRangeStart = null;
799
+ this.dateRangeEnd = null;
800
+ this.open = false;
801
+ if (!opt.value) {
802
+ this.removeThis();
803
+ return;
804
+ }
805
+ const end = moment();
806
+ let start;
807
+ switch (opt.value) {
808
+ case 'hour':
809
+ start = moment().subtract(1, 'hours');
810
+ break;
811
+ case 'week':
812
+ start = moment().subtract(7, 'days');
813
+ break;
814
+ case 'month':
815
+ start = moment().subtract(1, 'month');
816
+ break;
817
+ case 'day':
818
+ default:
819
+ start = moment().subtract(24, 'hours');
820
+ break;
821
+ }
822
+ this.filterService.updateFilter({
823
+ filterName: this.column.label,
824
+ columnId: this.field,
825
+ value: [encodeURIComponent(start.toISOString()), encodeURIComponent(end.toISOString())],
826
+ label: opt.label,
827
+ type: 'DATETIME',
828
+ });
829
+ }
830
+ openCustomRange(event) {
831
+ event.stopPropagation();
832
+ this.open = false;
833
+ // open the Material calendar on the next tick (after the menu closes)
834
+ _.delay(() => this.rangePicker?.open(), 0);
835
+ }
836
+ onRangeStartChanged(date) {
837
+ this.dateRangeStart = date;
838
+ this.emitCustomRange();
839
+ }
840
+ onRangeEndChanged(date) {
841
+ this.dateRangeEnd = date;
842
+ this.emitCustomRange();
843
+ }
844
+ emitCustomRange() {
845
+ if (!this.dateRangeStart || !this.dateRangeEnd) {
846
+ return;
847
+ }
848
+ this.selectedValue = 'custom';
849
+ const start = moment(this.dateRangeStart).startOf('day');
850
+ const end = moment(this.dateRangeEnd).endOf('day');
851
+ this.filterService.updateFilter({
852
+ filterName: this.column.label,
853
+ columnId: this.field,
854
+ value: [encodeURIComponent(start.toISOString()), encodeURIComponent(end.toISOString())],
855
+ label: `${start.format('M/D/YY')} – ${end.format('M/D/YY')}`,
856
+ type: 'DATETIME',
857
+ });
858
+ }
859
+ // ------------------------------------------------------------------ clear
860
+ clear(event) {
861
+ event.stopPropagation();
862
+ this.selectedValue = '';
863
+ this.selectedValues = [];
864
+ this.textValue = '';
865
+ this.dateRangeStart = null;
866
+ this.dateRangeEnd = null;
867
+ this.attrSemantic = 'AnyOf';
868
+ this.removeThis();
869
+ }
870
+ removeThis() {
871
+ this.filterService.removeFilter({
872
+ filterName: this.column.label,
873
+ columnId: this.field,
874
+ value: '',
875
+ label: '',
876
+ });
877
+ }
878
+ // ------------------------------------------------------------------ sync
879
+ syncFromFilters(filters) {
880
+ const match = filters.find((f) => f.columnId === this.field);
881
+ if (!match) {
882
+ this.textValue = '';
883
+ this.selectedValue = '';
884
+ this.selectedValues = [];
885
+ this.dateRangeStart = null;
886
+ this.dateRangeEnd = null;
887
+ return;
888
+ }
889
+ if (this.mode === 'text') {
890
+ this.textValue = match.value ?? '';
891
+ }
892
+ else if (this.mode === 'multi') {
893
+ this.selectedValues = _.isArray(match.value) ? [...match.value] : [match.value];
894
+ if (this.isAttributeMode) {
895
+ this.attrSemantic = match.semantic === 'AllOf' ? 'AllOf' : 'AnyOf';
896
+ }
897
+ }
898
+ else if (this.mode === 'daterange') {
899
+ // keep the locally chosen preset label; match.value is the raw [start,end] range
900
+ }
901
+ else {
902
+ this.selectedValue = match.value;
903
+ }
904
+ }
905
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.34", ngImport: i0, type: ListFilterCellComponent, deps: [{ token: DataTableFilterService }, { token: ListTableService }], target: i0.ɵɵFactoryTarget.Component }); }
906
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.34", type: ListFilterCellComponent, isStandalone: true, selector: "lib-list-filter-cell", inputs: { column: "column" }, viewQueries: [{ propertyName: "trigger", first: true, predicate: ["trigger"], descendants: true }, { propertyName: "rangePicker", first: true, predicate: ["rangePicker"], descendants: true }], ngImport: i0, template: "@switch (mode) {\n @case ('text') {\n <div class=\"fc-text\">\n <input\n class=\"fc-input\"\n type=\"text\"\n [placeholder]=\"column.filterPlaceholder || 'Filter'\"\n [(ngModel)]=\"textValue\"\n (keyup)=\"onTextInput()\"\n />\n @if (textValue) {\n <span class=\"fc-clear\" (click)=\"clear($event)\" title=\"Clear\" aria-label=\"Clear filter\">&times;</span>\n }\n </div>\n }\n @default {\n <button\n #trigger\n type=\"button\"\n class=\"fc-pill\"\n [class.has-value]=\"hasValue\"\n [class.is-open]=\"open\"\n (click)=\"toggleMenu($event)\"\n aria-haspopup=\"listbox\"\n [attr.aria-expanded]=\"open\"\n >\n <span class=\"fc-pill-field\">{{ column.label }}</span>\n @if (hasValue) {\n <span class=\"fc-pill-value\" [title]=\"valueLabel\">{{ valueLabel }}</span>\n <span class=\"fc-clear\" (click)=\"clear($event)\" title=\"Clear\" aria-label=\"Clear filter\">&times;</span>\n } @else {\n <span class=\"fc-caret\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 10 6\" width=\"9\" height=\"6\"><path fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M1 1.5 5 5 9 1.5\"/></svg>\n </span>\n }\n </button>\n\n @if (open) {\n <div\n class=\"fc-menu\"\n (clickOutside)=\"closeMenu()\"\n [ngStyle]=\"{ left: menuLeft + 'px', top: menuTop + 'px', 'min-width.px': menuWidth }\"\n >\n @if (mode === 'multi') {\n <div class=\"fc-search\">\n <svg class=\"fc-search-icon\" viewBox=\"0 0 16 16\" width=\"13\" height=\"13\" aria-hidden=\"true\">\n <circle cx=\"7\" cy=\"7\" r=\"4.3\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n <line x1=\"10.4\" y1=\"10.4\" x2=\"14.6\" y2=\"14.6\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/>\n </svg>\n <input\n class=\"fc-search-input\"\n type=\"text\"\n [(ngModel)]=\"attrSearch\"\n [placeholder]=\"isAttributeMode ? 'Search attributes' : 'Search'\"\n (click)=\"$event.stopPropagation()\"\n autocomplete=\"off\"\n />\n </div>\n @if (isAttributeMode) {\n <div class=\"fc-semantic\" role=\"group\" aria-label=\"Match\" (click)=\"$event.stopPropagation()\">\n <button type=\"button\" class=\"fc-semantic-opt\" [class.active]=\"attrSemantic === 'AnyOf'\" (click)=\"setSemantic('AnyOf')\">Any of</button>\n <button type=\"button\" class=\"fc-semantic-opt\" [class.active]=\"attrSemantic === 'AllOf'\" (click)=\"setSemantic('AllOf')\">All of</button>\n </div>\n }\n @if (!filteredOptions.length) {\n <div class=\"fc-empty\">{{ options.length ? 'No matches' : (isAttributeMode ? 'No attributes available' : 'No options available') }}</div>\n }\n <div class=\"fc-opt-list\">\n @for (opt of filteredOptions; track opt.value) {\n <button type=\"button\" class=\"fc-opt\" [class.selected]=\"isOptionSelected(opt)\" (click)=\"toggleOption(opt)\">\n <span class=\"fc-check\" aria-hidden=\"true\">\n @if (isOptionSelected(opt)) {\n <svg viewBox=\"0 0 12 12\" width=\"10\" height=\"10\"><path fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2 6.3 4.6 9 10 3\"/></svg>\n }\n </span>\n @if (isAttributeMode) {\n <span class=\"fc-attr\" [class.at]=\"isNamedOption(opt)\">{{ optionLabel(opt) }}</span>\n } @else if (opt.chipClass) {\n <span [ngClass]=\"opt.chipClass\">{{ opt.label }}</span>\n } @else {\n <span class=\"fc-opt-label\">{{ optionLabel(opt) }}</span>\n }\n </button>\n }\n </div>\n } @else if (mode === 'daterange') {\n @for (opt of options; track opt.value) {\n <button type=\"button\" class=\"fc-opt\" [class.selected]=\"isOptionSelected(opt)\" (click)=\"selectDatePreset(opt)\">\n <span class=\"fc-opt-label\">{{ opt.label }}</span>\n </button>\n }\n <div class=\"fc-divider\"></div>\n <button type=\"button\" class=\"fc-opt\" [class.selected]=\"selectedValue === 'custom'\" (click)=\"openCustomRange($event)\">\n <span class=\"fc-opt-label\">Custom range\u2026</span>\n </button>\n } @else {\n @for (opt of options; track opt.value) {\n <button type=\"button\" class=\"fc-opt\" [class.selected]=\"isOptionSelected(opt)\" (click)=\"selectOption(opt)\">\n @if (opt.chipClass) {\n <span [ngClass]=\"opt.chipClass\">{{ opt.label }}</span>\n } @else {\n <span class=\"fc-opt-label\">{{ opt.label }}</span>\n }\n </button>\n }\n }\n </div>\n }\n }\n}\n\n@if (mode === 'daterange') {\n <!-- hidden Material range input; the calendar is opened from the \"Custom range\u2026\" option -->\n <mat-date-range-input class=\"fc-hidden-range\" [rangePicker]=\"rangePicker\">\n <input matStartDate [(ngModel)]=\"dateRangeStart\" (dateChange)=\"onRangeStartChanged($event.value)\" tabindex=\"-1\" aria-hidden=\"true\" />\n <input matEndDate [(ngModel)]=\"dateRangeEnd\" (dateChange)=\"onRangeEndChanged($event.value)\" tabindex=\"-1\" aria-hidden=\"true\" />\n </mat-date-range-input>\n <mat-date-range-picker #rangePicker xPosition=\"start\" yPosition=\"below\"></mat-date-range-picker>\n}\n", styles: [":host{display:inline-flex;position:relative}.fc-hidden-range{position:absolute;left:0;top:100%;width:1px;height:1px;opacity:0;pointer-events:none;overflow:hidden}.fc-divider{height:1px;margin:.25rem;background:var(--lt-border)}.fc-text{position:relative;display:flex;align-items:center}.fc-input{width:100%;height:1.875rem;padding:0 1.5rem 0 .5rem;font-family:var(--lt-font);font-size:.75rem;color:var(--lt-cell-text);background:var(--lt-control-bg);border:1px solid var(--lt-border);border-radius:var(--lt-radius);outline:none}.fc-input::placeholder{color:var(--placeholder)}.fc-input:focus{border-color:var(--lt-link)}.fc-pill{display:inline-flex;align-items:center;gap:.375rem;height:var(--lt-toolbar-height);max-width:14rem;padding:0 var(--lt-control-padding-x);font-family:var(--lt-font);font-size:.8125rem;font-weight:var(--lt-control-font-weight);color:var(--lt-cell-text);background:var(--lt-control-bg);border:var(--lt-control-border-full);border-radius:var(--lt-radius);cursor:pointer;white-space:nowrap;transition:border-color .15s,background .15s,color .15s}.fc-pill:hover,.fc-pill.is-open{border-color:var(--lt-link)}.fc-pill.has-value{border-color:var(--lt-link);background:color-mix(in srgb,var(--lt-link) 8%,var(--lt-control-bg))}.fc-pill-field{color:var(--lt-cell-text)}.fc-pill.has-value .fc-pill-field{color:var(--groupText)}.fc-pill.has-value .fc-pill-field:after{content:\":\"}.fc-pill-value{color:var(--lt-link);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fc-caret{flex:0 0 auto;display:inline-flex;color:var(--lt-icon)}.fc-clear{flex:0 0 auto;font-size:1rem;line-height:1;color:var(--lt-icon);cursor:pointer}.fc-clear:hover{color:var(--lt-icon-active)}.fc-text .fc-clear{position:absolute;right:.5rem}.fc-menu{position:fixed;z-index:1000;max-height:20rem;overflow-y:auto;padding:.375rem;background:var(--lt-control-bg);border:1px solid var(--lt-border);border-radius:var(--lt-radius);box-shadow:var(--shadowLarge);animation:fc-in .1s ease-out}@keyframes fc-in{0%{opacity:0;transform:translateY(-.25rem)}to{opacity:1;transform:translateY(0)}}.fc-semantic{display:flex;gap:.1875rem;margin:.125rem 0 .4375rem;padding:.1875rem;background:color-mix(in srgb,var(--lt-border) 30%,var(--lt-control-bg));border:1px solid var(--lt-border);border-radius:var(--lt-radius)}.fc-semantic-opt{flex:1 1 0;padding:.4375rem .5rem;font-family:var(--lt-font);font-size:.8125rem;font-weight:500;color:var(--lt-cell-text);background:transparent;border:none;border-radius:calc(var(--lt-radius) - .0625rem);cursor:pointer}.fc-semantic-opt:hover:not(.active){color:var(--lt-link)}.fc-semantic-opt.active{color:var(--lt-link);background:var(--lt-control-bg);box-shadow:0 1px 2px color-mix(in srgb,var(--lt-cell-text) 14%,transparent)}.fc-opt{display:flex;align-items:center;gap:.5625rem;width:100%;padding:.5rem;font-family:var(--lt-font);font-size:.875rem;color:var(--lt-cell-text);background:transparent;border:none;border-radius:var(--lt-radius);cursor:pointer;text-align:left}.fc-opt:hover{background:var(--lt-row-hover)}.fc-opt.selected{color:var(--lt-link);font-weight:600}.fc-opt-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fc-check{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.125rem;height:1.125rem;box-sizing:border-box;border:.09375rem solid var(--lt-border);border-radius:.25rem;color:var(--lt-control-bg);background:var(--lt-control-bg)}.fc-opt.selected .fc-check{background:var(--lt-link);border-color:var(--lt-link)}.fc-empty{padding:.5rem;font-size:.75rem;color:var(--groupText)}.fc-search{position:sticky;top:0;z-index:1;padding:.125rem 0 .4375rem;background:var(--lt-control-bg)}.fc-search-icon{position:absolute;left:.625rem;top:.125rem;display:flex;align-items:center;height:2.125rem;color:var(--lt-icon);pointer-events:none}.fc-search-input{width:100%;box-sizing:border-box;height:2.125rem;padding:0 .625rem 0 2rem;font-family:var(--lt-font);font-size:.8125rem;color:var(--lt-cell-text);background:var(--lt-control-bg);border:1px solid var(--lt-border);border-radius:var(--lt-radius);outline:none}.fc-search-input::placeholder{color:var(--placeholder)}.fc-search-input:focus{border-color:var(--lt-link)}.fc-opt-list{display:flex;flex-direction:column}.fc-attr{display:inline-flex;align-items:center;padding:.1875rem .5rem;font-size:.8125rem;font-weight:600;line-height:1.4;border-radius:.25rem;color:var(--lt-link);background:color-mix(in srgb,var(--lt-link) 12%,transparent);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fc-attr.at{color:var(--secondary);background:color-mix(in srgb,var(--secondary) 14%,transparent)}.fc-opt.selected .fc-attr{color:var(--lt-link)}.fc-opt.selected .fc-attr.at{color:var(--secondary)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i5.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: i5.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i5.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatDatepickerModule }, { kind: "component", type: i5$1.MatDateRangeInput, selector: "mat-date-range-input", inputs: ["rangePicker", "required", "dateFilter", "min", "max", "disabled", "separator", "comparisonStart", "comparisonEnd"], exportAs: ["matDateRangeInput"] }, { kind: "directive", type: i5$1.MatStartDate, selector: "input[matStartDate]", outputs: ["dateChange", "dateInput"] }, { kind: "directive", type: i5$1.MatEndDate, selector: "input[matEndDate]", outputs: ["dateChange", "dateInput"] }, { kind: "component", type: i5$1.MatDateRangePicker, selector: "mat-date-range-picker", exportAs: ["matDateRangePicker"] }, { kind: "ngmodule", type: MatNativeDateModule }, { kind: "directive", type: ClickOutsideDirective, selector: "[clickOutside]", outputs: ["clickOutside"] }] }); }
907
+ }
908
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.34", ngImport: i0, type: ListFilterCellComponent, decorators: [{
909
+ type: Component,
910
+ args: [{ selector: 'lib-list-filter-cell', standalone: true, imports: [
911
+ CommonModule,
912
+ FormsModule,
913
+ MatDatepickerModule,
914
+ MatNativeDateModule,
915
+ ClickOutsideDirective,
916
+ ], template: "@switch (mode) {\n @case ('text') {\n <div class=\"fc-text\">\n <input\n class=\"fc-input\"\n type=\"text\"\n [placeholder]=\"column.filterPlaceholder || 'Filter'\"\n [(ngModel)]=\"textValue\"\n (keyup)=\"onTextInput()\"\n />\n @if (textValue) {\n <span class=\"fc-clear\" (click)=\"clear($event)\" title=\"Clear\" aria-label=\"Clear filter\">&times;</span>\n }\n </div>\n }\n @default {\n <button\n #trigger\n type=\"button\"\n class=\"fc-pill\"\n [class.has-value]=\"hasValue\"\n [class.is-open]=\"open\"\n (click)=\"toggleMenu($event)\"\n aria-haspopup=\"listbox\"\n [attr.aria-expanded]=\"open\"\n >\n <span class=\"fc-pill-field\">{{ column.label }}</span>\n @if (hasValue) {\n <span class=\"fc-pill-value\" [title]=\"valueLabel\">{{ valueLabel }}</span>\n <span class=\"fc-clear\" (click)=\"clear($event)\" title=\"Clear\" aria-label=\"Clear filter\">&times;</span>\n } @else {\n <span class=\"fc-caret\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 10 6\" width=\"9\" height=\"6\"><path fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M1 1.5 5 5 9 1.5\"/></svg>\n </span>\n }\n </button>\n\n @if (open) {\n <div\n class=\"fc-menu\"\n (clickOutside)=\"closeMenu()\"\n [ngStyle]=\"{ left: menuLeft + 'px', top: menuTop + 'px', 'min-width.px': menuWidth }\"\n >\n @if (mode === 'multi') {\n <div class=\"fc-search\">\n <svg class=\"fc-search-icon\" viewBox=\"0 0 16 16\" width=\"13\" height=\"13\" aria-hidden=\"true\">\n <circle cx=\"7\" cy=\"7\" r=\"4.3\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n <line x1=\"10.4\" y1=\"10.4\" x2=\"14.6\" y2=\"14.6\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/>\n </svg>\n <input\n class=\"fc-search-input\"\n type=\"text\"\n [(ngModel)]=\"attrSearch\"\n [placeholder]=\"isAttributeMode ? 'Search attributes' : 'Search'\"\n (click)=\"$event.stopPropagation()\"\n autocomplete=\"off\"\n />\n </div>\n @if (isAttributeMode) {\n <div class=\"fc-semantic\" role=\"group\" aria-label=\"Match\" (click)=\"$event.stopPropagation()\">\n <button type=\"button\" class=\"fc-semantic-opt\" [class.active]=\"attrSemantic === 'AnyOf'\" (click)=\"setSemantic('AnyOf')\">Any of</button>\n <button type=\"button\" class=\"fc-semantic-opt\" [class.active]=\"attrSemantic === 'AllOf'\" (click)=\"setSemantic('AllOf')\">All of</button>\n </div>\n }\n @if (!filteredOptions.length) {\n <div class=\"fc-empty\">{{ options.length ? 'No matches' : (isAttributeMode ? 'No attributes available' : 'No options available') }}</div>\n }\n <div class=\"fc-opt-list\">\n @for (opt of filteredOptions; track opt.value) {\n <button type=\"button\" class=\"fc-opt\" [class.selected]=\"isOptionSelected(opt)\" (click)=\"toggleOption(opt)\">\n <span class=\"fc-check\" aria-hidden=\"true\">\n @if (isOptionSelected(opt)) {\n <svg viewBox=\"0 0 12 12\" width=\"10\" height=\"10\"><path fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2 6.3 4.6 9 10 3\"/></svg>\n }\n </span>\n @if (isAttributeMode) {\n <span class=\"fc-attr\" [class.at]=\"isNamedOption(opt)\">{{ optionLabel(opt) }}</span>\n } @else if (opt.chipClass) {\n <span [ngClass]=\"opt.chipClass\">{{ opt.label }}</span>\n } @else {\n <span class=\"fc-opt-label\">{{ optionLabel(opt) }}</span>\n }\n </button>\n }\n </div>\n } @else if (mode === 'daterange') {\n @for (opt of options; track opt.value) {\n <button type=\"button\" class=\"fc-opt\" [class.selected]=\"isOptionSelected(opt)\" (click)=\"selectDatePreset(opt)\">\n <span class=\"fc-opt-label\">{{ opt.label }}</span>\n </button>\n }\n <div class=\"fc-divider\"></div>\n <button type=\"button\" class=\"fc-opt\" [class.selected]=\"selectedValue === 'custom'\" (click)=\"openCustomRange($event)\">\n <span class=\"fc-opt-label\">Custom range\u2026</span>\n </button>\n } @else {\n @for (opt of options; track opt.value) {\n <button type=\"button\" class=\"fc-opt\" [class.selected]=\"isOptionSelected(opt)\" (click)=\"selectOption(opt)\">\n @if (opt.chipClass) {\n <span [ngClass]=\"opt.chipClass\">{{ opt.label }}</span>\n } @else {\n <span class=\"fc-opt-label\">{{ opt.label }}</span>\n }\n </button>\n }\n }\n </div>\n }\n }\n}\n\n@if (mode === 'daterange') {\n <!-- hidden Material range input; the calendar is opened from the \"Custom range\u2026\" option -->\n <mat-date-range-input class=\"fc-hidden-range\" [rangePicker]=\"rangePicker\">\n <input matStartDate [(ngModel)]=\"dateRangeStart\" (dateChange)=\"onRangeStartChanged($event.value)\" tabindex=\"-1\" aria-hidden=\"true\" />\n <input matEndDate [(ngModel)]=\"dateRangeEnd\" (dateChange)=\"onRangeEndChanged($event.value)\" tabindex=\"-1\" aria-hidden=\"true\" />\n </mat-date-range-input>\n <mat-date-range-picker #rangePicker xPosition=\"start\" yPosition=\"below\"></mat-date-range-picker>\n}\n", styles: [":host{display:inline-flex;position:relative}.fc-hidden-range{position:absolute;left:0;top:100%;width:1px;height:1px;opacity:0;pointer-events:none;overflow:hidden}.fc-divider{height:1px;margin:.25rem;background:var(--lt-border)}.fc-text{position:relative;display:flex;align-items:center}.fc-input{width:100%;height:1.875rem;padding:0 1.5rem 0 .5rem;font-family:var(--lt-font);font-size:.75rem;color:var(--lt-cell-text);background:var(--lt-control-bg);border:1px solid var(--lt-border);border-radius:var(--lt-radius);outline:none}.fc-input::placeholder{color:var(--placeholder)}.fc-input:focus{border-color:var(--lt-link)}.fc-pill{display:inline-flex;align-items:center;gap:.375rem;height:var(--lt-toolbar-height);max-width:14rem;padding:0 var(--lt-control-padding-x);font-family:var(--lt-font);font-size:.8125rem;font-weight:var(--lt-control-font-weight);color:var(--lt-cell-text);background:var(--lt-control-bg);border:var(--lt-control-border-full);border-radius:var(--lt-radius);cursor:pointer;white-space:nowrap;transition:border-color .15s,background .15s,color .15s}.fc-pill:hover,.fc-pill.is-open{border-color:var(--lt-link)}.fc-pill.has-value{border-color:var(--lt-link);background:color-mix(in srgb,var(--lt-link) 8%,var(--lt-control-bg))}.fc-pill-field{color:var(--lt-cell-text)}.fc-pill.has-value .fc-pill-field{color:var(--groupText)}.fc-pill.has-value .fc-pill-field:after{content:\":\"}.fc-pill-value{color:var(--lt-link);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fc-caret{flex:0 0 auto;display:inline-flex;color:var(--lt-icon)}.fc-clear{flex:0 0 auto;font-size:1rem;line-height:1;color:var(--lt-icon);cursor:pointer}.fc-clear:hover{color:var(--lt-icon-active)}.fc-text .fc-clear{position:absolute;right:.5rem}.fc-menu{position:fixed;z-index:1000;max-height:20rem;overflow-y:auto;padding:.375rem;background:var(--lt-control-bg);border:1px solid var(--lt-border);border-radius:var(--lt-radius);box-shadow:var(--shadowLarge);animation:fc-in .1s ease-out}@keyframes fc-in{0%{opacity:0;transform:translateY(-.25rem)}to{opacity:1;transform:translateY(0)}}.fc-semantic{display:flex;gap:.1875rem;margin:.125rem 0 .4375rem;padding:.1875rem;background:color-mix(in srgb,var(--lt-border) 30%,var(--lt-control-bg));border:1px solid var(--lt-border);border-radius:var(--lt-radius)}.fc-semantic-opt{flex:1 1 0;padding:.4375rem .5rem;font-family:var(--lt-font);font-size:.8125rem;font-weight:500;color:var(--lt-cell-text);background:transparent;border:none;border-radius:calc(var(--lt-radius) - .0625rem);cursor:pointer}.fc-semantic-opt:hover:not(.active){color:var(--lt-link)}.fc-semantic-opt.active{color:var(--lt-link);background:var(--lt-control-bg);box-shadow:0 1px 2px color-mix(in srgb,var(--lt-cell-text) 14%,transparent)}.fc-opt{display:flex;align-items:center;gap:.5625rem;width:100%;padding:.5rem;font-family:var(--lt-font);font-size:.875rem;color:var(--lt-cell-text);background:transparent;border:none;border-radius:var(--lt-radius);cursor:pointer;text-align:left}.fc-opt:hover{background:var(--lt-row-hover)}.fc-opt.selected{color:var(--lt-link);font-weight:600}.fc-opt-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fc-check{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.125rem;height:1.125rem;box-sizing:border-box;border:.09375rem solid var(--lt-border);border-radius:.25rem;color:var(--lt-control-bg);background:var(--lt-control-bg)}.fc-opt.selected .fc-check{background:var(--lt-link);border-color:var(--lt-link)}.fc-empty{padding:.5rem;font-size:.75rem;color:var(--groupText)}.fc-search{position:sticky;top:0;z-index:1;padding:.125rem 0 .4375rem;background:var(--lt-control-bg)}.fc-search-icon{position:absolute;left:.625rem;top:.125rem;display:flex;align-items:center;height:2.125rem;color:var(--lt-icon);pointer-events:none}.fc-search-input{width:100%;box-sizing:border-box;height:2.125rem;padding:0 .625rem 0 2rem;font-family:var(--lt-font);font-size:.8125rem;color:var(--lt-cell-text);background:var(--lt-control-bg);border:1px solid var(--lt-border);border-radius:var(--lt-radius);outline:none}.fc-search-input::placeholder{color:var(--placeholder)}.fc-search-input:focus{border-color:var(--lt-link)}.fc-opt-list{display:flex;flex-direction:column}.fc-attr{display:inline-flex;align-items:center;padding:.1875rem .5rem;font-size:.8125rem;font-weight:600;line-height:1.4;border-radius:.25rem;color:var(--lt-link);background:color-mix(in srgb,var(--lt-link) 12%,transparent);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fc-attr.at{color:var(--secondary);background:color-mix(in srgb,var(--secondary) 14%,transparent)}.fc-opt.selected .fc-attr{color:var(--lt-link)}.fc-opt.selected .fc-attr.at{color:var(--secondary)}\n"] }]
917
+ }], ctorParameters: () => [{ type: DataTableFilterService }, { type: ListTableService }], propDecorators: { column: [{
918
+ type: Input
919
+ }], trigger: [{
920
+ type: ViewChild,
921
+ args: ['trigger']
922
+ }], rangePicker: [{
923
+ type: ViewChild,
924
+ args: ['rangePicker']
925
+ }] } });
926
+
927
+ /*
928
+ Copyright NetFoundry Inc.
929
+
930
+ Licensed under the Apache License, Version 2.0 (the "License");
931
+ you may not use this file except in compliance with the License.
932
+ You may obtain a copy of the License at
933
+
934
+ https://www.apache.org/licenses/LICENSE-2.0
935
+
936
+ Unless required by applicable law or agreed to in writing, software
937
+ distributed under the License is distributed on an "AS IS" BASIS,
938
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
939
+ See the License for the specific language governing permissions and
940
+ limitations under the License.
941
+ */
942
+ /** Header cell for {@link ListTableComponent}: label and 3-state sort (asc/desc). */
943
+ class ListHeaderCellComponent {
944
+ constructor() {
945
+ this.sortChanged = new EventEmitter();
946
+ }
947
+ get enableSorting() {
948
+ return !!(this.column?.sortable || this.column?.enableSorting);
949
+ }
950
+ sort() {
951
+ if (!this.enableSorting) {
952
+ return;
953
+ }
954
+ const dir = !this.sortDir || this.sortDir === 'desc' ? 'asc' : 'desc';
955
+ this.sortDir = dir;
956
+ this.column.onSort?.(this.column.key, dir);
957
+ this.sortChanged.emit({ colId: this.column.key, dir });
958
+ }
959
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.34", ngImport: i0, type: ListHeaderCellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
960
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.34", type: ListHeaderCellComponent, isStandalone: true, selector: "lib-list-header-cell", inputs: { column: "column", sortDir: "sortDir" }, outputs: { sortChanged: "sortChanged" }, ngImport: i0, template: "<div\n (click)=\"sort()\"\n [ngClass]=\"{ 'nf-can-sort': enableSorting }\"\n class=\"lt-header-container\"\n id=\"TableHeaderContainer_{{ column.key }}\"\n>\n <div class=\"lt-header-label\">\n <span\n [title]=\"column.label || null\"\n class=\"lt-header-text\"\n [class.is-sorted]=\"sortDir === 'asc' || sortDir === 'desc'\"\n id=\"HeaderName_{{ column.key }}\"\n unselectable=\"on\"\n >{{ column.label }}</span>\n @if (enableSorting) {\n <span class=\"lt-sort\" [class.asc]=\"sortDir === 'asc'\" [class.desc]=\"sortDir === 'desc'\">\n <svg viewBox=\"0 0 8 14\" width=\"8\" height=\"13\" aria-hidden=\"true\">\n <path class=\"lt-sort-up\" d=\"M4 0 7.5 4.5H0.5z\"/>\n <path class=\"lt-sort-down\" d=\"M4 14 0.5 9.5H7.5z\"/>\n </svg>\n </span>\n }\n </div>\n</div>\n", styles: [":host{display:flex;width:100%;align-items:center}.lt-header-container{display:flex;align-items:center;width:100%;height:100%;cursor:default;gap:.25rem;position:relative}.lt-header-container.nf-can-sort{cursor:pointer}.lt-header-label{display:flex;align-items:center;flex:1 1 auto;min-width:0;gap:.25rem}.lt-header-text{color:var(--lt-header-text);font-family:var(--lt-font);font-size:.6875rem;font-weight:600;letter-spacing:.045em;text-transform:uppercase;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .15s}.lt-header-text.is-sorted{color:var(--lt-header-sorted-text, var(--lt-icon-active));font-weight:var(--lt-header-sorted-weight, 700)}.lt-header-text-container{display:flex;align-items:center;min-width:0;flex:1 1 auto}.lt-header-text-container.show-input-filter{width:100%}.lt-sort{display:inline-flex;align-items:center;flex:0 0 auto;margin-left:auto;margin-right:.375rem}.lt-sort svg{display:block}.lt-sort .lt-sort-up,.lt-sort .lt-sort-down{fill:var(--lt-icon);opacity:.35;transition:opacity .15s,fill .15s}.lt-sort.asc .lt-sort-up,.lt-sort.desc .lt-sort-down{fill:var(--lt-icon-active);opacity:1}.nf-hidden{display:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] }); }
961
+ }
962
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.34", ngImport: i0, type: ListHeaderCellComponent, decorators: [{
963
+ type: Component,
964
+ args: [{ selector: 'lib-list-header-cell', standalone: true, imports: [CommonModule], template: "<div\n (click)=\"sort()\"\n [ngClass]=\"{ 'nf-can-sort': enableSorting }\"\n class=\"lt-header-container\"\n id=\"TableHeaderContainer_{{ column.key }}\"\n>\n <div class=\"lt-header-label\">\n <span\n [title]=\"column.label || null\"\n class=\"lt-header-text\"\n [class.is-sorted]=\"sortDir === 'asc' || sortDir === 'desc'\"\n id=\"HeaderName_{{ column.key }}\"\n unselectable=\"on\"\n >{{ column.label }}</span>\n @if (enableSorting) {\n <span class=\"lt-sort\" [class.asc]=\"sortDir === 'asc'\" [class.desc]=\"sortDir === 'desc'\">\n <svg viewBox=\"0 0 8 14\" width=\"8\" height=\"13\" aria-hidden=\"true\">\n <path class=\"lt-sort-up\" d=\"M4 0 7.5 4.5H0.5z\"/>\n <path class=\"lt-sort-down\" d=\"M4 14 0.5 9.5H7.5z\"/>\n </svg>\n </span>\n }\n </div>\n</div>\n", styles: [":host{display:flex;width:100%;align-items:center}.lt-header-container{display:flex;align-items:center;width:100%;height:100%;cursor:default;gap:.25rem;position:relative}.lt-header-container.nf-can-sort{cursor:pointer}.lt-header-label{display:flex;align-items:center;flex:1 1 auto;min-width:0;gap:.25rem}.lt-header-text{color:var(--lt-header-text);font-family:var(--lt-font);font-size:.6875rem;font-weight:600;letter-spacing:.045em;text-transform:uppercase;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .15s}.lt-header-text.is-sorted{color:var(--lt-header-sorted-text, var(--lt-icon-active));font-weight:var(--lt-header-sorted-weight, 700)}.lt-header-text-container{display:flex;align-items:center;min-width:0;flex:1 1 auto}.lt-header-text-container.show-input-filter{width:100%}.lt-sort{display:inline-flex;align-items:center;flex:0 0 auto;margin-left:auto;margin-right:.375rem}.lt-sort svg{display:block}.lt-sort .lt-sort-up,.lt-sort .lt-sort-down{fill:var(--lt-icon);opacity:.35;transition:opacity .15s,fill .15s}.lt-sort.asc .lt-sort-up,.lt-sort.desc .lt-sort-down{fill:var(--lt-icon-active);opacity:1}.nf-hidden{display:none}\n"] }]
965
+ }], propDecorators: { column: [{
966
+ type: Input
967
+ }], sortDir: [{
968
+ type: Input
969
+ }], sortChanged: [{
970
+ type: Output
971
+ }] } });
972
+
973
+ /*
974
+ Copyright NetFoundry Inc.
975
+
976
+ Licensed under the Apache License, Version 2.0 (the "License");
977
+ you may not use this file except in compliance with the License.
978
+ You may obtain a copy of the License at
979
+
980
+ https://www.apache.org/licenses/LICENSE-2.0
981
+
982
+ Unless required by applicable law or agreed to in writing, software
983
+ distributed under the License is distributed on an "AS IS" BASIS,
984
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
985
+ See the License for the specific language governing permissions and
986
+ limitations under the License.
987
+ */
988
+ /** Auto-refresh control: a pill + interval menu. Owns the timer and emits `refresh` each tick. */
989
+ class ListTableAutoRefreshComponent {
990
+ constructor() {
991
+ this._seconds = 0;
992
+ /** Skip a tick while a refresh is already in flight. */
993
+ this.loading = false;
994
+ this.intervals = [
995
+ { label: '15s', seconds: 15 },
996
+ { label: '30s', seconds: 30 },
997
+ { label: '1m', seconds: 60 },
998
+ { label: '5m', seconds: 300 },
999
+ ];
1000
+ this.refresh = new EventEmitter();
1001
+ this.secondsChange = new EventEmitter();
1002
+ this.open = false;
1003
+ /** Menu geometry, all measured when the menu opens: left offset so labels line up
1004
+ * under the button's value, width so the right edge lands on the button's right
1005
+ * edge, and right padding so the check centers under the chevron. */
1006
+ this.menuLeft = 0;
1007
+ this.menuWidth = 0;
1008
+ this.menuPadRight = 0;
1009
+ }
1010
+ /** Current interval in seconds; 0 means off. */
1011
+ set seconds(value) {
1012
+ this._seconds = value || 0;
1013
+ this.restartTimer();
1014
+ }
1015
+ get seconds() {
1016
+ return this._seconds;
1017
+ }
1018
+ get enabled() {
1019
+ return this._seconds > 0;
1020
+ }
1021
+ get valueLabel() {
1022
+ if (!this.enabled) {
1023
+ return 'Off';
1024
+ }
1025
+ const match = this.intervals.find((i) => i.seconds === this._seconds);
1026
+ return match ? match.label : `${this._seconds}s`;
1027
+ }
1028
+ toggleMenu(event) {
1029
+ event.stopPropagation();
1030
+ if (this.open) {
1031
+ this.open = false;
1032
+ return;
1033
+ }
1034
+ this.alignMenuToValue();
1035
+ this.open = true;
1036
+ }
1037
+ /** Position + size the menu so its labels sit directly under the button's value,
1038
+ * its right edge lands on the button's right edge (one shared border), and the
1039
+ * right-aligned check centers under the chevron. */
1040
+ alignMenuToValue() {
1041
+ const el = this.arEl?.nativeElement;
1042
+ const btn = el?.querySelector('.lt-ar-btn');
1043
+ const value = this.valueEl?.nativeElement?.getBoundingClientRect();
1044
+ const chevron = this.chevronEl?.nativeElement?.getBoundingClientRect();
1045
+ const container = el?.getBoundingClientRect();
1046
+ if (!btn || !value || !chevron || !container) {
1047
+ return;
1048
+ }
1049
+ const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
1050
+ // left padding we back out of the value position so labels sit under it
1051
+ const itemPadLeft = 0.875 * rem;
1052
+ // half the check's box, so we can land its center rather than its edge
1053
+ const tickHalf = 0.375 * rem;
1054
+ const borderWidth = parseFloat(getComputedStyle(btn).borderRightWidth) || 1;
1055
+ this.menuLeft = Math.max(0, value.left - container.left - itemPadLeft);
1056
+ this.menuWidth = container.width - this.menuLeft;
1057
+ // the check is right-aligned, so its right padding is the gap from the menu's
1058
+ // right edge to the chevron center, less the border and half the check box
1059
+ const chevronCenter = (chevron.left + chevron.right) / 2;
1060
+ this.menuPadRight = Math.max(0, container.right - chevronCenter - borderWidth - tickHalf);
1061
+ }
1062
+ closeMenu() {
1063
+ this.open = false;
1064
+ }
1065
+ isSelected(seconds) {
1066
+ return this._seconds === seconds;
1067
+ }
1068
+ select(seconds) {
1069
+ this.open = false;
1070
+ if (seconds === this._seconds) {
1071
+ return;
1072
+ }
1073
+ this._seconds = seconds;
1074
+ this.secondsChange.emit(seconds);
1075
+ this.restartTimer();
1076
+ }
1077
+ restartTimer() {
1078
+ if (this.timer) {
1079
+ clearInterval(this.timer);
1080
+ this.timer = undefined;
1081
+ }
1082
+ if (this._seconds > 0) {
1083
+ this.timer = setInterval(() => {
1084
+ if (!this.loading) {
1085
+ this.refresh.emit();
1086
+ }
1087
+ }, this._seconds * 1000);
1088
+ }
1089
+ }
1090
+ ngOnDestroy() {
1091
+ if (this.timer) {
1092
+ clearInterval(this.timer);
1093
+ }
1094
+ }
1095
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.34", ngImport: i0, type: ListTableAutoRefreshComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1096
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.34", type: ListTableAutoRefreshComponent, isStandalone: true, selector: "lib-list-table-auto-refresh", inputs: { seconds: "seconds", loading: "loading", intervals: "intervals" }, outputs: { refresh: "refresh", secondsChange: "secondsChange" }, viewQueries: [{ propertyName: "arEl", first: true, predicate: ["arEl"], descendants: true }, { propertyName: "valueEl", first: true, predicate: ["valueEl"], descendants: true }, { propertyName: "chevronEl", first: true, predicate: ["chevronEl"], descendants: true }], ngImport: i0, template: "<div class=\"lt-ar\" #arEl [class.is-open]=\"open\">\n <button\n type=\"button\"\n class=\"lt-ar-btn\"\n [class.is-on]=\"enabled\"\n [class.is-open]=\"open\"\n (click)=\"toggleMenu($event)\"\n aria-haspopup=\"menu\"\n [attr.aria-expanded]=\"open\"\n [attr.aria-label]=\"'Auto-refresh: ' + valueLabel\"\n id=\"AutoRefreshButton\"\n >\n <svg class=\"lt-ar-icon\" viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" aria-hidden=\"true\">\n <path fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 6.4A5.6 5.6 0 1 1 2.4 9\"/>\n <path fill=\"currentColor\" d=\"M1.4 3.1l.7 3.3 3.2-.9z\"/>\n <path fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 5.3V8.2l2 1.3\"/>\n </svg>\n <span class=\"lt-ar-prefix\">Auto-refresh:</span>\n <span class=\"lt-ar-value\" #valueEl>{{ valueLabel }}</span>\n <svg class=\"lt-ar-chevron\" #chevronEl viewBox=\"0 0 10 6\" width=\"9\" height=\"6\" aria-hidden=\"true\"><path fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M1 1.5 5 5 9 1.5\"/></svg>\n </button>\n\n @if (open) {\n <div class=\"lt-ar-menu\" [style.left.px]=\"menuLeft\" [style.width.px]=\"menuWidth\" [style.--lt-ar-pad-right]=\"menuPadRight + 'px'\" (clickOutside)=\"closeMenu()\" role=\"menu\">\n <button type=\"button\" class=\"lt-ar-item\" [class.is-selected]=\"!enabled\" (click)=\"select(0)\" role=\"menuitemradio\" [attr.aria-checked]=\"!enabled\">\n <span class=\"lt-ar-label\">Off</span>\n <span class=\"lt-ar-tick\" aria-hidden=\"true\">\n @if (!enabled) {\n <svg viewBox=\"0 0 12 12\" width=\"11\" height=\"11\"><path fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2 6.3 4.6 9 10 3\"/></svg>\n }\n </span>\n </button>\n @for (interval of intervals; track interval.seconds) {\n <button type=\"button\" class=\"lt-ar-item\" [class.is-selected]=\"isSelected(interval.seconds)\" (click)=\"select(interval.seconds)\" role=\"menuitemradio\" [attr.aria-checked]=\"isSelected(interval.seconds)\">\n <span class=\"lt-ar-label\">{{ interval.label }}</span>\n <span class=\"lt-ar-tick\" aria-hidden=\"true\">\n @if (isSelected(interval.seconds)) {\n <svg viewBox=\"0 0 12 12\" width=\"11\" height=\"11\"><path fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2 6.3 4.6 9 10 3\"/></svg>\n }\n </span>\n </button>\n }\n </div>\n }\n</div>\n", styles: [":host{display:inline-flex}.lt-ar{position:relative;display:inline-flex}.lt-ar-btn{display:inline-flex;align-items:center;gap:var(--lt-control-gap);height:var(--lt-toolbar-height);padding:0 var(--lt-control-padding-x);font-family:var(--lt-font);font-size:var(--lt-control-font-size);font-weight:var(--lt-control-font-weight);color:var(--lt-cell-text);background:var(--lt-control-bg);border:var(--lt-control-border-full);border-radius:var(--lt-radius);cursor:pointer;white-space:nowrap;transition:border-color .15s,background .15s,color .15s}.lt-ar-btn svg{display:block}.lt-ar-btn:hover,.lt-ar-btn.is-on{border-color:var(--lt-link);color:var(--lt-link)}.lt-ar-btn.is-on{background:color-mix(in srgb,var(--lt-link) 8%,var(--lt-control-bg))}.lt-ar-btn.is-open{border-color:var(--lt-link);color:var(--lt-link);border-bottom-right-radius:0}.lt-ar-value{font-weight:700}.lt-ar-chevron{opacity:.7}.lt-ar-menu{position:absolute;top:100%;left:0;z-index:1000;min-width:0;margin-top:calc(var(--lt-control-border-width) * -1);display:flex;flex-direction:column;padding:.25rem 0;background:var(--lt-control-bg);border:var(--lt-control-border-width) solid var(--lt-link);border-radius:0 0 var(--lt-radius) var(--lt-radius);box-shadow:var(--shadowLarge);animation:lt-ar-in .12s ease-out}@keyframes lt-ar-in{0%{opacity:0;transform:translateY(-.125rem)}to{opacity:1;transform:translateY(0)}}.lt-ar-item{display:flex;align-items:center;justify-content:space-between;gap:0;min-height:2rem;padding:0 var(--lt-ar-pad-right, .875rem) 0 .875rem;font-family:var(--lt-font);font-size:var(--lt-control-font-size);color:var(--lt-cell-text);background:transparent;border:none;cursor:pointer;text-align:left;transition:background .1s,color .1s}.lt-ar-item:hover{background:var(--lt-row-hover)}.lt-ar-item.is-selected{color:var(--lt-link);font-weight:700}.lt-ar-tick{display:inline-flex;flex:0 0 auto;justify-content:center;width:.75rem;color:var(--lt-link)}.lt-ar-tick svg{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: ClickOutsideDirective, selector: "[clickOutside]", outputs: ["clickOutside"] }] }); }
1097
+ }
1098
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.34", ngImport: i0, type: ListTableAutoRefreshComponent, decorators: [{
1099
+ type: Component,
1100
+ args: [{ selector: 'lib-list-table-auto-refresh', standalone: true, imports: [CommonModule, ClickOutsideDirective], template: "<div class=\"lt-ar\" #arEl [class.is-open]=\"open\">\n <button\n type=\"button\"\n class=\"lt-ar-btn\"\n [class.is-on]=\"enabled\"\n [class.is-open]=\"open\"\n (click)=\"toggleMenu($event)\"\n aria-haspopup=\"menu\"\n [attr.aria-expanded]=\"open\"\n [attr.aria-label]=\"'Auto-refresh: ' + valueLabel\"\n id=\"AutoRefreshButton\"\n >\n <svg class=\"lt-ar-icon\" viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" aria-hidden=\"true\">\n <path fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 6.4A5.6 5.6 0 1 1 2.4 9\"/>\n <path fill=\"currentColor\" d=\"M1.4 3.1l.7 3.3 3.2-.9z\"/>\n <path fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 5.3V8.2l2 1.3\"/>\n </svg>\n <span class=\"lt-ar-prefix\">Auto-refresh:</span>\n <span class=\"lt-ar-value\" #valueEl>{{ valueLabel }}</span>\n <svg class=\"lt-ar-chevron\" #chevronEl viewBox=\"0 0 10 6\" width=\"9\" height=\"6\" aria-hidden=\"true\"><path fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M1 1.5 5 5 9 1.5\"/></svg>\n </button>\n\n @if (open) {\n <div class=\"lt-ar-menu\" [style.left.px]=\"menuLeft\" [style.width.px]=\"menuWidth\" [style.--lt-ar-pad-right]=\"menuPadRight + 'px'\" (clickOutside)=\"closeMenu()\" role=\"menu\">\n <button type=\"button\" class=\"lt-ar-item\" [class.is-selected]=\"!enabled\" (click)=\"select(0)\" role=\"menuitemradio\" [attr.aria-checked]=\"!enabled\">\n <span class=\"lt-ar-label\">Off</span>\n <span class=\"lt-ar-tick\" aria-hidden=\"true\">\n @if (!enabled) {\n <svg viewBox=\"0 0 12 12\" width=\"11\" height=\"11\"><path fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2 6.3 4.6 9 10 3\"/></svg>\n }\n </span>\n </button>\n @for (interval of intervals; track interval.seconds) {\n <button type=\"button\" class=\"lt-ar-item\" [class.is-selected]=\"isSelected(interval.seconds)\" (click)=\"select(interval.seconds)\" role=\"menuitemradio\" [attr.aria-checked]=\"isSelected(interval.seconds)\">\n <span class=\"lt-ar-label\">{{ interval.label }}</span>\n <span class=\"lt-ar-tick\" aria-hidden=\"true\">\n @if (isSelected(interval.seconds)) {\n <svg viewBox=\"0 0 12 12\" width=\"11\" height=\"11\"><path fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2 6.3 4.6 9 10 3\"/></svg>\n }\n </span>\n </button>\n }\n </div>\n }\n</div>\n", styles: [":host{display:inline-flex}.lt-ar{position:relative;display:inline-flex}.lt-ar-btn{display:inline-flex;align-items:center;gap:var(--lt-control-gap);height:var(--lt-toolbar-height);padding:0 var(--lt-control-padding-x);font-family:var(--lt-font);font-size:var(--lt-control-font-size);font-weight:var(--lt-control-font-weight);color:var(--lt-cell-text);background:var(--lt-control-bg);border:var(--lt-control-border-full);border-radius:var(--lt-radius);cursor:pointer;white-space:nowrap;transition:border-color .15s,background .15s,color .15s}.lt-ar-btn svg{display:block}.lt-ar-btn:hover,.lt-ar-btn.is-on{border-color:var(--lt-link);color:var(--lt-link)}.lt-ar-btn.is-on{background:color-mix(in srgb,var(--lt-link) 8%,var(--lt-control-bg))}.lt-ar-btn.is-open{border-color:var(--lt-link);color:var(--lt-link);border-bottom-right-radius:0}.lt-ar-value{font-weight:700}.lt-ar-chevron{opacity:.7}.lt-ar-menu{position:absolute;top:100%;left:0;z-index:1000;min-width:0;margin-top:calc(var(--lt-control-border-width) * -1);display:flex;flex-direction:column;padding:.25rem 0;background:var(--lt-control-bg);border:var(--lt-control-border-width) solid var(--lt-link);border-radius:0 0 var(--lt-radius) var(--lt-radius);box-shadow:var(--shadowLarge);animation:lt-ar-in .12s ease-out}@keyframes lt-ar-in{0%{opacity:0;transform:translateY(-.125rem)}to{opacity:1;transform:translateY(0)}}.lt-ar-item{display:flex;align-items:center;justify-content:space-between;gap:0;min-height:2rem;padding:0 var(--lt-ar-pad-right, .875rem) 0 .875rem;font-family:var(--lt-font);font-size:var(--lt-control-font-size);color:var(--lt-cell-text);background:transparent;border:none;cursor:pointer;text-align:left;transition:background .1s,color .1s}.lt-ar-item:hover{background:var(--lt-row-hover)}.lt-ar-item.is-selected{color:var(--lt-link);font-weight:700}.lt-ar-tick{display:inline-flex;flex:0 0 auto;justify-content:center;width:.75rem;color:var(--lt-link)}.lt-ar-tick svg{display:block}\n"] }]
1101
+ }], propDecorators: { seconds: [{
1102
+ type: Input
1103
+ }], loading: [{
1104
+ type: Input
1105
+ }], intervals: [{
1106
+ type: Input
1107
+ }], refresh: [{
1108
+ type: Output
1109
+ }], secondsChange: [{
1110
+ type: Output
1111
+ }], arEl: [{
1112
+ type: ViewChild,
1113
+ args: ['arEl']
1114
+ }], valueEl: [{
1115
+ type: ViewChild,
1116
+ args: ['valueEl']
1117
+ }], chevronEl: [{
1118
+ type: ViewChild,
1119
+ args: ['chevronEl']
1120
+ }] } });
1121
+
1122
+ /**
1123
+ * Row selection toggle for {@link ListTableComponent}. Router / exclusive /
1124
+ * reserved / system rows cannot be toggled.
1125
+ */
1126
+ class SelectCellComponent {
1127
+ constructor() {
1128
+ this.item = {
1129
+ id: '',
1130
+ name: '',
1131
+ selected: false,
1132
+ hideSelect: false,
1133
+ };
1134
+ this.disableToggle = false;
1135
+ }
1136
+ set row(v) {
1137
+ this.item = v || this.item;
1138
+ this.disableToggle =
1139
+ _.get(this.item, 'typeId') === 'Router' ||
1140
+ _.get(this.item, 'exclusiveTo') ||
1141
+ _.get(this.item, 'reserved') === true ||
1142
+ _.get(this.item, 'isSystem') === true;
1143
+ }
1144
+ toggle(item) {
1145
+ if (this.disableToggle || !this.toggleItem) {
1146
+ return;
1147
+ }
1148
+ this.toggleItem(item);
1149
+ }
1150
+ get tooltip() {
1151
+ let tooltip = '';
1152
+ if (this.item?.isSystem) {
1153
+ tooltip = 'Deleting "system" entities is not allowed';
1154
+ }
1155
+ else if (this.item?.typeId === 'Router') {
1156
+ tooltip = 'Unable to delete "Router" Identities. Please delete the associated Router instead.';
1157
+ }
1158
+ return tooltip;
1159
+ }
1160
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.34", ngImport: i0, type: SelectCellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1161
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.34", type: SelectCellComponent, isStandalone: true, selector: "lib-select-cell", inputs: { row: "row", toggleItem: "toggleItem" }, ngImport: i0, template: "@if (!item.hideSelect) {\n <div\n #toggleButton\n (click)=\"toggle(item)\"\n [ngClass]=\"{ selected: item.selected, indeterminate: item.indeterminate && !item.selected, disabled: disableToggle }\"\n attr.name=\"RowSelect0_{{ item.name }}\"\n class=\"row-toggle tSelect\"\n id=\"RowSelect0_{{ item.id }}\"\n attr.data-id=\"{{item.id}}\"\n [matTooltip]=\"tooltip\"\n ></div>\n}\n", styles: [".row-toggle.tSelect{box-sizing:border-box;width:1rem;height:1rem;border:1px solid var(--lt-border);border-radius:.25rem;cursor:pointer;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background-color:var(--lt-control-bg);background-repeat:no-repeat;background-position:center center;transition:border-color .12s,background-color .12s}.row-toggle.tSelect:hover{border-color:var(--lt-link)}.row-toggle.tSelect.selected{border-color:var(--lt-link);background-color:var(--lt-link);background-size:.75rem;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' d='M2.5 7.6 5.7 10.8 11.5 3.6'/%3E%3C/svg%3E\")}.row-toggle.tSelect.indeterminate{border-color:var(--lt-link);background-color:var(--lt-link);background-size:.75rem;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath fill='none' stroke='white' stroke-width='2' stroke-linecap='round' d='M3 7 L11 7'/%3E%3C/svg%3E\")}.row-toggle.tSelect.disabled{opacity:.3;cursor:not-allowed;transform:translate(-50%,-50%)!important}.row-toggle.tSelect.disabled:hover{border-color:var(--lt-border);background-color:var(--lt-control-bg)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] }); }
1162
+ }
1163
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.34", ngImport: i0, type: SelectCellComponent, decorators: [{
1164
+ type: Component,
1165
+ args: [{ selector: 'lib-select-cell', standalone: true, imports: [CommonModule, MatTooltipModule], template: "@if (!item.hideSelect) {\n <div\n #toggleButton\n (click)=\"toggle(item)\"\n [ngClass]=\"{ selected: item.selected, indeterminate: item.indeterminate && !item.selected, disabled: disableToggle }\"\n attr.name=\"RowSelect0_{{ item.name }}\"\n class=\"row-toggle tSelect\"\n id=\"RowSelect0_{{ item.id }}\"\n attr.data-id=\"{{item.id}}\"\n [matTooltip]=\"tooltip\"\n ></div>\n}\n", styles: [".row-toggle.tSelect{box-sizing:border-box;width:1rem;height:1rem;border:1px solid var(--lt-border);border-radius:.25rem;cursor:pointer;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background-color:var(--lt-control-bg);background-repeat:no-repeat;background-position:center center;transition:border-color .12s,background-color .12s}.row-toggle.tSelect:hover{border-color:var(--lt-link)}.row-toggle.tSelect.selected{border-color:var(--lt-link);background-color:var(--lt-link);background-size:.75rem;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' d='M2.5 7.6 5.7 10.8 11.5 3.6'/%3E%3C/svg%3E\")}.row-toggle.tSelect.indeterminate{border-color:var(--lt-link);background-color:var(--lt-link);background-size:.75rem;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath fill='none' stroke='white' stroke-width='2' stroke-linecap='round' d='M3 7 L11 7'/%3E%3C/svg%3E\")}.row-toggle.tSelect.disabled{opacity:.3;cursor:not-allowed;transform:translate(-50%,-50%)!important}.row-toggle.tSelect.disabled:hover{border-color:var(--lt-border);background-color:var(--lt-control-bg)}\n"] }]
1166
+ }], propDecorators: { row: [{
1167
+ type: Input
1168
+ }], toggleItem: [{
1169
+ type: Input
1170
+ }] } });
1171
+
1172
+ /*
1173
+ Copyright NetFoundry Inc.
1174
+
1175
+ Licensed under the Apache License, Version 2.0 (the "License");
1176
+ you may not use this file except in compliance with the License.
1177
+ You may obtain a copy of the License at
1178
+
1179
+ https://www.apache.org/licenses/LICENSE-2.0
1180
+
1181
+ Unless required by applicable law or agreed to in writing, software
1182
+ distributed under the License is distributed on an "AS IS" BASIS,
1183
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1184
+ See the License for the specific language governing permissions and
1185
+ limitations under the License.
1186
+ */
1187
+ /**
1188
+ * Modern, themeable placeholder shown inside {@link ListTableComponent} when a list has
1189
+ * no rows. Four variants (first-use empty, no search results, access denied, load error),
1190
+ * per-entity default copy keyed by tableId, and a `[empty-state]` projection slot for a
1191
+ * fully custom placeholder. All copy/glyph/actions can be overridden per table.
1192
+ */
1193
+ class ListEmptyComponent {
1194
+ constructor() {
1195
+ this.variant = 'empty';
1196
+ /** tableId used to look up the default copy (e.g. 'services', 'identities'). */
1197
+ this.entityKey = '';
1198
+ /** Human label used to derive a noun when the key has no tailored copy. */
1199
+ this.entityLabel = '';
1200
+ /** What this list belongs to, e.g. 'network'. Set: "This network has no services";
1201
+ * blank: "There are no services", for lists not tied to a network. */
1202
+ this.scope = '';
1203
+ /** Label for the primary create action (first-use empty only). */
1204
+ this.createLabel = '';
1205
+ /** Whether the create action is offered (false hides it, e.g. read-only tables). */
1206
+ this.hasAdd = true;
1207
+ this.readonly = false;
1208
+ /** Draw the soft card. On for the full-page state; off inside the table, so a card isn't nested in the grid. */
1209
+ this.framed = true;
1210
+ this.create = new EventEmitter();
1211
+ this.clearFilters = new EventEmitter();
1212
+ this.retry = new EventEmitter();
1213
+ }
1214
+ // ---- per-entity default copy (first-use "empty" variant) ---------------------
1215
+ // Keyed by tableId. Anything not listed falls back to a generic noun-based template.
1216
+ static { this.COPY = {
1217
+ services: { noun: 'services', glyph: 'service', cta: 'Create service' },
1218
+ identities: { noun: 'identities', glyph: 'identity', cta: 'Create identity' },
1219
+ 'edge-routers': { noun: 'edge routers', glyph: 'router', cta: 'Create edge router' },
1220
+ 'service-policies': { noun: 'service policies', glyph: 'policy', cta: 'Create service policy' },
1221
+ 'edge-router-policies': { noun: 'edge router policies', glyph: 'policy', cta: 'Create edge router policy' },
1222
+ 'service-edge-router-policies': { noun: 'service edge router policies', glyph: 'policy', cta: 'Create policy' },
1223
+ 'posture-checks': { noun: 'posture checks', glyph: 'policy', cta: 'Create posture check' },
1224
+ configurations: { noun: 'configs', glyph: 'service', cta: 'Create config' },
1225
+ 'config-types': { noun: 'config types', glyph: 'service', cta: 'Create config type' },
1226
+ 'auth-policies': { noun: 'auth policies', glyph: 'policy', cta: 'Create auth policy' },
1227
+ 'certificate-authorities': { noun: 'certificate authorities', glyph: 'policy', cta: 'Create authority' },
1228
+ 'ext-jwt-signers': { noun: 'external JWT signers', glyph: 'policy', cta: 'Create signer' },
1229
+ terminators: { noun: 'terminators', glyph: 'router', cta: 'Create terminator' },
1230
+ 'transit-routers': { noun: 'transit routers', glyph: 'router', cta: 'Create transit router' },
1231
+ }; }
1232
+ // ---- resolved content --------------------------------------------------------
1233
+ get copy() {
1234
+ const hit = ListEmptyComponent.COPY[this.entityKey];
1235
+ if (hit) {
1236
+ return hit;
1237
+ }
1238
+ const noun = (this.entityLabel || 'items').toLowerCase();
1239
+ return {
1240
+ noun,
1241
+ glyph: 'service',
1242
+ cta: this.createLabel || 'Create',
1243
+ };
1244
+ }
1245
+ get glyph() {
1246
+ if (this.icon) {
1247
+ return this.icon;
1248
+ }
1249
+ switch (this.variant) {
1250
+ case 'no-results':
1251
+ return 'search';
1252
+ case 'access-denied':
1253
+ return 'lock';
1254
+ case 'error':
1255
+ return 'alert';
1256
+ default:
1257
+ // one generic icon for the no-data state on every page
1258
+ return 'generic';
1259
+ }
1260
+ }
1261
+ /** Tone drives the glyph colour away from the create-accent for non-create variants. */
1262
+ get tone() {
1263
+ if (this.variant === 'access-denied') {
1264
+ return 'warn';
1265
+ }
1266
+ if (this.variant === 'error') {
1267
+ return 'danger';
1268
+ }
1269
+ return '';
1270
+ }
1271
+ get noun() {
1272
+ return this.copy.noun;
1273
+ }
1274
+ get resolvedTitle() {
1275
+ if (this.title) {
1276
+ return this.title;
1277
+ }
1278
+ switch (this.variant) {
1279
+ case 'no-results':
1280
+ return `No matching ${this.noun}`;
1281
+ case 'access-denied':
1282
+ return 'Access restricted';
1283
+ case 'error':
1284
+ return `Couldn’t load ${this.noun}`;
1285
+ default:
1286
+ // first-use empty: name what the list belongs to when we know it
1287
+ return this.scope
1288
+ ? `This ${this.scope} has no ${this.noun}`
1289
+ : `There are no ${this.noun}`;
1290
+ }
1291
+ }
1292
+ get resolvedMessage() {
1293
+ if (this.message) {
1294
+ return this.message;
1295
+ }
1296
+ switch (this.variant) {
1297
+ case 'no-results':
1298
+ return `No ${this.noun} match your current filters. Try different terms or clear them to see everything.`;
1299
+ case 'access-denied':
1300
+ return `You don’t have permission to view ${this.noun}. Ask your network administrator to grant access.`;
1301
+ case 'error':
1302
+ return 'Something interrupted the request. Check the connection to the controller, then try again.';
1303
+ default:
1304
+ // first-use empty state keeps just the headline, no explanatory subtext
1305
+ return '';
1306
+ }
1307
+ }
1308
+ /** The primary create action shows only on the first-use empty state when allowed. */
1309
+ get showCreate() {
1310
+ return this.variant === 'empty' && this.hasAdd && !this.readonly;
1311
+ }
1312
+ get primaryLabel() {
1313
+ return this.createLabel || this.copy.cta;
1314
+ }
1315
+ onCreate() {
1316
+ this.create.emit();
1317
+ }
1318
+ onClear() {
1319
+ this.clearFilters.emit();
1320
+ }
1321
+ onRetry() {
1322
+ this.retry.emit();
1323
+ }
1324
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.34", ngImport: i0, type: ListEmptyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1325
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.34", type: ListEmptyComponent, isStandalone: true, selector: "lib-list-empty", inputs: { variant: "variant", entityKey: "entityKey", entityLabel: "entityLabel", scope: "scope", title: "title", message: "message", icon: "icon", createLabel: "createLabel", hasAdd: "hasAdd", readonly: "readonly", framed: "framed" }, outputs: { create: "create", clearFilters: "clearFilters", retry: "retry" }, ngImport: i0, template: "<!-- host-projected custom empty state; when nothing is projected the default below renders -->\n<div class=\"le-projected\" #projected><ng-content select=\"[empty-state]\"></ng-content></div>\n\n@if (projected.children.length === 0) {\n <div class=\"le\" [class.le-card]=\"framed\" [attr.data-tone]=\"tone\" role=\"status\">\n <div class=\"le-figure\" aria-hidden=\"true\">\n <div class=\"le-glyph\">\n @switch (glyph) {\n @case ('generic') {\n <!-- overlay (stacked layers): what Ziti is, a layer over the network -->\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M12 3 21 7.8 12 12.6 3 7.8z\"/>\n <path d=\"M3.4 12 12 16.6 20.6 12\" opacity=\"0.72\"/>\n <path d=\"M3.4 16 12 20.6 20.6 16\" opacity=\"0.4\"/>\n </svg>\n }\n @case ('service') {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"5\" r=\"2.2\"/><circle cx=\"5\" cy=\"18\" r=\"2.2\"/><circle cx=\"19\" cy=\"18\" r=\"2.2\"/>\n <path d=\"M12 7.2v4.3M12 11.5l-5.2 4.6M12 11.5l5.2 4.6\"/>\n </svg>\n }\n @case ('identity') {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"9\" r=\"3.2\"/><path d=\"M5.5 19.2a6.8 6.8 0 0 1 13 0\"/><circle cx=\"12\" cy=\"12\" r=\"9.2\" opacity=\"0.35\"/>\n </svg>\n }\n @case ('router') {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"3.5\" y=\"13\" width=\"17\" height=\"6\" rx=\"1.6\"/><path d=\"M7 16h.01M11 16h.01\"/><path d=\"M12 13V9M12 9l-3-3M12 9l3-3\"/>\n </svg>\n }\n @case ('policy') {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M12 3l7 3v5c0 4.4-3 7.6-7 9-4-1.4-7-4.6-7-9V6z\"/><path d=\"M9.2 12.2l2 2 3.6-3.8\"/>\n </svg>\n }\n @case ('search') {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"6\"/><line x1=\"15.5\" y1=\"15.5\" x2=\"20\" y2=\"20\"/>\n </svg>\n }\n @case ('lock') {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"5\" y=\"10.5\" width=\"14\" height=\"9\" rx=\"2\"/><path d=\"M8 10.5V7.5a4 4 0 0 1 8 0v3\"/><circle cx=\"12\" cy=\"15\" r=\"1.1\"/>\n </svg>\n }\n @case ('alert') {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M12 4.5l8.5 15H3.5z\"/><line x1=\"12\" y1=\"10\" x2=\"12\" y2=\"14\"/><circle cx=\"12\" cy=\"16.6\" r=\"0.6\" fill=\"currentColor\"/>\n </svg>\n }\n }\n </div>\n </div>\n\n <h2 class=\"le-title\">{{ resolvedTitle }}</h2>\n @if (resolvedMessage) {\n <p class=\"le-message\">{{ resolvedMessage }}</p>\n }\n\n @if (showCreate || variant === 'no-results' || variant === 'error') {\n <div class=\"le-actions\">\n @if (showCreate) {\n <button type=\"button\" class=\"le-btn le-btn-primary\" (click)=\"onCreate()\" id=\"EmptyCreateButton\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" aria-hidden=\"true\">\n <line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\"/><line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"/>\n </svg>\n <span>{{ primaryLabel }}</span>\n </button>\n }\n @if (variant === 'no-results') {\n <button type=\"button\" class=\"le-btn le-btn-ghost\" (click)=\"onClear()\" id=\"EmptyClearFiltersButton\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" aria-hidden=\"true\">\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/><line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/>\n </svg>\n <span>Clear filters</span>\n </button>\n }\n @if (variant === 'error') {\n <button type=\"button\" class=\"le-btn le-btn-ghost\" (click)=\"onRetry()\" id=\"EmptyRetryButton\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M20 11a8 8 0 1 0-1.3 5.5\"/><path d=\"M20 5v4h-4\"/>\n </svg>\n <span>Retry</span>\n </button>\n }\n </div>\n }\n </div>\n}\n", styles: [":host{--le-accent: var(--primary, var(--lt-link, #0027ab));--le-muted: color-mix(in srgb, var(--lt-cell-text) 62%, transparent);--le-dot: color-mix(in srgb, var(--lt-border) 75%, transparent);--le-soft: color-mix(in srgb, var(--le-accent) 12%, transparent);--le-warn: #b7791f;--le-danger: #c0392b;display:flex;flex-direction:column;align-items:center;justify-content:center;width:100%;min-height:100%;box-sizing:border-box;padding:1.5rem;font-family:var(--lt-font)}.le-projected{display:contents}.le{display:flex;flex-direction:column;align-items:center;text-align:center;gap:.85rem;width:100%;max-width:30rem;box-sizing:border-box;padding:2.75rem 1.75rem}.le-card{border:1px solid var(--lt-border);border-radius:1rem;background:var(--lt-bg);box-shadow:0 1px 2px #141e3c0a,0 12px 32px -12px #141e3c1f}.le-figure{position:relative;width:6.75rem;height:6.75rem;display:grid;place-items:center}.le-figure:before{content:\"\";position:absolute;inset:0;background-image:radial-gradient(circle at center,var(--le-dot) 1.3px,transparent 1.4px);background-size:.8125rem .8125rem;-webkit-mask-image:radial-gradient(circle at center,#000 38%,transparent 72%);mask-image:radial-gradient(circle at center,#000 38%,transparent 72%)}.le-figure:after{content:\"\";position:absolute;width:3.75rem;height:3.75rem;border-radius:50%;background:radial-gradient(circle,var(--le-soft) 0%,transparent 70%)}.le-glyph{position:relative;width:3.625rem;height:3.625rem;border-radius:.9375rem;display:grid;place-items:center;color:var(--le-accent);background:var(--lt-row-bg, var(--lt-bg));border:1px solid var(--lt-border);box-shadow:0 2px 10px #141e3c0f}.le-glyph svg{width:1.625rem;height:1.625rem}.le[data-tone=warn] .le-glyph{color:var(--le-warn)}.le[data-tone=danger] .le-glyph{color:var(--le-danger)}.le-title{margin:0;font-size:1.0625rem;font-weight:600;letter-spacing:-.01em;color:var(--lt-cell-text)}.le-message{margin:0;font-size:.875rem;line-height:1.5;color:var(--le-muted);max-width:42ch}.le-actions{display:flex;gap:.6rem;margin-top:.4rem;flex-wrap:wrap;justify-content:center}.le-btn{font:inherit;font-size:.8125rem;font-weight:500;border-radius:.5rem;padding:.5rem .95rem;cursor:pointer;display:inline-flex;align-items:center;gap:.45rem;border:1px solid transparent}.le-btn svg{width:.9375rem;height:.9375rem}.le-btn-primary{background:var(--le-accent);color:#fff}.le-btn-primary:hover{filter:brightness(1.07)}.le-btn-ghost{background:transparent;color:var(--lt-cell-text);border-color:var(--lt-border)}.le-btn-ghost:hover{border-color:color-mix(in srgb,var(--lt-border) 55%,var(--lt-icon, var(--lt-cell-text)))}.le-btn:focus-visible{outline:2px solid var(--le-accent);outline-offset:2px}@media(prefers-reduced-motion:no-preference){.le-glyph,.le-title,.le-message{animation:le-rise .28s ease both}.le-title{animation-delay:.03s}.le-message{animation-delay:.06s}@keyframes le-rise{0%{opacity:0;transform:translateY(.3rem)}to{opacity:1;transform:none}}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] }); }
1326
+ }
1327
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.34", ngImport: i0, type: ListEmptyComponent, decorators: [{
1328
+ type: Component,
1329
+ args: [{ selector: 'lib-list-empty', standalone: true, imports: [CommonModule], template: "<!-- host-projected custom empty state; when nothing is projected the default below renders -->\n<div class=\"le-projected\" #projected><ng-content select=\"[empty-state]\"></ng-content></div>\n\n@if (projected.children.length === 0) {\n <div class=\"le\" [class.le-card]=\"framed\" [attr.data-tone]=\"tone\" role=\"status\">\n <div class=\"le-figure\" aria-hidden=\"true\">\n <div class=\"le-glyph\">\n @switch (glyph) {\n @case ('generic') {\n <!-- overlay (stacked layers): what Ziti is, a layer over the network -->\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M12 3 21 7.8 12 12.6 3 7.8z\"/>\n <path d=\"M3.4 12 12 16.6 20.6 12\" opacity=\"0.72\"/>\n <path d=\"M3.4 16 12 20.6 20.6 16\" opacity=\"0.4\"/>\n </svg>\n }\n @case ('service') {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"5\" r=\"2.2\"/><circle cx=\"5\" cy=\"18\" r=\"2.2\"/><circle cx=\"19\" cy=\"18\" r=\"2.2\"/>\n <path d=\"M12 7.2v4.3M12 11.5l-5.2 4.6M12 11.5l5.2 4.6\"/>\n </svg>\n }\n @case ('identity') {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"9\" r=\"3.2\"/><path d=\"M5.5 19.2a6.8 6.8 0 0 1 13 0\"/><circle cx=\"12\" cy=\"12\" r=\"9.2\" opacity=\"0.35\"/>\n </svg>\n }\n @case ('router') {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"3.5\" y=\"13\" width=\"17\" height=\"6\" rx=\"1.6\"/><path d=\"M7 16h.01M11 16h.01\"/><path d=\"M12 13V9M12 9l-3-3M12 9l3-3\"/>\n </svg>\n }\n @case ('policy') {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M12 3l7 3v5c0 4.4-3 7.6-7 9-4-1.4-7-4.6-7-9V6z\"/><path d=\"M9.2 12.2l2 2 3.6-3.8\"/>\n </svg>\n }\n @case ('search') {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"6\"/><line x1=\"15.5\" y1=\"15.5\" x2=\"20\" y2=\"20\"/>\n </svg>\n }\n @case ('lock') {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"5\" y=\"10.5\" width=\"14\" height=\"9\" rx=\"2\"/><path d=\"M8 10.5V7.5a4 4 0 0 1 8 0v3\"/><circle cx=\"12\" cy=\"15\" r=\"1.1\"/>\n </svg>\n }\n @case ('alert') {\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M12 4.5l8.5 15H3.5z\"/><line x1=\"12\" y1=\"10\" x2=\"12\" y2=\"14\"/><circle cx=\"12\" cy=\"16.6\" r=\"0.6\" fill=\"currentColor\"/>\n </svg>\n }\n }\n </div>\n </div>\n\n <h2 class=\"le-title\">{{ resolvedTitle }}</h2>\n @if (resolvedMessage) {\n <p class=\"le-message\">{{ resolvedMessage }}</p>\n }\n\n @if (showCreate || variant === 'no-results' || variant === 'error') {\n <div class=\"le-actions\">\n @if (showCreate) {\n <button type=\"button\" class=\"le-btn le-btn-primary\" (click)=\"onCreate()\" id=\"EmptyCreateButton\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" aria-hidden=\"true\">\n <line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\"/><line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"/>\n </svg>\n <span>{{ primaryLabel }}</span>\n </button>\n }\n @if (variant === 'no-results') {\n <button type=\"button\" class=\"le-btn le-btn-ghost\" (click)=\"onClear()\" id=\"EmptyClearFiltersButton\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" aria-hidden=\"true\">\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/><line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/>\n </svg>\n <span>Clear filters</span>\n </button>\n }\n @if (variant === 'error') {\n <button type=\"button\" class=\"le-btn le-btn-ghost\" (click)=\"onRetry()\" id=\"EmptyRetryButton\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M20 11a8 8 0 1 0-1.3 5.5\"/><path d=\"M20 5v4h-4\"/>\n </svg>\n <span>Retry</span>\n </button>\n }\n </div>\n }\n </div>\n}\n", styles: [":host{--le-accent: var(--primary, var(--lt-link, #0027ab));--le-muted: color-mix(in srgb, var(--lt-cell-text) 62%, transparent);--le-dot: color-mix(in srgb, var(--lt-border) 75%, transparent);--le-soft: color-mix(in srgb, var(--le-accent) 12%, transparent);--le-warn: #b7791f;--le-danger: #c0392b;display:flex;flex-direction:column;align-items:center;justify-content:center;width:100%;min-height:100%;box-sizing:border-box;padding:1.5rem;font-family:var(--lt-font)}.le-projected{display:contents}.le{display:flex;flex-direction:column;align-items:center;text-align:center;gap:.85rem;width:100%;max-width:30rem;box-sizing:border-box;padding:2.75rem 1.75rem}.le-card{border:1px solid var(--lt-border);border-radius:1rem;background:var(--lt-bg);box-shadow:0 1px 2px #141e3c0a,0 12px 32px -12px #141e3c1f}.le-figure{position:relative;width:6.75rem;height:6.75rem;display:grid;place-items:center}.le-figure:before{content:\"\";position:absolute;inset:0;background-image:radial-gradient(circle at center,var(--le-dot) 1.3px,transparent 1.4px);background-size:.8125rem .8125rem;-webkit-mask-image:radial-gradient(circle at center,#000 38%,transparent 72%);mask-image:radial-gradient(circle at center,#000 38%,transparent 72%)}.le-figure:after{content:\"\";position:absolute;width:3.75rem;height:3.75rem;border-radius:50%;background:radial-gradient(circle,var(--le-soft) 0%,transparent 70%)}.le-glyph{position:relative;width:3.625rem;height:3.625rem;border-radius:.9375rem;display:grid;place-items:center;color:var(--le-accent);background:var(--lt-row-bg, var(--lt-bg));border:1px solid var(--lt-border);box-shadow:0 2px 10px #141e3c0f}.le-glyph svg{width:1.625rem;height:1.625rem}.le[data-tone=warn] .le-glyph{color:var(--le-warn)}.le[data-tone=danger] .le-glyph{color:var(--le-danger)}.le-title{margin:0;font-size:1.0625rem;font-weight:600;letter-spacing:-.01em;color:var(--lt-cell-text)}.le-message{margin:0;font-size:.875rem;line-height:1.5;color:var(--le-muted);max-width:42ch}.le-actions{display:flex;gap:.6rem;margin-top:.4rem;flex-wrap:wrap;justify-content:center}.le-btn{font:inherit;font-size:.8125rem;font-weight:500;border-radius:.5rem;padding:.5rem .95rem;cursor:pointer;display:inline-flex;align-items:center;gap:.45rem;border:1px solid transparent}.le-btn svg{width:.9375rem;height:.9375rem}.le-btn-primary{background:var(--le-accent);color:#fff}.le-btn-primary:hover{filter:brightness(1.07)}.le-btn-ghost{background:transparent;color:var(--lt-cell-text);border-color:var(--lt-border)}.le-btn-ghost:hover{border-color:color-mix(in srgb,var(--lt-border) 55%,var(--lt-icon, var(--lt-cell-text)))}.le-btn:focus-visible{outline:2px solid var(--le-accent);outline-offset:2px}@media(prefers-reduced-motion:no-preference){.le-glyph,.le-title,.le-message{animation:le-rise .28s ease both}.le-title{animation-delay:.03s}.le-message{animation-delay:.06s}@keyframes le-rise{0%{opacity:0;transform:translateY(.3rem)}to{opacity:1;transform:none}}}\n"] }]
1330
+ }], propDecorators: { variant: [{
1331
+ type: Input
1332
+ }], entityKey: [{
1333
+ type: Input
1334
+ }], entityLabel: [{
1335
+ type: Input
1336
+ }], scope: [{
1337
+ type: Input
1338
+ }], title: [{
1339
+ type: Input
1340
+ }], message: [{
1341
+ type: Input
1342
+ }], icon: [{
1343
+ type: Input
1344
+ }], createLabel: [{
1345
+ type: Input
1346
+ }], hasAdd: [{
1347
+ type: Input
1348
+ }], readonly: [{
1349
+ type: Input
1350
+ }], framed: [{
1351
+ type: Input
1352
+ }], create: [{
1353
+ type: Output
1354
+ }], clearFilters: [{
1355
+ type: Output
1356
+ }], retry: [{
1357
+ type: Output
1358
+ }] } });
1359
+
1360
+ /*
1361
+ Copyright NetFoundry Inc.
1362
+
1363
+ Licensed under the Apache License, Version 2.0 (the "License");
1364
+ you may not use this file except in compliance with the License.
1365
+ You may obtain a copy of the License at
1366
+
1367
+ https://www.apache.org/licenses/LICENSE-2.0
1368
+
1369
+ Unless required by applicable law or agreed to in writing, software
1370
+ distributed under the License is distributed on an "AS IS" BASIS,
1371
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1372
+ See the License for the specific language governing permissions and
1373
+ limitations under the License.
1374
+ */
1375
+ /**
1376
+ * Custom, token-skinnable list table. Columns are declared with {@link ListColumn};
1377
+ * sort, filter, and pagination flow through {@link DataTableFilterService}.
1378
+ */
1379
+ class ListTableComponent {
1380
+ set tableId(id) {
1381
+ this._tableId = id;
1382
+ this.svc.tableId = id;
1383
+ }
1384
+ get tableId() {
1385
+ return this._tableId;
1386
+ }
1387
+ set columns(value) {
1388
+ this._rawColumns = value || [];
1389
+ if (this._initialized) {
1390
+ this.buildColumns();
1391
+ }
1392
+ }
1393
+ set rowData(value) {
1394
+ this._rowData = value || [];
1395
+ // once real rows have arrived, later loads are "refresh" (keep rows + top bar)
1396
+ // rather than "initial" (skeleton). Never flips back on a later empty result.
1397
+ if (this._rowData.length > 0) {
1398
+ this.hasLoadedOnce = true;
1399
+ }
1400
+ this.updateSkeleton();
1401
+ this.rebuildFlatRows();
1402
+ }
1403
+ get rowData() {
1404
+ return this._rowData;
1405
+ }
1406
+ set isLoading(value) {
1407
+ this._isLoading = value;
1408
+ this.updateRefreshBar();
1409
+ this.updateSkeleton();
1410
+ }
1411
+ get isLoading() {
1412
+ return this._isLoading;
1413
+ }
1414
+ /** The logical "first load with nothing on screen yet" state. The skeleton renders
1415
+ * off showSkeleton (below), which debounces this so a very fast first load goes
1416
+ * straight to rows without a skeleton flash. */
1417
+ get isInitialLoad() {
1418
+ return this._isLoading && !this.hasLoadedOnce;
1419
+ }
1420
+ updateSkeleton() {
1421
+ if (this.isInitialLoad) {
1422
+ // arm the show-delay once; a fast first load that finishes before it fires
1423
+ // never shows the skeleton - the rows just appear
1424
+ if (!this.showSkeleton && !this.skeletonShowTimer) {
1425
+ this.skeletonShowTimer = setTimeout(() => {
1426
+ this.skeletonShowTimer = undefined;
1427
+ this.showSkeleton = true;
1428
+ }, this.REFRESH_SHOW_DELAY);
1429
+ }
1430
+ }
1431
+ else {
1432
+ // first load ended (rows arrived) or this is not a first load: cancel a
1433
+ // pending skeleton and hide it now so real rows show at once
1434
+ clearTimeout(this.skeletonShowTimer);
1435
+ this.skeletonShowTimer = undefined;
1436
+ this.showSkeleton = false;
1437
+ }
1438
+ }
1439
+ updateRefreshBar() {
1440
+ // the skeleton covers the first load; the bar is only for reloads with rows present
1441
+ const refreshing = this._isLoading && this.hasLoadedOnce;
1442
+ if (refreshing) {
1443
+ // a reload is running: cancel any pending hide and arm the show timer once
1444
+ clearTimeout(this.refreshHideTimer);
1445
+ this.refreshHideTimer = undefined;
1446
+ if (!this.showRefreshBar && !this.refreshShowTimer) {
1447
+ this.refreshShowTimer = setTimeout(() => {
1448
+ this.refreshShowTimer = undefined;
1449
+ this.showRefreshBar = true;
1450
+ this.refreshShownAt = Date.now();
1451
+ }, this.REFRESH_SHOW_DELAY);
1452
+ }
1453
+ }
1454
+ else {
1455
+ // reload ended (or it was the first load): drop a not-yet-shown bar at once,
1456
+ // but let a visible one finish its minimum time on screen so it doesn't blink
1457
+ if (this.refreshShowTimer) {
1458
+ clearTimeout(this.refreshShowTimer);
1459
+ this.refreshShowTimer = undefined;
1460
+ }
1461
+ if (this.showRefreshBar && !this.refreshHideTimer) {
1462
+ const remaining = Math.max(0, this.REFRESH_MIN_VISIBLE - (Date.now() - this.refreshShownAt));
1463
+ this.refreshHideTimer = setTimeout(() => {
1464
+ this.refreshHideTimer = undefined;
1465
+ this.showRefreshBar = false;
1466
+ }, remaining);
1467
+ }
1468
+ }
1469
+ }
1470
+ /** Row indexes for the skeleton loop (at least one). */
1471
+ get skeletonRows() {
1472
+ const n = Math.max(1, this.skeletonRowCount || 0);
1473
+ return Array.from({ length: n }, (_v, i) => i);
1474
+ }
1475
+ /** Skeleton bar width (as a %) for a given column/row, varied but stable. */
1476
+ skeletonBarWidth(colIndex, rowIndex) {
1477
+ const base = this.SK_WIDTHS[colIndex % this.SK_WIDTHS.length];
1478
+ const jitter = ((rowIndex * 7 + colIndex * 3) % 5) * 4 - 8; // -8..+8
1479
+ return Math.max(30, Math.min(92, base + jitter));
1480
+ }
1481
+ // fitColumns: keep the table within its visible width when resizing - a column can only grow until
1482
+ // the other columns reach their set or minimum width, then the drag stops, so nothing is pushed
1483
+ // off screen into the horizontal scroll. Off by default (columns can grow past the view and scroll).
1484
+ set options(value) {
1485
+ const wasTree = !!this._options?.tree;
1486
+ this._options = value || {};
1487
+ this.toolbarControls = this.normalizeToolbar(this._options.toolbar);
1488
+ // rebuild the render list if tree mode was flipped after init, otherwise flatRows
1489
+ // keeps the pre-toggle shape
1490
+ if (this._initialized && !!this._options.tree !== wasTree) {
1491
+ this.rebuildFlatRows();
1492
+ }
1493
+ }
1494
+ get options() {
1495
+ return this._options;
1496
+ }
1497
+ normalizeToolbar(toolbar) {
1498
+ const resolve = (c) => ({
1499
+ show: c?.show !== false,
1500
+ disabled: !!c?.disabled,
1501
+ });
1502
+ return {
1503
+ search: resolve(toolbar?.search),
1504
+ filters: resolve(toolbar?.filters),
1505
+ refresh: resolve(toolbar?.refresh),
1506
+ autoRefresh: resolve(toolbar?.autoRefresh),
1507
+ columns: resolve(toolbar?.columns),
1508
+ pageSize: resolve(toolbar?.pageSize),
1509
+ pagination: resolve(toolbar?.pagination),
1510
+ };
1511
+ }
1512
+ /** Tree mode: columns per depth level (index = level; level 0 uses `columns`).
1513
+ * Nested rows render in their own grid with a per-level sub-header, so children
1514
+ * need not match the parent. Unset renders children with the parent's columns.
1515
+ * The bound value is the default; users can show/hide/reorder/resize child columns
1516
+ * per depth from the Columns menu, persisted per table. */
1517
+ set childColumnsByLevel(v) {
1518
+ this._declaredChildColumnsByLevel = v;
1519
+ this.buildChildColumns();
1520
+ }
1521
+ get childColumnsByLevel() {
1522
+ return this._childColumnsByLevel;
1523
+ }
1524
+ invalidateColumnCache() {
1525
+ this._childColsVersion++;
1526
+ }
1527
+ syncTreeMemo() {
1528
+ if (this._treeMemoVersion !== this._childColsVersion) {
1529
+ this._treeKeyCache.clear();
1530
+ this._leadOffsetCache.clear();
1531
+ this._treeMemoVersion = this._childColsVersion;
1532
+ }
1533
+ }
1534
+ /** Rebuilds the effective per-level child columns from the declared defaults with
1535
+ * any persisted order/visibility/width applied. */
1536
+ buildChildColumns() {
1537
+ const declared = this._declaredChildColumnsByLevel;
1538
+ if (!declared) {
1539
+ this._childColumnsByLevel = undefined;
1540
+ this.invalidateColumnCache();
1541
+ return;
1542
+ }
1543
+ this._childColumnsByLevel = declared.map((set, level) => set && set.length ? this.svc.applyPersistedChildState(level, set) : (set ? [...set] : set));
1544
+ this.invalidateColumnCache();
1545
+ }
1546
+ /** Tree look: 'card' floats each nested level as a bordered card; 'flat' renders
1547
+ * nested rows inline with the table, sub-headers as plain strips, no card chrome.
1548
+ * The bound value is the default; the user can switch it from the Columns menu and
1549
+ * the choice is remembered per table. */
1550
+ set treeVariant(v) {
1551
+ this._declaredTreeVariant = v;
1552
+ this._treeVariant = v;
1553
+ }
1554
+ get treeVariant() {
1555
+ return this._treeVariant;
1556
+ }
1557
+ get isFlatTree() {
1558
+ return this.isTreeMode && this.treeVariant === 'flat';
1559
+ }
1560
+ /** Show the flat/nested switch only when the table actually renders nested rows. */
1561
+ get canSwitchTreeVariant() {
1562
+ return this.isTreeMode && this.hasChildColumns;
1563
+ }
1564
+ setTreeVariant(variant) {
1565
+ if (this._treeVariant === variant) {
1566
+ return;
1567
+ }
1568
+ this._treeVariant = variant;
1569
+ this.svc.saveTreeVariant(variant);
1570
+ }
1571
+ constructor(svc, tableFilterService, el, ngZone, sanitizer) {
1572
+ this.svc = svc;
1573
+ this.tableFilterService = tableFilterService;
1574
+ this.el = el;
1575
+ this.ngZone = ngZone;
1576
+ this.sanitizer = sanitizer;
1577
+ this._tableId = '';
1578
+ this._rowData = [];
1579
+ this._isLoading = false;
1580
+ /** Placeholder rows drawn during the first load, before any data has arrived. */
1581
+ this.skeletonRowCount = 8;
1582
+ /** True once the table has shown real rows at least once, so a later load shows the
1583
+ * refresh bar over the existing rows instead of the full-page skeleton again. */
1584
+ this.hasLoadedOnce = false;
1585
+ /** Whether the skeleton placeholder is visible. Debounced from isInitialLoad with the
1586
+ * same show-delay as the refresh bar: a first load shorter than REFRESH_SHOW_DELAY
1587
+ * never shows it. Unlike the bar there is no minimum-visible hold, so real rows
1588
+ * replace the skeleton the moment they arrive. */
1589
+ this.showSkeleton = false;
1590
+ /** Whether the thin top refresh bar is visible. Debounced from isLoading via
1591
+ * updateRefreshBar rather than read straight off it. */
1592
+ this.showRefreshBar = false;
1593
+ // The refresh bar is debounced so quick reloads (including fast auto-refresh polls)
1594
+ // don't flash it: it only appears once a reload has run past REFRESH_SHOW_DELAY, and
1595
+ // once shown it stays at least REFRESH_MIN_VISIBLE so it never blinks in and out.
1596
+ this.REFRESH_SHOW_DELAY = 150;
1597
+ this.REFRESH_MIN_VISIBLE = 450;
1598
+ this.refreshShownAt = 0;
1599
+ // A small spread of bar widths so skeleton cells don't read as an identical grid.
1600
+ // Deterministic (no randomness) so the widths don't flicker across change detection.
1601
+ this.SK_WIDTHS = [70, 52, 84, 46, 64, 58, 76, 48];
1602
+ this._options = { noSelect: false, noMenu: false };
1603
+ /** Resolved show/disabled state for each toolbar control, read by the template.
1604
+ * Every control shows and is enabled unless the host's `options.toolbar` says otherwise. */
1605
+ this.toolbarControls = this.normalizeToolbar(undefined);
1606
+ // Cached per level; a width change mutates the same column objects, so no rebuild is needed.
1607
+ this._childLevelCache = new Map();
1608
+ this._childColsVersion = 0;
1609
+ this._childLevelCacheVersion = -1;
1610
+ // per-level derived values, recomputed only when the column set changes, so the
1611
+ // template's per-cell/per-row calls stay cheap on every change-detection pass
1612
+ this._treeKeyCache = new Map();
1613
+ this._leadOffsetCache = new Map();
1614
+ this._treeMemoVersion = -1;
1615
+ this.nodeToggled = new EventEmitter();
1616
+ this.hasNext = false;
1617
+ this.hasPrev = false;
1618
+ this.startCount = '-';
1619
+ this.endCount = '-';
1620
+ this.totalCount = '-';
1621
+ this.filterApplied = false;
1622
+ this.menuItems = [];
1623
+ this.headerActions = [];
1624
+ /** @deprecated kept for the old lib-no-items bindings; the new empty state uses line glyphs, not images. */
1625
+ this.noItemsImage = 'nodata';
1626
+ this.showNoItemsAdd = true;
1627
+ this.noItemsReadonly = false;
1628
+ /** Primary create label shown on the first-use empty state (else the per-entity default). */
1629
+ this.createLabel = '';
1630
+ /** What these rows belong to, used in the empty headline ("This network has no services").
1631
+ * Defaults to 'network' since the Ziti pages are network-scoped; set '' for a list that is not. */
1632
+ this.emptyScope = 'network';
1633
+ this.accessDenied = false;
1634
+ this.entityTypeName = '';
1635
+ this.filterName = 'name';
1636
+ this.filterColumn = 'name';
1637
+ this.actionRequested = new EventEmitter();
1638
+ this.tableReady = new EventEmitter();
1639
+ /** Full set of selected rows (all depths) whenever the selection changes. */
1640
+ this.selectionChanged = new EventEmitter();
1641
+ /** The row the pointer is over, or null when it leaves. Emitted once per row (at the
1642
+ * row level, not per cell) so consumers can react to the whole hovered row, for
1643
+ * example to highlight that row's path in a connected diagram. */
1644
+ this.rowHover = new EventEmitter();
1645
+ /** Full column list (includes hidden), after persisted state. */
1646
+ this.allColumns = [];
1647
+ /** Currently rendered columns (visible + ordered). */
1648
+ this.visibleColumns = [];
1649
+ /** Hidden columns, shaped for the shared hidden-columns bar (`headerName`). */
1650
+ this.hiddenColumns = [];
1651
+ this.entityTypeLabel = '';
1652
+ // ---- action menus ----
1653
+ this.openMenu = false;
1654
+ this.openHeaderMenu = false;
1655
+ this.menuLeft = 0;
1656
+ this.menuTop = 0;
1657
+ this.selectedItem = { actionList: [] };
1658
+ // ---- selection ----
1659
+ this.allToggled = false;
1660
+ /** Rows actually rendered: in tree mode this is the roots plus the expanded
1661
+ * descendants in order; otherwise it is `rowData` unchanged. */
1662
+ this.flatRows = [];
1663
+ this._treeVariant = 'card';
1664
+ this._declaredTreeVariant = 'card';
1665
+ /** Show the flat/nested row-layout switch in the Columns menu. Hidden for now;
1666
+ * the variant still comes from the `treeVariant` input. */
1667
+ this.showTreeVariantToggle = false;
1668
+ /** Flat variant only: draw a running guide rail down the left of each nested level. */
1669
+ this.treeRail = false;
1670
+ this.treeIndentRem = 1.25;
1671
+ // nudges cards past the first nested level right so their edge lands on the
1672
+ // parent chevron (a nested chevron sits one grid-padding deeper than the root's)
1673
+ this.treeDeepAlignRem = 0.625;
1674
+ // flat first-column indent: base clears the select gutter, then one chevron-width
1675
+ // per level so each sub-header lines up under its parent row's name
1676
+ this.treeFlatBaseRem = 3.75;
1677
+ this.treeFlatStepRem = 2.125;
1678
+ // nested card's left inset before per-depth indent; must match the --lt-card-inset token
1679
+ this.treeCardBaseRem = 4;
1680
+ // width of the leading checkbox column injected into nested grids when selectChildren is on
1681
+ this.treeSelectColRem = 2.25;
1682
+ // ---- column chooser (skinned show/hide + reorder panel) ----
1683
+ this.showColumnChooser = false;
1684
+ // ---- integrated toolbar: search + filter chips + pagination ----
1685
+ this.searchString = '';
1686
+ this.activeFilters = [];
1687
+ this.filtering = false;
1688
+ this.inputChangedDebounced = _.debounce(() => this.applySearch(), 400);
1689
+ // ---- responsive filter overflow: controls that don't fit collapse into "More" ----
1690
+ this.visibleFilterColumns = [];
1691
+ this.overflowFilterColumns = [];
1692
+ this.showFilterOverflow = false;
1693
+ this.filterWidths = new Map();
1694
+ this.filtersDirty = true;
1695
+ this.overflowRaf = 0;
1696
+ this.FILTER_GAP = 8; // px, matches the .lt-filter-zone/.lt-filters gap
1697
+ this.MORE_BTN_W = 92; // px reserved for the "More" trigger when overflowing
1698
+ this.onPointerMove = (e) => this.handleResizeMove(e);
1699
+ this.onPointerUp = () => this.handleResizeEnd();
1700
+ this._rawColumns = [];
1701
+ this._initialColumns = [];
1702
+ this._initialized = false;
1703
+ this.subscription = new Subscription();
1704
+ // expansion + lazily-loaded children keyed by __treeRowKey, so a rowData refresh
1705
+ // (auto-refresh, paging, filter) keeps the tree open instead of collapsing it
1706
+ this._expandedKeys = new Set();
1707
+ this._childrenCache = new Map();
1708
+ this._destroyed = false;
1709
+ this.trackRow = (index, row) => row?.__treeRowKey ?? row?.id ?? row?.name ?? index;
1710
+ this.trackCol = (index, col) => col.key;
1711
+ // ---------------------------------------------------------------- hover tooltip
1712
+ this.hoverTooltip = null;
1713
+ // ---------------------------------------------------------------- row hover
1714
+ /** The row currently under the pointer, so we only emit when it actually changes. */
1715
+ this.hoveredRow = null;
1716
+ this.toggleItem = (item) => {
1717
+ item.selected = !item.selected;
1718
+ this.syncSelectAll();
1719
+ this.actionRequested.emit({ action: 'toggleItem', item });
1720
+ this.emitSelection();
1721
+ };
1722
+ this._railCache = new Map();
1723
+ // The whole chooser row is draggable, and the label sits inside the visibility
1724
+ // toggle button, so a reorder drag ends with a click on that button. This guard
1725
+ // swallows that one click so dragging a row never also hides its column.
1726
+ this._chooserDragged = false;
1727
+ // ---- nested-column chooser: one expandable group per depth level ----
1728
+ this.nestedChooserOpen = new Set();
1729
+ // ---------------------------------------------------------------- action menus
1730
+ this.openActionMenu = (event, item) => {
1731
+ this.selectedItem = item;
1732
+ this.openMenu = true;
1733
+ _.delay(() => {
1734
+ const height = this.contextMenu?.nativeElement?.offsetHeight || 120;
1735
+ const windowOffset = window.innerHeight - event.clientY;
1736
+ const menuOffset = windowOffset <= height ? height - windowOffset : 0;
1737
+ this.menuLeft = event.clientX - 100;
1738
+ this.menuTop = event.clientY + 5 - menuOffset;
1739
+ }, 10);
1740
+ };
1741
+ this.closeActionMenu = () => {
1742
+ this.selectedItem = { actionList: [] };
1743
+ this.openMenu = false;
1744
+ this.openHeaderMenu = false;
1745
+ };
1746
+ this.closeHeaderActionMenu = () => {
1747
+ this.openHeaderMenu = false;
1748
+ };
1749
+ /** Restore-default (rotate) icon for the built-in header action. */
1750
+ this.RESET_TABLE_ICON = '<svg viewBox="0 0 16 16" width="15" height="15"><path fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" d="M3.2 8a4.8 4.8 0 1 0 1.3-3.3"/><path fill="currentColor" d="M2.8 2.8l-.3 2.9 2.8-.4z"/></svg>';
1751
+ // ---- rows-per-page + numbered pagination (the skin footer) ----
1752
+ this.pageSizeOptions = [25, 50, 100];
1753
+ /** Default rows-per-page when nothing is remembered (the persisted choice wins). */
1754
+ this.initialPageSize = 50;
1755
+ /** Auto-refresh interval in seconds a page starts the table at (0 = off). This is the
1756
+ * hook for a page to default the control on; left at 0, the table behaves as before
1757
+ * (off until the user picks an interval). The user's in-session choice is not persisted. */
1758
+ this.autoRefreshSeconds = 0;
1759
+ // ---- row action icons ----
1760
+ // A menu item's host-supplied `icon` (SVG markup) is rendered before its label in
1761
+ // the row's kebab menu. Sanitized once per unique SVG and cached.
1762
+ this.iconCache = new Map();
1763
+ }
1764
+ ngOnInit() {
1765
+ this._initialized = true;
1766
+ // set directly (no emit) to avoid forcing a second fetch when a host pre-seeded it
1767
+ this.tableFilterService.pageSize = this.svc.restorePageSize(this.initialPageSize);
1768
+ this._treeVariant = this.svc.restoreTreeVariant(this._declaredTreeVariant);
1769
+ this.buildChildColumns();
1770
+ this.buildColumns();
1771
+ // all inputs are bound by now, so settle the render list against the final
1772
+ // tree flag (the rowData setter may have run before `options` arrived)
1773
+ this.rebuildFlatRows();
1774
+ this.updateEntityTypeLabel();
1775
+ this.subscription.add(this.tableFilterService.filtersChanged.subscribe((filters) => {
1776
+ // chips: every non-hidden filter except the free-text search (shown in the box)
1777
+ this.activeFilters = filters.filter((f) => f.hidden !== true && f.columnId !== this.filterColumn);
1778
+ const match = filters.find((f) => f.columnId === this.filterColumn);
1779
+ this.searchString = match ? match.value : '';
1780
+ // an activated filter changes which controls are pinned and how wide they render
1781
+ this.filtersDirty = true;
1782
+ this.scheduleFilterOverflow();
1783
+ }));
1784
+ this.subscription.add(this.tableFilterService.filtering.subscribe((f) => (this.filtering = f)));
1785
+ // close the column chooser whenever a filter dropdown (or anything else) opens
1786
+ this.subscription.add(this.svc.menuOpened$.subscribe((owner) => {
1787
+ if (owner !== this) {
1788
+ this.showColumnChooser = false;
1789
+ }
1790
+ }));
1791
+ this.tableReady.emit({ component: this });
1792
+ }
1793
+ ngAfterViewInit() {
1794
+ this.ensureFilterResizeObserver();
1795
+ }
1796
+ ngAfterViewChecked() {
1797
+ // the whole table sits behind @if(showTable), so the toolbar may not exist
1798
+ // until row data arrives; (re)attach the observer once/whenever it appears
1799
+ this.ensureFilterResizeObserver();
1800
+ }
1801
+ ngOnDestroy() {
1802
+ this._destroyed = true;
1803
+ this.subscription.unsubscribe();
1804
+ this.detachResizeListeners();
1805
+ this.filterRO?.disconnect();
1806
+ if (this.overflowRaf) {
1807
+ cancelAnimationFrame(this.overflowRaf);
1808
+ }
1809
+ clearTimeout(this.refreshShowTimer);
1810
+ clearTimeout(this.refreshHideTimer);
1811
+ clearTimeout(this.skeletonShowTimer);
1812
+ }
1813
+ get showTable() {
1814
+ // always keep the table chrome (toolbar, headers, footer); the body shows a
1815
+ // skeleton while loading and an in-table empty state when there are no rows
1816
+ return true;
1817
+ }
1818
+ /** Columns surfaced as inline filter controls in the toolbar. Text filters are
1819
+ * excluded because the free-text search box already covers the primary text field. */
1820
+ get filterableColumns() {
1821
+ return this.allColumns.filter((c) => c.filterType && c.filterType !== 'TEXTINPUT');
1822
+ }
1823
+ // -------------------------------------------------- responsive filter overflow
1824
+ /** A filter column is "active" when it has an applied filter. */
1825
+ isFilterActive(col) {
1826
+ return this.activeFilters.some((f) => f.columnId === col.key);
1827
+ }
1828
+ /** Applied filters that are currently hidden in the overflow dropdown. */
1829
+ get hiddenActiveCount() {
1830
+ return this.overflowFilterColumns.filter((c) => this.isFilterActive(c)).length;
1831
+ }
1832
+ toggleFilterOverflow(event) {
1833
+ event.stopPropagation();
1834
+ this.showFilterOverflow = !this.showFilterOverflow;
1835
+ if (this.showFilterOverflow) {
1836
+ this.svc.notifyMenuOpened(this);
1837
+ }
1838
+ }
1839
+ closeFilterOverflow(event) {
1840
+ // keep open when the click lands in a filter's own Material overlay (e.g. the
1841
+ // date-range calendar), which renders outside this popover's DOM subtree
1842
+ const target = event?.target;
1843
+ if (target?.closest?.('.cdk-overlay-container')) {
1844
+ return;
1845
+ }
1846
+ this.showFilterOverflow = false;
1847
+ }
1848
+ ensureFilterResizeObserver() {
1849
+ // observe the toolbar row (not the content-sized zone) so a window resize,
1850
+ // which changes the row's available width, actually fires the observer
1851
+ const target = this.filterZone?.nativeElement?.parentElement ?? undefined;
1852
+ if (!target || typeof ResizeObserver === 'undefined' || target === this.observedToolbar) {
1853
+ return;
1854
+ }
1855
+ // ResizeObserver fires outside Angular's zone; re-enter so the recomputed
1856
+ // visible/overflow split actually triggers change detection.
1857
+ if (!this.filterRO) {
1858
+ this.filterRO = new ResizeObserver(() => this.ngZone.run(() => this.scheduleFilterOverflow()));
1859
+ }
1860
+ else {
1861
+ this.filterRO.disconnect();
1862
+ }
1863
+ this.filterRO.observe(target);
1864
+ this.observedToolbar = target;
1865
+ // compute the split for the newly-observed row (deferred, so it never mutates
1866
+ // bound state inside the change-detection pass that called us)
1867
+ this.scheduleFilterOverflow();
1868
+ }
1869
+ scheduleFilterOverflow() {
1870
+ if (this.overflowRaf) {
1871
+ cancelAnimationFrame(this.overflowRaf);
1872
+ }
1873
+ this.overflowRaf = requestAnimationFrame(() => {
1874
+ this.overflowRaf = 0;
1875
+ this.ensureFilterResizeObserver();
1876
+ this.recomputeFilterOverflow();
1877
+ });
1878
+ }
1879
+ recomputeFilterOverflow() {
1880
+ const cols = this.filterableColumns;
1881
+ if (!cols.length) {
1882
+ this.visibleFilterColumns = [];
1883
+ this.overflowFilterColumns = [];
1884
+ return;
1885
+ }
1886
+ const needMeasure = this.filtersDirty || cols.some((c) => !this.filterWidths.has(c.key));
1887
+ if (!needMeasure) {
1888
+ this.applyFilterSplit();
1889
+ return;
1890
+ }
1891
+ // render every control inline for one frame so each can be measured, then collapse
1892
+ this.visibleFilterColumns = cols;
1893
+ this.overflowFilterColumns = [];
1894
+ requestAnimationFrame(() => {
1895
+ const row = this.filtersRow?.nativeElement;
1896
+ if (row) {
1897
+ row.querySelectorAll('[data-filter-key]').forEach((el) => {
1898
+ const key = el.getAttribute('data-filter-key');
1899
+ if (key) {
1900
+ this.filterWidths.set(key, Math.ceil(el.getBoundingClientRect().width));
1901
+ }
1902
+ });
1903
+ }
1904
+ this.filtersDirty = false;
1905
+ this.applyFilterSplit();
1906
+ });
1907
+ }
1908
+ /** Split filter controls into inline vs. overflow, pinning active filters and
1909
+ * dropping inactive (then, only if forced, active) from the right until they fit. */
1910
+ applyFilterSplit() {
1911
+ const zone = this.filterZone?.nativeElement;
1912
+ const cols = this.filterableColumns;
1913
+ if (!zone || !cols.length) {
1914
+ return;
1915
+ }
1916
+ // budget = the toolbar row's width minus its fixed siblings (search / divider /
1917
+ // Columns) and the gaps between them, i.e. the space filters may take before
1918
+ // they'd push the Columns control off the row
1919
+ const toolbarLeft = zone.parentElement;
1920
+ const siblingWidth = (sel) => {
1921
+ const node = toolbarLeft?.querySelector(sel);
1922
+ return node ? Math.ceil(node.getBoundingClientRect().width) : 0;
1923
+ };
1924
+ const available = (toolbarLeft?.clientWidth ?? zone.clientWidth) -
1925
+ siblingWidth('.lt-search') -
1926
+ siblingWidth('.lt-divider') -
1927
+ siblingWidth('.lt-columns') -
1928
+ 3 * this.FILTER_GAP;
1929
+ const widthOf = (c) => this.filterWidths.get(c.key) ?? 0;
1930
+ const totalOf = (list, withMore) => {
1931
+ let w = 0;
1932
+ list.forEach((c, i) => (w += widthOf(c) + (i > 0 ? this.FILTER_GAP : 0)));
1933
+ if (withMore) {
1934
+ w += (list.length ? this.FILTER_GAP : 0) + this.MORE_BTN_W;
1935
+ }
1936
+ return w;
1937
+ };
1938
+ if (totalOf(cols, false) <= available) {
1939
+ this.visibleFilterColumns = cols;
1940
+ this.overflowFilterColumns = [];
1941
+ return;
1942
+ }
1943
+ // removal priority: inactive right-to-left first, then active right-to-left
1944
+ const inactive = cols.filter((c) => !this.isFilterActive(c)).reverse();
1945
+ const active = cols.filter((c) => this.isFilterActive(c)).reverse();
1946
+ const removalOrder = [...inactive, ...active];
1947
+ const overflow = new Set();
1948
+ for (const c of removalOrder) {
1949
+ overflow.add(c.key);
1950
+ const visible = cols.filter((x) => !overflow.has(x.key));
1951
+ if (totalOf(visible, true) <= available) {
1952
+ break;
1953
+ }
1954
+ }
1955
+ this.visibleFilterColumns = cols.filter((c) => !overflow.has(c.key));
1956
+ this.overflowFilterColumns = cols.filter((c) => overflow.has(c.key));
1957
+ if (!this.overflowFilterColumns.length) {
1958
+ this.showFilterOverflow = false;
1959
+ }
1960
+ }
1961
+ // ---------------------------------------------------------------- columns
1962
+ buildColumns() {
1963
+ this._initialColumns = this._rawColumns.map((c) => ({ ...c }));
1964
+ this.allColumns = this.svc.applyPersistedState(this._rawColumns);
1965
+ this.updateVisible();
1966
+ // start with every filter control visible; the overflow pass collapses to fit
1967
+ this.visibleFilterColumns = this.filterableColumns;
1968
+ this.overflowFilterColumns = [];
1969
+ this.filtersDirty = true;
1970
+ this.scheduleFilterOverflow();
1971
+ }
1972
+ updateVisible() {
1973
+ this.visibleColumns = this.allColumns.filter((c) => !c.hidden);
1974
+ this.hiddenColumns = this.allColumns
1975
+ .filter((c) => c.hidden)
1976
+ .map((c) => ({ ...c, headerName: c.label }));
1977
+ this.invalidateColumnCache();
1978
+ }
1979
+ colWidth(col) {
1980
+ return col.width != null ? `${col.width}px` : null;
1981
+ }
1982
+ // ---------------------------------------------------------------- cells
1983
+ rawValue(col, row) {
1984
+ return _.get(row, col.field || col.key);
1985
+ }
1986
+ cellContext(col, row, i) {
1987
+ return {
1988
+ row,
1989
+ value: this.rawValue(col, row),
1990
+ colId: col.key,
1991
+ rowIndex: i,
1992
+ emitAction: (action, item = row) => this.actionRequested.emit({ action, item }),
1993
+ };
1994
+ }
1995
+ legacyShim(col, row, i) {
1996
+ return { data: row, value: this.rawValue(col, row), column: { colId: col.key }, rowIndex: i };
1997
+ }
1998
+ renderMode(col) {
1999
+ if (col.cell) {
2000
+ return 'component';
2001
+ }
2002
+ if (col.cellHtml) {
2003
+ return 'html';
2004
+ }
2005
+ return 'text';
2006
+ }
2007
+ cellInputs(col, row, i) {
2008
+ if (col.cellInputs) {
2009
+ return col.cellInputs(this.cellContext(col, row, i));
2010
+ }
2011
+ return { row, value: this.rawValue(col, row) };
2012
+ }
2013
+ cellHtml(col, row, i) {
2014
+ return col.cellHtml ? col.cellHtml(this.legacyShim(col, row, i)) : '';
2015
+ }
2016
+ cellText(col, row, i) {
2017
+ if (col.valueFormatter) {
2018
+ return col.valueFormatter(this.legacyShim(col, row, i));
2019
+ }
2020
+ return this.rawValue(col, row);
2021
+ }
2022
+ cellClass(col) {
2023
+ return col.cellClass || '';
2024
+ }
2025
+ onCellClick(col, row, i) {
2026
+ col.onCellClick?.(this.cellContext(col, row, i));
2027
+ }
2028
+ tooltipInputs(col, row, i) {
2029
+ return col.tooltipInputs ? col.tooltipInputs(this.cellContext(col, row, i)) : { row };
2030
+ }
2031
+ onCellHover(col, row, i, event) {
2032
+ if (!col.tooltip) {
2033
+ return;
2034
+ }
2035
+ const rect = event.currentTarget.getBoundingClientRect();
2036
+ this.hoverTooltip = {
2037
+ component: col.tooltip,
2038
+ inputs: this.tooltipInputs(col, row, i),
2039
+ left: rect.left,
2040
+ top: rect.bottom + 6,
2041
+ };
2042
+ }
2043
+ onCellLeave() {
2044
+ this.hoverTooltip = null;
2045
+ }
2046
+ onRowHover(row) {
2047
+ if (this.hoveredRow === row) {
2048
+ return;
2049
+ }
2050
+ this.hoveredRow = row;
2051
+ this.rowHover.emit(row);
2052
+ }
2053
+ onRowLeave(row) {
2054
+ if (this.hoveredRow !== row) {
2055
+ return;
2056
+ }
2057
+ this.hoveredRow = null;
2058
+ this.rowHover.emit(null);
2059
+ }
2060
+ // ---------------------------------------------------------------- selection
2061
+ /** True when nested rows get checkboxes and join the selection (opt-in). */
2062
+ get childSelectEnabled() {
2063
+ return !!this.options?.selectChildren && !this.options?.noSelect;
2064
+ }
2065
+ /** Rows the checkboxes and select-all act on: top-level only, unless selectChildren
2066
+ * is set. Keeps select-all from marking nested rows a host that reads rowData never sees. */
2067
+ get selectableRows() {
2068
+ if (!this.isTreeMode || this.childSelectEnabled) {
2069
+ return this.flatRows;
2070
+ }
2071
+ return this.flatRows.filter((r) => (r.__treeLevel ?? 0) === 0);
2072
+ }
2073
+ /** Some rows selected but the header select-all isn't fully on (indeterminate). */
2074
+ get someSelected() {
2075
+ return !this.allToggled && this.selectableRows.some((r) => r.selected);
2076
+ }
2077
+ toggleAll() {
2078
+ const newState = !this.allToggled;
2079
+ this.allToggled = newState;
2080
+ this.selectableRows.forEach((row) => (row.selected = newState));
2081
+ this.actionRequested.emit({ action: 'toggleAll' });
2082
+ this.emitSelection();
2083
+ }
2084
+ syncSelectAll() {
2085
+ const rows = this.selectableRows;
2086
+ this.allToggled = rows.length > 0 && rows.every((r) => r.selected);
2087
+ }
2088
+ /** Emit every selected row across all depths. Walks the full data, not just the rendered
2089
+ * rows, so a collapsed parent's selected children are still reported. */
2090
+ emitSelection() {
2091
+ const out = [];
2092
+ const walk = (rows) => {
2093
+ (rows || []).forEach((r) => {
2094
+ if (r.selected) {
2095
+ out.push(r);
2096
+ }
2097
+ if (Array.isArray(r.children)) {
2098
+ walk(r.children);
2099
+ }
2100
+ });
2101
+ };
2102
+ walk(this._rowData);
2103
+ this.selectionChanged.emit(out);
2104
+ }
2105
+ // ---------------------------------------------------------------- tree rows
2106
+ get isTreeMode() {
2107
+ return !!this.options?.tree;
2108
+ }
2109
+ /** True when per-level child columns are configured (children render in their own
2110
+ * grid with a sub-header rather than the parent's columns). */
2111
+ get hasChildColumns() {
2112
+ return !!(this.childColumnsByLevel && this.childColumnsByLevel.length);
2113
+ }
2114
+ /** Columns used at a given depth: level 0 is the main (visible) columns; deeper
2115
+ * levels use `childColumnsByLevel`, falling back to the deepest set defined. */
2116
+ columnsForLevel(level) {
2117
+ if (level <= 0 || !this.hasChildColumns) {
2118
+ return this.visibleColumns;
2119
+ }
2120
+ if (this._childLevelCacheVersion !== this._childColsVersion) {
2121
+ this._childLevelCache.clear();
2122
+ this._childLevelCacheVersion = this._childColsVersion;
2123
+ }
2124
+ const cached = this._childLevelCache.get(level);
2125
+ if (cached) {
2126
+ return cached;
2127
+ }
2128
+ const sets = this.childColumnsByLevel;
2129
+ const set = sets[level] ?? sets[sets.length - 1];
2130
+ const cols = (set ?? this.visibleColumns).filter((c) => !c.hidden);
2131
+ this._childLevelCache.set(level, cols);
2132
+ return cols;
2133
+ }
2134
+ /** Child columns the chooser lists at a depth: all declared ones (including hidden),
2135
+ * minus structural columns like the select checkbox or row-actions gutter. */
2136
+ allColumnsForLevel(level) {
2137
+ return (this._childColumnsByLevel?.[level] ?? []).filter((c) => !c.excludeFromChooser);
2138
+ }
2139
+ /** Depths that offer configurable columns (skips the level-0 placeholder and any
2140
+ * level that only holds structural columns), one chooser group per level. */
2141
+ get childLevels() {
2142
+ const sets = this._childColumnsByLevel;
2143
+ if (!sets) {
2144
+ return [];
2145
+ }
2146
+ const levels = [];
2147
+ sets.forEach((_set, level) => {
2148
+ if (level > 0 && this.allColumnsForLevel(level).length) {
2149
+ levels.push(level);
2150
+ }
2151
+ });
2152
+ return levels;
2153
+ }
2154
+ /** Chooser hint for a nested level: its first few column labels, then "+N". */
2155
+ childLevelLabel(level) {
2156
+ const labels = this.allColumnsForLevel(level)
2157
+ .map((c) => c.label)
2158
+ .filter(Boolean);
2159
+ const max = 3;
2160
+ if (labels.length <= max) {
2161
+ return labels.join(' / ');
2162
+ }
2163
+ return labels.slice(0, max).join(' / ') + ' +' + (labels.length - max);
2164
+ }
2165
+ /** Key of the column that carries the expand control + indentation for a column set:
2166
+ * the flagged one, else the first real data column (skipping a leading structural
2167
+ * column such as a select checkbox the host put at the start). */
2168
+ treeColumnKeyFor(cols) {
2169
+ return (cols.find((c) => c.treeColumn) || cols.find((c) => !c.excludeFromChooser) || cols[0])?.key;
2170
+ }
2171
+ treeColumnKeyForLevel(level) {
2172
+ this.syncTreeMemo();
2173
+ if (!this._treeKeyCache.has(level)) {
2174
+ this._treeKeyCache.set(level, this.treeColumnKeyFor(this.columnsForLevel(level)));
2175
+ }
2176
+ return this._treeKeyCache.get(level);
2177
+ }
2178
+ /** Width of the columns before the tree column, used to pull the flat first-column
2179
+ * indent back by whatever structural columns (like a select checkbox) sit ahead of
2180
+ * it, so its content still lands under the parent row's name. */
2181
+ treeLeadOffset(level) {
2182
+ this.syncTreeMemo();
2183
+ const cacheKey = `${level}|${this.childSelectEnabled}`;
2184
+ const hit = this._leadOffsetCache.get(cacheKey);
2185
+ if (hit !== undefined) {
2186
+ return hit;
2187
+ }
2188
+ const cols = this.columnsForLevel(level);
2189
+ const treeKey = this.treeColumnKeyForLevel(level);
2190
+ let px = 0;
2191
+ let count = 0;
2192
+ for (const c of cols) {
2193
+ if (c.key === treeKey) {
2194
+ break;
2195
+ }
2196
+ px += c.width ?? 0;
2197
+ count++;
2198
+ }
2199
+ const gap = 'var(--lt-tree-col-gap, 0.5rem)';
2200
+ // the injected select column (when on) sits before every data column
2201
+ const selRem = this.childSelectEnabled ? this.treeSelectColRem : 0;
2202
+ const gaps = count + (this.childSelectEnabled ? 1 : 0);
2203
+ const result = (!gaps && !selRem && !px) ? '0px' : `calc(${selRem}rem + ${px}px + ${gaps} * ${gap})`;
2204
+ this._leadOffsetCache.set(cacheKey, result);
2205
+ return result;
2206
+ }
2207
+ isTreeColumn(col, level) {
2208
+ return this.isTreeMode && col.key === this.treeColumnKeyForLevel(level);
2209
+ }
2210
+ isTreeCell(col) {
2211
+ return this.isTreeColumn(col, 0);
2212
+ }
2213
+ /** Columns a full-width nested row spans: the data columns plus the select and
2214
+ * menu gutters, matching the header's cell count. */
2215
+ get treeSpanCount() {
2216
+ return this.visibleColumns.length + (this.options?.noSelect ? 0 : 1) + (this.options?.noMenu ? 0 : 1);
2217
+ }
2218
+ /** Per-level indent (rem); the card variant nudges levels past the first so the edge meets the parent chevron. */
2219
+ treeIndentFor(level) {
2220
+ const align = this.treeVariant === 'card' && level >= 2 ? this.treeDeepAlignRem : 0;
2221
+ return level * this.treeIndentRem + align;
2222
+ }
2223
+ /** Flat variant: left pad (rem) of a nested row's first column (sub-header + data),
2224
+ * one chevron-width per level so it lines up under the parent row's name. */
2225
+ treeFlatFirstIndentRem(level) {
2226
+ return this.treeFlatBaseRem + level * this.treeFlatStepRem;
2227
+ }
2228
+ /** Flat variant: drop the chevron slot for a leaf-only level so values sit under their header. */
2229
+ showTreeLead(row) {
2230
+ return !(this.isFlatTree && row?.__treeSiblingsCanExpand === false);
2231
+ }
2232
+ /** Left inset (rem) of a nested card at a given depth, matching the td padding. */
2233
+ treeCardLeftRem(level) {
2234
+ return this.treeCardBaseRem + this.treeIndentFor(level);
2235
+ }
2236
+ /** Vertical guide lines redrawing each ancestor card's left edge at this row's
2237
+ * position, so a card's left border stays continuous where a nested card is
2238
+ * inserted between its rows. Returns a stack of 1px background gradients, or null
2239
+ * when there is no ancestor to redraw (roots and the first nested level). */
2240
+ treeAncestorRails(level) {
2241
+ const key = `${level}|${this._treeVariant}|${this.treeRail}|${this.isTreeMode}`;
2242
+ const cached = this._railCache.get(key);
2243
+ if (cached !== undefined) {
2244
+ return cached;
2245
+ }
2246
+ const rails = this.computeAncestorRails(level);
2247
+ this._railCache.set(key, rails);
2248
+ return rails;
2249
+ }
2250
+ computeAncestorRails(level) {
2251
+ if (!this.isTreeMode || level < 2) {
2252
+ return null;
2253
+ }
2254
+ const flat = this.treeVariant === 'flat';
2255
+ // card mode always redraws ancestor card borders; flat mode draws the rail
2256
+ // only when the optional running rail is turned on
2257
+ if (flat && !this.treeRail) {
2258
+ return null;
2259
+ }
2260
+ const color = flat
2261
+ ? 'var(--lt-tree-rail, color-mix(in srgb, var(--lt-border) 55%, var(--lt-icon)))'
2262
+ : 'var(--lt-tree-card-border, color-mix(in srgb, var(--lt-border) 70%, var(--lt-icon)))';
2263
+ const rails = [];
2264
+ for (let k = 1; k < level; k++) {
2265
+ const x = this.treeCardLeftRem(k);
2266
+ rails.push(`linear-gradient(90deg, transparent ${x}rem, ${color} ${x}rem, ${color} calc(${x}rem + 1px), transparent calc(${x}rem + 1px))`);
2267
+ }
2268
+ return rails.join(', ');
2269
+ }
2270
+ gridTemplate(level) {
2271
+ const tracks = this.columnsForLevel(level)
2272
+ .map((c) => (c.width != null ? `${c.width}px` : 'minmax(0, 1fr)'));
2273
+ if (this.childSelectEnabled) {
2274
+ tracks.unshift(`${this.treeSelectColRem}rem`);
2275
+ }
2276
+ return tracks.join(' ');
2277
+ }
2278
+ hasChildren(row) {
2279
+ return Array.isArray(row?.children) && row.children.length > 0;
2280
+ }
2281
+ /** A row can be expanded unless it is a leaf, has no loaded children, and has no
2282
+ * loader left to try. */
2283
+ canExpand(row) {
2284
+ if (!this.isTreeMode || row?.leaf === true) {
2285
+ return false;
2286
+ }
2287
+ if (this.hasChildren(row)) {
2288
+ return true;
2289
+ }
2290
+ const key = row?.__treeRowKey;
2291
+ if (key && this._childrenCache.has(key)) {
2292
+ return true;
2293
+ }
2294
+ return !!this.loadChildren && row?.__treeLoaded !== true;
2295
+ }
2296
+ toggleNode(row) {
2297
+ if (!this.isTreeMode || row?.__treeLoading) {
2298
+ return;
2299
+ }
2300
+ const key = row.__treeRowKey;
2301
+ if (this._expandedKeys.has(key)) {
2302
+ this._expandedKeys.delete(key);
2303
+ this.rebuildFlatRows();
2304
+ this.nodeToggled.emit({ row, expanded: false });
2305
+ return;
2306
+ }
2307
+ if (this.loadChildren && !this.hasChildren(row) && !this._childrenCache.has(key)) {
2308
+ row.__treeLoading = true;
2309
+ row.__treeLoadError = false; // clear any previous failure on retry
2310
+ this.rebuildFlatRows(); // surface the spinner while children load
2311
+ // run the loader inside the executor so a synchronous throw rejects the
2312
+ // promise instead of escaping and leaving __treeLoading stuck on forever
2313
+ new Promise((resolve) => resolve(this.loadChildren(row))).then((children) => this.onChildrenLoaded(row, children), () => this.onChildrenLoaded(row, [], true));
2314
+ return;
2315
+ }
2316
+ this._expandedKeys.add(key);
2317
+ this.rebuildFlatRows();
2318
+ this.nodeToggled.emit({ row, expanded: true });
2319
+ }
2320
+ onChildrenLoaded(row, children, failed = false) {
2321
+ if (this._destroyed) {
2322
+ return;
2323
+ }
2324
+ row.__treeLoading = false;
2325
+ if (failed) {
2326
+ // flag the failure so the toggle shows a retry affordance, and leave it collapsed
2327
+ row.__treeLoadError = true;
2328
+ this.rebuildFlatRows();
2329
+ return;
2330
+ }
2331
+ const list = Array.isArray(children) ? children : [];
2332
+ this._childrenCache.set(row.__treeRowKey, list);
2333
+ row.children = list;
2334
+ row.__treeLoaded = true;
2335
+ if (!list.length) {
2336
+ row.leaf = true; // nothing under it, so stop offering the control
2337
+ }
2338
+ this._expandedKeys.add(row.__treeRowKey);
2339
+ this.rebuildFlatRows();
2340
+ this.nodeToggled.emit({ row, expanded: true });
2341
+ }
2342
+ /** Flatten the tree into the ordered render list, tagging each row with its depth.
2343
+ * Outside tree mode the render list is `rowData` as-is. */
2344
+ rebuildFlatRows() {
2345
+ if (!this.isTreeMode) {
2346
+ this.flatRows = this._rowData;
2347
+ return;
2348
+ }
2349
+ const out = [];
2350
+ const walk = (rows, level, parentKey) => {
2351
+ const list = rows || [];
2352
+ // when no sibling can expand, the level needs no chevron slot
2353
+ const siblingsCanExpand = list.some((r) => this.canExpand(r));
2354
+ list.forEach((row, idx) => {
2355
+ row.__treeLevel = level;
2356
+ row.__treeSiblingsCanExpand = siblingsCanExpand;
2357
+ // first child of a group drives the per-expansion sub-header;
2358
+ // first and last mark the top and bottom of the nested card
2359
+ row.__treeFirstChild = level > 0 && idx === 0;
2360
+ row.__treeLastChild = level > 0 && idx === list.length - 1;
2361
+ // branch-unique track key: the same entity can appear under two parents
2362
+ // in a flat tree, so fold the parent path in to keep @for keys distinct
2363
+ row.__treeRowKey = parentKey + '/' + (row?.id ?? row?.name ?? idx);
2364
+ // a rowData refresh replaces the row object; re-attach children we loaded
2365
+ // earlier so an expanded lazy node stays populated instead of emptying out
2366
+ if (!this.hasChildren(row) && this._childrenCache.has(row.__treeRowKey)) {
2367
+ row.children = this._childrenCache.get(row.__treeRowKey);
2368
+ row.__treeLoaded = true;
2369
+ }
2370
+ row.__treeExpanded = this._expandedKeys.has(row.__treeRowKey);
2371
+ out.push(row);
2372
+ if (row.__treeExpanded && this.hasChildren(row)) {
2373
+ walk(row.children, level + 1, row.__treeRowKey);
2374
+ }
2375
+ });
2376
+ };
2377
+ walk(this._rowData, 0, '');
2378
+ this.flatRows = out;
2379
+ }
2380
+ // ---------------------------------------------------------------- sort
2381
+ onHeaderSort(event) {
2382
+ this.allColumns.forEach((c) => {
2383
+ c.sortDir = c.key === event.colId ? event.dir : undefined;
2384
+ });
2385
+ }
2386
+ // ---------------------------------------------------------------- show / reorder
2387
+ showColumn(event) {
2388
+ const target = this.allColumns.find((c) => c.key === event.column?.key);
2389
+ if (!target) {
2390
+ return;
2391
+ }
2392
+ target.hidden = false;
2393
+ this.updateVisible();
2394
+ this.svc.saveState(this.allColumns);
2395
+ }
2396
+ onReorder(event) {
2397
+ if (event.previousIndex === event.currentIndex) {
2398
+ return;
2399
+ }
2400
+ moveItemInArray(this.visibleColumns, event.previousIndex, event.currentIndex);
2401
+ const nextVisible = [...this.visibleColumns];
2402
+ let vi = 0;
2403
+ this.allColumns = this.allColumns.map((c) => (c.hidden ? c : nextVisible[vi++]));
2404
+ this.updateVisible();
2405
+ this.svc.saveState(this.allColumns);
2406
+ }
2407
+ resetTableColumns() {
2408
+ this.svc.reset();
2409
+ this._treeVariant = this._declaredTreeVariant;
2410
+ this.allColumns = this._initialColumns.map((c) => ({ ...c }));
2411
+ this._childColumnsByLevel = this._declaredChildColumnsByLevel?.map((set) => set ? set.map((c) => ({ ...c })) : set);
2412
+ this.updateVisible();
2413
+ this.closeActionMenu();
2414
+ }
2415
+ // ---------------------------------------------------------------- column chooser
2416
+ /** Count of columns the user has hidden beyond the defaults (drives the badge).
2417
+ * Columns hidden by default don't count, so a fresh/default load shows no badge. */
2418
+ get hiddenColumnCount() {
2419
+ return this.allColumns.filter((c) => c.hidden && !c.excludeFromChooser && !this.isDefaultHidden(c)).length;
2420
+ }
2421
+ /** True when visibility, order, and width all match the originally declared
2422
+ * columns. Width is included so resizing a column alone still enables the
2423
+ * reset button; a column with no declared width uses null on both sides so
2424
+ * it only counts as changed once the user drags it to a concrete width. */
2425
+ get columnsAtDefault() {
2426
+ if (this.canSwitchTreeVariant && this._treeVariant !== this._declaredTreeVariant) {
2427
+ return false;
2428
+ }
2429
+ if (this.allColumns.length !== this._initialColumns.length) {
2430
+ return false;
2431
+ }
2432
+ const parentAtDefault = this.allColumns.every((c, i) => {
2433
+ const init = this._initialColumns[i];
2434
+ return init
2435
+ && init.key === c.key
2436
+ && !!init.hidden === !!c.hidden
2437
+ && (init.width ?? null) === (c.width ?? null);
2438
+ });
2439
+ return parentAtDefault && this.childColumnsAtDefault;
2440
+ }
2441
+ /** True when every nested level's columns match their declared order/visibility/width. */
2442
+ get childColumnsAtDefault() {
2443
+ const declared = this._declaredChildColumnsByLevel;
2444
+ const current = this._childColumnsByLevel;
2445
+ if (!declared || !current) {
2446
+ return true;
2447
+ }
2448
+ return declared.every((set, level) => {
2449
+ const cur = current[level] ?? [];
2450
+ if (!set || set.length !== cur.length) {
2451
+ return set ? set.length === cur.length : true;
2452
+ }
2453
+ return set.every((d, i) => {
2454
+ const c = cur[i];
2455
+ return c
2456
+ && d.key === c.key
2457
+ && !!d.hidden === !!c.hidden
2458
+ && (d.width ?? null) === (c.width ?? null);
2459
+ });
2460
+ });
2461
+ }
2462
+ /** True when the column is hidden in the originally declared defaults. */
2463
+ isDefaultHidden(col) {
2464
+ const init = this._initialColumns.find((c) => c.key === col.key);
2465
+ return !!init?.hidden;
2466
+ }
2467
+ toggleColumnChooser(event) {
2468
+ event.stopPropagation();
2469
+ this.showColumnChooser = !this.showColumnChooser;
2470
+ if (this.showColumnChooser) {
2471
+ this.svc.notifyMenuOpened(this);
2472
+ }
2473
+ }
2474
+ closeColumnChooser() {
2475
+ this.showColumnChooser = false;
2476
+ }
2477
+ onChooserDragStarted() {
2478
+ this._chooserDragged = true;
2479
+ }
2480
+ onChooserDragEnded() {
2481
+ // clear after the post-drag click has been handled (and skipped) below
2482
+ setTimeout(() => (this._chooserDragged = false));
2483
+ }
2484
+ /** Toggle a column's visibility. `level` undefined targets the top-level columns;
2485
+ * a number targets that nested depth's columns. */
2486
+ toggleColumnVisible(col, level) {
2487
+ if (this._chooserDragged || col.hideable === false) {
2488
+ return;
2489
+ }
2490
+ const set = level == null ? this.allColumns : this._childColumnsByLevel?.[level];
2491
+ const target = set?.find((c) => c.key === col.key);
2492
+ if (!target) {
2493
+ return;
2494
+ }
2495
+ if (!target.hidden && this.isLastVisibleColumn(target, level)) {
2496
+ return;
2497
+ }
2498
+ target.hidden = !target.hidden;
2499
+ this.afterColumnChange(level, set);
2500
+ }
2501
+ /** True when this is the only visible column at its level, so its toggle is disabled
2502
+ * to keep at least one column on screen. */
2503
+ isLastVisibleColumn(col, level) {
2504
+ const list = level == null ? this.chooserColumns : this.allColumnsForLevel(level);
2505
+ return !col.hidden && list.filter((c) => !c.hidden).length <= 1;
2506
+ }
2507
+ /** Columns shown in the top-level Columns menu. */
2508
+ get chooserColumns() {
2509
+ return this.allColumns.filter((c) => !c.excludeFromChooser);
2510
+ }
2511
+ /** Reorder columns from a chooser drag. `level` undefined targets the top-level
2512
+ * columns; a number targets that nested depth's columns. */
2513
+ chooserReorder(event, level) {
2514
+ if (event.previousIndex === event.currentIndex) {
2515
+ return;
2516
+ }
2517
+ // the menu lists a chooser subset, so map the drag indices back onto the full set
2518
+ const full = level == null ? this.allColumns : this._childColumnsByLevel?.[level];
2519
+ const list = level == null ? this.chooserColumns : this.allColumnsForLevel(level);
2520
+ if (!full) {
2521
+ return;
2522
+ }
2523
+ const from = full.indexOf(list[event.previousIndex]);
2524
+ const to = full.indexOf(list[event.currentIndex]);
2525
+ if (from < 0 || to < 0) {
2526
+ return;
2527
+ }
2528
+ moveItemInArray(full, from, to);
2529
+ this.afterColumnChange(level, full);
2530
+ }
2531
+ /** Refresh and persist after a column visibility/order change, routing to the
2532
+ * top-level or nested-level persistence as appropriate. */
2533
+ afterColumnChange(level, set) {
2534
+ if (level == null) {
2535
+ this.updateVisible();
2536
+ this.svc.saveState(this.allColumns);
2537
+ }
2538
+ else {
2539
+ this.invalidateColumnCache();
2540
+ this.svc.saveChildState(level, set);
2541
+ }
2542
+ }
2543
+ isNestedGroupOpen(id) {
2544
+ return this.nestedChooserOpen.has(id);
2545
+ }
2546
+ toggleNestedGroup(id) {
2547
+ if (this.nestedChooserOpen.has(id)) {
2548
+ this.nestedChooserOpen.delete(id);
2549
+ }
2550
+ else {
2551
+ this.nestedChooserOpen.add(id);
2552
+ }
2553
+ }
2554
+ // ---------------------------------------------------------------- resize
2555
+ startResize(col, event) {
2556
+ if (col.resizable === false) {
2557
+ return;
2558
+ }
2559
+ event.preventDefault();
2560
+ event.stopPropagation();
2561
+ const th = event.target?.closest('th');
2562
+ const startWidth = col.width ?? th?.offsetWidth ?? 150;
2563
+ this.resizing = { col, startX: event.clientX, startWidth, persist: true };
2564
+ this.setupFitCompensation(col, startWidth, this.visibleColumns, (key) => this.measuredHeaderWidth(key));
2565
+ document.addEventListener('pointermove', this.onPointerMove);
2566
+ document.addEventListener('pointerup', this.onPointerUp);
2567
+ }
2568
+ /** Resize a nested row's column. Measures the sub-header cell's current rendered
2569
+ * width so a flexible (1fr) column converts to a concrete width before dragging;
2570
+ * the grid reflows live because its template reads each column's width. */
2571
+ startChildResize(col, level, event) {
2572
+ if (col.resizable === false) {
2573
+ return;
2574
+ }
2575
+ event.preventDefault();
2576
+ event.stopPropagation();
2577
+ const cell = event.target?.closest('.lt-tree-subhead-cell');
2578
+ const startWidth = col.width ?? Math.round(cell?.getBoundingClientRect().width ?? 150);
2579
+ const grid = cell?.closest('.lt-tree-grid');
2580
+ this.resizing = { col, startX: event.clientX, startWidth, persist: true, level };
2581
+ this.setupFitCompensation(col, startWidth, this.columnsForLevel(level), (key) => this.measuredCellWidth(grid, level, key));
2582
+ document.addEventListener('pointermove', this.onPointerMove);
2583
+ document.addEventListener('pointerup', this.onPointerUp);
2584
+ }
2585
+ handleResizeMove(event) {
2586
+ if (!this.resizing) {
2587
+ return;
2588
+ }
2589
+ const r = this.resizing;
2590
+ const delta = event.clientX - r.startX;
2591
+ if (r.sink && r.sinkStart != null) {
2592
+ // fitColumns: the column grows and the sink shrinks by the same amount, so the table stays
2593
+ // the same width and no other column (notably the tree column) shifts. The drag stops once
2594
+ // the column or the sink reaches its minimum.
2595
+ const d = Math.min(r.maxDelta ?? delta, Math.max(r.minDelta ?? delta, delta));
2596
+ r.col.width = Math.round(r.startWidth + d);
2597
+ r.sink.width = Math.round(r.sinkStart - d);
2598
+ }
2599
+ else {
2600
+ const min = r.col.minWidth ?? 40;
2601
+ r.col.width = Math.max(min, Math.round(r.startWidth + delta));
2602
+ }
2603
+ }
2604
+ /** fitColumns: pick the sink column and the range the drag may travel, so the column and the sink
2605
+ * trade width while every other column stays fixed. No-op (plain resize) when fitColumns is off. */
2606
+ setupFitCompensation(col, startWidth, columns, measure) {
2607
+ if (!this.options?.fitColumns || !this.resizing) {
2608
+ return;
2609
+ }
2610
+ const sink = this.pickSink(col, columns);
2611
+ if (!sink) {
2612
+ return;
2613
+ }
2614
+ const FLOOR = 60;
2615
+ // measure() returns 0 when the element isn't found; fall back so a width of 0
2616
+ // never gets frozen in (and later persisted) for the sink or a flex column
2617
+ const sinkStart = sink.width ?? (measure(sink.key) || startWidth);
2618
+ // Freeze any other flexible column to its rendered width so only the column and the sink move
2619
+ // (otherwise the browser would still redistribute slack among the flexible columns). These are
2620
+ // restored to flexible when the drag ends, so one resize doesn't pin the whole table to px.
2621
+ const frozen = [];
2622
+ columns.forEach((c) => {
2623
+ if (c.key !== col.key && c.key !== sink.key && c.width == null) {
2624
+ const w = measure(c.key);
2625
+ if (w > 0) {
2626
+ c.width = w;
2627
+ frozen.push(c);
2628
+ }
2629
+ }
2630
+ });
2631
+ this.resizing.sink = sink;
2632
+ this.resizing.sinkStart = sinkStart;
2633
+ this.resizing.frozen = frozen;
2634
+ this.resizing.maxDelta = Math.max(0, sinkStart - (sink.minWidth ?? FLOOR)); // grow until the sink hits its min
2635
+ this.resizing.minDelta = Math.min(0, (col.minWidth ?? 40) - startWidth); // shrink until the column hits its min
2636
+ }
2637
+ /** The column that absorbs a resize: a flexible column first (it is meant to take up slack), else
2638
+ * the nearest resizable column that is not the tree column, so indentation never shifts. */
2639
+ pickSink(col, columns) {
2640
+ // never pick the tree column (explicit or implicit) as the sink, or resizing
2641
+ // would move the expand chevrons
2642
+ const treeKey = this.treeColumnKeyFor(columns);
2643
+ const usable = columns.filter((c) => c.key !== col.key && c.resizable !== false && c.key !== treeKey);
2644
+ const flex = usable.find((c) => c.width == null);
2645
+ if (flex) {
2646
+ return flex;
2647
+ }
2648
+ const idx = columns.findIndex((c) => c.key === col.key);
2649
+ return usable.find((c) => columns.indexOf(c) > idx) ?? usable[usable.length - 1];
2650
+ }
2651
+ measuredHeaderWidth(key) {
2652
+ const host = this.el?.nativeElement;
2653
+ const th = host?.querySelector(`[id="Header_${key}"]`);
2654
+ return th ? Math.round(th.getBoundingClientRect().width) : 0;
2655
+ }
2656
+ measuredCellWidth(grid, level, key) {
2657
+ const idx = this.columnsForLevel(level).findIndex((c) => c.key === key);
2658
+ const cells = grid?.querySelectorAll('.lt-tree-subhead-cell');
2659
+ const cell = cells ? cells[idx] : undefined;
2660
+ return cell ? Math.round(cell.getBoundingClientRect().width) : 0;
2661
+ }
2662
+ handleResizeEnd() {
2663
+ const r = this.resizing;
2664
+ // columns frozen only to steady the drag go back to flexible, so the table keeps
2665
+ // filling the viewport and their temporary px widths are not persisted by saveState
2666
+ r?.frozen?.forEach((c) => (c.width = undefined));
2667
+ if (r?.persist) {
2668
+ if (r.level != null) {
2669
+ const set = this._childColumnsByLevel?.[r.level];
2670
+ if (set) {
2671
+ this.svc.saveChildWidth(r.level, r.col.key, r.col.width, set);
2672
+ }
2673
+ }
2674
+ else {
2675
+ this.svc.saveWidth(r.col.key, r.col.width, this.allColumns);
2676
+ }
2677
+ }
2678
+ this.resizing = undefined;
2679
+ this.detachResizeListeners();
2680
+ }
2681
+ detachResizeListeners() {
2682
+ document.removeEventListener('pointermove', this.onPointerMove);
2683
+ document.removeEventListener('pointerup', this.onPointerUp);
2684
+ }
2685
+ openHeaderActionMenu(event) {
2686
+ this.menuLeft = event.clientX - 150;
2687
+ this.menuTop = event.clientY + 5;
2688
+ _.delay(() => {
2689
+ this.openHeaderMenu = true;
2690
+ }, 100);
2691
+ }
2692
+ hideMenuItem(menuItem, selectedItem) {
2693
+ if (_.isFunction(menuItem.hidden)) {
2694
+ return menuItem.hidden(selectedItem);
2695
+ }
2696
+ else if (_.isBoolean(menuItem.hidden)) {
2697
+ return menuItem.hidden;
2698
+ }
2699
+ return !selectedItem.actionList?.includes(menuItem.action);
2700
+ }
2701
+ /** Header action menu rows: the reset action followed by host-provided header actions.
2702
+ * A host action's `icon` (SVG markup) is rendered before its label, like the row menu. */
2703
+ get headerMenuItems() {
2704
+ const items = [
2705
+ { label: 'Restore Default Table', action: '__reset_table__', id: 'ResetTableButton', iconSvg: this.safeIcon(this.RESET_TABLE_ICON) },
2706
+ ];
2707
+ _.forEach(this.headerActions, (ha) => {
2708
+ items.push({
2709
+ label: ha.name || ha.label,
2710
+ action: ha.action,
2711
+ id: 'HeaderAction_' + ha.action,
2712
+ iconSvg: ha.icon ? this.safeIcon(ha.icon) : undefined,
2713
+ data: ha,
2714
+ });
2715
+ });
2716
+ return items;
2717
+ }
2718
+ onHeaderMenuClick(evt) {
2719
+ if (evt.item.action === '__reset_table__') {
2720
+ this.resetTableColumns();
2721
+ }
2722
+ else {
2723
+ this.actionRequested.emit({ action: evt.item.action });
2724
+ }
2725
+ this.closeHeaderActionMenu();
2726
+ }
2727
+ /** Row action menu rows, built from `menuItems` against the currently selected row.
2728
+ * A menu item's `icon` (SVG markup) is rendered before its label in the menu. */
2729
+ get rowMenuItems() {
2730
+ return _.map(this.menuItems, (mi) => ({
2731
+ label: mi.name || mi.label,
2732
+ action: mi.action,
2733
+ id: 'TableActionButton_' + mi.action,
2734
+ hidden: this.hideMenuItem(mi, this.selectedItem),
2735
+ iconSvg: mi.icon ? this.safeIcon(mi.icon) : undefined,
2736
+ data: mi,
2737
+ }));
2738
+ }
2739
+ onRowMenuClick(evt) {
2740
+ this.actionRequested.emit({ action: evt.item.action, item: this.selectedItem });
2741
+ this.closeActionMenu();
2742
+ }
2743
+ openCreate() {
2744
+ this.actionRequested.emit({ action: 'create' });
2745
+ }
2746
+ /** Clear the search box and every active filter, which drives the host to refetch
2747
+ * unfiltered (used by the "no results" empty state's Clear filters action). */
2748
+ clearTableFilters() {
2749
+ this.searchString = '';
2750
+ this.tableFilterService.clearAllFilters();
2751
+ }
2752
+ // ---------------------------------------------------------------- toolbar search + pagination
2753
+ onSearchInput() {
2754
+ this.inputChangedDebounced();
2755
+ }
2756
+ applySearch() {
2757
+ this.tableFilterService.currentPage = 1;
2758
+ this.tableFilterService.updateFilter({
2759
+ filterName: this.filterName,
2760
+ columnId: this.filterColumn,
2761
+ value: this.searchString,
2762
+ label: this.searchString,
2763
+ });
2764
+ }
2765
+ nextPage() {
2766
+ if (this.nextDisabled) {
2767
+ return;
2768
+ }
2769
+ this.tableFilterService.changePage((this.tableFilterService.currentPage || 1) + 1);
2770
+ }
2771
+ prevPage() {
2772
+ if (this.prevDisabled) {
2773
+ return;
2774
+ }
2775
+ this.tableFilterService.changePage((this.tableFilterService.currentPage || 1) - 1);
2776
+ }
2777
+ get cursorPager() {
2778
+ return this.options?.pager === 'cursor';
2779
+ }
2780
+ get nextDisabled() {
2781
+ if (this.filtering) {
2782
+ return true;
2783
+ }
2784
+ if (this.cursorPager) {
2785
+ return !this.hasNext;
2786
+ }
2787
+ if (!_.isNumber(this.totalCount) || !_.isNumber(this.endCount)) {
2788
+ return true;
2789
+ }
2790
+ return Number(this.endCount) >= Number(this.totalCount);
2791
+ }
2792
+ get prevDisabled() {
2793
+ if (this.filtering) {
2794
+ return true;
2795
+ }
2796
+ if (this.cursorPager) {
2797
+ return !this.hasPrev;
2798
+ }
2799
+ if (!_.isNumber(this.startCount) || !_.isNumber(this.totalCount)) {
2800
+ return true;
2801
+ }
2802
+ return Number(this.startCount) <= 1;
2803
+ }
2804
+ get pageSize() {
2805
+ return this.tableFilterService.pageSize;
2806
+ }
2807
+ get currentPageNum() {
2808
+ return this.tableFilterService.currentPage || 1;
2809
+ }
2810
+ get totalPages() {
2811
+ if (!_.isNumber(this.totalCount) || Number(this.totalCount) <= 0) {
2812
+ return 1;
2813
+ }
2814
+ return Math.max(1, Math.ceil(Number(this.totalCount) / this.pageSize));
2815
+ }
2816
+ /** Page numbers to render, with `0` marking a gap ("..."). Shows the first and
2817
+ * last page plus a window around the current page so long lists stay compact. */
2818
+ get pageWindow() {
2819
+ const total = this.totalPages;
2820
+ const cur = this.currentPageNum;
2821
+ if (total <= 7) {
2822
+ return Array.from({ length: total }, (_v, i) => i + 1);
2823
+ }
2824
+ const pages = new Set([1, total, cur, cur - 1, cur + 1]);
2825
+ const sorted = [...pages].filter((p) => p >= 1 && p <= total).sort((a, b) => a - b);
2826
+ const out = [];
2827
+ let prev = 0;
2828
+ for (const p of sorted) {
2829
+ if (prev && p - prev > 1) {
2830
+ out.push(0); // gap sentinel
2831
+ }
2832
+ out.push(p);
2833
+ prev = p;
2834
+ }
2835
+ return out;
2836
+ }
2837
+ goToPage(page) {
2838
+ if (page < 1 || page > this.totalPages || page === this.currentPageNum || this.filtering) {
2839
+ return;
2840
+ }
2841
+ this.tableFilterService.changePage(page);
2842
+ }
2843
+ onPageSizeChange(size) {
2844
+ if (!size || size === this.pageSize) {
2845
+ return;
2846
+ }
2847
+ this.svc.savePageSize(size);
2848
+ this.tableFilterService.changePageSize(size);
2849
+ }
2850
+ /** Re-fetch the current page (re-emits the page so the host reloads its data). */
2851
+ refresh() {
2852
+ if (this.filtering) {
2853
+ return;
2854
+ }
2855
+ this.tableFilterService.changePage(this.currentPageNum);
2856
+ }
2857
+ safeIcon(svg) {
2858
+ let safe = this.iconCache.get(svg);
2859
+ if (!safe) {
2860
+ safe = this.sanitizer.bypassSecurityTrustHtml(svg);
2861
+ this.iconCache.set(svg, safe);
2862
+ }
2863
+ return safe;
2864
+ }
2865
+ updateEntityTypeLabel() {
2866
+ const map = {
2867
+ 'api-sessions': 'API Sessions',
2868
+ 'auth-policies': 'Auth Policies',
2869
+ 'certificate-authorities': 'Certificate Authorities',
2870
+ 'ext-jwt-signers': 'External JWT Signers',
2871
+ identities: 'Identities',
2872
+ 'posture-checks': 'Posture Checks',
2873
+ 'edge-routers': 'Edge Routers',
2874
+ services: 'Services',
2875
+ configurations: 'Configs',
2876
+ 'config-types': 'Config Types',
2877
+ 'service-policies': 'Service Policies',
2878
+ 'edge-router-policies': 'Edge Router Policies',
2879
+ 'service-edge-router-policies': 'Service Edge Router Policies',
2880
+ sessions: 'Sessions',
2881
+ terminators: 'Terminators',
2882
+ 'transit-routers': 'Transit Routers',
2883
+ };
2884
+ this.entityTypeLabel = map[this._tableId] || this.entityTypeName || '';
2885
+ }
2886
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.34", ngImport: i0, type: ListTableComponent, deps: [{ token: ListTableService }, { token: DataTableFilterService }, { token: i0.ElementRef }, { token: i0.NgZone }, { token: i3.DomSanitizer }], target: i0.ɵɵFactoryTarget.Component }); }
2887
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.34", type: ListTableComponent, isStandalone: true, selector: "lib-list-table", inputs: { tableId: "tableId", columns: "columns", rowData: "rowData", isLoading: "isLoading", skeletonRowCount: "skeletonRowCount", options: "options", loadChildren: "loadChildren", childColumnsByLevel: "childColumnsByLevel", hasNext: "hasNext", hasPrev: "hasPrev", startCount: "startCount", endCount: "endCount", totalCount: "totalCount", filterApplied: "filterApplied", menuItems: "menuItems", headerActions: "headerActions", noItemsImage: "noItemsImage", showNoItemsAdd: "showNoItemsAdd", noItemsReadonly: "noItemsReadonly", createLabel: "createLabel", emptyScope: "emptyScope", accessDenied: "accessDenied", entityTypeName: "entityTypeName", filterName: "filterName", filterColumn: "filterColumn", treeVariant: "treeVariant", showTreeVariantToggle: "showTreeVariantToggle", treeRail: "treeRail", initialPageSize: "initialPageSize", autoRefreshSeconds: "autoRefreshSeconds" }, outputs: { nodeToggled: "nodeToggled", actionRequested: "actionRequested", tableReady: "tableReady", selectionChanged: "selectionChanged", rowHover: "rowHover" }, host: { properties: { "class.lt-tree-flat": "this.isFlatTree" } }, providers: [ListTableService], viewQueries: [{ propertyName: "contextMenu", first: true, predicate: ["contextMenu"], descendants: true }, { propertyName: "filterZone", first: true, predicate: ["filterZone"], descendants: true }, { propertyName: "filtersRow", first: true, predicate: ["filtersRow"], descendants: true }], ngImport: i0, template: "@if (showTable && !accessDenied) {\n <div class=\"lt-container\">\n <div class=\"lt-toolbar\">\n <div class=\"lt-toolbar-left\">\n @if (toolbarControls.search.show) {\n <div class=\"lt-search\" [class.lt-ctl-disabled]=\"toolbarControls.search.disabled\">\n <svg class=\"lt-search-icon\" viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" aria-hidden=\"true\">\n <circle cx=\"7\" cy=\"7\" r=\"4.3\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n <line x1=\"10.4\" y1=\"10.4\" x2=\"14.6\" y2=\"14.6\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/>\n </svg>\n <input\n class=\"lt-search-input\"\n id=\"SearchFilter\"\n type=\"text\"\n [placeholder]=\"'Search ' + (entityTypeLabel || 'items')\"\n [(ngModel)]=\"searchString\"\n [disabled]=\"toolbarControls.search.disabled\"\n (keyup)=\"onSearchInput()\"\n />\n </div>\n }\n @if (toolbarControls.filters.show && filterableColumns.length) {\n <div class=\"lt-filter-zone\" [class.lt-ctl-disabled]=\"toolbarControls.filters.disabled\" #filterZone>\n <div class=\"lt-filters\" #filtersRow>\n @for (col of visibleFilterColumns; track col.key) {\n <span class=\"lt-filter-item\" [attr.data-filter-key]=\"col.key\">\n <lib-list-filter-cell [column]=\"col\"></lib-list-filter-cell>\n </span>\n }\n </div>\n @if (overflowFilterColumns.length) {\n <div class=\"lt-filter-more\">\n <button\n type=\"button\"\n class=\"lt-filter-more-btn\"\n [class.is-on]=\"hiddenActiveCount > 0\"\n [class.is-open]=\"showFilterOverflow\"\n (click)=\"toggleFilterOverflow($event)\"\n aria-haspopup=\"true\"\n [attr.aria-expanded]=\"showFilterOverflow\"\n [attr.aria-label]=\"overflowFilterColumns.length + ' more filters'\"\n id=\"FilterOverflowButton\"\n >\n <span class=\"lt-filter-more-icon\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\"><path fill=\"currentColor\" d=\"M2 3h12l-4.6 5.6v4l-2.8 1.4v-5.4z\"/></svg>\n </span>\n <span class=\"lt-filter-more-label\">More</span>\n <span class=\"lt-filter-more-count\">{{ overflowFilterColumns.length }}</span>\n </button>\n @if (showFilterOverflow) {\n <div class=\"lt-filter-overflow\" (clickOutside)=\"closeFilterOverflow($event)\">\n <div class=\"lt-filter-overflow-title\">Filters</div>\n @for (col of overflowFilterColumns; track col.key) {\n <div class=\"lt-filter-overflow-row\" [class.is-active]=\"isFilterActive(col)\">\n <lib-list-filter-cell [column]=\"col\"></lib-list-filter-cell>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n <!-- Consumer-supplied toolbar controls (e.g. page-specific filters), rendered inline beside search. -->\n <ng-content select=\"[toolbar-filters]\"></ng-content>\n </div>\n <div class=\"lt-toolbar-right\">\n <ng-template #refreshIcon>\n <svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" aria-hidden=\"true\">\n <path fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" d=\"M13.4 8a5.4 5.4 0 1 1-1.5-3.8\"/>\n <path fill=\"currentColor\" d=\"M13.9 2.1l.5 3.2-3.2-.5z\"/>\n </svg>\n </ng-template>\n @if (toolbarControls.refresh.show) {\n <button\n type=\"button\"\n class=\"lt-refresh-btn\"\n (click)=\"refresh()\"\n [disabled]=\"filtering || toolbarControls.refresh.disabled\"\n aria-label=\"Refresh\"\n title=\"Refresh\"\n id=\"RefreshTableButton\"\n >\n <ng-container [ngTemplateOutlet]=\"refreshIcon\"></ng-container>\n </button>\n }\n @if (toolbarControls.autoRefresh.show) {\n <span class=\"lt-autorefresh-wrap\" [class.lt-ctl-disabled]=\"toolbarControls.autoRefresh.disabled\">\n <lib-list-table-auto-refresh [seconds]=\"autoRefreshSeconds\" [loading]=\"filtering\" (refresh)=\"refresh()\"></lib-list-table-auto-refresh>\n </span>\n }\n @if (toolbarControls.columns.show) {\n <div class=\"lt-columns\">\n <button\n type=\"button\"\n class=\"lt-columns-btn\"\n [class.is-on]=\"hiddenColumnCount > 0\"\n [class.is-open]=\"showColumnChooser\"\n (click)=\"toggleColumnChooser($event)\"\n [disabled]=\"toolbarControls.columns.disabled\"\n aria-haspopup=\"true\"\n [attr.aria-expanded]=\"showColumnChooser\"\n id=\"ColumnChooserButton\"\n >\n <svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" aria-hidden=\"true\">\n <path fill=\"currentColor\" d=\"M2 3.5h9v1.4H2zM2 7.3h6v1.4H2zM2 11.1h11v1.4H2z\"/>\n <circle cx=\"13\" cy=\"4.2\" r=\"1.7\" fill=\"currentColor\"/>\n <circle cx=\"10\" cy=\"8\" r=\"1.7\" fill=\"currentColor\"/>\n <circle cx=\"14.4\" cy=\"11.8\" r=\"1.7\" fill=\"currentColor\"/>\n </svg>\n <span>Columns</span>\n @if (hiddenColumnCount > 0) {\n <span class=\"lt-columns-count\">{{ hiddenColumnCount }}</span>\n }\n </button>\n @if (showColumnChooser) {\n <div class=\"lt-column-chooser\" (clickOutside)=\"closeColumnChooser()\">\n <div class=\"lt-chooser-header\">\n <span class=\"lt-chooser-title\">Columns</span>\n <span class=\"lt-chooser-subtitle\">Customize order and visibility</span>\n </div>\n @if (canSwitchTreeVariant && showTreeVariantToggle) {\n <div class=\"lt-chooser-view\" role=\"group\" aria-label=\"Expanded row layout\">\n <span class=\"lt-chooser-view-label\">Expanded rows</span>\n <div class=\"lt-chooser-view-toggle\">\n <button type=\"button\" class=\"lt-chooser-view-opt\" [class.active]=\"treeVariant === 'flat'\" (click)=\"setTreeVariant('flat')\">Flat</button>\n <button type=\"button\" class=\"lt-chooser-view-opt\" [class.active]=\"treeVariant === 'card'\" (click)=\"setTreeVariant('card')\">Nested</button>\n </div>\n </div>\n }\n <div class=\"lt-chooser-scroll\">\n <ul class=\"lt-chooser-list\" cdkDropList cdkDropListLockAxis=\"y\" (cdkDropListDropped)=\"chooserReorder($event)\">\n @for (col of chooserColumns; track col.key) {\n <li class=\"lt-chooser-row\" cdkDrag cdkDragLockAxis=\"y\" [cdkDragDisabled]=\"col.reorderable === false\"\n (cdkDragStarted)=\"onChooserDragStarted()\" (cdkDragEnded)=\"onChooserDragEnded()\">\n <ng-container *ngTemplateOutlet=\"chooserRowBody; context: { col: col }\"></ng-container>\n </li>\n }\n </ul>\n @if (canSwitchTreeVariant && childLevels.length) {\n <div class=\"lt-chooser-nested\">\n <button\n type=\"button\"\n class=\"lt-chooser-group\"\n (click)=\"toggleNestedGroup('__root')\"\n [attr.aria-expanded]=\"isNestedGroupOpen('__root')\"\n >\n <span class=\"lt-chooser-chevron\" [class.open]=\"isNestedGroupOpen('__root')\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 16 16\" width=\"11\" height=\"11\"><path fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 4l4 4-4 4\"/></svg>\n </span>\n <span class=\"lt-chooser-group-label\">Nested rows</span>\n </button>\n @if (isNestedGroupOpen('__root')) {\n @for (level of childLevels; track level) {\n <button\n type=\"button\"\n class=\"lt-chooser-group lt-chooser-group-sub\"\n (click)=\"toggleNestedGroup('lvl' + level)\"\n [attr.aria-expanded]=\"isNestedGroupOpen('lvl' + level)\"\n >\n <span class=\"lt-chooser-chevron\" [class.open]=\"isNestedGroupOpen('lvl' + level)\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 16 16\" width=\"11\" height=\"11\"><path fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 4l4 4-4 4\"/></svg>\n </span>\n <span class=\"lt-chooser-group-label\">Level {{ level }}</span>\n <span class=\"lt-chooser-group-hint\">{{ childLevelLabel(level) }}</span>\n </button>\n @if (isNestedGroupOpen('lvl' + level)) {\n <ul class=\"lt-chooser-list lt-chooser-sublist\" cdkDropList cdkDropListLockAxis=\"y\" (cdkDropListDropped)=\"chooserReorder($event, level)\">\n @for (col of allColumnsForLevel(level); track col.key) {\n <li class=\"lt-chooser-row\" cdkDrag cdkDragLockAxis=\"y\" [cdkDragDisabled]=\"col.reorderable === false\"\n (cdkDragStarted)=\"onChooserDragStarted()\" (cdkDragEnded)=\"onChooserDragEnded()\">\n <ng-container *ngTemplateOutlet=\"chooserRowBody; context: { col: col, level: level }\"></ng-container>\n </li>\n }\n </ul>\n }\n }\n }\n </div>\n }\n </div>\n <div class=\"lt-chooser-footer\">\n <button\n type=\"button\"\n class=\"lt-chooser-reset\"\n [disabled]=\"columnsAtDefault\"\n (click)=\"resetTableColumns()\"\n id=\"ResetColumnsButton\"\n >\n Reset to default\n </button>\n </div>\n </div>\n }\n </div>\n }\n </div>\n </div>\n\n <!-- reload with rows already on screen: a thin indeterminate bar under the toolbar,\n rows stay visible (dimmed) and usable. role=progressbar (indeterminate). -->\n @if (showRefreshBar) {\n <div class=\"lt-refresh-bar\" role=\"progressbar\" aria-label=\"Refreshing\" aria-busy=\"true\">\n <span class=\"lt-refresh-bar-fill\"></span>\n </div>\n }\n\n <!-- inner content of a Columns-chooser row, shared by the top-level and nested lists.\n The <li cdkDrag> wrapper stays inline in each list (above) so cdkDropList's content\n query still registers it as a draggable; only the body is shared here.\n `level` undefined targets the top-level columns, a number targets that nested depth. -->\n <ng-template #chooserRowBody let-col=\"col\" let-level=\"level\">\n <button\n type=\"button\"\n class=\"lt-chooser-toggle\"\n [class.checked]=\"!col.hidden\"\n [disabled]=\"col.hideable === false || isLastVisibleColumn(col, level)\"\n [attr.title]=\"isLastVisibleColumn(col, level) ? 'At least one column must stay visible' : null\"\n (click)=\"toggleColumnVisible(col, level)\"\n [attr.aria-pressed]=\"!col.hidden\"\n >\n <span class=\"lt-chooser-box\" aria-hidden=\"true\">\n <!-- always rendered: the check is white, so it stays invisible until the box\n turns blue when checked. Keeping it in the DOM means clicking to hide a\n column never detaches the click target, which would otherwise make the\n click-outside handler think the click was outside and close the menu. -->\n <svg viewBox=\"0 0 12 12\" width=\"10\" height=\"10\">\n <path fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2 6.3 4.6 9 10 3\"/>\n </svg>\n </span>\n <span class=\"lt-chooser-text\">{{ col.label }}</span>\n @if (level == null && isDefaultHidden(col)) {\n <span class=\"lt-chooser-default-tag\">hidden by default</span>\n }\n </button>\n @if (col.reorderable !== false) {\n <!-- the whole row is the drag target (cdkDrag on the <li>); this is just the grip affordance -->\n <span class=\"lt-chooser-handle\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 10 16\" width=\"10\" height=\"15\" fill=\"currentColor\">\n <circle cx=\"3\" cy=\"3\" r=\"1.25\"/><circle cx=\"7\" cy=\"3\" r=\"1.25\"/>\n <circle cx=\"3\" cy=\"8\" r=\"1.25\"/><circle cx=\"7\" cy=\"8\" r=\"1.25\"/>\n <circle cx=\"3\" cy=\"13\" r=\"1.25\"/><circle cx=\"7\" cy=\"13\" r=\"1.25\"/>\n </svg>\n </span>\n }\n </ng-template>\n\n <div class=\"lt-scroll\" id=\"ItemTable\" [class.lt-is-refreshing]=\"showRefreshBar\" [class.lt-is-empty]=\"!isLoading && !rowData?.length\" [attr.aria-busy]=\"isLoading ? 'true' : null\">\n <!-- expand/collapse control, reused by root rows and nested child rows -->\n <ng-template #treeToggle let-row=\"row\">\n @if (canExpand(row)) {\n <button\n type=\"button\"\n class=\"lt-tree-toggle\"\n [class.is-expanded]=\"row.__treeExpanded\"\n [class.is-loading]=\"row.__treeLoading\"\n [class.is-error]=\"row.__treeLoadError\"\n (click)=\"$event.stopPropagation(); toggleNode(row)\"\n [attr.aria-expanded]=\"!!row.__treeExpanded\"\n [attr.aria-busy]=\"!!row.__treeLoading\"\n [attr.title]=\"row.__treeLoadError ? 'Could not load children - click to retry' : null\"\n [attr.aria-label]=\"row.__treeExpanded ? 'Collapse row' : 'Expand row'\"\n >\n <svg class=\"lt-tree-chevron\" viewBox=\"0 0 16 16\" width=\"12\" height=\"12\" aria-hidden=\"true\">\n <path fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 4l4 4-4 4\"/>\n </svg>\n @if (row.__treeLoading) {\n <span class=\"lt-tree-spinner\" aria-hidden=\"true\"></span>\n }\n </button>\n } @else {\n <span class=\"lt-tree-spacer\" aria-hidden=\"true\"></span>\n }\n </ng-template>\n\n <!-- a single cell's content, reused across the fixed grid and the nested grid -->\n <ng-template #cellBody let-col=\"col\" let-row=\"row\" let-i=\"i\">\n @switch (renderMode(col)) {\n @case ('component') {\n <ng-container *ngComponentOutlet=\"col.cell; inputs: cellInputs(col, row, i)\"></ng-container>\n }\n @case ('html') {\n <span class=\"lt-cell-html\" [innerHTML]=\"cellHtml(col, row, i)\"></span>\n }\n @default {\n <span class=\"lt-cell-text\">{{ cellText(col, row, i) }}</span>\n }\n }\n </ng-template>\n\n <table class=\"lt-table\">\n <colgroup>\n @if (!options.noSelect) { <col class=\"lt-col-select\" /> }\n @for (col of visibleColumns; track trackCol($index, col)) {\n <col [style.width]=\"colWidth(col)\" />\n }\n @if (!options.noMenu) { <col class=\"lt-col-menu\" /> }\n </colgroup>\n <thead class=\"lt-head\">\n <tr cdkDropList cdkDropListOrientation=\"horizontal\" (cdkDropListDropped)=\"onReorder($event)\">\n @if (!options.noSelect) {\n <th class=\"lt-select-cell\">\n <div\n class=\"lt-select-all\"\n [class.checked]=\"allToggled\"\n [class.indeterminate]=\"someSelected\"\n (click)=\"toggleAll()\"\n id=\"SelectAllRows\"\n ></div>\n </th>\n }\n @for (col of visibleColumns; track trackCol($index, col)) {\n <th\n cdkDrag\n [cdkDragDisabled]=\"col.reorderable === false\"\n class=\"lt-th\"\n [class.lt-th-tree]=\"isTreeCell(col)\"\n [ngClass]=\"col.cellClass\"\n id=\"Header_{{ col.key }}\"\n >\n <lib-list-header-cell\n [column]=\"col\"\n [sortDir]=\"col.sortDir\"\n (sortChanged)=\"onHeaderSort($event)\"\n ></lib-list-header-cell>\n @if (col.resizable !== false) {\n <span class=\"lt-resize\" (pointerdown)=\"startResize(col, $event)\"></span>\n }\n </th>\n }\n @if (!options.noMenu) {\n <th class=\"lt-menu-cell\">\n <div class=\"lt-header-menu-trigger\" (click)=\"openHeaderActionMenu($event)\" id=\"ColumnMenuHeader\">\n &#8942;\n </div>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @if (showSkeleton) {\n <!-- first load, no data yet: placeholder rows matching the real column layout.\n aria-hidden so screen readers get the region's aria-busy, not fake rows. -->\n @for (sk of skeletonRows; track sk; let r = $index) {\n <tr class=\"lt-row lt-skeleton-row\" aria-hidden=\"true\">\n @if (!options.noSelect) {\n <td class=\"lt-select-cell\"><span class=\"lt-sk lt-sk-check\"></span></td>\n }\n @for (col of visibleColumns; track trackCol($index, col); let c = $index) {\n <td class=\"lt-cell\" [class.lt-tree-cell]=\"isTreeCell(col)\">\n @if (isTreeCell(col)) {\n <span class=\"lt-sk lt-sk-dot\"></span>\n }\n <span class=\"lt-sk lt-sk-bar\" [style.width.%]=\"skeletonBarWidth(c, r)\"></span>\n </td>\n }\n @if (!options.noMenu) {\n <td class=\"lt-menu-cell\"></td>\n }\n </tr>\n }\n } @else {\n @for (row of flatRows; track trackRow($index, row); let i = $index) {\n @if (isTreeMode && row.__treeLevel > 0 && hasChildColumns) {\n <!-- nested rows with their own per-level columns, laid out in a grid\n that spans the full table width (so they need not match the parent) -->\n @if (row.__treeFirstChild) {\n <tr class=\"lt-tree-subhead-row\">\n <td\n [attr.colspan]=\"treeSpanCount\"\n [style.--lt-tree-indent.rem]=\"treeIndentFor(row.__treeLevel)\"\n [style.--lt-tree-first-indent.rem]=\"treeFlatFirstIndentRem(row.__treeLevel)\"\n [style.--lt-tree-lead-offset]=\"treeLeadOffset(row.__treeLevel)\"\n [style.background-image]=\"treeAncestorRails(row.__treeLevel)\"\n >\n <div\n class=\"lt-tree-grid\"\n [style.grid-template-columns]=\"gridTemplate(row.__treeLevel)\"\n >\n @if (childSelectEnabled) {\n <span class=\"lt-tree-subhead-cell lt-tree-select-cell\"></span>\n }\n @for (col of columnsForLevel(row.__treeLevel); track col.key; let last = $last) {\n <span class=\"lt-tree-subhead-cell\" [class.lt-tree-indent-col]=\"isTreeColumn(col, row.__treeLevel)\" [ngClass]=\"cellClass(col)\">\n {{ col.label }}\n @if (!last && col.resizable !== false) {\n <span class=\"lt-resize lt-tree-resize\" (pointerdown)=\"startChildResize(col, row.__treeLevel, $event)\"></span>\n }\n </span>\n }\n </div>\n </td>\n </tr>\n }\n <tr\n class=\"lt-row lt-tree-child-row\"\n [class.lt-tree-last-child]=\"row.__treeLastChild\"\n [class.lt-row-selected]=\"row.selected || row.open\"\n [class.lt-row-expanded]=\"row.__treeExpanded\"\n (mouseenter)=\"onRowHover(row)\"\n (mouseleave)=\"onRowLeave(row)\"\n >\n <td\n [attr.colspan]=\"treeSpanCount\"\n [style.--lt-tree-indent.rem]=\"treeIndentFor(row.__treeLevel)\"\n [style.--lt-tree-first-indent.rem]=\"treeFlatFirstIndentRem(row.__treeLevel)\"\n [style.--lt-tree-lead-offset]=\"treeLeadOffset(row.__treeLevel)\"\n [style.background-image]=\"treeAncestorRails(row.__treeLevel)\"\n >\n <div\n class=\"lt-tree-grid\"\n [style.grid-template-columns]=\"gridTemplate(row.__treeLevel)\"\n >\n @if (childSelectEnabled) {\n <div class=\"lt-tree-cellbox lt-tree-select-cell\">\n <lib-select-cell [row]=\"row\" [toggleItem]=\"toggleItem\"></lib-select-cell>\n </div>\n }\n @for (col of columnsForLevel(row.__treeLevel); track col.key) {\n <div\n class=\"lt-tree-cellbox\"\n [class.lt-tree-indent-col]=\"isTreeColumn(col, row.__treeLevel)\"\n [ngClass]=\"cellClass(col)\"\n (click)=\"onCellClick(col, row, i)\"\n (mouseenter)=\"onCellHover(col, row, i, $event)\"\n (mouseleave)=\"onCellLeave()\"\n >\n @if (isTreeColumn(col, row.__treeLevel) && showTreeLead(row)) {\n <span class=\"lt-tree-lead\">\n <ng-container *ngTemplateOutlet=\"treeToggle; context: {row: row}\"></ng-container>\n </span>\n }\n <ng-container *ngTemplateOutlet=\"cellBody; context: {col: col, row: row, i: i}\"></ng-container>\n </div>\n }\n </div>\n </td>\n </tr>\n } @else {\n <tr\n class=\"lt-row\"\n [class.lt-row-odd]=\"i % 2 === 1\"\n [class.lt-row-selected]=\"row.selected || row.open\"\n [class.lt-row-expanded]=\"row.__treeExpanded\"\n [class.row-invalid]=\"row.invalid\"\n [class.row-copied]=\"row.copied\"\n (mouseenter)=\"onRowHover(row)\"\n (mouseleave)=\"onRowLeave(row)\"\n >\n @if (!options.noSelect) {\n <td class=\"lt-select-cell\">\n <lib-select-cell [row]=\"row\" [toggleItem]=\"toggleItem\"></lib-select-cell>\n </td>\n }\n @for (col of visibleColumns; track trackCol($index, col)) {\n <td\n class=\"lt-cell\"\n [class.lt-tree-cell]=\"isTreeCell(col)\"\n [ngClass]=\"cellClass(col)\"\n (click)=\"onCellClick(col, row, i)\"\n (mouseenter)=\"onCellHover(col, row, i, $event)\"\n (mouseleave)=\"onCellLeave()\"\n >\n @if (isTreeCell(col)) {\n <span class=\"lt-tree-cell-inner\">\n <span class=\"lt-tree-lead\" [style.margin-left.rem]=\"(row.__treeLevel || 0) * treeIndentRem\">\n <ng-container *ngTemplateOutlet=\"treeToggle; context: {row: row}\"></ng-container>\n </span>\n <ng-container *ngTemplateOutlet=\"cellBody; context: {col: col, row: row, i: i}\"></ng-container>\n </span>\n } @else {\n <ng-container *ngTemplateOutlet=\"cellBody; context: {col: col, row: row, i: i}\"></ng-container>\n }\n </td>\n }\n @if (!options.noMenu) {\n <td class=\"lt-menu-cell\">\n @if (row.actionList?.length) {\n <button\n type=\"button\"\n class=\"lt-row-menu-trigger\"\n (click)=\"$event.stopPropagation(); openActionMenu($event, row)\"\n attr.id=\"RowMenu_{{ row.id }}\"\n aria-label=\"Row actions\"\n >&#8942;</button>\n }\n </td>\n }\n </tr>\n }\n }\n }\n </tbody>\n </table>\n @if (!isLoading && (!rowData || rowData.length === 0)) {\n <lib-list-empty\n [variant]=\"filterApplied ? 'no-results' : 'empty'\"\n [entityKey]=\"tableId\"\n [entityLabel]=\"entityTypeLabel\"\n [scope]=\"emptyScope\"\n [createLabel]=\"createLabel\"\n [hasAdd]=\"showNoItemsAdd\"\n [readonly]=\"noItemsReadonly\"\n [framed]=\"false\"\n (create)=\"openCreate()\"\n (clearFilters)=\"clearTableFilters()\"\n >\n <ng-content select=\"[empty-state]\" ngProjectAs=\"[empty-state]\"></ng-content>\n </lib-list-empty>\n }\n </div>\n\n <div class=\"lt-footer\">\n <div class=\"lt-footer-count\">\n Showing <span id=\"Start\">{{ startCount }}</span>&#8211;<span id=\"End\">{{ endCount }}</span>\n of <span id=\"Total\">{{ totalCount }}</span>\n </div>\n <div class=\"lt-footer-controls\">\n @if (toolbarControls.pageSize.show) {\n <div class=\"lt-rpp\" [class.lt-ctl-disabled]=\"toolbarControls.pageSize.disabled\">\n <span class=\"lt-rpp-label\">Rows per page</span>\n <select\n class=\"lt-rpp-select\"\n [disabled]=\"filtering || toolbarControls.pageSize.disabled\"\n [value]=\"pageSize\"\n (change)=\"onPageSizeChange(+$any($event.target).value)\"\n aria-label=\"Rows per page\"\n >\n @for (opt of pageSizeOptions; track opt) {\n <option [value]=\"opt\">{{ opt }}</option>\n }\n </select>\n </div>\n }\n @if (toolbarControls.pagination.show) {\n <div class=\"lt-pager\" [class.lt-ctl-disabled]=\"toolbarControls.pagination.disabled\">\n <button type=\"button\" class=\"lt-page-btn icon-prev\" [disabled]=\"prevDisabled\" (click)=\"prevPage()\" aria-label=\"Previous page\"></button>\n @if (!cursorPager) {\n @for (p of pageWindow; track $index) {\n @if (p === 0) {\n <span class=\"lt-page-gap\">&#8230;</span>\n } @else {\n <button\n type=\"button\"\n class=\"lt-page-num\"\n [class.is-current]=\"p === currentPageNum\"\n (click)=\"goToPage(p)\"\n >{{ p }}</button>\n }\n }\n }\n <button type=\"button\" class=\"lt-page-btn icon-next\" [disabled]=\"nextDisabled\" (click)=\"nextPage()\" aria-label=\"Next page\"></button>\n </div>\n }\n </div>\n </div>\n\n <!-- header action menu -->\n <lib-list-menu\n menuId=\"HeaderActionMenu\"\n [open]=\"openHeaderMenu\"\n [items]=\"headerMenuItems\"\n [left]=\"menuLeft\"\n [top]=\"menuTop\"\n (itemClick)=\"onHeaderMenuClick($event)\"\n (closed)=\"closeHeaderActionMenu()\"\n ></lib-list-menu>\n\n <!-- row action menu -->\n <lib-list-menu\n menuId=\"RowActionMenu\"\n [open]=\"openMenu\"\n [items]=\"rowMenuItems\"\n [left]=\"menuLeft\"\n [top]=\"menuTop\"\n (itemClick)=\"onRowMenuClick($event)\"\n (closed)=\"closeActionMenu()\"\n ></lib-list-menu>\n </div>\n} @else {\n <!-- access denied: a full-page wall (no table chrome to show without permission) -->\n <lib-list-empty\n variant=\"access-denied\"\n [entityKey]=\"tableId\"\n [entityLabel]=\"entityTypeLabel\"\n [scope]=\"emptyScope\"\n ></lib-list-empty>\n}\n\n@if (hoverTooltip) {\n <div class=\"lt-tooltip\" [ngStyle]=\"{ left: hoverTooltip.left + 'px', top: hoverTooltip.top + 'px' }\">\n <ng-container *ngComponentOutlet=\"hoverTooltip.component; inputs: hoverTooltip.inputs\"></ng-container>\n </div>\n}\n", styles: [":host{display:flex;flex-direction:column;width:100%;height:100%}.lt-columns-btn,.lt-refresh-btn,.lt-filter-more-btn{display:inline-flex;align-items:center;gap:var(--lt-control-gap);height:var(--lt-toolbar-height);padding:0 var(--lt-control-padding-x);font-family:var(--lt-font);font-size:var(--lt-control-font-size);font-weight:var(--lt-control-font-weight);color:var(--lt-cell-text);background:var(--lt-control-bg);border:var(--lt-control-border-full);border-radius:var(--lt-radius);cursor:pointer;white-space:nowrap;transition:border-color .15s,background .15s,color .15s}.lt-columns-btn svg,.lt-refresh-btn svg,.lt-filter-more-btn svg{display:block}.lt-columns-btn:hover,.lt-refresh-btn:hover,.lt-filter-more-btn:hover,.is-open.lt-columns-btn,.is-open.lt-refresh-btn,.is-open.lt-filter-more-btn,.is-on.lt-columns-btn,.is-on.lt-refresh-btn,.is-on.lt-filter-more-btn{border-color:var(--lt-link);color:var(--lt-link)}.is-on.lt-columns-btn,.is-on.lt-refresh-btn,.is-on.lt-filter-more-btn{background:color-mix(in srgb,var(--lt-link) 8%,var(--lt-control-bg))}.lt-columns-count,.lt-filter-more-count{display:inline-flex;align-items:center;justify-content:center;min-width:1.125rem;height:1.125rem;padding:0 .3125rem;font-size:.6875rem;font-weight:700;color:var(--lt-control-bg);background:var(--lt-link);border-radius:.5625rem}.lt-container{display:flex;position:relative;height:calc(100% - var(--lt-container-reserve, 5.625rem));width:100%;flex-direction:column}.lt-scroll{position:relative;z-index:1;width:100%;flex:0 1 auto;min-height:0;overflow:auto;background-color:var(--lt-bg)}.lt-scroll.lt-is-empty{flex:1 1 auto;display:flex;flex-direction:column}.lt-scroll.lt-is-empty lib-list-empty{flex:1 1 auto;min-height:0}.lt-table{width:100%;border-collapse:collapse;table-layout:fixed;font-family:var(--lt-font);font-size:.8125rem;color:var(--lt-cell-text)}.lt-skeleton-row td{height:var(--lt-row-height, 2.9rem)}.lt-skeleton-row:nth-child(1) .lt-sk{animation-delay:0s}.lt-skeleton-row:nth-child(2) .lt-sk{animation-delay:.08s}.lt-skeleton-row:nth-child(3) .lt-sk{animation-delay:.16s}.lt-skeleton-row:nth-child(4) .lt-sk{animation-delay:.24s}.lt-skeleton-row:nth-child(5) .lt-sk{animation-delay:.32s}.lt-skeleton-row:nth-child(6) .lt-sk{animation-delay:.4s}.lt-skeleton-row:nth-child(7) .lt-sk{animation-delay:.48s}.lt-skeleton-row:nth-child(8) .lt-sk{animation-delay:.56s}.lt-sk{display:inline-block;vertical-align:middle;border-radius:.375rem;background:color-mix(in srgb,var(--lt-border) 60%,transparent);animation:lt-skeleton-pulse 1.4s ease-in-out infinite}.lt-sk-bar{height:.7rem}.lt-sk-dot{width:1rem;height:1rem;border-radius:50%;margin-right:.5rem}.lt-sk-check{display:block;width:1rem;height:1rem;margin:0 auto;border-radius:.25rem}@keyframes lt-skeleton-pulse{0%,to{opacity:1}50%{opacity:.45}}.lt-refresh-bar{--lt-progress: var(--primary, var(--lt-link, #0027ab));position:relative;flex:0 0 auto;height:2px;margin-bottom:-2px;z-index:4;width:100%;overflow:hidden;background:color-mix(in srgb,var(--lt-progress) 20%,transparent)}.lt-refresh-bar-fill{position:absolute;top:0;left:0;height:100%;width:40%;background:var(--lt-progress);border-radius:1px;animation:lt-refresh-indeterminate 1.1s ease-in-out infinite}@keyframes lt-refresh-indeterminate{0%{left:-40%;width:40%}50%{width:55%}to{left:100%;width:40%}}.lt-is-refreshing tbody{opacity:.6;transition:opacity .15s ease}@media(prefers-reduced-motion:reduce){.lt-sk{animation:none}.lt-refresh-bar-fill{left:0;width:100%;animation:lt-refresh-pulse 1.4s ease-in-out infinite}@keyframes lt-refresh-pulse{0%,to{opacity:.45}50%{opacity:1}}}.lt-head{position:sticky;top:0;z-index:10}.lt-head th{position:relative;box-sizing:border-box;height:2.6rem;padding:0 .3125rem 0 1.25rem;text-align:left;vertical-align:middle;background-color:var(--lt-header-bg);border-bottom:1px solid var(--lt-header-underline);white-space:nowrap;-webkit-user-select:none;user-select:none}.lt-head th.lt-th-tree{padding-left:2.75rem}.lt-head .lt-select-cell,.lt-head .lt-menu-cell{width:3.75rem;padding:0}.lt-resize{position:absolute;right:0;top:50%;transform:translateY(-50%);width:.75rem;height:2.25rem;cursor:col-resize}.lt-resize:before,.lt-resize:after{content:\"\";position:absolute;top:50%;height:1.25rem;transform:translateY(-50%);width:1px;background:color-mix(in srgb,var(--lt-border) 50%,var(--lt-icon));transition:background .15s}.lt-resize:before{right:.3125rem}.lt-resize:after{right:.125rem}.lt-resize:hover:before,.lt-resize:hover:after{background:color-mix(in srgb,var(--lt-icon) 72%,var(--lt-cell-text))}.lt-select-all{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);box-sizing:border-box;width:1rem;height:1rem;border:1px solid var(--lt-control-border, var(--lt-border));border-radius:.25rem;background:var(--lt-control-bg);background-repeat:no-repeat;background-position:center center;cursor:pointer;transition:border-color .12s,background-color .12s}.lt-select-all:hover{border-color:var(--lt-link)}.lt-select-all.checked{background-color:var(--lt-link);border-color:var(--lt-link);background-size:.75rem;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' d='M2.5 7.6 5.7 10.8 11.5 3.6'/%3E%3C/svg%3E\")}.lt-select-all.indeterminate{background-color:var(--lt-link);border-color:var(--lt-link);background-image:linear-gradient(#fff,#fff);background-size:.5rem .125rem}.lt-header-menu-trigger{display:flex;align-items:center;justify-content:center;height:100%;font-size:1.25rem;color:var(--lt-icon);cursor:pointer}.lt-header-menu-trigger:hover{color:var(--lt-icon-active)}.lt-row-menu-trigger{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;padding:0;font-size:1.25rem;line-height:1;color:var(--lt-icon);background:transparent;border:none;border-radius:var(--lt-radius);cursor:pointer;transition:color .15s,background .15s}.lt-row-menu-trigger:hover{color:var(--lt-icon-active);background:var(--lt-row-hover)}.cdk-drag-preview{box-sizing:border-box;background-color:var(--lt-header-bg);color:var(--lt-header-text);box-shadow:var(--shadowLarge);padding:0 .625rem;display:flex;align-items:center;border-radius:var(--lt-radius)}.cdk-drag-placeholder{opacity:.3}.cdk-drag-animating{transition:transform .2s cubic-bezier(0,0,.2,1)}.lt-row{height:var(--lt-row-height);background-color:var(--lt-row-bg);color:var(--lt-cell-text)}.lt-row td{border-bottom:1px solid var(--lt-border)}.lt-row.lt-row-odd{background-color:var(--lt-row-alt-bg)}.lt-row:hover{background-color:var(--lt-row-hover)}.lt-row.lt-row-selected{background-color:var(--lt-row-selected)}.lt-row.lt-row-selected:hover{background-color:var(--lt-row-hover)}.lt-row.row-invalid{background-color:var(--invalid)!important}.lt-row.row-copied{background-color:var(--copiedRow)!important}.lt-cell{height:var(--lt-row-height);padding:0 .625rem 0 1.25rem;text-align:left;vertical-align:middle;border:none;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;-webkit-user-select:text;user-select:text}.lt-cell-text{display:inline-block;overflow:hidden;text-overflow:ellipsis;max-width:100%;vertical-align:middle}.lt-cell.lt-cell-center{text-align:center}.lt-badge-cell .lt-cell-text{padding:.1875rem .5rem;font-size:.75rem;font-weight:600;line-height:1.2;color:var(--lt-cell-text);background:color-mix(in srgb,var(--lt-border) 45%,transparent);border:1px solid var(--lt-border);border-radius:999px}.lt-badge-cell .lt-cell-text:empty{display:none}.lt-select-cell,.lt-menu-cell{width:3.75rem;padding:0;text-align:center;vertical-align:middle;overflow:visible;position:relative}.lt-tree-cell{padding-left:.625rem}.lt-tree-cell-inner{display:flex;align-items:center}.lt-tree-lead{display:inline-flex;align-items:center;flex:0 0 auto;margin-right:.375rem}.lt-tree-toggle{position:relative;display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;padding:0;color:var(--lt-icon);background:transparent;border:none;border-radius:50%;cursor:pointer;transition:color .15s,background .15s}.lt-tree-toggle:hover{color:var(--lt-link);background:color-mix(in srgb,var(--lt-link) 18%,transparent)}.lt-tree-chevron{display:block;width:13.8px;height:13.8px;transition:transform .15s ease}.lt-tree-toggle.is-expanded .lt-tree-chevron{transform:rotate(90deg)}.lt-tree-toggle.is-error .lt-tree-chevron{color:var(--lt-negative, #dc2626)}.lt-tree-spacer{display:inline-block;width:1.75rem;height:1.75rem}.lt-tree-spinner{position:absolute;top:50%;left:50%;box-sizing:border-box;width:1.875rem;height:1.875rem;margin:-.9375rem 0 0 -.9375rem;border:2px solid color-mix(in srgb,var(--lt-link) 25%,transparent);border-top-color:var(--lt-link);border-radius:50%;animation:loading .6s linear infinite;pointer-events:none}.lt-row-expanded>.lt-tree-cell .lt-cell-text{font-weight:600}.lt-tree-subhead-row>td,.lt-tree-child-row>td{padding:0 1rem 0 calc(var(--lt-card-inset, 4rem) + var(--lt-tree-indent, 0rem));background:transparent;border-bottom:none}.lt-tree-subhead-row,.lt-tree-child-row,.lt-tree-child-row:hover{background-color:var(--lt-tree-well, color-mix(in srgb, var(--lt-border) 22%, var(--lt-row-bg)))}.lt-tree-subhead-row>td{padding-top:1rem}.lt-tree-child-row.lt-tree-last-child>td{padding-bottom:1rem}.lt-tree-grid{display:grid;align-items:center;--lt-tree-col-gap: .5rem;column-gap:var(--lt-tree-col-gap);min-height:var(--lt-row-height);padding-left:.625rem;padding-right:.625rem;background:var(--lt-row-bg);border-left:1px solid var(--lt-tree-card-border, color-mix(in srgb, var(--lt-border) 70%, var(--lt-icon)));border-right:1px solid var(--lt-tree-card-border, color-mix(in srgb, var(--lt-border) 70%, var(--lt-icon)))}.lt-tree-child-row:hover .lt-tree-grid{background:var(--lt-row-hover)}.lt-tree-subhead-row .lt-tree-grid{min-height:2rem;background:var(--lt-header-bg);border-top:1px solid var(--lt-tree-card-border, color-mix(in srgb, var(--lt-border) 70%, var(--lt-icon)));border-top-left-radius:var(--lt-radius, 8px);border-top-right-radius:var(--lt-radius, 8px)}.lt-tree-child-row.lt-tree-last-child .lt-tree-grid{border-bottom:1px solid var(--lt-tree-card-border, color-mix(in srgb, var(--lt-border) 70%, var(--lt-icon)));border-bottom-left-radius:var(--lt-radius, 8px);border-bottom-right-radius:var(--lt-radius, 8px);box-shadow:var(--lt-tree-card-shadow, 0 2px 4px color-mix(in srgb, #000 10%, transparent))}.lt-tree-child-row:not(.lt-tree-last-child) .lt-tree-grid{border-bottom:1px solid color-mix(in srgb,var(--lt-border) 55%,transparent)}.lt-tree-subhead-cell{position:relative;padding:0 .3125rem;font-size:.6875rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--lt-header-text);overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.lt-tree-cellbox{display:flex;align-items:center;min-width:0;padding:0 .3125rem;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.lt-tree-cellbox .lt-cell-text{overflow:hidden;text-overflow:ellipsis}.lt-tree-grid>.lt-tree-select-cell{display:flex;align-items:center;justify-content:center;padding:0;overflow:visible}:host(.lt-tree-flat) .lt-tree-subhead-row,:host(.lt-tree-flat) .lt-tree-child-row,:host(.lt-tree-flat) .lt-tree-child-row:hover{background-color:transparent}:host(.lt-tree-flat) .lt-tree-subhead-row>td,:host(.lt-tree-flat) .lt-tree-child-row>td{padding:0}:host(.lt-tree-flat) .lt-tree-grid>.lt-tree-indent-col{padding-left:calc(var(--lt-tree-first-indent, 5.875rem) - var(--lt-tree-lead-offset, 0px))}:host(.lt-tree-flat) .lt-tree-grid{background:transparent;border-left:none;border-right:none;border-radius:0}:host(.lt-tree-flat) .lt-tree-subhead-row .lt-tree-grid{background:var(--lt-header-bg);border:none;border-bottom:1px solid var(--lt-border);border-radius:0}:host(.lt-tree-flat) .lt-tree-child-row .lt-tree-grid,:host(.lt-tree-flat) .lt-tree-child-row.lt-tree-last-child .lt-tree-grid{border-left:none;border-right:none;border-radius:0;box-shadow:none;border-bottom:1px solid color-mix(in srgb,var(--lt-border) 55%,transparent)}.lt-toolbar{display:flex;align-items:center;justify-content:space-between;gap:.75rem;margin:var(--lt-toolbar-margin-top, .75rem) 0 var(--lt-toolbar-margin-bottom, 0);padding:0 0 var(--lt-toolbar-pad-bottom, .625rem);min-height:var(--lt-toolbar-height);position:relative;z-index:30}.lt-ctl-disabled{opacity:.5;pointer-events:none}.lt-toolbar-left{display:flex;align-items:center;gap:var(--lt-control-row-gap);flex:1 1 auto;min-width:0;flex-wrap:nowrap}.lt-filter-zone{display:flex;align-items:center;flex-wrap:nowrap;gap:var(--lt-control-row-gap);flex:0 1 auto;min-width:0}.lt-filters{display:flex;align-items:center;flex-wrap:nowrap;gap:var(--lt-control-row-gap);min-width:0;flex:0 1 auto;overflow:hidden}.lt-filter-item{display:inline-flex;flex:0 0 auto}.lt-filter-more{position:relative;flex:0 0 auto}.lt-filter-more-btn{gap:.375rem;padding:0 .625rem}.lt-filter-more-btn .lt-filter-more-icon{display:inline-flex}.lt-filter-overflow{position:absolute;top:calc(100% + .25rem);right:0;z-index:1000;min-width:15rem;max-width:min(22rem,90vw);display:flex;flex-direction:column;gap:.5rem;padding:.625rem;background:var(--lt-control-bg);border:1px solid var(--lt-border);border-radius:var(--lt-radius);box-shadow:var(--shadowLarge);animation:lt-chooser-in .12s ease-out}.lt-filter-overflow-title{font-family:var(--lt-font);font-size:.6875rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--groupText)}.lt-filter-overflow-row{display:flex}.lt-filter-overflow-row lib-list-filter-cell,.lt-filter-overflow-row .lt-filter-item{flex:1 1 auto;min-width:0}.lt-toolbar-right{display:flex;align-items:center;gap:var(--lt-control-row-gap);flex:0 0 auto}.lt-divider{flex:0 0 auto;width:1px;height:1.5rem;background:var(--lt-border)}.lt-search{position:relative;display:flex;align-items:center;width:18rem;max-width:40vw;flex:0 1 18rem}.lt-search-icon{position:absolute;left:.625rem;display:block;color:var(--lt-icon);pointer-events:none}.lt-search-input{width:100%;height:var(--lt-toolbar-height);padding:0 .75rem 0 1.75rem;font-family:var(--lt-font);font-size:.8125rem;color:var(--lt-cell-text);background:var(--lt-control-bg);border:var(--lt-control-border-full);border-radius:var(--lt-radius);outline:none;transition:border-color .15s,box-shadow .15s}.lt-search-input::placeholder{color:var(--placeholder)}.lt-search-input:focus{border-color:var(--lt-link);box-shadow:0 0 0 .1875rem color-mix(in srgb,var(--lt-link) 18%,transparent)}.lt-count{font-size:.8125rem;color:var(--groupText);white-space:nowrap}.lt-count .lt-count-of{color:var(--groupText)}.lt-pager{display:flex;align-items:center;gap:.25rem}.lt-page-btn{display:inline-flex;align-items:center;justify-content:center;width:var(--lt-toolbar-height);height:var(--lt-toolbar-height);color:var(--lt-icon);background:var(--lt-control-bg);border:var(--lt-control-border-full);border-radius:var(--lt-radius);cursor:pointer;font-size:.9375rem;transition:border-color .15s,color .15s}.lt-page-btn:not(:disabled):hover{border-color:var(--lt-link);color:var(--lt-link)}.lt-page-btn:disabled{opacity:.4;cursor:default}.lt-refresh-btn{padding:0;width:var(--lt-toolbar-height);justify-content:center}.lt-refresh-btn:disabled{opacity:.5;cursor:default;border-color:var(--lt-border);color:var(--lt-cell-text)}.lt-columns{position:relative;flex:0 0 auto}.lt-column-chooser{position:absolute;top:calc(100% + .25rem);right:0;z-index:1000;width:18rem;max-height:calc(100vh - 9rem);display:flex;flex-direction:column;background:var(--lt-control-bg);border:1px solid var(--lt-border);border-radius:var(--lt-radius);box-shadow:var(--shadowLarge);overflow:hidden;animation:lt-chooser-in .12s ease-out}@keyframes lt-chooser-in{0%{opacity:0;transform:translateY(-.25rem)}to{opacity:1;transform:translateY(0)}}.lt-chooser-header{flex:0 0 auto;display:flex;flex-direction:column;padding:.625rem .75rem .5rem;border-bottom:1px solid var(--lt-border)}.lt-chooser-title{font-size:.8125rem;font-weight:700;color:var(--lt-cell-text)}.lt-chooser-subtitle{font-size:.6875rem;color:var(--groupText)}.lt-chooser-view{flex:0 0 auto;display:flex;align-items:center;justify-content:space-between;gap:.75rem;padding:.5rem .75rem;border-bottom:1px solid var(--lt-border)}.lt-chooser-view-label{font-size:.75rem;font-weight:600;color:var(--lt-cell-text)}.lt-chooser-view-toggle{display:flex;gap:.1875rem;padding:.1875rem;background:color-mix(in srgb,var(--lt-border) 30%,var(--lt-control-bg));border:1px solid var(--lt-border);border-radius:var(--lt-radius)}.lt-chooser-view-opt{padding:.25rem .625rem;font-family:var(--lt-font);font-size:.75rem;font-weight:500;color:var(--lt-cell-text);background:transparent;border:none;border-radius:calc(var(--lt-radius) - .0625rem);cursor:pointer}.lt-chooser-view-opt:hover:not(.active){color:var(--lt-link)}.lt-chooser-view-opt.active{color:var(--lt-link);background:var(--lt-control-bg);box-shadow:0 1px 2px color-mix(in srgb,var(--lt-cell-text) 14%,transparent)}.lt-chooser-scroll{flex:1 1 auto;min-height:0;overflow-y:auto}.lt-chooser-list{list-style:none;margin:0;padding:.375rem .5rem}.lt-chooser-nested{padding:0 .5rem .375rem;border-top:1px solid var(--lt-border)}.lt-chooser-group{display:flex;align-items:center;gap:.375rem;width:100%;min-height:2.25rem;padding:.375rem .25rem;font-family:var(--lt-font);font-size:.75rem;font-weight:600;color:var(--lt-cell-text);background:transparent;border:none;border-radius:var(--lt-radius);cursor:pointer;text-align:left}.lt-chooser-group:hover{background:var(--lt-row-hover)}.lt-chooser-group-sub{padding-left:1.25rem;font-weight:500}.lt-chooser-group-label{flex:0 0 auto}.lt-chooser-chevron{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:.875rem;color:var(--lt-icon);transition:transform .15s ease}.lt-chooser-chevron svg{display:block}.lt-chooser-chevron.open{transform:rotate(90deg)}.lt-chooser-group-hint{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.6875rem;font-weight:400;color:var(--groupText)}.lt-chooser-sublist{padding-left:1.75rem}.lt-chooser-row{display:flex;align-items:center;justify-content:space-between;min-height:2.5rem;padding:.5rem;border-radius:var(--lt-radius);background:var(--lt-control-bg);cursor:grab}.lt-chooser-row:active{cursor:grabbing}.lt-chooser-row.cdk-drag-disabled{cursor:default}.lt-chooser-row:hover{background:var(--lt-row-hover)}.lt-chooser-row:hover .lt-chooser-handle{opacity:1}.lt-chooser-toggle{display:flex;align-items:center;gap:.625rem;flex:1 1 auto;min-width:0;padding:0;background:transparent;border:none;font:inherit;font-size:.875rem;color:var(--lt-cell-text);text-align:left;cursor:pointer}.lt-chooser-toggle:disabled{cursor:default;opacity:.5}.lt-chooser-box{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;width:1rem;height:1rem;border:.09375rem solid var(--lt-control-border, var(--lt-border));border-radius:.25rem;color:var(--lt-control-bg);background:var(--lt-control-bg);transition:background .12s,border-color .12s}.lt-chooser-toggle.checked .lt-chooser-box{background:var(--lt-link);border-color:var(--lt-link)}.lt-chooser-toggle:not(:disabled):hover .lt-chooser-box{border-color:var(--lt-link)}.lt-chooser-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.lt-chooser-default-tag{flex:0 0 auto;margin-left:.375rem;font-size:.6875rem;font-weight:400;font-style:italic;color:var(--groupText);white-space:nowrap}.lt-chooser-handle{flex:0 0 auto;display:inline-flex;align-items:center;color:var(--lt-icon);cursor:grab;padding-left:.5rem;-webkit-user-select:none;user-select:none}.lt-chooser-handle:active{cursor:grabbing}.lt-chooser-footer{flex:0 0 auto;padding:.5rem .75rem;border-top:1px solid var(--lt-border)}.lt-chooser-reset{width:100%;height:1.875rem;font-size:.75rem;font-weight:600;color:var(--lt-link);background:transparent;border:1px solid var(--lt-border);border-radius:var(--lt-radius);cursor:pointer}.lt-chooser-reset:disabled{color:var(--groupText);cursor:default;opacity:.6}.lt-chooser-reset:not(:disabled):hover{border-color:var(--lt-link)}.lt-footer{flex:0 0 auto;display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;padding:.625rem .125rem 0;font-family:var(--lt-font);font-size:.8125rem;color:var(--groupText)}.lt-footer-count{white-space:nowrap}.lt-footer-count span{color:var(--lt-cell-text);font-weight:600}.lt-footer-controls{display:flex;align-items:center;gap:1.25rem;flex-wrap:nowrap}.lt-rpp{display:inline-flex;align-items:center;gap:.5rem;white-space:nowrap}.lt-rpp-label{text-transform:none;font-weight:400;color:var(--groupText)}.lt-rpp-select{width:auto;min-width:4rem;height:var(--lt-toolbar-height);padding:0 1.75rem 0 .625rem;font-family:var(--lt-font);font-size:.8125rem;color:var(--lt-cell-text);background-color:var(--lt-control-bg);background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='none' stroke='%236b7280' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' d='M3 4.5 6 7.5 9 4.5'/%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right .5rem center;border:var(--lt-control-border-full);border-radius:var(--lt-radius);appearance:none;-webkit-appearance:none;cursor:pointer}.lt-rpp-select:focus{outline:none;border-color:var(--lt-link)}.lt-page-num{display:inline-flex;align-items:center;justify-content:center;min-width:var(--lt-toolbar-height);height:var(--lt-toolbar-height);padding:0 .5rem;font-family:var(--lt-font);font-size:.8125rem;font-weight:600;color:var(--lt-cell-text);background:var(--lt-control-bg);border:var(--lt-control-border-full);border-radius:var(--lt-radius);cursor:pointer;transition:border-color .15s,color .15s,background .15s}.lt-page-num:hover{border-color:var(--lt-link);color:var(--lt-link)}.lt-page-num.is-current{color:#fff;background:var(--lt-link);border-color:var(--lt-link);cursor:default}.lt-page-gap{display:inline-flex;align-items:center;justify-content:center;min-width:1.25rem;color:var(--groupText)}.lt-tooltip{position:fixed;z-index:1100;pointer-events:none;animation:lt-tooltip-in .12s ease-out}@keyframes lt-tooltip-in{0%{opacity:0;transform:translateY(-.1875rem)}to{opacity:1;transform:translateY(0)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i5.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i5.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i5.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: i5.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i5.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i6.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i6.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: ClickOutsideDirective, selector: "[clickOutside]", outputs: ["clickOutside"] }, { kind: "component", type: ListFilterCellComponent, selector: "lib-list-filter-cell", inputs: ["column"] }, { kind: "component", type: ListHeaderCellComponent, selector: "lib-list-header-cell", inputs: ["column", "sortDir"], outputs: ["sortChanged"] }, { kind: "component", type: ListMenuComponent, selector: "lib-list-menu", inputs: ["open", "items", "left", "top", "menuId"], outputs: ["itemClick", "closed"] }, { kind: "component", type: ListTableAutoRefreshComponent, selector: "lib-list-table-auto-refresh", inputs: ["seconds", "loading", "intervals"], outputs: ["refresh", "secondsChange"] }, { kind: "component", type: SelectCellComponent, selector: "lib-select-cell", inputs: ["row", "toggleItem"] }, { kind: "component", type: ListEmptyComponent, selector: "lib-list-empty", inputs: ["variant", "entityKey", "entityLabel", "scope", "title", "message", "icon", "createLabel", "hasAdd", "readonly", "framed"], outputs: ["create", "clearFilters", "retry"] }] }); }
2888
+ }
2889
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.34", ngImport: i0, type: ListTableComponent, decorators: [{
2890
+ type: Component,
2891
+ args: [{ selector: 'lib-list-table', standalone: true, imports: [
2892
+ CommonModule,
2893
+ FormsModule,
2894
+ DragDropModule,
2895
+ ClickOutsideDirective,
2896
+ ListFilterCellComponent,
2897
+ ListHeaderCellComponent,
2898
+ ListMenuComponent,
2899
+ ListTableAutoRefreshComponent,
2900
+ SelectCellComponent,
2901
+ ListEmptyComponent,
2902
+ ], providers: [ListTableService], template: "@if (showTable && !accessDenied) {\n <div class=\"lt-container\">\n <div class=\"lt-toolbar\">\n <div class=\"lt-toolbar-left\">\n @if (toolbarControls.search.show) {\n <div class=\"lt-search\" [class.lt-ctl-disabled]=\"toolbarControls.search.disabled\">\n <svg class=\"lt-search-icon\" viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" aria-hidden=\"true\">\n <circle cx=\"7\" cy=\"7\" r=\"4.3\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n <line x1=\"10.4\" y1=\"10.4\" x2=\"14.6\" y2=\"14.6\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/>\n </svg>\n <input\n class=\"lt-search-input\"\n id=\"SearchFilter\"\n type=\"text\"\n [placeholder]=\"'Search ' + (entityTypeLabel || 'items')\"\n [(ngModel)]=\"searchString\"\n [disabled]=\"toolbarControls.search.disabled\"\n (keyup)=\"onSearchInput()\"\n />\n </div>\n }\n @if (toolbarControls.filters.show && filterableColumns.length) {\n <div class=\"lt-filter-zone\" [class.lt-ctl-disabled]=\"toolbarControls.filters.disabled\" #filterZone>\n <div class=\"lt-filters\" #filtersRow>\n @for (col of visibleFilterColumns; track col.key) {\n <span class=\"lt-filter-item\" [attr.data-filter-key]=\"col.key\">\n <lib-list-filter-cell [column]=\"col\"></lib-list-filter-cell>\n </span>\n }\n </div>\n @if (overflowFilterColumns.length) {\n <div class=\"lt-filter-more\">\n <button\n type=\"button\"\n class=\"lt-filter-more-btn\"\n [class.is-on]=\"hiddenActiveCount > 0\"\n [class.is-open]=\"showFilterOverflow\"\n (click)=\"toggleFilterOverflow($event)\"\n aria-haspopup=\"true\"\n [attr.aria-expanded]=\"showFilterOverflow\"\n [attr.aria-label]=\"overflowFilterColumns.length + ' more filters'\"\n id=\"FilterOverflowButton\"\n >\n <span class=\"lt-filter-more-icon\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\"><path fill=\"currentColor\" d=\"M2 3h12l-4.6 5.6v4l-2.8 1.4v-5.4z\"/></svg>\n </span>\n <span class=\"lt-filter-more-label\">More</span>\n <span class=\"lt-filter-more-count\">{{ overflowFilterColumns.length }}</span>\n </button>\n @if (showFilterOverflow) {\n <div class=\"lt-filter-overflow\" (clickOutside)=\"closeFilterOverflow($event)\">\n <div class=\"lt-filter-overflow-title\">Filters</div>\n @for (col of overflowFilterColumns; track col.key) {\n <div class=\"lt-filter-overflow-row\" [class.is-active]=\"isFilterActive(col)\">\n <lib-list-filter-cell [column]=\"col\"></lib-list-filter-cell>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n <!-- Consumer-supplied toolbar controls (e.g. page-specific filters), rendered inline beside search. -->\n <ng-content select=\"[toolbar-filters]\"></ng-content>\n </div>\n <div class=\"lt-toolbar-right\">\n <ng-template #refreshIcon>\n <svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" aria-hidden=\"true\">\n <path fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" d=\"M13.4 8a5.4 5.4 0 1 1-1.5-3.8\"/>\n <path fill=\"currentColor\" d=\"M13.9 2.1l.5 3.2-3.2-.5z\"/>\n </svg>\n </ng-template>\n @if (toolbarControls.refresh.show) {\n <button\n type=\"button\"\n class=\"lt-refresh-btn\"\n (click)=\"refresh()\"\n [disabled]=\"filtering || toolbarControls.refresh.disabled\"\n aria-label=\"Refresh\"\n title=\"Refresh\"\n id=\"RefreshTableButton\"\n >\n <ng-container [ngTemplateOutlet]=\"refreshIcon\"></ng-container>\n </button>\n }\n @if (toolbarControls.autoRefresh.show) {\n <span class=\"lt-autorefresh-wrap\" [class.lt-ctl-disabled]=\"toolbarControls.autoRefresh.disabled\">\n <lib-list-table-auto-refresh [seconds]=\"autoRefreshSeconds\" [loading]=\"filtering\" (refresh)=\"refresh()\"></lib-list-table-auto-refresh>\n </span>\n }\n @if (toolbarControls.columns.show) {\n <div class=\"lt-columns\">\n <button\n type=\"button\"\n class=\"lt-columns-btn\"\n [class.is-on]=\"hiddenColumnCount > 0\"\n [class.is-open]=\"showColumnChooser\"\n (click)=\"toggleColumnChooser($event)\"\n [disabled]=\"toolbarControls.columns.disabled\"\n aria-haspopup=\"true\"\n [attr.aria-expanded]=\"showColumnChooser\"\n id=\"ColumnChooserButton\"\n >\n <svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" aria-hidden=\"true\">\n <path fill=\"currentColor\" d=\"M2 3.5h9v1.4H2zM2 7.3h6v1.4H2zM2 11.1h11v1.4H2z\"/>\n <circle cx=\"13\" cy=\"4.2\" r=\"1.7\" fill=\"currentColor\"/>\n <circle cx=\"10\" cy=\"8\" r=\"1.7\" fill=\"currentColor\"/>\n <circle cx=\"14.4\" cy=\"11.8\" r=\"1.7\" fill=\"currentColor\"/>\n </svg>\n <span>Columns</span>\n @if (hiddenColumnCount > 0) {\n <span class=\"lt-columns-count\">{{ hiddenColumnCount }}</span>\n }\n </button>\n @if (showColumnChooser) {\n <div class=\"lt-column-chooser\" (clickOutside)=\"closeColumnChooser()\">\n <div class=\"lt-chooser-header\">\n <span class=\"lt-chooser-title\">Columns</span>\n <span class=\"lt-chooser-subtitle\">Customize order and visibility</span>\n </div>\n @if (canSwitchTreeVariant && showTreeVariantToggle) {\n <div class=\"lt-chooser-view\" role=\"group\" aria-label=\"Expanded row layout\">\n <span class=\"lt-chooser-view-label\">Expanded rows</span>\n <div class=\"lt-chooser-view-toggle\">\n <button type=\"button\" class=\"lt-chooser-view-opt\" [class.active]=\"treeVariant === 'flat'\" (click)=\"setTreeVariant('flat')\">Flat</button>\n <button type=\"button\" class=\"lt-chooser-view-opt\" [class.active]=\"treeVariant === 'card'\" (click)=\"setTreeVariant('card')\">Nested</button>\n </div>\n </div>\n }\n <div class=\"lt-chooser-scroll\">\n <ul class=\"lt-chooser-list\" cdkDropList cdkDropListLockAxis=\"y\" (cdkDropListDropped)=\"chooserReorder($event)\">\n @for (col of chooserColumns; track col.key) {\n <li class=\"lt-chooser-row\" cdkDrag cdkDragLockAxis=\"y\" [cdkDragDisabled]=\"col.reorderable === false\"\n (cdkDragStarted)=\"onChooserDragStarted()\" (cdkDragEnded)=\"onChooserDragEnded()\">\n <ng-container *ngTemplateOutlet=\"chooserRowBody; context: { col: col }\"></ng-container>\n </li>\n }\n </ul>\n @if (canSwitchTreeVariant && childLevels.length) {\n <div class=\"lt-chooser-nested\">\n <button\n type=\"button\"\n class=\"lt-chooser-group\"\n (click)=\"toggleNestedGroup('__root')\"\n [attr.aria-expanded]=\"isNestedGroupOpen('__root')\"\n >\n <span class=\"lt-chooser-chevron\" [class.open]=\"isNestedGroupOpen('__root')\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 16 16\" width=\"11\" height=\"11\"><path fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 4l4 4-4 4\"/></svg>\n </span>\n <span class=\"lt-chooser-group-label\">Nested rows</span>\n </button>\n @if (isNestedGroupOpen('__root')) {\n @for (level of childLevels; track level) {\n <button\n type=\"button\"\n class=\"lt-chooser-group lt-chooser-group-sub\"\n (click)=\"toggleNestedGroup('lvl' + level)\"\n [attr.aria-expanded]=\"isNestedGroupOpen('lvl' + level)\"\n >\n <span class=\"lt-chooser-chevron\" [class.open]=\"isNestedGroupOpen('lvl' + level)\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 16 16\" width=\"11\" height=\"11\"><path fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 4l4 4-4 4\"/></svg>\n </span>\n <span class=\"lt-chooser-group-label\">Level {{ level }}</span>\n <span class=\"lt-chooser-group-hint\">{{ childLevelLabel(level) }}</span>\n </button>\n @if (isNestedGroupOpen('lvl' + level)) {\n <ul class=\"lt-chooser-list lt-chooser-sublist\" cdkDropList cdkDropListLockAxis=\"y\" (cdkDropListDropped)=\"chooserReorder($event, level)\">\n @for (col of allColumnsForLevel(level); track col.key) {\n <li class=\"lt-chooser-row\" cdkDrag cdkDragLockAxis=\"y\" [cdkDragDisabled]=\"col.reorderable === false\"\n (cdkDragStarted)=\"onChooserDragStarted()\" (cdkDragEnded)=\"onChooserDragEnded()\">\n <ng-container *ngTemplateOutlet=\"chooserRowBody; context: { col: col, level: level }\"></ng-container>\n </li>\n }\n </ul>\n }\n }\n }\n </div>\n }\n </div>\n <div class=\"lt-chooser-footer\">\n <button\n type=\"button\"\n class=\"lt-chooser-reset\"\n [disabled]=\"columnsAtDefault\"\n (click)=\"resetTableColumns()\"\n id=\"ResetColumnsButton\"\n >\n Reset to default\n </button>\n </div>\n </div>\n }\n </div>\n }\n </div>\n </div>\n\n <!-- reload with rows already on screen: a thin indeterminate bar under the toolbar,\n rows stay visible (dimmed) and usable. role=progressbar (indeterminate). -->\n @if (showRefreshBar) {\n <div class=\"lt-refresh-bar\" role=\"progressbar\" aria-label=\"Refreshing\" aria-busy=\"true\">\n <span class=\"lt-refresh-bar-fill\"></span>\n </div>\n }\n\n <!-- inner content of a Columns-chooser row, shared by the top-level and nested lists.\n The <li cdkDrag> wrapper stays inline in each list (above) so cdkDropList's content\n query still registers it as a draggable; only the body is shared here.\n `level` undefined targets the top-level columns, a number targets that nested depth. -->\n <ng-template #chooserRowBody let-col=\"col\" let-level=\"level\">\n <button\n type=\"button\"\n class=\"lt-chooser-toggle\"\n [class.checked]=\"!col.hidden\"\n [disabled]=\"col.hideable === false || isLastVisibleColumn(col, level)\"\n [attr.title]=\"isLastVisibleColumn(col, level) ? 'At least one column must stay visible' : null\"\n (click)=\"toggleColumnVisible(col, level)\"\n [attr.aria-pressed]=\"!col.hidden\"\n >\n <span class=\"lt-chooser-box\" aria-hidden=\"true\">\n <!-- always rendered: the check is white, so it stays invisible until the box\n turns blue when checked. Keeping it in the DOM means clicking to hide a\n column never detaches the click target, which would otherwise make the\n click-outside handler think the click was outside and close the menu. -->\n <svg viewBox=\"0 0 12 12\" width=\"10\" height=\"10\">\n <path fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2 6.3 4.6 9 10 3\"/>\n </svg>\n </span>\n <span class=\"lt-chooser-text\">{{ col.label }}</span>\n @if (level == null && isDefaultHidden(col)) {\n <span class=\"lt-chooser-default-tag\">hidden by default</span>\n }\n </button>\n @if (col.reorderable !== false) {\n <!-- the whole row is the drag target (cdkDrag on the <li>); this is just the grip affordance -->\n <span class=\"lt-chooser-handle\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 10 16\" width=\"10\" height=\"15\" fill=\"currentColor\">\n <circle cx=\"3\" cy=\"3\" r=\"1.25\"/><circle cx=\"7\" cy=\"3\" r=\"1.25\"/>\n <circle cx=\"3\" cy=\"8\" r=\"1.25\"/><circle cx=\"7\" cy=\"8\" r=\"1.25\"/>\n <circle cx=\"3\" cy=\"13\" r=\"1.25\"/><circle cx=\"7\" cy=\"13\" r=\"1.25\"/>\n </svg>\n </span>\n }\n </ng-template>\n\n <div class=\"lt-scroll\" id=\"ItemTable\" [class.lt-is-refreshing]=\"showRefreshBar\" [class.lt-is-empty]=\"!isLoading && !rowData?.length\" [attr.aria-busy]=\"isLoading ? 'true' : null\">\n <!-- expand/collapse control, reused by root rows and nested child rows -->\n <ng-template #treeToggle let-row=\"row\">\n @if (canExpand(row)) {\n <button\n type=\"button\"\n class=\"lt-tree-toggle\"\n [class.is-expanded]=\"row.__treeExpanded\"\n [class.is-loading]=\"row.__treeLoading\"\n [class.is-error]=\"row.__treeLoadError\"\n (click)=\"$event.stopPropagation(); toggleNode(row)\"\n [attr.aria-expanded]=\"!!row.__treeExpanded\"\n [attr.aria-busy]=\"!!row.__treeLoading\"\n [attr.title]=\"row.__treeLoadError ? 'Could not load children - click to retry' : null\"\n [attr.aria-label]=\"row.__treeExpanded ? 'Collapse row' : 'Expand row'\"\n >\n <svg class=\"lt-tree-chevron\" viewBox=\"0 0 16 16\" width=\"12\" height=\"12\" aria-hidden=\"true\">\n <path fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 4l4 4-4 4\"/>\n </svg>\n @if (row.__treeLoading) {\n <span class=\"lt-tree-spinner\" aria-hidden=\"true\"></span>\n }\n </button>\n } @else {\n <span class=\"lt-tree-spacer\" aria-hidden=\"true\"></span>\n }\n </ng-template>\n\n <!-- a single cell's content, reused across the fixed grid and the nested grid -->\n <ng-template #cellBody let-col=\"col\" let-row=\"row\" let-i=\"i\">\n @switch (renderMode(col)) {\n @case ('component') {\n <ng-container *ngComponentOutlet=\"col.cell; inputs: cellInputs(col, row, i)\"></ng-container>\n }\n @case ('html') {\n <span class=\"lt-cell-html\" [innerHTML]=\"cellHtml(col, row, i)\"></span>\n }\n @default {\n <span class=\"lt-cell-text\">{{ cellText(col, row, i) }}</span>\n }\n }\n </ng-template>\n\n <table class=\"lt-table\">\n <colgroup>\n @if (!options.noSelect) { <col class=\"lt-col-select\" /> }\n @for (col of visibleColumns; track trackCol($index, col)) {\n <col [style.width]=\"colWidth(col)\" />\n }\n @if (!options.noMenu) { <col class=\"lt-col-menu\" /> }\n </colgroup>\n <thead class=\"lt-head\">\n <tr cdkDropList cdkDropListOrientation=\"horizontal\" (cdkDropListDropped)=\"onReorder($event)\">\n @if (!options.noSelect) {\n <th class=\"lt-select-cell\">\n <div\n class=\"lt-select-all\"\n [class.checked]=\"allToggled\"\n [class.indeterminate]=\"someSelected\"\n (click)=\"toggleAll()\"\n id=\"SelectAllRows\"\n ></div>\n </th>\n }\n @for (col of visibleColumns; track trackCol($index, col)) {\n <th\n cdkDrag\n [cdkDragDisabled]=\"col.reorderable === false\"\n class=\"lt-th\"\n [class.lt-th-tree]=\"isTreeCell(col)\"\n [ngClass]=\"col.cellClass\"\n id=\"Header_{{ col.key }}\"\n >\n <lib-list-header-cell\n [column]=\"col\"\n [sortDir]=\"col.sortDir\"\n (sortChanged)=\"onHeaderSort($event)\"\n ></lib-list-header-cell>\n @if (col.resizable !== false) {\n <span class=\"lt-resize\" (pointerdown)=\"startResize(col, $event)\"></span>\n }\n </th>\n }\n @if (!options.noMenu) {\n <th class=\"lt-menu-cell\">\n <div class=\"lt-header-menu-trigger\" (click)=\"openHeaderActionMenu($event)\" id=\"ColumnMenuHeader\">\n &#8942;\n </div>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @if (showSkeleton) {\n <!-- first load, no data yet: placeholder rows matching the real column layout.\n aria-hidden so screen readers get the region's aria-busy, not fake rows. -->\n @for (sk of skeletonRows; track sk; let r = $index) {\n <tr class=\"lt-row lt-skeleton-row\" aria-hidden=\"true\">\n @if (!options.noSelect) {\n <td class=\"lt-select-cell\"><span class=\"lt-sk lt-sk-check\"></span></td>\n }\n @for (col of visibleColumns; track trackCol($index, col); let c = $index) {\n <td class=\"lt-cell\" [class.lt-tree-cell]=\"isTreeCell(col)\">\n @if (isTreeCell(col)) {\n <span class=\"lt-sk lt-sk-dot\"></span>\n }\n <span class=\"lt-sk lt-sk-bar\" [style.width.%]=\"skeletonBarWidth(c, r)\"></span>\n </td>\n }\n @if (!options.noMenu) {\n <td class=\"lt-menu-cell\"></td>\n }\n </tr>\n }\n } @else {\n @for (row of flatRows; track trackRow($index, row); let i = $index) {\n @if (isTreeMode && row.__treeLevel > 0 && hasChildColumns) {\n <!-- nested rows with their own per-level columns, laid out in a grid\n that spans the full table width (so they need not match the parent) -->\n @if (row.__treeFirstChild) {\n <tr class=\"lt-tree-subhead-row\">\n <td\n [attr.colspan]=\"treeSpanCount\"\n [style.--lt-tree-indent.rem]=\"treeIndentFor(row.__treeLevel)\"\n [style.--lt-tree-first-indent.rem]=\"treeFlatFirstIndentRem(row.__treeLevel)\"\n [style.--lt-tree-lead-offset]=\"treeLeadOffset(row.__treeLevel)\"\n [style.background-image]=\"treeAncestorRails(row.__treeLevel)\"\n >\n <div\n class=\"lt-tree-grid\"\n [style.grid-template-columns]=\"gridTemplate(row.__treeLevel)\"\n >\n @if (childSelectEnabled) {\n <span class=\"lt-tree-subhead-cell lt-tree-select-cell\"></span>\n }\n @for (col of columnsForLevel(row.__treeLevel); track col.key; let last = $last) {\n <span class=\"lt-tree-subhead-cell\" [class.lt-tree-indent-col]=\"isTreeColumn(col, row.__treeLevel)\" [ngClass]=\"cellClass(col)\">\n {{ col.label }}\n @if (!last && col.resizable !== false) {\n <span class=\"lt-resize lt-tree-resize\" (pointerdown)=\"startChildResize(col, row.__treeLevel, $event)\"></span>\n }\n </span>\n }\n </div>\n </td>\n </tr>\n }\n <tr\n class=\"lt-row lt-tree-child-row\"\n [class.lt-tree-last-child]=\"row.__treeLastChild\"\n [class.lt-row-selected]=\"row.selected || row.open\"\n [class.lt-row-expanded]=\"row.__treeExpanded\"\n (mouseenter)=\"onRowHover(row)\"\n (mouseleave)=\"onRowLeave(row)\"\n >\n <td\n [attr.colspan]=\"treeSpanCount\"\n [style.--lt-tree-indent.rem]=\"treeIndentFor(row.__treeLevel)\"\n [style.--lt-tree-first-indent.rem]=\"treeFlatFirstIndentRem(row.__treeLevel)\"\n [style.--lt-tree-lead-offset]=\"treeLeadOffset(row.__treeLevel)\"\n [style.background-image]=\"treeAncestorRails(row.__treeLevel)\"\n >\n <div\n class=\"lt-tree-grid\"\n [style.grid-template-columns]=\"gridTemplate(row.__treeLevel)\"\n >\n @if (childSelectEnabled) {\n <div class=\"lt-tree-cellbox lt-tree-select-cell\">\n <lib-select-cell [row]=\"row\" [toggleItem]=\"toggleItem\"></lib-select-cell>\n </div>\n }\n @for (col of columnsForLevel(row.__treeLevel); track col.key) {\n <div\n class=\"lt-tree-cellbox\"\n [class.lt-tree-indent-col]=\"isTreeColumn(col, row.__treeLevel)\"\n [ngClass]=\"cellClass(col)\"\n (click)=\"onCellClick(col, row, i)\"\n (mouseenter)=\"onCellHover(col, row, i, $event)\"\n (mouseleave)=\"onCellLeave()\"\n >\n @if (isTreeColumn(col, row.__treeLevel) && showTreeLead(row)) {\n <span class=\"lt-tree-lead\">\n <ng-container *ngTemplateOutlet=\"treeToggle; context: {row: row}\"></ng-container>\n </span>\n }\n <ng-container *ngTemplateOutlet=\"cellBody; context: {col: col, row: row, i: i}\"></ng-container>\n </div>\n }\n </div>\n </td>\n </tr>\n } @else {\n <tr\n class=\"lt-row\"\n [class.lt-row-odd]=\"i % 2 === 1\"\n [class.lt-row-selected]=\"row.selected || row.open\"\n [class.lt-row-expanded]=\"row.__treeExpanded\"\n [class.row-invalid]=\"row.invalid\"\n [class.row-copied]=\"row.copied\"\n (mouseenter)=\"onRowHover(row)\"\n (mouseleave)=\"onRowLeave(row)\"\n >\n @if (!options.noSelect) {\n <td class=\"lt-select-cell\">\n <lib-select-cell [row]=\"row\" [toggleItem]=\"toggleItem\"></lib-select-cell>\n </td>\n }\n @for (col of visibleColumns; track trackCol($index, col)) {\n <td\n class=\"lt-cell\"\n [class.lt-tree-cell]=\"isTreeCell(col)\"\n [ngClass]=\"cellClass(col)\"\n (click)=\"onCellClick(col, row, i)\"\n (mouseenter)=\"onCellHover(col, row, i, $event)\"\n (mouseleave)=\"onCellLeave()\"\n >\n @if (isTreeCell(col)) {\n <span class=\"lt-tree-cell-inner\">\n <span class=\"lt-tree-lead\" [style.margin-left.rem]=\"(row.__treeLevel || 0) * treeIndentRem\">\n <ng-container *ngTemplateOutlet=\"treeToggle; context: {row: row}\"></ng-container>\n </span>\n <ng-container *ngTemplateOutlet=\"cellBody; context: {col: col, row: row, i: i}\"></ng-container>\n </span>\n } @else {\n <ng-container *ngTemplateOutlet=\"cellBody; context: {col: col, row: row, i: i}\"></ng-container>\n }\n </td>\n }\n @if (!options.noMenu) {\n <td class=\"lt-menu-cell\">\n @if (row.actionList?.length) {\n <button\n type=\"button\"\n class=\"lt-row-menu-trigger\"\n (click)=\"$event.stopPropagation(); openActionMenu($event, row)\"\n attr.id=\"RowMenu_{{ row.id }}\"\n aria-label=\"Row actions\"\n >&#8942;</button>\n }\n </td>\n }\n </tr>\n }\n }\n }\n </tbody>\n </table>\n @if (!isLoading && (!rowData || rowData.length === 0)) {\n <lib-list-empty\n [variant]=\"filterApplied ? 'no-results' : 'empty'\"\n [entityKey]=\"tableId\"\n [entityLabel]=\"entityTypeLabel\"\n [scope]=\"emptyScope\"\n [createLabel]=\"createLabel\"\n [hasAdd]=\"showNoItemsAdd\"\n [readonly]=\"noItemsReadonly\"\n [framed]=\"false\"\n (create)=\"openCreate()\"\n (clearFilters)=\"clearTableFilters()\"\n >\n <ng-content select=\"[empty-state]\" ngProjectAs=\"[empty-state]\"></ng-content>\n </lib-list-empty>\n }\n </div>\n\n <div class=\"lt-footer\">\n <div class=\"lt-footer-count\">\n Showing <span id=\"Start\">{{ startCount }}</span>&#8211;<span id=\"End\">{{ endCount }}</span>\n of <span id=\"Total\">{{ totalCount }}</span>\n </div>\n <div class=\"lt-footer-controls\">\n @if (toolbarControls.pageSize.show) {\n <div class=\"lt-rpp\" [class.lt-ctl-disabled]=\"toolbarControls.pageSize.disabled\">\n <span class=\"lt-rpp-label\">Rows per page</span>\n <select\n class=\"lt-rpp-select\"\n [disabled]=\"filtering || toolbarControls.pageSize.disabled\"\n [value]=\"pageSize\"\n (change)=\"onPageSizeChange(+$any($event.target).value)\"\n aria-label=\"Rows per page\"\n >\n @for (opt of pageSizeOptions; track opt) {\n <option [value]=\"opt\">{{ opt }}</option>\n }\n </select>\n </div>\n }\n @if (toolbarControls.pagination.show) {\n <div class=\"lt-pager\" [class.lt-ctl-disabled]=\"toolbarControls.pagination.disabled\">\n <button type=\"button\" class=\"lt-page-btn icon-prev\" [disabled]=\"prevDisabled\" (click)=\"prevPage()\" aria-label=\"Previous page\"></button>\n @if (!cursorPager) {\n @for (p of pageWindow; track $index) {\n @if (p === 0) {\n <span class=\"lt-page-gap\">&#8230;</span>\n } @else {\n <button\n type=\"button\"\n class=\"lt-page-num\"\n [class.is-current]=\"p === currentPageNum\"\n (click)=\"goToPage(p)\"\n >{{ p }}</button>\n }\n }\n }\n <button type=\"button\" class=\"lt-page-btn icon-next\" [disabled]=\"nextDisabled\" (click)=\"nextPage()\" aria-label=\"Next page\"></button>\n </div>\n }\n </div>\n </div>\n\n <!-- header action menu -->\n <lib-list-menu\n menuId=\"HeaderActionMenu\"\n [open]=\"openHeaderMenu\"\n [items]=\"headerMenuItems\"\n [left]=\"menuLeft\"\n [top]=\"menuTop\"\n (itemClick)=\"onHeaderMenuClick($event)\"\n (closed)=\"closeHeaderActionMenu()\"\n ></lib-list-menu>\n\n <!-- row action menu -->\n <lib-list-menu\n menuId=\"RowActionMenu\"\n [open]=\"openMenu\"\n [items]=\"rowMenuItems\"\n [left]=\"menuLeft\"\n [top]=\"menuTop\"\n (itemClick)=\"onRowMenuClick($event)\"\n (closed)=\"closeActionMenu()\"\n ></lib-list-menu>\n </div>\n} @else {\n <!-- access denied: a full-page wall (no table chrome to show without permission) -->\n <lib-list-empty\n variant=\"access-denied\"\n [entityKey]=\"tableId\"\n [entityLabel]=\"entityTypeLabel\"\n [scope]=\"emptyScope\"\n ></lib-list-empty>\n}\n\n@if (hoverTooltip) {\n <div class=\"lt-tooltip\" [ngStyle]=\"{ left: hoverTooltip.left + 'px', top: hoverTooltip.top + 'px' }\">\n <ng-container *ngComponentOutlet=\"hoverTooltip.component; inputs: hoverTooltip.inputs\"></ng-container>\n </div>\n}\n", styles: [":host{display:flex;flex-direction:column;width:100%;height:100%}.lt-columns-btn,.lt-refresh-btn,.lt-filter-more-btn{display:inline-flex;align-items:center;gap:var(--lt-control-gap);height:var(--lt-toolbar-height);padding:0 var(--lt-control-padding-x);font-family:var(--lt-font);font-size:var(--lt-control-font-size);font-weight:var(--lt-control-font-weight);color:var(--lt-cell-text);background:var(--lt-control-bg);border:var(--lt-control-border-full);border-radius:var(--lt-radius);cursor:pointer;white-space:nowrap;transition:border-color .15s,background .15s,color .15s}.lt-columns-btn svg,.lt-refresh-btn svg,.lt-filter-more-btn svg{display:block}.lt-columns-btn:hover,.lt-refresh-btn:hover,.lt-filter-more-btn:hover,.is-open.lt-columns-btn,.is-open.lt-refresh-btn,.is-open.lt-filter-more-btn,.is-on.lt-columns-btn,.is-on.lt-refresh-btn,.is-on.lt-filter-more-btn{border-color:var(--lt-link);color:var(--lt-link)}.is-on.lt-columns-btn,.is-on.lt-refresh-btn,.is-on.lt-filter-more-btn{background:color-mix(in srgb,var(--lt-link) 8%,var(--lt-control-bg))}.lt-columns-count,.lt-filter-more-count{display:inline-flex;align-items:center;justify-content:center;min-width:1.125rem;height:1.125rem;padding:0 .3125rem;font-size:.6875rem;font-weight:700;color:var(--lt-control-bg);background:var(--lt-link);border-radius:.5625rem}.lt-container{display:flex;position:relative;height:calc(100% - var(--lt-container-reserve, 5.625rem));width:100%;flex-direction:column}.lt-scroll{position:relative;z-index:1;width:100%;flex:0 1 auto;min-height:0;overflow:auto;background-color:var(--lt-bg)}.lt-scroll.lt-is-empty{flex:1 1 auto;display:flex;flex-direction:column}.lt-scroll.lt-is-empty lib-list-empty{flex:1 1 auto;min-height:0}.lt-table{width:100%;border-collapse:collapse;table-layout:fixed;font-family:var(--lt-font);font-size:.8125rem;color:var(--lt-cell-text)}.lt-skeleton-row td{height:var(--lt-row-height, 2.9rem)}.lt-skeleton-row:nth-child(1) .lt-sk{animation-delay:0s}.lt-skeleton-row:nth-child(2) .lt-sk{animation-delay:.08s}.lt-skeleton-row:nth-child(3) .lt-sk{animation-delay:.16s}.lt-skeleton-row:nth-child(4) .lt-sk{animation-delay:.24s}.lt-skeleton-row:nth-child(5) .lt-sk{animation-delay:.32s}.lt-skeleton-row:nth-child(6) .lt-sk{animation-delay:.4s}.lt-skeleton-row:nth-child(7) .lt-sk{animation-delay:.48s}.lt-skeleton-row:nth-child(8) .lt-sk{animation-delay:.56s}.lt-sk{display:inline-block;vertical-align:middle;border-radius:.375rem;background:color-mix(in srgb,var(--lt-border) 60%,transparent);animation:lt-skeleton-pulse 1.4s ease-in-out infinite}.lt-sk-bar{height:.7rem}.lt-sk-dot{width:1rem;height:1rem;border-radius:50%;margin-right:.5rem}.lt-sk-check{display:block;width:1rem;height:1rem;margin:0 auto;border-radius:.25rem}@keyframes lt-skeleton-pulse{0%,to{opacity:1}50%{opacity:.45}}.lt-refresh-bar{--lt-progress: var(--primary, var(--lt-link, #0027ab));position:relative;flex:0 0 auto;height:2px;margin-bottom:-2px;z-index:4;width:100%;overflow:hidden;background:color-mix(in srgb,var(--lt-progress) 20%,transparent)}.lt-refresh-bar-fill{position:absolute;top:0;left:0;height:100%;width:40%;background:var(--lt-progress);border-radius:1px;animation:lt-refresh-indeterminate 1.1s ease-in-out infinite}@keyframes lt-refresh-indeterminate{0%{left:-40%;width:40%}50%{width:55%}to{left:100%;width:40%}}.lt-is-refreshing tbody{opacity:.6;transition:opacity .15s ease}@media(prefers-reduced-motion:reduce){.lt-sk{animation:none}.lt-refresh-bar-fill{left:0;width:100%;animation:lt-refresh-pulse 1.4s ease-in-out infinite}@keyframes lt-refresh-pulse{0%,to{opacity:.45}50%{opacity:1}}}.lt-head{position:sticky;top:0;z-index:10}.lt-head th{position:relative;box-sizing:border-box;height:2.6rem;padding:0 .3125rem 0 1.25rem;text-align:left;vertical-align:middle;background-color:var(--lt-header-bg);border-bottom:1px solid var(--lt-header-underline);white-space:nowrap;-webkit-user-select:none;user-select:none}.lt-head th.lt-th-tree{padding-left:2.75rem}.lt-head .lt-select-cell,.lt-head .lt-menu-cell{width:3.75rem;padding:0}.lt-resize{position:absolute;right:0;top:50%;transform:translateY(-50%);width:.75rem;height:2.25rem;cursor:col-resize}.lt-resize:before,.lt-resize:after{content:\"\";position:absolute;top:50%;height:1.25rem;transform:translateY(-50%);width:1px;background:color-mix(in srgb,var(--lt-border) 50%,var(--lt-icon));transition:background .15s}.lt-resize:before{right:.3125rem}.lt-resize:after{right:.125rem}.lt-resize:hover:before,.lt-resize:hover:after{background:color-mix(in srgb,var(--lt-icon) 72%,var(--lt-cell-text))}.lt-select-all{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);box-sizing:border-box;width:1rem;height:1rem;border:1px solid var(--lt-control-border, var(--lt-border));border-radius:.25rem;background:var(--lt-control-bg);background-repeat:no-repeat;background-position:center center;cursor:pointer;transition:border-color .12s,background-color .12s}.lt-select-all:hover{border-color:var(--lt-link)}.lt-select-all.checked{background-color:var(--lt-link);border-color:var(--lt-link);background-size:.75rem;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' d='M2.5 7.6 5.7 10.8 11.5 3.6'/%3E%3C/svg%3E\")}.lt-select-all.indeterminate{background-color:var(--lt-link);border-color:var(--lt-link);background-image:linear-gradient(#fff,#fff);background-size:.5rem .125rem}.lt-header-menu-trigger{display:flex;align-items:center;justify-content:center;height:100%;font-size:1.25rem;color:var(--lt-icon);cursor:pointer}.lt-header-menu-trigger:hover{color:var(--lt-icon-active)}.lt-row-menu-trigger{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;padding:0;font-size:1.25rem;line-height:1;color:var(--lt-icon);background:transparent;border:none;border-radius:var(--lt-radius);cursor:pointer;transition:color .15s,background .15s}.lt-row-menu-trigger:hover{color:var(--lt-icon-active);background:var(--lt-row-hover)}.cdk-drag-preview{box-sizing:border-box;background-color:var(--lt-header-bg);color:var(--lt-header-text);box-shadow:var(--shadowLarge);padding:0 .625rem;display:flex;align-items:center;border-radius:var(--lt-radius)}.cdk-drag-placeholder{opacity:.3}.cdk-drag-animating{transition:transform .2s cubic-bezier(0,0,.2,1)}.lt-row{height:var(--lt-row-height);background-color:var(--lt-row-bg);color:var(--lt-cell-text)}.lt-row td{border-bottom:1px solid var(--lt-border)}.lt-row.lt-row-odd{background-color:var(--lt-row-alt-bg)}.lt-row:hover{background-color:var(--lt-row-hover)}.lt-row.lt-row-selected{background-color:var(--lt-row-selected)}.lt-row.lt-row-selected:hover{background-color:var(--lt-row-hover)}.lt-row.row-invalid{background-color:var(--invalid)!important}.lt-row.row-copied{background-color:var(--copiedRow)!important}.lt-cell{height:var(--lt-row-height);padding:0 .625rem 0 1.25rem;text-align:left;vertical-align:middle;border:none;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;-webkit-user-select:text;user-select:text}.lt-cell-text{display:inline-block;overflow:hidden;text-overflow:ellipsis;max-width:100%;vertical-align:middle}.lt-cell.lt-cell-center{text-align:center}.lt-badge-cell .lt-cell-text{padding:.1875rem .5rem;font-size:.75rem;font-weight:600;line-height:1.2;color:var(--lt-cell-text);background:color-mix(in srgb,var(--lt-border) 45%,transparent);border:1px solid var(--lt-border);border-radius:999px}.lt-badge-cell .lt-cell-text:empty{display:none}.lt-select-cell,.lt-menu-cell{width:3.75rem;padding:0;text-align:center;vertical-align:middle;overflow:visible;position:relative}.lt-tree-cell{padding-left:.625rem}.lt-tree-cell-inner{display:flex;align-items:center}.lt-tree-lead{display:inline-flex;align-items:center;flex:0 0 auto;margin-right:.375rem}.lt-tree-toggle{position:relative;display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;padding:0;color:var(--lt-icon);background:transparent;border:none;border-radius:50%;cursor:pointer;transition:color .15s,background .15s}.lt-tree-toggle:hover{color:var(--lt-link);background:color-mix(in srgb,var(--lt-link) 18%,transparent)}.lt-tree-chevron{display:block;width:13.8px;height:13.8px;transition:transform .15s ease}.lt-tree-toggle.is-expanded .lt-tree-chevron{transform:rotate(90deg)}.lt-tree-toggle.is-error .lt-tree-chevron{color:var(--lt-negative, #dc2626)}.lt-tree-spacer{display:inline-block;width:1.75rem;height:1.75rem}.lt-tree-spinner{position:absolute;top:50%;left:50%;box-sizing:border-box;width:1.875rem;height:1.875rem;margin:-.9375rem 0 0 -.9375rem;border:2px solid color-mix(in srgb,var(--lt-link) 25%,transparent);border-top-color:var(--lt-link);border-radius:50%;animation:loading .6s linear infinite;pointer-events:none}.lt-row-expanded>.lt-tree-cell .lt-cell-text{font-weight:600}.lt-tree-subhead-row>td,.lt-tree-child-row>td{padding:0 1rem 0 calc(var(--lt-card-inset, 4rem) + var(--lt-tree-indent, 0rem));background:transparent;border-bottom:none}.lt-tree-subhead-row,.lt-tree-child-row,.lt-tree-child-row:hover{background-color:var(--lt-tree-well, color-mix(in srgb, var(--lt-border) 22%, var(--lt-row-bg)))}.lt-tree-subhead-row>td{padding-top:1rem}.lt-tree-child-row.lt-tree-last-child>td{padding-bottom:1rem}.lt-tree-grid{display:grid;align-items:center;--lt-tree-col-gap: .5rem;column-gap:var(--lt-tree-col-gap);min-height:var(--lt-row-height);padding-left:.625rem;padding-right:.625rem;background:var(--lt-row-bg);border-left:1px solid var(--lt-tree-card-border, color-mix(in srgb, var(--lt-border) 70%, var(--lt-icon)));border-right:1px solid var(--lt-tree-card-border, color-mix(in srgb, var(--lt-border) 70%, var(--lt-icon)))}.lt-tree-child-row:hover .lt-tree-grid{background:var(--lt-row-hover)}.lt-tree-subhead-row .lt-tree-grid{min-height:2rem;background:var(--lt-header-bg);border-top:1px solid var(--lt-tree-card-border, color-mix(in srgb, var(--lt-border) 70%, var(--lt-icon)));border-top-left-radius:var(--lt-radius, 8px);border-top-right-radius:var(--lt-radius, 8px)}.lt-tree-child-row.lt-tree-last-child .lt-tree-grid{border-bottom:1px solid var(--lt-tree-card-border, color-mix(in srgb, var(--lt-border) 70%, var(--lt-icon)));border-bottom-left-radius:var(--lt-radius, 8px);border-bottom-right-radius:var(--lt-radius, 8px);box-shadow:var(--lt-tree-card-shadow, 0 2px 4px color-mix(in srgb, #000 10%, transparent))}.lt-tree-child-row:not(.lt-tree-last-child) .lt-tree-grid{border-bottom:1px solid color-mix(in srgb,var(--lt-border) 55%,transparent)}.lt-tree-subhead-cell{position:relative;padding:0 .3125rem;font-size:.6875rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--lt-header-text);overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.lt-tree-cellbox{display:flex;align-items:center;min-width:0;padding:0 .3125rem;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.lt-tree-cellbox .lt-cell-text{overflow:hidden;text-overflow:ellipsis}.lt-tree-grid>.lt-tree-select-cell{display:flex;align-items:center;justify-content:center;padding:0;overflow:visible}:host(.lt-tree-flat) .lt-tree-subhead-row,:host(.lt-tree-flat) .lt-tree-child-row,:host(.lt-tree-flat) .lt-tree-child-row:hover{background-color:transparent}:host(.lt-tree-flat) .lt-tree-subhead-row>td,:host(.lt-tree-flat) .lt-tree-child-row>td{padding:0}:host(.lt-tree-flat) .lt-tree-grid>.lt-tree-indent-col{padding-left:calc(var(--lt-tree-first-indent, 5.875rem) - var(--lt-tree-lead-offset, 0px))}:host(.lt-tree-flat) .lt-tree-grid{background:transparent;border-left:none;border-right:none;border-radius:0}:host(.lt-tree-flat) .lt-tree-subhead-row .lt-tree-grid{background:var(--lt-header-bg);border:none;border-bottom:1px solid var(--lt-border);border-radius:0}:host(.lt-tree-flat) .lt-tree-child-row .lt-tree-grid,:host(.lt-tree-flat) .lt-tree-child-row.lt-tree-last-child .lt-tree-grid{border-left:none;border-right:none;border-radius:0;box-shadow:none;border-bottom:1px solid color-mix(in srgb,var(--lt-border) 55%,transparent)}.lt-toolbar{display:flex;align-items:center;justify-content:space-between;gap:.75rem;margin:var(--lt-toolbar-margin-top, .75rem) 0 var(--lt-toolbar-margin-bottom, 0);padding:0 0 var(--lt-toolbar-pad-bottom, .625rem);min-height:var(--lt-toolbar-height);position:relative;z-index:30}.lt-ctl-disabled{opacity:.5;pointer-events:none}.lt-toolbar-left{display:flex;align-items:center;gap:var(--lt-control-row-gap);flex:1 1 auto;min-width:0;flex-wrap:nowrap}.lt-filter-zone{display:flex;align-items:center;flex-wrap:nowrap;gap:var(--lt-control-row-gap);flex:0 1 auto;min-width:0}.lt-filters{display:flex;align-items:center;flex-wrap:nowrap;gap:var(--lt-control-row-gap);min-width:0;flex:0 1 auto;overflow:hidden}.lt-filter-item{display:inline-flex;flex:0 0 auto}.lt-filter-more{position:relative;flex:0 0 auto}.lt-filter-more-btn{gap:.375rem;padding:0 .625rem}.lt-filter-more-btn .lt-filter-more-icon{display:inline-flex}.lt-filter-overflow{position:absolute;top:calc(100% + .25rem);right:0;z-index:1000;min-width:15rem;max-width:min(22rem,90vw);display:flex;flex-direction:column;gap:.5rem;padding:.625rem;background:var(--lt-control-bg);border:1px solid var(--lt-border);border-radius:var(--lt-radius);box-shadow:var(--shadowLarge);animation:lt-chooser-in .12s ease-out}.lt-filter-overflow-title{font-family:var(--lt-font);font-size:.6875rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--groupText)}.lt-filter-overflow-row{display:flex}.lt-filter-overflow-row lib-list-filter-cell,.lt-filter-overflow-row .lt-filter-item{flex:1 1 auto;min-width:0}.lt-toolbar-right{display:flex;align-items:center;gap:var(--lt-control-row-gap);flex:0 0 auto}.lt-divider{flex:0 0 auto;width:1px;height:1.5rem;background:var(--lt-border)}.lt-search{position:relative;display:flex;align-items:center;width:18rem;max-width:40vw;flex:0 1 18rem}.lt-search-icon{position:absolute;left:.625rem;display:block;color:var(--lt-icon);pointer-events:none}.lt-search-input{width:100%;height:var(--lt-toolbar-height);padding:0 .75rem 0 1.75rem;font-family:var(--lt-font);font-size:.8125rem;color:var(--lt-cell-text);background:var(--lt-control-bg);border:var(--lt-control-border-full);border-radius:var(--lt-radius);outline:none;transition:border-color .15s,box-shadow .15s}.lt-search-input::placeholder{color:var(--placeholder)}.lt-search-input:focus{border-color:var(--lt-link);box-shadow:0 0 0 .1875rem color-mix(in srgb,var(--lt-link) 18%,transparent)}.lt-count{font-size:.8125rem;color:var(--groupText);white-space:nowrap}.lt-count .lt-count-of{color:var(--groupText)}.lt-pager{display:flex;align-items:center;gap:.25rem}.lt-page-btn{display:inline-flex;align-items:center;justify-content:center;width:var(--lt-toolbar-height);height:var(--lt-toolbar-height);color:var(--lt-icon);background:var(--lt-control-bg);border:var(--lt-control-border-full);border-radius:var(--lt-radius);cursor:pointer;font-size:.9375rem;transition:border-color .15s,color .15s}.lt-page-btn:not(:disabled):hover{border-color:var(--lt-link);color:var(--lt-link)}.lt-page-btn:disabled{opacity:.4;cursor:default}.lt-refresh-btn{padding:0;width:var(--lt-toolbar-height);justify-content:center}.lt-refresh-btn:disabled{opacity:.5;cursor:default;border-color:var(--lt-border);color:var(--lt-cell-text)}.lt-columns{position:relative;flex:0 0 auto}.lt-column-chooser{position:absolute;top:calc(100% + .25rem);right:0;z-index:1000;width:18rem;max-height:calc(100vh - 9rem);display:flex;flex-direction:column;background:var(--lt-control-bg);border:1px solid var(--lt-border);border-radius:var(--lt-radius);box-shadow:var(--shadowLarge);overflow:hidden;animation:lt-chooser-in .12s ease-out}@keyframes lt-chooser-in{0%{opacity:0;transform:translateY(-.25rem)}to{opacity:1;transform:translateY(0)}}.lt-chooser-header{flex:0 0 auto;display:flex;flex-direction:column;padding:.625rem .75rem .5rem;border-bottom:1px solid var(--lt-border)}.lt-chooser-title{font-size:.8125rem;font-weight:700;color:var(--lt-cell-text)}.lt-chooser-subtitle{font-size:.6875rem;color:var(--groupText)}.lt-chooser-view{flex:0 0 auto;display:flex;align-items:center;justify-content:space-between;gap:.75rem;padding:.5rem .75rem;border-bottom:1px solid var(--lt-border)}.lt-chooser-view-label{font-size:.75rem;font-weight:600;color:var(--lt-cell-text)}.lt-chooser-view-toggle{display:flex;gap:.1875rem;padding:.1875rem;background:color-mix(in srgb,var(--lt-border) 30%,var(--lt-control-bg));border:1px solid var(--lt-border);border-radius:var(--lt-radius)}.lt-chooser-view-opt{padding:.25rem .625rem;font-family:var(--lt-font);font-size:.75rem;font-weight:500;color:var(--lt-cell-text);background:transparent;border:none;border-radius:calc(var(--lt-radius) - .0625rem);cursor:pointer}.lt-chooser-view-opt:hover:not(.active){color:var(--lt-link)}.lt-chooser-view-opt.active{color:var(--lt-link);background:var(--lt-control-bg);box-shadow:0 1px 2px color-mix(in srgb,var(--lt-cell-text) 14%,transparent)}.lt-chooser-scroll{flex:1 1 auto;min-height:0;overflow-y:auto}.lt-chooser-list{list-style:none;margin:0;padding:.375rem .5rem}.lt-chooser-nested{padding:0 .5rem .375rem;border-top:1px solid var(--lt-border)}.lt-chooser-group{display:flex;align-items:center;gap:.375rem;width:100%;min-height:2.25rem;padding:.375rem .25rem;font-family:var(--lt-font);font-size:.75rem;font-weight:600;color:var(--lt-cell-text);background:transparent;border:none;border-radius:var(--lt-radius);cursor:pointer;text-align:left}.lt-chooser-group:hover{background:var(--lt-row-hover)}.lt-chooser-group-sub{padding-left:1.25rem;font-weight:500}.lt-chooser-group-label{flex:0 0 auto}.lt-chooser-chevron{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:.875rem;color:var(--lt-icon);transition:transform .15s ease}.lt-chooser-chevron svg{display:block}.lt-chooser-chevron.open{transform:rotate(90deg)}.lt-chooser-group-hint{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.6875rem;font-weight:400;color:var(--groupText)}.lt-chooser-sublist{padding-left:1.75rem}.lt-chooser-row{display:flex;align-items:center;justify-content:space-between;min-height:2.5rem;padding:.5rem;border-radius:var(--lt-radius);background:var(--lt-control-bg);cursor:grab}.lt-chooser-row:active{cursor:grabbing}.lt-chooser-row.cdk-drag-disabled{cursor:default}.lt-chooser-row:hover{background:var(--lt-row-hover)}.lt-chooser-row:hover .lt-chooser-handle{opacity:1}.lt-chooser-toggle{display:flex;align-items:center;gap:.625rem;flex:1 1 auto;min-width:0;padding:0;background:transparent;border:none;font:inherit;font-size:.875rem;color:var(--lt-cell-text);text-align:left;cursor:pointer}.lt-chooser-toggle:disabled{cursor:default;opacity:.5}.lt-chooser-box{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;width:1rem;height:1rem;border:.09375rem solid var(--lt-control-border, var(--lt-border));border-radius:.25rem;color:var(--lt-control-bg);background:var(--lt-control-bg);transition:background .12s,border-color .12s}.lt-chooser-toggle.checked .lt-chooser-box{background:var(--lt-link);border-color:var(--lt-link)}.lt-chooser-toggle:not(:disabled):hover .lt-chooser-box{border-color:var(--lt-link)}.lt-chooser-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.lt-chooser-default-tag{flex:0 0 auto;margin-left:.375rem;font-size:.6875rem;font-weight:400;font-style:italic;color:var(--groupText);white-space:nowrap}.lt-chooser-handle{flex:0 0 auto;display:inline-flex;align-items:center;color:var(--lt-icon);cursor:grab;padding-left:.5rem;-webkit-user-select:none;user-select:none}.lt-chooser-handle:active{cursor:grabbing}.lt-chooser-footer{flex:0 0 auto;padding:.5rem .75rem;border-top:1px solid var(--lt-border)}.lt-chooser-reset{width:100%;height:1.875rem;font-size:.75rem;font-weight:600;color:var(--lt-link);background:transparent;border:1px solid var(--lt-border);border-radius:var(--lt-radius);cursor:pointer}.lt-chooser-reset:disabled{color:var(--groupText);cursor:default;opacity:.6}.lt-chooser-reset:not(:disabled):hover{border-color:var(--lt-link)}.lt-footer{flex:0 0 auto;display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;padding:.625rem .125rem 0;font-family:var(--lt-font);font-size:.8125rem;color:var(--groupText)}.lt-footer-count{white-space:nowrap}.lt-footer-count span{color:var(--lt-cell-text);font-weight:600}.lt-footer-controls{display:flex;align-items:center;gap:1.25rem;flex-wrap:nowrap}.lt-rpp{display:inline-flex;align-items:center;gap:.5rem;white-space:nowrap}.lt-rpp-label{text-transform:none;font-weight:400;color:var(--groupText)}.lt-rpp-select{width:auto;min-width:4rem;height:var(--lt-toolbar-height);padding:0 1.75rem 0 .625rem;font-family:var(--lt-font);font-size:.8125rem;color:var(--lt-cell-text);background-color:var(--lt-control-bg);background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='none' stroke='%236b7280' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' d='M3 4.5 6 7.5 9 4.5'/%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right .5rem center;border:var(--lt-control-border-full);border-radius:var(--lt-radius);appearance:none;-webkit-appearance:none;cursor:pointer}.lt-rpp-select:focus{outline:none;border-color:var(--lt-link)}.lt-page-num{display:inline-flex;align-items:center;justify-content:center;min-width:var(--lt-toolbar-height);height:var(--lt-toolbar-height);padding:0 .5rem;font-family:var(--lt-font);font-size:.8125rem;font-weight:600;color:var(--lt-cell-text);background:var(--lt-control-bg);border:var(--lt-control-border-full);border-radius:var(--lt-radius);cursor:pointer;transition:border-color .15s,color .15s,background .15s}.lt-page-num:hover{border-color:var(--lt-link);color:var(--lt-link)}.lt-page-num.is-current{color:#fff;background:var(--lt-link);border-color:var(--lt-link);cursor:default}.lt-page-gap{display:inline-flex;align-items:center;justify-content:center;min-width:1.25rem;color:var(--groupText)}.lt-tooltip{position:fixed;z-index:1100;pointer-events:none;animation:lt-tooltip-in .12s ease-out}@keyframes lt-tooltip-in{0%{opacity:0;transform:translateY(-.1875rem)}to{opacity:1;transform:translateY(0)}}\n"] }]
2903
+ }], ctorParameters: () => [{ type: ListTableService }, { type: DataTableFilterService }, { type: i0.ElementRef }, { type: i0.NgZone }, { type: i3.DomSanitizer }], propDecorators: { tableId: [{
2904
+ type: Input
2905
+ }], columns: [{
2906
+ type: Input
2907
+ }], rowData: [{
2908
+ type: Input
2909
+ }], isLoading: [{
2910
+ type: Input
2911
+ }], skeletonRowCount: [{
2912
+ type: Input
2913
+ }], options: [{
2914
+ type: Input
2915
+ }], loadChildren: [{
2916
+ type: Input
2917
+ }], childColumnsByLevel: [{
2918
+ type: Input
2919
+ }], nodeToggled: [{
2920
+ type: Output
2921
+ }], hasNext: [{
2922
+ type: Input
2923
+ }], hasPrev: [{
2924
+ type: Input
2925
+ }], startCount: [{
2926
+ type: Input
2927
+ }], endCount: [{
2928
+ type: Input
2929
+ }], totalCount: [{
2930
+ type: Input
2931
+ }], filterApplied: [{
2932
+ type: Input
2933
+ }], menuItems: [{
2934
+ type: Input
2935
+ }], headerActions: [{
2936
+ type: Input
2937
+ }], noItemsImage: [{
2938
+ type: Input
2939
+ }], showNoItemsAdd: [{
2940
+ type: Input
2941
+ }], noItemsReadonly: [{
2942
+ type: Input
2943
+ }], createLabel: [{
2944
+ type: Input
2945
+ }], emptyScope: [{
2946
+ type: Input
2947
+ }], accessDenied: [{
2948
+ type: Input
2949
+ }], entityTypeName: [{
2950
+ type: Input
2951
+ }], filterName: [{
2952
+ type: Input
2953
+ }], filterColumn: [{
2954
+ type: Input
2955
+ }], actionRequested: [{
2956
+ type: Output
2957
+ }], tableReady: [{
2958
+ type: Output
2959
+ }], selectionChanged: [{
2960
+ type: Output
2961
+ }], rowHover: [{
2962
+ type: Output
2963
+ }], contextMenu: [{
2964
+ type: ViewChild,
2965
+ args: ['contextMenu']
2966
+ }], filterZone: [{
2967
+ type: ViewChild,
2968
+ args: ['filterZone']
2969
+ }], filtersRow: [{
2970
+ type: ViewChild,
2971
+ args: ['filtersRow']
2972
+ }], treeVariant: [{
2973
+ type: Input
2974
+ }], showTreeVariantToggle: [{
2975
+ type: Input
2976
+ }], treeRail: [{
2977
+ type: Input
2978
+ }], isFlatTree: [{
2979
+ type: HostBinding,
2980
+ args: ['class.lt-tree-flat']
2981
+ }], initialPageSize: [{
2982
+ type: Input
2983
+ }], autoRefreshSeconds: [{
2984
+ type: Input
2985
+ }] } });
2986
+
2987
+ class NoItemsComponent {
2988
+ constructor() {
2989
+ this.image = 'No_Gateways';
2990
+ this.typeName = '';
2991
+ this.isEmpty = false;
2992
+ this.hasAdd = true;
2993
+ this.hiddenResults = false;
2994
+ this.isLoading = false;
2995
+ this.readonly = false;
2996
+ this.accessDenied = false;
2997
+ this.clickEmit = new EventEmitter();
2998
+ this.refresh = new EventEmitter();
2999
+ }
3000
+ ngOnChanges() {
3001
+ this.background = {
3002
+ 'background-image': 'url(assets/svgs/' + this.image + '.svg)',
3003
+ };
3004
+ }
3005
+ iconClick() {
3006
+ this.clickEmit.emit();
3007
+ }
3008
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.34", ngImport: i0, type: NoItemsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3009
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.34", type: NoItemsComponent, isStandalone: true, selector: "lib-no-items", inputs: { image: "image", typeName: "typeName", isEmpty: "isEmpty", hasAdd: "hasAdd", hiddenResults: "hiddenResults", isLoading: "isLoading", readonly: "readonly", accessDenied: "accessDenied" }, outputs: { clickEmit: "clickEmit", refresh: "refresh" }, usesOnChanges: true, ngImport: i0, template: "<div [hidden]=\"!isEmpty || isLoading\" class=\"noarea\" id=\"NoArea\">\n <div (click)=\"iconClick()\">\n <div [ngStyle]=\"background\" class=\"icon\"></div>\n <div class=\"title\">\n @if (accessDenied) {\n You do not have permission to view {{ typeName }}.\n } @else {\n This network currently has no {{ typeName }} to display.<br/>\n <span [hidden]=\"!hasAdd || readonly\"\n >Tap on the <span class=\"addExample\"></span> to add one.</span\n >\n }\n </div>\n </div>\n</div>\n", styles: [":host{display:flex;flex-direction:column;height:calc(100% - 12.5rem);width:100%;align-items:center;justify-content:center;position:relative}.addExample{display:inline-block;width:2.1875rem;height:2.1875rem;background-image:url(/assets/svgs/No_Plus.svg);background-size:contain;background-repeat:no-repeat;margin-bottom:-.5rem}.addExample:hover{filter:brightness(90%);transition:.3s}.addExample:active{filter:brightness(80%);transform:scale(.97);transition:.3s}.noarea .icon{width:100%;height:15.625rem;background-position:center center;background-size:contain;position:relative;float:left;z-index:unset;left:unset;cursor:pointer;transition:.3s}@media only screen and (max-width:715px){.addExample{width:1rem;height:1rem}}@media only screen and (max-width:1500px){.noarea{transform:scale(.75)}}@media only screen and (max-width:1250px){.noarea{transform:scale(.6)}}.hidden-results{margin-top:1.25rem}.hidden-results span{font-size:1.375rem;cursor:default}.hidden-results span a{cursor:pointer}.no-items-loader-container{position:absolute;width:100%;height:100%;left:0;top:0;background:#ffffff40;z-index:99999}::ng-deep .no-items-loader-container #Loading{position:absolute;width:100%;height:100%;z-index:9999;background:#fff3}.title{text-align:center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] }); }
3010
+ }
3011
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.34", ngImport: i0, type: NoItemsComponent, decorators: [{
3012
+ type: Component,
3013
+ args: [{ selector: 'lib-no-items', standalone: true, imports: [CommonModule], template: "<div [hidden]=\"!isEmpty || isLoading\" class=\"noarea\" id=\"NoArea\">\n <div (click)=\"iconClick()\">\n <div [ngStyle]=\"background\" class=\"icon\"></div>\n <div class=\"title\">\n @if (accessDenied) {\n You do not have permission to view {{ typeName }}.\n } @else {\n This network currently has no {{ typeName }} to display.<br/>\n <span [hidden]=\"!hasAdd || readonly\"\n >Tap on the <span class=\"addExample\"></span> to add one.</span\n >\n }\n </div>\n </div>\n</div>\n", styles: [":host{display:flex;flex-direction:column;height:calc(100% - 12.5rem);width:100%;align-items:center;justify-content:center;position:relative}.addExample{display:inline-block;width:2.1875rem;height:2.1875rem;background-image:url(/assets/svgs/No_Plus.svg);background-size:contain;background-repeat:no-repeat;margin-bottom:-.5rem}.addExample:hover{filter:brightness(90%);transition:.3s}.addExample:active{filter:brightness(80%);transform:scale(.97);transition:.3s}.noarea .icon{width:100%;height:15.625rem;background-position:center center;background-size:contain;position:relative;float:left;z-index:unset;left:unset;cursor:pointer;transition:.3s}@media only screen and (max-width:715px){.addExample{width:1rem;height:1rem}}@media only screen and (max-width:1500px){.noarea{transform:scale(.75)}}@media only screen and (max-width:1250px){.noarea{transform:scale(.6)}}.hidden-results{margin-top:1.25rem}.hidden-results span{font-size:1.375rem;cursor:default}.hidden-results span a{cursor:pointer}.no-items-loader-container{position:absolute;width:100%;height:100%;left:0;top:0;background:#ffffff40;z-index:99999}::ng-deep .no-items-loader-container #Loading{position:absolute;width:100%;height:100%;z-index:9999;background:#fff3}.title{text-align:center}\n"] }]
3014
+ }], ctorParameters: () => [], propDecorators: { image: [{
3015
+ type: Input
3016
+ }], typeName: [{
3017
+ type: Input
3018
+ }], isEmpty: [{
3019
+ type: Input
3020
+ }], hasAdd: [{
3021
+ type: Input
3022
+ }], hiddenResults: [{
3023
+ type: Input
3024
+ }], isLoading: [{
3025
+ type: Input
3026
+ }], readonly: [{
3027
+ type: Input
3028
+ }], accessDenied: [{
3029
+ type: Input
3030
+ }], clickEmit: [{
3031
+ type: Output
3032
+ }], refresh: [{
3033
+ type: Output
3034
+ }] } });
3035
+
3036
+ /*
3037
+ Copyright NetFoundry Inc.
3038
+
3039
+ Licensed under the Apache License, Version 2.0 (the "License");
3040
+ you may not use this file except in compliance with the License.
3041
+ You may obtain a copy of the License at
3042
+
3043
+ https://www.apache.org/licenses/LICENSE-2.0
3044
+
3045
+ Unless required by applicable law or agreed to in writing, software
3046
+ distributed under the License is distributed on an "AS IS" BASIS,
3047
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3048
+ See the License for the specific language governing permissions and
3049
+ limitations under the License.
3050
+ */
3051
+ /*
3052
+ * Secondary entry point: `@openziti/ziti-console-lib/table-core`.
3053
+ *
3054
+ * The standalone list-table and the self-contained set of pieces it is built from. This entry's
3055
+ * compiled bundle and type declarations never include the library's main NgModule, so none of
3056
+ * that module's heavy, optional peers (ag-grid, primeng, lottie, oauth, json-editor, qrcode, csv,
3057
+ * yaml, json-ref-resolver) are reachable - not even as a build-time import edge. A consumer that
3058
+ * imports the table from this subpath builds with only the lean peers it genuinely needs
3059
+ * (@angular/*, @angular/cdk, @angular/material, rxjs, lodash, moment).
3060
+ */
3061
+
3062
+ /**
3063
+ * Generated bundle index. Do not edit.
3064
+ */
3065
+
3066
+ export { ClickOutsideDirective, DataTableFilterService, ListEmptyComponent, ListFilterCellComponent, ListHeaderCellComponent, ListMenuComponent, ListTableAutoRefreshComponent, ListTableComponent, ListTableService, NoItemsComponent, SelectCellComponent };
3067
+ //# sourceMappingURL=openziti-ziti-console-lib-table-core.mjs.map