@bluevolt-tech/lumen 2.3.0 → 2.3.1
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.
- package/dist/components/bv-date-range-input/bv-date-range-input.js +31 -0
- package/dist/components/bv-date-range-picker/bv-date-range-picker.d.ts +1 -0
- package/dist/components/bv-date-range-picker/bv-date-range-picker.js +31 -0
- package/dist/components/bv-pagination/bv-pagination.d.ts +3 -1
- package/dist/components/bv-pagination/bv-pagination.js +11 -3
- package/dist/components/bv-search-picker/bv-search-picker.d.ts +9 -0
- package/dist/components/bv-search-picker/bv-search-picker.js +35 -4
- package/dist/components/bv-table/bv-table.d.ts +28 -0
- package/dist/components/bv-table/bv-table.js +274 -58
- package/dist/storybook/stories/screens/cet-report.data.d.ts +0 -1
- package/dist/storybook/stories/screens/cet-report.data.js +0 -42
- package/dist/themes/base.css +1 -1
- package/package.json +1 -1
- package/themes/base.css +1 -1
|
@@ -556,12 +556,43 @@ var BvDateRangePicker = class extends HTMLElement {
|
|
|
556
556
|
if (name === "preset") this._preset = val ?? "";
|
|
557
557
|
if (!this._rendered) return;
|
|
558
558
|
if (name === "start" || name === "end" || name === "preset") {
|
|
559
|
+
this._maybeRepositionView();
|
|
559
560
|
this._syncCalendars();
|
|
560
561
|
this._syncInputs();
|
|
561
562
|
this._syncPresets();
|
|
562
563
|
}
|
|
563
564
|
this._syncValidity();
|
|
564
565
|
}
|
|
566
|
+
// Hoist the left panel to the anchor's month, but ONLY when *neither*
|
|
567
|
+
// endpoint of the current start/end pair is already in the visible pair.
|
|
568
|
+
// Checking both endpoints matters after manual navigation: if the user
|
|
569
|
+
// scrolled far from `_start` and the consumer only touches `_end`, the
|
|
570
|
+
// change may land inside the visible pair even though `_start` is well
|
|
571
|
+
// out of view — the guard must keep the user's navigation intact in
|
|
572
|
+
// that case. The anchor for the actual reposition still prefers
|
|
573
|
+
// `_start` (fallback to `_end`) so common consumer updates that write
|
|
574
|
+
// both fields converge to the start's month. See BTN-10755 in
|
|
575
|
+
// `attributeChangedCallback` for the flow context. Returns silently on
|
|
576
|
+
// empty / malformed values — the sync pass afterwards can still render
|
|
577
|
+
// the cleared selection over the current view without a jump.
|
|
578
|
+
_maybeRepositionView() {
|
|
579
|
+
const anchor = this._start || this._end;
|
|
580
|
+
if (!anchor) return;
|
|
581
|
+
const parseYearMonth = (s2) => {
|
|
582
|
+
const dt = parseIso(s2);
|
|
583
|
+
if (!dt) return null;
|
|
584
|
+
return { y: dt.getFullYear(), m: dt.getMonth() };
|
|
585
|
+
};
|
|
586
|
+
const right = this._getRightMonth();
|
|
587
|
+
const isVisible = (y, m) => y === this._leftYear && m === this._leftMonth || y === right.year && m === right.month;
|
|
588
|
+
const s = this._start ? parseYearMonth(this._start) : null;
|
|
589
|
+
const e = this._end ? parseYearMonth(this._end) : null;
|
|
590
|
+
if (s && isVisible(s.y, s.m) || e && isVisible(e.y, e.m)) return;
|
|
591
|
+
const target = parseYearMonth(anchor);
|
|
592
|
+
if (!target) return;
|
|
593
|
+
this._leftYear = target.y;
|
|
594
|
+
this._leftMonth = target.m;
|
|
595
|
+
}
|
|
565
596
|
/**
|
|
566
597
|
* BTN-10312 — public reset of the picker's internal draft state to the
|
|
567
598
|
* currently committed `start` / `end` / `preset` attributes. Called by
|
|
@@ -51,6 +51,7 @@ export declare class BvDateRangePicker extends HTMLElement {
|
|
|
51
51
|
set preset(v: string);
|
|
52
52
|
connectedCallback(): void;
|
|
53
53
|
attributeChangedCallback(name: string, _old: string | null, val: string | null): void;
|
|
54
|
+
private _maybeRepositionView;
|
|
54
55
|
/**
|
|
55
56
|
* BTN-10312 — public reset of the picker's internal draft state to the
|
|
56
57
|
* currently committed `start` / `end` / `preset` attributes. Called by
|
|
@@ -495,12 +495,43 @@ var BvDateRangePicker = class extends HTMLElement {
|
|
|
495
495
|
if (name === "preset") this._preset = val ?? "";
|
|
496
496
|
if (!this._rendered) return;
|
|
497
497
|
if (name === "start" || name === "end" || name === "preset") {
|
|
498
|
+
this._maybeRepositionView();
|
|
498
499
|
this._syncCalendars();
|
|
499
500
|
this._syncInputs();
|
|
500
501
|
this._syncPresets();
|
|
501
502
|
}
|
|
502
503
|
this._syncValidity();
|
|
503
504
|
}
|
|
505
|
+
// Hoist the left panel to the anchor's month, but ONLY when *neither*
|
|
506
|
+
// endpoint of the current start/end pair is already in the visible pair.
|
|
507
|
+
// Checking both endpoints matters after manual navigation: if the user
|
|
508
|
+
// scrolled far from `_start` and the consumer only touches `_end`, the
|
|
509
|
+
// change may land inside the visible pair even though `_start` is well
|
|
510
|
+
// out of view — the guard must keep the user's navigation intact in
|
|
511
|
+
// that case. The anchor for the actual reposition still prefers
|
|
512
|
+
// `_start` (fallback to `_end`) so common consumer updates that write
|
|
513
|
+
// both fields converge to the start's month. See BTN-10755 in
|
|
514
|
+
// `attributeChangedCallback` for the flow context. Returns silently on
|
|
515
|
+
// empty / malformed values — the sync pass afterwards can still render
|
|
516
|
+
// the cleared selection over the current view without a jump.
|
|
517
|
+
_maybeRepositionView() {
|
|
518
|
+
const anchor = this._start || this._end;
|
|
519
|
+
if (!anchor) return;
|
|
520
|
+
const parseYearMonth = (s2) => {
|
|
521
|
+
const dt = parseIso(s2);
|
|
522
|
+
if (!dt) return null;
|
|
523
|
+
return { y: dt.getFullYear(), m: dt.getMonth() };
|
|
524
|
+
};
|
|
525
|
+
const right = this._getRightMonth();
|
|
526
|
+
const isVisible = (y, m) => y === this._leftYear && m === this._leftMonth || y === right.year && m === right.month;
|
|
527
|
+
const s = this._start ? parseYearMonth(this._start) : null;
|
|
528
|
+
const e = this._end ? parseYearMonth(this._end) : null;
|
|
529
|
+
if (s && isVisible(s.y, s.m) || e && isVisible(e.y, e.m)) return;
|
|
530
|
+
const target = parseYearMonth(anchor);
|
|
531
|
+
if (!target) return;
|
|
532
|
+
this._leftYear = target.y;
|
|
533
|
+
this._leftMonth = target.m;
|
|
534
|
+
}
|
|
504
535
|
/**
|
|
505
536
|
* BTN-10312 — public reset of the picker's internal draft state to the
|
|
506
537
|
* currently committed `start` / `end` / `preset` attributes. Called by
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare class BvPagination extends HTMLElement {
|
|
2
|
-
static readonly observedAttributes: readonly ["page", "total-pages", "total", "page-size", "siblings", "show-results", "show-page-size", "page-size-options", "compact"];
|
|
2
|
+
static readonly observedAttributes: readonly ["page", "total-pages", "total", "page-size", "siblings", "show-results", "show-page-size", "page-size-options", "compact", "hide-range"];
|
|
3
3
|
private _rendered;
|
|
4
4
|
private _metaEl;
|
|
5
5
|
private _pagesEl;
|
|
@@ -24,6 +24,8 @@ export declare class BvPagination extends HTMLElement {
|
|
|
24
24
|
set showPageSize(v: boolean);
|
|
25
25
|
get compact(): boolean;
|
|
26
26
|
set compact(v: boolean);
|
|
27
|
+
get hideRange(): boolean;
|
|
28
|
+
set hideRange(v: boolean);
|
|
27
29
|
get pageSizeOptions(): number[];
|
|
28
30
|
private _go;
|
|
29
31
|
private _render;
|
|
@@ -32,7 +32,8 @@ var BvPagination = class extends HTMLElement {
|
|
|
32
32
|
"show-results",
|
|
33
33
|
"show-page-size",
|
|
34
34
|
"page-size-options",
|
|
35
|
-
"compact"
|
|
35
|
+
"compact",
|
|
36
|
+
"hide-range"
|
|
36
37
|
];
|
|
37
38
|
_rendered = false;
|
|
38
39
|
_metaEl = null;
|
|
@@ -90,6 +91,13 @@ var BvPagination = class extends HTMLElement {
|
|
|
90
91
|
if (v) this.setAttribute("compact", "");
|
|
91
92
|
else this.removeAttribute("compact");
|
|
92
93
|
}
|
|
94
|
+
get hideRange() {
|
|
95
|
+
return this.hasAttribute("hide-range");
|
|
96
|
+
}
|
|
97
|
+
set hideRange(v) {
|
|
98
|
+
if (v) this.setAttribute("hide-range", "");
|
|
99
|
+
else this.removeAttribute("hide-range");
|
|
100
|
+
}
|
|
93
101
|
get pageSizeOptions() {
|
|
94
102
|
const raw = this.getAttribute("page-size-options") ?? "10,25,50,100";
|
|
95
103
|
return raw.split(",").map((v) => parseInt(v.trim(), 10)).filter((v) => !isNaN(v));
|
|
@@ -169,7 +177,7 @@ var BvPagination = class extends HTMLElement {
|
|
|
169
177
|
this._update();
|
|
170
178
|
}
|
|
171
179
|
_update() {
|
|
172
|
-
const { page, totalPages, total, pageSize, siblings, compact } = this;
|
|
180
|
+
const { page, totalPages, total, pageSize, siblings, compact, hideRange } = this;
|
|
173
181
|
if (this._prevBtn) this._prevBtn.disabled = page <= 1;
|
|
174
182
|
if (this._nextBtn) this._nextBtn.disabled = page >= totalPages;
|
|
175
183
|
if (this._pageLabelEl) {
|
|
@@ -180,7 +188,7 @@ var BvPagination = class extends HTMLElement {
|
|
|
180
188
|
this._pagesEl.hidden = compact;
|
|
181
189
|
}
|
|
182
190
|
if (this._rangeEl) {
|
|
183
|
-
if (compact) {
|
|
191
|
+
if (compact || hideRange) {
|
|
184
192
|
this._rangeEl.hidden = true;
|
|
185
193
|
} else if (total > 0) {
|
|
186
194
|
const start = (page - 1) * pageSize + 1;
|
|
@@ -66,6 +66,14 @@ export declare class BvSearchPicker extends HTMLElement {
|
|
|
66
66
|
private _searchRequestId;
|
|
67
67
|
/** BTN-10322 — timer handle for the internal debounce on `bv-search`. */
|
|
68
68
|
private _searchDebounceHandle;
|
|
69
|
+
/**
|
|
70
|
+
* BTN-10755 — set synchronously the moment a keystroke enters over-threshold
|
|
71
|
+
* territory and the debounce is armed; cleared when the debounce fires (or
|
|
72
|
+
* on any reset / clear path). `_syncResultsEmptyText` treats it the same as
|
|
73
|
+
* `loading`, so the "No X found." copy can't flash in the ~300ms gap
|
|
74
|
+
* between the keystroke and the dispatched `bv-search`.
|
|
75
|
+
*/
|
|
76
|
+
private _searchPending;
|
|
69
77
|
/**
|
|
70
78
|
* BTN-10322 — the reason to display in the cascade cleared affordance,
|
|
71
79
|
* or `null` when the banner is hidden. Set by `reset({ reason })`,
|
|
@@ -145,6 +153,7 @@ export declare class BvSearchPicker extends HTMLElement {
|
|
|
145
153
|
private _syncDisabled;
|
|
146
154
|
private _syncShowInactive;
|
|
147
155
|
private _syncResults;
|
|
156
|
+
private _effectiveQueryLength;
|
|
148
157
|
private _syncResultsEmptyText;
|
|
149
158
|
private _syncResultsLimitNote;
|
|
150
159
|
private _syncBasket;
|
|
@@ -587,6 +587,14 @@ var BvSearchPicker = class extends HTMLElement {
|
|
|
587
587
|
_searchRequestId = 0;
|
|
588
588
|
/** BTN-10322 — timer handle for the internal debounce on `bv-search`. */
|
|
589
589
|
_searchDebounceHandle = null;
|
|
590
|
+
/**
|
|
591
|
+
* BTN-10755 — set synchronously the moment a keystroke enters over-threshold
|
|
592
|
+
* territory and the debounce is armed; cleared when the debounce fires (or
|
|
593
|
+
* on any reset / clear path). `_syncResultsEmptyText` treats it the same as
|
|
594
|
+
* `loading`, so the "No X found." copy can't flash in the ~300ms gap
|
|
595
|
+
* between the keystroke and the dispatched `bv-search`.
|
|
596
|
+
*/
|
|
597
|
+
_searchPending = false;
|
|
590
598
|
/**
|
|
591
599
|
* BTN-10322 — the reason to display in the cascade cleared affordance,
|
|
592
600
|
* or `null` when the banner is hidden. Set by `reset({ reason })`,
|
|
@@ -713,6 +721,7 @@ var BvSearchPicker = class extends HTMLElement {
|
|
|
713
721
|
clearTimeout(this._searchDebounceHandle);
|
|
714
722
|
this._searchDebounceHandle = null;
|
|
715
723
|
}
|
|
724
|
+
this._searchPending = false;
|
|
716
725
|
}
|
|
717
726
|
attributeChangedCallback(name, _old, _val) {
|
|
718
727
|
if (!this._rendered) return;
|
|
@@ -850,8 +859,8 @@ var BvSearchPicker = class extends HTMLElement {
|
|
|
850
859
|
searchEl.addEventListener("bv-search", (e) => {
|
|
851
860
|
e.stopPropagation();
|
|
852
861
|
this._query = e.detail.value.trim();
|
|
853
|
-
this._syncResultsEmptyText();
|
|
854
862
|
this._scheduleSearchDispatch();
|
|
863
|
+
this._syncResultsEmptyText();
|
|
855
864
|
});
|
|
856
865
|
searchEl.addEventListener("keydown", (e) => {
|
|
857
866
|
const key = e.key;
|
|
@@ -1049,6 +1058,7 @@ var BvSearchPicker = class extends HTMLElement {
|
|
|
1049
1058
|
}
|
|
1050
1059
|
_syncResults() {
|
|
1051
1060
|
if (!this._resultsRowsEl || !this._resultsEmptyEl) return;
|
|
1061
|
+
this._searchPending = false;
|
|
1052
1062
|
if (this._resultsListEl) {
|
|
1053
1063
|
this._resultsListEl.setAttribute("aria-busy", this.loading ? "true" : "false");
|
|
1054
1064
|
}
|
|
@@ -1085,12 +1095,20 @@ var BvSearchPicker = class extends HTMLElement {
|
|
|
1085
1095
|
this._syncResultsEmptyText();
|
|
1086
1096
|
this._syncResultsLimitNote();
|
|
1087
1097
|
}
|
|
1098
|
+
// BTN-10729 — the min-query-length gate counts non-whitespace characters,
|
|
1099
|
+
// not raw length. Otherwise `"a b"` (2 letters + 1 space) passes a 3-char
|
|
1100
|
+
// gate and hits the backend with an effective 2-letter search. Both the
|
|
1101
|
+
// empty-state copy and _dispatchSearch consult the same helper so their
|
|
1102
|
+
// decisions can never disagree.
|
|
1103
|
+
_effectiveQueryLength() {
|
|
1104
|
+
return this._query.replace(/\s+/g, "").length;
|
|
1105
|
+
}
|
|
1088
1106
|
_syncResultsEmptyText() {
|
|
1089
1107
|
if (!this._resultsEmptyEl || !this._resultsRowsEl) return;
|
|
1090
|
-
const isEmpty = !this.loading && this._resultsRowsEl.children.length === 0;
|
|
1108
|
+
const isEmpty = !this.loading && !this._searchPending && this._resultsRowsEl.children.length === 0;
|
|
1091
1109
|
this._resultsEmptyEl.hidden = !isEmpty;
|
|
1092
1110
|
if (!isEmpty) return;
|
|
1093
|
-
if (this.
|
|
1111
|
+
if (this._effectiveQueryLength() < this.minQueryLength) {
|
|
1094
1112
|
this._resultsEmptyEl.textContent = this._interp(
|
|
1095
1113
|
this.getAttribute("label-min-chars") ?? "Type at least {n} characters to search.",
|
|
1096
1114
|
{ n: this.minQueryLength }
|
|
@@ -1245,6 +1263,11 @@ var BvSearchPicker = class extends HTMLElement {
|
|
|
1245
1263
|
var _a;
|
|
1246
1264
|
this._query = "";
|
|
1247
1265
|
(_a = this._searchEl) == null ? void 0 : _a.setAttribute("value", "");
|
|
1266
|
+
if (this._searchDebounceHandle !== null) {
|
|
1267
|
+
clearTimeout(this._searchDebounceHandle);
|
|
1268
|
+
this._searchDebounceHandle = null;
|
|
1269
|
+
}
|
|
1270
|
+
this._searchPending = false;
|
|
1248
1271
|
this.basket = [];
|
|
1249
1272
|
this._dispatchApply();
|
|
1250
1273
|
}
|
|
@@ -1307,7 +1330,7 @@ var BvSearchPicker = class extends HTMLElement {
|
|
|
1307
1330
|
);
|
|
1308
1331
|
}
|
|
1309
1332
|
_dispatchSearch() {
|
|
1310
|
-
if (this.
|
|
1333
|
+
if (this._effectiveQueryLength() < this.minQueryLength) return;
|
|
1311
1334
|
this._searchRequestId += 1;
|
|
1312
1335
|
const requestId = this._searchRequestId;
|
|
1313
1336
|
this.dispatchEvent(
|
|
@@ -1325,9 +1348,12 @@ var BvSearchPicker = class extends HTMLElement {
|
|
|
1325
1348
|
if (this._searchDebounceHandle !== null) {
|
|
1326
1349
|
clearTimeout(this._searchDebounceHandle);
|
|
1327
1350
|
}
|
|
1351
|
+
this._searchPending = this._effectiveQueryLength() >= this.minQueryLength;
|
|
1328
1352
|
this._searchDebounceHandle = setTimeout(() => {
|
|
1329
1353
|
this._searchDebounceHandle = null;
|
|
1354
|
+
this._searchPending = false;
|
|
1330
1355
|
this._dispatchSearch();
|
|
1356
|
+
this._syncResultsEmptyText();
|
|
1331
1357
|
}, this.debounceMs);
|
|
1332
1358
|
}
|
|
1333
1359
|
/**
|
|
@@ -1420,6 +1446,11 @@ var BvSearchPicker = class extends HTMLElement {
|
|
|
1420
1446
|
this._stagedBasket = [];
|
|
1421
1447
|
this._query = "";
|
|
1422
1448
|
(_a = this._searchEl) == null ? void 0 : _a.setAttribute("value", "");
|
|
1449
|
+
if (this._searchDebounceHandle !== null) {
|
|
1450
|
+
clearTimeout(this._searchDebounceHandle);
|
|
1451
|
+
this._searchDebounceHandle = null;
|
|
1452
|
+
}
|
|
1453
|
+
this._searchPending = false;
|
|
1423
1454
|
this.results = [];
|
|
1424
1455
|
this.resultsTotal = null;
|
|
1425
1456
|
this._clearedReason = reason;
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import '../bv-tooltip/bv-tooltip.ts';
|
|
2
|
+
/**
|
|
3
|
+
* BTN-10755 followup — public event detail for `bv-sort`. `column` is
|
|
4
|
+
* guaranteed to be a string (not null): the header enhancer refuses to
|
|
5
|
+
* wire a sortable th without `data-column`, so this dispatch site can
|
|
6
|
+
* never emit a null column.
|
|
7
|
+
*/
|
|
8
|
+
export interface BvSortEventDetail {
|
|
9
|
+
column: string;
|
|
10
|
+
direction: 'asc' | 'desc' | 'none';
|
|
11
|
+
}
|
|
2
12
|
export declare class BvTable extends HTMLElement {
|
|
3
13
|
static readonly observedAttributes: string[];
|
|
4
14
|
private _isDragging;
|
|
@@ -7,8 +17,15 @@ export declare class BvTable extends HTMLElement {
|
|
|
7
17
|
private _boundMouseDown;
|
|
8
18
|
private _boundMouseMove;
|
|
9
19
|
private _boundMouseUp;
|
|
20
|
+
private _boundScroll;
|
|
21
|
+
private _resizeObserver;
|
|
22
|
+
private _mutationObserver;
|
|
23
|
+
private _bodyObserver;
|
|
24
|
+
private _headerObserver;
|
|
10
25
|
get loading(): boolean;
|
|
11
26
|
set loading(v: boolean);
|
|
27
|
+
get fitViewport(): boolean;
|
|
28
|
+
set fitViewport(v: boolean);
|
|
12
29
|
connectedCallback(): void;
|
|
13
30
|
disconnectedCallback(): void;
|
|
14
31
|
attributeChangedCallback(name: string, _old: string | null, val: string | null): void;
|
|
@@ -16,10 +33,21 @@ export declare class BvTable extends HTMLElement {
|
|
|
16
33
|
private _removeSkeleton;
|
|
17
34
|
private _setupDrag;
|
|
18
35
|
private _teardownDrag;
|
|
36
|
+
private _setupStickyColumns;
|
|
37
|
+
private _teardownStickyColumns;
|
|
38
|
+
private _onScroll;
|
|
39
|
+
private _updateStickyColumnOffsets;
|
|
19
40
|
private _enhanceBodyCells;
|
|
41
|
+
private _enhanceOneCell;
|
|
42
|
+
private _observeBodyMutations;
|
|
20
43
|
private _onMouseDown;
|
|
21
44
|
private _onMouseMove;
|
|
22
45
|
private _onMouseUp;
|
|
23
46
|
private _isOverText;
|
|
24
47
|
private _enhanceHeaders;
|
|
48
|
+
private _enhanceOneHeader;
|
|
49
|
+
private _extractHeaderLabel;
|
|
50
|
+
private _syncSortableTh;
|
|
51
|
+
private _cycleSort;
|
|
52
|
+
private _observeHeaderMutations;
|
|
25
53
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// css-inline:/home/runner/work/BV_lumen/BV_lumen/components/bv-table/bv-table.css
|
|
2
|
-
var bv_table_default = "bv-table {\n display: block;\n width: 100%;\n box-sizing: border-box;\n overflow-x: auto;\n border: 1px solid var(--bv-color-neutral-200);\n font-family: var(--bv-font-family-base);\n cursor: grab;\n}\n\n/* During drag: override cursor for the entire table and all children */\nbv-table[data-dragging],\nbv-table[data-dragging] * {\n cursor: grabbing !important;\n user-select: none !important;\n}\n\nbv-table tbody td {\n cursor: grab;\n}\n\n/* Only the rendered glyphs are text-selectable \u2014 see _enhanceBodyCells in bv-table.ts */\nbv-table tbody td .bv-cell-text {\n cursor: text;\n}\n\nbv-table table {\n width: 100%;\n border-collapse: collapse;\n min-width: 0;\n}\n\n/* \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500*/\n\nbv-table thead tr {\n background: var(--bv-color-neutral-100);\n}\n\nbv-table thead th {\n padding: 12px 16px;\n text-align: left;\n font-size: var(--bv-font-size-xs);\n font-weight: var(--bv-font-weight-semibold);\n color: var(--bv-color-neutral-800);\n border-bottom: 1px solid var(--bv-color-neutral-200);\n white-space: nowrap;\n user-select: none;\n vertical-align: middle;\n}\n\nbv-table thead th[data-sortable] {\n cursor: pointer;\n}\n\nbv-table thead th[data-sortable]:hover {\n color: var(--bv-color-neutral-700);\n}\n\nbv-table thead th[data-sort='asc'] .bv-sort-icon,\nbv-table thead th[data-sort='desc'] .bv-sort-icon {\n color: var(--bv-color-primary);\n}\n\n.bv-th-content {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n}\n\n.bv-sort-icon {\n width: 14px;\n height: 14px;\n flex-shrink: 0;\n}\n\n/* \u2500\u2500 Body \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500*/\n\nbv-table tbody td {\n padding: 12px 16px;\n font-size: var(--bv-font-size-sm);\n color: var(--bv-color-neutral-700);\n border-bottom: 1px solid var(--bv-color-neutral-100);\n vertical-align: middle;\n white-space: nowrap;\n}\n\nbv-table tbody tr:last-child td {\n border-bottom: none;\n}\n\nbv-table tbody tr:hover td {\n background: var(--bv-color-neutral-100);\n}\n\n/* \u2500\u2500 Column alignment \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500*/\n\nbv-table thead th[data-align='right'],\nbv-table tbody td[data-align='right'] {\n text-align: right;\n}\n\nbv-table thead th[data-align='center'],\nbv-table tbody td[data-align='center'] {\n text-align: center;\n}\n\n/* \u2500\u2500 Cell modifier classes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500*/\n\n/* Muted: em dash or empty placeholder */\nbv-table td.bv-cell-muted {\n color: var(--bv-color-neutral-400);\n}\n\n/* Hidden: redacted identity for external users */\nbv-table td.bv-cell-hidden {\n color: var(--bv-color-neutral-400);\n font-style: italic;\n}\n\n/* Colored text for numeric highlight states */\nbv-table td.bv-cell-success {\n color: var(--bv-color-success);\n font-weight: var(--bv-font-weight-medium);\n}\n\nbv-table td.bv-cell-warning {\n color: var(--bv-color-warning);\n font-weight: var(--bv-font-weight-medium);\n}\n\nbv-table td.bv-cell-danger {\n color: var(--bv-color-danger);\n font-weight: var(--bv-font-weight-medium);\n}\n\nbv-table td.bv-cell-info {\n color: var(--bv-color-primary);\n font-weight: var(--bv-font-weight-medium);\n}\n\n/* \u2500\u2500 Modifiers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500*/\n\nbv-table[striped] tbody tr:nth-child(even) td {\n background: var(--bv-color-neutral-100);\n}\n\nbv-table[bordered] thead th,\nbv-table[bordered] tbody td {\n border-right: 1px solid var(--bv-color-neutral-100);\n}\n\nbv-table[bordered] thead th:last-child,\nbv-table[bordered] tbody td:last-child {\n border-right: none;\n}\n\n/* \u2500\u2500 Column divider \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500*/\n\nbv-table thead th[data-divider],\nbv-table tbody td[data-divider] {\n border-left: 1px solid var(--bv-color-neutral-200);\n}\n\n/* \u2500\u2500 Checkbox column \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500*/\n\nbv-table thead th.bv-col-check,\nbv-table tbody td.bv-col-check {\n width: 48px;\n padding: 12px 16px;\n}\n\n/* \u2500\u2500 BTN-10191 \xB7 loading skeleton \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Hide the real table while the skeleton is showing. Consumers keep\n their <thead>/<tbody> markup in place so no re-hydration is needed\n when data lands \u2014 only display switches. */\nbv-table[loading] > table {\n display: none;\n}\n\n.bv-table-skeleton {\n display: flex;\n flex-direction: column;\n gap: var(--bv-spacing-md-minus);\n padding: var(--bv-spacing-md);\n}\n\n.bv-table-skeleton-row {\n display: flex;\n gap: var(--bv-spacing-lg);\n}\n\n.bv-table-skeleton-bar {\n flex: 1;\n height: 14px;\n border-radius: var(--bv-radius-sm);\n background: var(--bv-color-neutral-200);\n animation: bv-table-skeleton-shimmer 1.5s ease-in-out infinite;\n}\n\n/* Slight offset per row so the shimmer isn't in lockstep \u2014 reads more\n like real content loading than a pulsing wall. */\n.bv-table-skeleton-row:nth-child(even) .bv-table-skeleton-bar {\n animation-delay: 0.2s;\n}\n\n@keyframes bv-table-skeleton-shimmer {\n 0%,\n 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.5;\n }\n}\n";
|
|
2
|
+
var bv_table_default = "bv-table {\n display: block;\n width: 100%;\n box-sizing: border-box;\n overflow-x: auto;\n border: 1px solid var(--bv-color-neutral-200);\n font-family: var(--bv-font-family-base);\n cursor: grab;\n}\n\n/* \u2500\u2500 fit-viewport (BTN-10755) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Consumer wraps the table in a flex container that fills the viewport bottom\n * (e.g. `.wrap { display: flex; flex-direction: column; height: 100vh }` +\n * `bv-table { flex: 1 }`). Then this attribute:\n * 1. Adds vertical scrolling on the host (in addition to the horizontal one\n * that already exists), so the body scrolls without pushing page layout.\n * 2. Pins the header row(s) via `position: sticky; top: 0` inside the\n * scrolling viewport, so column labels stay visible while the user\n * pages through rows.\n * The horizontal scrollbar stays at the bottom of the host \u2014 always visible,\n * per the CET spec.\n */\nbv-table[fit-viewport] {\n overflow: auto;\n height: 100%;\n min-height: 0;\n}\n\nbv-table[fit-viewport] thead th {\n position: sticky;\n top: 0;\n z-index: 2;\n}\n\n/* Header row 2 (subheaders) sits below row 1 in the sticky stack \u2014 offset by\n * the height of row 1 so both rows pin. Split down the middle so the visual\n * height of a 2-row header still equals a single-row header. */\nbv-table[fit-viewport] thead tr:nth-child(2) th {\n top: calc(var(--bv-sticky-head-row1-height, 24px));\n}\n\n/* During drag: override cursor for the entire table and all children */\nbv-table[data-dragging],\nbv-table[data-dragging] * {\n cursor: grabbing !important;\n user-select: none !important;\n}\n\nbv-table tbody td {\n cursor: grab;\n}\n\n/* Only the rendered glyphs are text-selectable \u2014 see _enhanceBodyCells in bv-table.ts */\nbv-table tbody td .bv-cell-text {\n cursor: text;\n}\n\nbv-table table {\n width: 100%;\n border-collapse: collapse;\n min-width: 0;\n}\n\n/* \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500*/\n\nbv-table thead th {\n /* Own background per <th> (was on <tr>) \u2014 under `fit-viewport` every\n * header cell becomes `position: sticky` and the sticky cell floats at\n * top:0 while its parent <tr> scrolls off-screen. A <tr>-level bg would\n * scroll away with the row and leave the pinned header transparent\n * (BTN-10755 followup). */\n background: var(--bv-color-neutral-100);\n padding: 12px 16px;\n text-align: left;\n font-size: var(--bv-font-size-xs);\n font-weight: var(--bv-font-weight-semibold);\n color: var(--bv-color-neutral-800);\n border-bottom: 1px solid var(--bv-color-neutral-200);\n white-space: nowrap;\n user-select: none;\n vertical-align: middle;\n}\n\nbv-table thead th[data-sortable] {\n cursor: pointer;\n}\n\nbv-table thead th[data-sortable]:hover {\n color: var(--bv-color-neutral-700);\n}\n\nbv-table thead th[data-sort='asc'] .bv-sort-icon,\nbv-table thead th[data-sort='desc'] .bv-sort-icon {\n color: var(--bv-color-primary);\n}\n\n.bv-th-content {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n}\n\n.bv-sort-icon {\n width: 14px;\n height: 14px;\n flex-shrink: 0;\n}\n\n/* \u2500\u2500 Body \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500*/\n\nbv-table tbody td {\n padding: 12px 16px;\n font-size: var(--bv-font-size-sm);\n color: var(--bv-color-neutral-700);\n border-bottom: 1px solid var(--bv-color-neutral-100);\n vertical-align: middle;\n white-space: nowrap;\n}\n\nbv-table tbody tr:last-child td {\n border-bottom: none;\n}\n\nbv-table tbody tr:hover td {\n background: var(--bv-color-neutral-100);\n}\n\n/* \u2500\u2500 Column alignment \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500*/\n\nbv-table thead th[data-align='right'],\nbv-table tbody td[data-align='right'] {\n text-align: right;\n}\n\nbv-table thead th[data-align='center'],\nbv-table tbody td[data-align='center'] {\n text-align: center;\n}\n\n/* \u2500\u2500 Cell modifier classes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500*/\n\n/* Muted: em dash or empty placeholder */\nbv-table td.bv-cell-muted {\n color: var(--bv-color-neutral-400);\n}\n\n/* Hidden: redacted identity for external users */\nbv-table td.bv-cell-hidden {\n color: var(--bv-color-neutral-400);\n font-style: italic;\n}\n\n/* Colored text for numeric highlight states */\nbv-table td.bv-cell-success {\n color: var(--bv-color-success);\n font-weight: var(--bv-font-weight-medium);\n}\n\nbv-table td.bv-cell-warning {\n color: var(--bv-color-warning);\n font-weight: var(--bv-font-weight-medium);\n}\n\nbv-table td.bv-cell-danger {\n color: var(--bv-color-danger);\n font-weight: var(--bv-font-weight-medium);\n}\n\nbv-table td.bv-cell-info {\n color: var(--bv-color-primary);\n font-weight: var(--bv-font-weight-medium);\n}\n\n/* \u2500\u2500 Modifiers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500*/\n\nbv-table[striped] tbody tr:nth-child(even) td {\n background: var(--bv-color-neutral-100);\n}\n\nbv-table[bordered] thead th,\nbv-table[bordered] tbody td {\n border-right: 1px solid var(--bv-color-neutral-100);\n}\n\nbv-table[bordered] thead th:last-child,\nbv-table[bordered] tbody td:last-child {\n border-right: none;\n}\n\n/* \u2500\u2500 Column divider \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500*/\n\nbv-table thead th[data-divider],\nbv-table tbody td[data-divider] {\n border-left: 1px solid var(--bv-color-neutral-200);\n}\n\n/* \u2500\u2500 Grouped header (BTN-10755) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500*\n *\n * 2-row header pattern: consumer writes a first-row <th colspan=\"N\">\n * spanning the group cells, then a second <tr> in <thead> with N\n * sub-header <th>s below it. Cells outside the group use rowspan=\"2\".\n *\n * Sub-headers are visually lighter than main headers and get their own\n * divider from the group cell above. The group cell centers its label\n * horizontally over the axis of the sub-columns (data-align=\"center\"\n * on the group th, per convention).\n */\nbv-table thead tr + tr th {\n font-size: var(--bv-font-size-xs);\n font-weight: var(--bv-font-weight-medium);\n color: var(--bv-color-neutral-600);\n /* Half of the base 8px 16px so the two-row header packs tighter \u2014 the\n * rowspan=2 cells still control the row-1 height, so this only affects\n * how much air sits around the group label and sub-headers. */\n padding: 4px 16px;\n}\n\nbv-table thead th[data-group] {\n text-align: center;\n /* Same halving as the sub-header row \u2014 matches the visual density of the\n * grouped-header stack (label + 3 subs) with the surrounding row-1 cells. */\n padding: 6px 16px;\n /* The base `thead th { border-bottom }` rule paints the divider between\n * the header and tbody. On a group cell that divider would appear\n * *inside* the header block (between the group label and its sub-headers)\n * \u2014 the sub-header row's own border-bottom already covers the header/body\n * boundary. */\n border-bottom: none;\n}\n\n/* \u2500\u2500 Sticky columns (BTN-10755) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500*\n *\n * Consumers opt in per cell with `data-sticky` on the th/td of the\n * columns they want to freeze during horizontal scroll. The component\n * computes cumulative `left` offsets from the natural widths of the\n * sticky <th>s and writes them as a `--bv-sticky-left` inline style\n * on every sticky cell (see _updateStickyColumnOffsets in bv-table.ts).\n *\n * The last sticky cell in each row is marked with `data-sticky-last`\n * so CSS can hang the scroll shadow gradient on its trailing edge \u2014\n * only visible when the host has scrolled off `scrollLeft: 0` (that\n * toggles `data-scrolled-x` on the host from bv-table.ts).\n */\nbv-table td[data-sticky],\nbv-table th[data-sticky] {\n position: sticky;\n left: var(--bv-sticky-left, 0);\n /* --bv-sticky-bg is the sole knob for state-dependent sticky background\n * (default / header / striped / hover). The gap-fill box-shadow below\n * reads the same variable, so the two always stay in sync. */\n --bv-sticky-bg: var(--bv-color-neutral-0);\n background: var(--bv-sticky-bg);\n z-index: 1;\n}\n\n/* Sub-pixel gap guard (BTN-10755 followup) \u2014 the computed sticky `left`\n * offset comes from `offsetWidth`, which rounds to the nearest integer\n * pixel while the browser renders the column widths as fractional pixels.\n * That mismatch leaves a hairline gap between adjacent sticky cells,\n * through which the scrolling columns behind bleed visible text. Extend\n * each non-last sticky cell 1px to the right with a box-shadow in the\n * cell's own background color to close the gap. The last sticky cell has\n * the scroll-shadow gradient (::after below) doing the same visual job. */\nbv-table td[data-sticky]:not([data-sticky-last]),\nbv-table th[data-sticky]:not([data-sticky-last]) {\n box-shadow: 1px 0 0 0 var(--bv-sticky-bg);\n}\n\n/* Header sticky cells stack above body sticky cells during vertical\n * scroll (relevant only under [fit-viewport]) and above the row\n * hovering neighbors that also carry sticky. */\nbv-table thead th[data-sticky] {\n --bv-sticky-bg: var(--bv-color-neutral-100);\n z-index: 3;\n}\n\n/* Under fit-viewport, header sticky cells sit above the horizontal\n * sticky \"corner\" area \u2014 their z stacking must beat both. */\nbv-table[fit-viewport] thead th[data-sticky] {\n z-index: 4;\n}\n\n/* Striped rows: sticky cells match the row's background so the frozen\n * column doesn't visually break the alternating band. */\nbv-table[striped] tbody tr:nth-child(even) td[data-sticky] {\n --bv-sticky-bg: var(--bv-color-neutral-100);\n}\n\n/* Hover: same override \u2014 sticky cell background must track the row's\n * hover background or the frozen cells look \"unclickable\". */\nbv-table tbody tr:hover td[data-sticky] {\n --bv-sticky-bg: var(--bv-color-neutral-100);\n}\n\n/* Scroll shadow \u2014 spec gradient from the ticket:\n * linear-gradient(to right, rgba(15,23,42,.10), rgba(15,23,42,0))\n * Painted as an absolutely-positioned ::after so it extends outside the\n * sticky cell without affecting its own width. Only paints when the\n * host has scrolled off the initial x-position (data-scrolled-x set\n * by _onScroll in bv-table.ts).\n */\nbv-table[data-scrolled-x] td[data-sticky-last]::after,\nbv-table[data-scrolled-x] th[data-sticky-last]::after {\n content: '';\n position: absolute;\n top: 0;\n right: -12px;\n bottom: 0;\n width: 12px;\n /* Ticket spec was `linear-gradient(to right, rgba(15,23,42,.10), rgba(15,23,42,0))`.\n * `rgba(15, 23, 42, ...)` is `--bv-color-neutral-900` (#0f172a) \u2014 drive it\n * through the token so a future dark theme automatically rethemes the\n * shadow and CLAUDE.md's \"no hardcoded colors\" rule holds. `color-mix`\n * has been baseline-supported since Chrome 111 / Firefox 113 / Safari 16.2. */\n background: linear-gradient(\n to right,\n color-mix(in srgb, var(--bv-color-neutral-900) 10%, transparent),\n transparent\n );\n pointer-events: none;\n}\n\n/* \u2500\u2500 Checkbox column \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500*/\n\nbv-table thead th.bv-col-check,\nbv-table tbody td.bv-col-check {\n width: 48px;\n padding: 12px 16px;\n}\n\n/* \u2500\u2500 BTN-10191 \xB7 loading skeleton \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Hide the real table while the skeleton is showing. Consumers keep\n their <thead>/<tbody> markup in place so no re-hydration is needed\n when data lands \u2014 only display switches. */\nbv-table[loading] > table {\n display: none;\n}\n\n.bv-table-skeleton {\n display: flex;\n flex-direction: column;\n gap: var(--bv-spacing-md-minus);\n padding: var(--bv-spacing-md);\n}\n\n.bv-table-skeleton-row {\n display: flex;\n gap: var(--bv-spacing-lg);\n}\n\n.bv-table-skeleton-bar {\n flex: 1;\n height: 14px;\n border-radius: var(--bv-radius-sm);\n background: var(--bv-color-neutral-200);\n animation: bv-table-skeleton-shimmer 1.5s ease-in-out infinite;\n}\n\n/* Slight offset per row so the shimmer isn't in lockstep \u2014 reads more\n like real content loading than a pulsing wall. */\n.bv-table-skeleton-row:nth-child(even) .bv-table-skeleton-bar {\n animation-delay: 0.2s;\n}\n\n@keyframes bv-table-skeleton-shimmer {\n 0%,\n 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.5;\n }\n}\n";
|
|
3
3
|
|
|
4
4
|
// css-inline:/home/runner/work/BV_lumen/BV_lumen/components/bv-tooltip/bv-tooltip.css
|
|
5
5
|
var bv_tooltip_default = ":host {\n display: inline-flex;\n align-items: center;\n position: relative;\n font-family: var(--bv-font-family-base);\n}\n\n:host([hidden]) {\n display: none;\n}\n\n.wrap {\n display: inline-flex;\n align-items: center;\n position: relative;\n}\n\n.trigger {\n display: inline-flex;\n align-items: center;\n cursor: help;\n color: var(--bv-color-neutral-400);\n outline: none;\n border: none;\n background: none;\n padding: 0;\n line-height: 0;\n}\n\n.trigger:hover,\n.trigger:focus {\n color: var(--bv-color-neutral-600);\n}\n\n.trigger svg {\n display: block;\n width: 14px;\n height: 14px;\n}\n\n/* \u2500\u2500 Popup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.popup {\n /* Shown/positioned entirely via JS \u2014 display starts as none */\n display: none;\n position: absolute;\n z-index: 500;\n min-width: 200px;\n max-width: 300px;\n background: var(--bv-color-neutral-900);\n color: var(--bv-color-neutral-0);\n border-radius: var(--bv-radius-md);\n padding: 10px 12px;\n font-size: var(--bv-font-size-xs);\n font-weight: var(--bv-font-weight-regular);\n line-height: 1.6;\n box-shadow: var(--bv-shadow-lg);\n pointer-events: none;\n white-space: normal;\n}\n\n/* \u2500\u2500 Content \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.text-content {\n display: none;\n}\n\n:host([text]) .text-content {\n display: block;\n}\n\n:host([text]) slot {\n display: none;\n}\n\n::slotted(p) {\n margin: 0 0 4px;\n}\n\n::slotted(p:last-child) {\n margin-bottom: 0;\n}\n";
|
|
@@ -167,13 +167,24 @@ function injectTableStyles() {
|
|
|
167
167
|
document.head.appendChild(style);
|
|
168
168
|
}
|
|
169
169
|
var BvTable = class extends HTMLElement {
|
|
170
|
-
static observedAttributes = [
|
|
170
|
+
static observedAttributes = [
|
|
171
|
+
"striped",
|
|
172
|
+
"bordered",
|
|
173
|
+
"loading",
|
|
174
|
+
"loading-rows",
|
|
175
|
+
"fit-viewport"
|
|
176
|
+
];
|
|
171
177
|
_isDragging = false;
|
|
172
178
|
_dragStartX = 0;
|
|
173
179
|
_dragScrollLeft = 0;
|
|
174
180
|
_boundMouseDown = (e) => this._onMouseDown(e);
|
|
175
181
|
_boundMouseMove = (e) => this._onMouseMove(e);
|
|
176
182
|
_boundMouseUp = () => this._onMouseUp();
|
|
183
|
+
_boundScroll = () => this._onScroll();
|
|
184
|
+
_resizeObserver = null;
|
|
185
|
+
_mutationObserver = null;
|
|
186
|
+
_bodyObserver = null;
|
|
187
|
+
_headerObserver = null;
|
|
177
188
|
get loading() {
|
|
178
189
|
return this.hasAttribute("loading");
|
|
179
190
|
}
|
|
@@ -181,15 +192,31 @@ var BvTable = class extends HTMLElement {
|
|
|
181
192
|
if (v) this.setAttribute("loading", "");
|
|
182
193
|
else this.removeAttribute("loading");
|
|
183
194
|
}
|
|
195
|
+
get fitViewport() {
|
|
196
|
+
return this.hasAttribute("fit-viewport");
|
|
197
|
+
}
|
|
198
|
+
set fitViewport(v) {
|
|
199
|
+
if (v) this.setAttribute("fit-viewport", "");
|
|
200
|
+
else this.removeAttribute("fit-viewport");
|
|
201
|
+
}
|
|
184
202
|
connectedCallback() {
|
|
185
203
|
injectTableStyles();
|
|
186
204
|
this._enhanceHeaders();
|
|
187
205
|
this._enhanceBodyCells();
|
|
188
206
|
this._setupDrag();
|
|
207
|
+
this._setupStickyColumns();
|
|
208
|
+
this._observeBodyMutations();
|
|
209
|
+
this._observeHeaderMutations();
|
|
189
210
|
if (this.hasAttribute("loading")) this._renderSkeleton();
|
|
190
211
|
}
|
|
191
212
|
disconnectedCallback() {
|
|
213
|
+
var _a, _b;
|
|
192
214
|
this._teardownDrag();
|
|
215
|
+
this._teardownStickyColumns();
|
|
216
|
+
(_a = this._bodyObserver) == null ? void 0 : _a.disconnect();
|
|
217
|
+
this._bodyObserver = null;
|
|
218
|
+
(_b = this._headerObserver) == null ? void 0 : _b.disconnect();
|
|
219
|
+
this._headerObserver = null;
|
|
193
220
|
}
|
|
194
221
|
attributeChangedCallback(name, _old, val) {
|
|
195
222
|
if (!this.isConnected) return;
|
|
@@ -201,6 +228,10 @@ var BvTable = class extends HTMLElement {
|
|
|
201
228
|
this._removeSkeleton();
|
|
202
229
|
this._renderSkeleton();
|
|
203
230
|
}
|
|
231
|
+
if (name === "fit-viewport") {
|
|
232
|
+
this._teardownStickyColumns();
|
|
233
|
+
this._setupStickyColumns();
|
|
234
|
+
}
|
|
204
235
|
}
|
|
205
236
|
// Renders skeleton rows as a light-DOM sibling to the user's <table>.
|
|
206
237
|
// The `loading` attribute on the host + CSS in bv-table.css handles the
|
|
@@ -241,20 +272,126 @@ var BvTable = class extends HTMLElement {
|
|
|
241
272
|
window.removeEventListener("mousemove", this._boundMouseMove);
|
|
242
273
|
window.removeEventListener("mouseup", this._boundMouseUp);
|
|
243
274
|
}
|
|
275
|
+
// ── Sticky columns (BTN-10755) ───────────────────────────────────────
|
|
276
|
+
//
|
|
277
|
+
// Consumers opt in per-cell via `data-sticky` on the th/td they want to
|
|
278
|
+
// freeze. Cumulative `left` offsets are computed from the natural widths
|
|
279
|
+
// of the sticky <th>s in the first thead row and set as inline styles on
|
|
280
|
+
// every sticky cell (th + td, across every body row). The final sticky
|
|
281
|
+
// <th>/<td> in each row is marked with `data-sticky-last` so CSS can hang
|
|
282
|
+
// the scroll-shadow gradient on its trailing edge when the host has
|
|
283
|
+
// scrolled off the initial x-position.
|
|
284
|
+
_setupStickyColumns() {
|
|
285
|
+
if (!this.querySelector("[data-sticky]") && !this.hasAttribute("fit-viewport")) return;
|
|
286
|
+
this.addEventListener("scroll", this._boundScroll, { passive: true });
|
|
287
|
+
requestAnimationFrame(() => {
|
|
288
|
+
if (!this.isConnected) return;
|
|
289
|
+
this._updateStickyColumnOffsets();
|
|
290
|
+
this._onScroll();
|
|
291
|
+
});
|
|
292
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
293
|
+
this._resizeObserver = new ResizeObserver(() => this._updateStickyColumnOffsets());
|
|
294
|
+
this._resizeObserver.observe(this);
|
|
295
|
+
}
|
|
296
|
+
this._mutationObserver = new MutationObserver(() => this._updateStickyColumnOffsets());
|
|
297
|
+
this._mutationObserver.observe(this, { childList: true, subtree: true });
|
|
298
|
+
}
|
|
299
|
+
_teardownStickyColumns() {
|
|
300
|
+
var _a, _b;
|
|
301
|
+
this.removeEventListener("scroll", this._boundScroll);
|
|
302
|
+
(_a = this._resizeObserver) == null ? void 0 : _a.disconnect();
|
|
303
|
+
this._resizeObserver = null;
|
|
304
|
+
(_b = this._mutationObserver) == null ? void 0 : _b.disconnect();
|
|
305
|
+
this._mutationObserver = null;
|
|
306
|
+
}
|
|
307
|
+
_onScroll() {
|
|
308
|
+
if (this.scrollLeft > 0) this.setAttribute("data-scrolled-x", "");
|
|
309
|
+
else this.removeAttribute("data-scrolled-x");
|
|
310
|
+
}
|
|
311
|
+
_updateStickyColumnOffsets() {
|
|
312
|
+
const firstHeadRow = this.querySelector("thead tr");
|
|
313
|
+
if (!firstHeadRow) return;
|
|
314
|
+
const firstRowHeight = firstHeadRow.offsetHeight;
|
|
315
|
+
if (firstRowHeight > 0) {
|
|
316
|
+
this.style.setProperty("--bv-sticky-head-row1-height", `${firstRowHeight}px`);
|
|
317
|
+
}
|
|
318
|
+
const headerCells = Array.from(firstHeadRow.children);
|
|
319
|
+
const widths = /* @__PURE__ */ new Map();
|
|
320
|
+
let cumulative = 0;
|
|
321
|
+
let lastStickyColIndex = -1;
|
|
322
|
+
let stickyCount = 0;
|
|
323
|
+
let col = 0;
|
|
324
|
+
for (const cell of headerCells) {
|
|
325
|
+
const colspan = Number(cell.getAttribute("colspan") ?? "1") || 1;
|
|
326
|
+
if (cell.hasAttribute("data-sticky")) {
|
|
327
|
+
const overlap = stickyCount === 0 ? 0 : 1;
|
|
328
|
+
widths.set(col, cumulative - overlap);
|
|
329
|
+
cumulative += cell.offsetWidth;
|
|
330
|
+
lastStickyColIndex = col;
|
|
331
|
+
stickyCount++;
|
|
332
|
+
}
|
|
333
|
+
col += colspan;
|
|
334
|
+
}
|
|
335
|
+
if (lastStickyColIndex < 0) return;
|
|
336
|
+
const applyToRow = (row) => {
|
|
337
|
+
const cells = Array.from(row.children);
|
|
338
|
+
let col2 = 0;
|
|
339
|
+
for (const cell of cells) {
|
|
340
|
+
const colspan = Number(cell.getAttribute("colspan") ?? "1") || 1;
|
|
341
|
+
if (cell.hasAttribute("data-sticky")) {
|
|
342
|
+
const left = widths.get(col2);
|
|
343
|
+
if (left !== void 0) cell.style.setProperty("--bv-sticky-left", `${left}px`);
|
|
344
|
+
if (col2 === lastStickyColIndex) cell.setAttribute("data-sticky-last", "");
|
|
345
|
+
else cell.removeAttribute("data-sticky-last");
|
|
346
|
+
}
|
|
347
|
+
col2 += colspan;
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
this.querySelectorAll("thead tr, tbody tr").forEach(applyToRow);
|
|
351
|
+
}
|
|
244
352
|
// Wraps each plain-text body cell's content in an inline span. Unlike the
|
|
245
353
|
// <td> itself, an inline span only occupies the rendered width of its text —
|
|
246
354
|
// so hovering the cell's padding (short values, blank space) stays grab-able,
|
|
247
355
|
// and only the glyphs themselves resolve to a text-selectable target.
|
|
248
356
|
_enhanceBodyCells() {
|
|
249
|
-
this.querySelectorAll("tbody td").forEach((td) =>
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
357
|
+
this.querySelectorAll("tbody td").forEach((td) => this._enhanceOneCell(td));
|
|
358
|
+
}
|
|
359
|
+
// Single-cell wrapper. Idempotent: already-wrapped cells (any child element)
|
|
360
|
+
// are skipped, so re-running against a re-emitted row does not double-wrap.
|
|
361
|
+
// Bails on TDs that live outside a <tbody> (e.g. accidentally-placed th/td
|
|
362
|
+
// in the header) — those go through the separate `_enhanceHeaders` path.
|
|
363
|
+
// Called from `_enhanceBodyCells` (initial pass) AND from `_observeBodyMutations`
|
|
364
|
+
// (dynamic rows added by consumer frameworks after connectedCallback).
|
|
365
|
+
_enhanceOneCell(td) {
|
|
366
|
+
if (!td.closest("tbody")) return;
|
|
367
|
+
if (td.children.length > 0) return;
|
|
368
|
+
const text = td.textContent;
|
|
369
|
+
if (!text || !text.trim()) return;
|
|
370
|
+
const span = document.createElement("span");
|
|
371
|
+
span.className = "bv-cell-text";
|
|
372
|
+
span.textContent = text;
|
|
373
|
+
td.replaceChildren(span);
|
|
374
|
+
}
|
|
375
|
+
// Watches the host for TDs materialized after the initial `_enhanceBodyCells`
|
|
376
|
+
// pass — Angular *ngFor, React lists, or any consumer that mounts <bv-table>
|
|
377
|
+
// with an empty tbody and appends rows in a subsequent render tick. Without
|
|
378
|
+
// this observer, those new TDs keep their direct text-node children, which
|
|
379
|
+
// makes `_isOverText` return true on the fallback childNodes branch — every
|
|
380
|
+
// cell then blocks drag-to-scroll even on padding hits, while CSS still
|
|
381
|
+
// shows `cursor: grab`. The observer runs unconditionally (not gated on
|
|
382
|
+
// sticky / fit-viewport like `_mutationObserver`) because plain tables also
|
|
383
|
+
// rely on drag-to-scroll.
|
|
384
|
+
_observeBodyMutations() {
|
|
385
|
+
this._bodyObserver = new MutationObserver((mutations) => {
|
|
386
|
+
for (const m of mutations) {
|
|
387
|
+
m.addedNodes.forEach((node) => {
|
|
388
|
+
if (!(node instanceof HTMLElement)) return;
|
|
389
|
+
if (node.matches("td")) this._enhanceOneCell(node);
|
|
390
|
+
node.querySelectorAll("td").forEach((td) => this._enhanceOneCell(td));
|
|
391
|
+
});
|
|
392
|
+
}
|
|
257
393
|
});
|
|
394
|
+
this._bodyObserver.observe(this, { childList: true, subtree: true });
|
|
258
395
|
}
|
|
259
396
|
_onMouseDown(e) {
|
|
260
397
|
if (e.button !== 0) return;
|
|
@@ -287,58 +424,137 @@ var BvTable = class extends HTMLElement {
|
|
|
287
424
|
return false;
|
|
288
425
|
}
|
|
289
426
|
_enhanceHeaders() {
|
|
290
|
-
this.querySelectorAll("thead th").forEach((th) =>
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
th.
|
|
309
|
-
|
|
427
|
+
this.querySelectorAll("thead th").forEach((th) => this._enhanceOneHeader(th));
|
|
428
|
+
}
|
|
429
|
+
// BTN-10755 — per-th enhancer. Handles sortable + tooltip (and their
|
|
430
|
+
// intersection). Idempotent: safe to re-run when the header observer
|
|
431
|
+
// sees data-sortable added at runtime. Non-sortable, non-tooltip ths
|
|
432
|
+
// are left untouched (drag / text-selection ergonomics rely on th
|
|
433
|
+
// childNodes being the original text nodes).
|
|
434
|
+
_enhanceOneHeader(th) {
|
|
435
|
+
const hasSortable = th.hasAttribute("data-sortable");
|
|
436
|
+
const hasTooltip = th.hasAttribute("data-tooltip");
|
|
437
|
+
if (!hasSortable && !hasTooltip) return;
|
|
438
|
+
if (hasSortable && !th.hasAttribute("data-column")) {
|
|
439
|
+
console.warn(
|
|
440
|
+
"[bv-table] <th data-sortable> requires data-column; skipping sort enhancement for:",
|
|
441
|
+
th
|
|
442
|
+
);
|
|
443
|
+
if (!hasTooltip) return;
|
|
444
|
+
const label = this._extractHeaderLabel(th);
|
|
445
|
+
const tip = escapeAttr(th.getAttribute("data-tooltip") ?? "");
|
|
446
|
+
th.innerHTML = `<span class="bv-th-content">${label}<bv-tooltip text="${tip}" position="bottom"></bv-tooltip></span>`;
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (hasSortable) {
|
|
450
|
+
if (!th.dataset["bvSortableWired"]) {
|
|
451
|
+
th.dataset["bvSortableWired"] = "true";
|
|
310
452
|
th.addEventListener("click", () => {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const otherTooltip = other.hasAttribute("data-tooltip") ? `<bv-tooltip text="${escapeAttr(other.getAttribute("data-tooltip") ?? "")}" position="bottom"></bv-tooltip>` : "";
|
|
320
|
-
other.innerHTML = `<span class="bv-th-content">${otherLabel}${otherTooltip}${SORT_NONE}</span>`;
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
const currentLabel = ((_b = (_a2 = th.querySelector(".bv-th-content")) == null ? void 0 : _a2.childNodes[0]) == null ? void 0 : _b.textContent) ?? label;
|
|
324
|
-
const currentTooltip = hasTooltip ? `<bv-tooltip text="${escapeAttr(th.getAttribute("data-tooltip") ?? "")}" position="bottom"></bv-tooltip>` : "";
|
|
325
|
-
if (next === "none") {
|
|
326
|
-
th.removeAttribute("data-sort");
|
|
327
|
-
th.innerHTML = `<span class="bv-th-content">${currentLabel}${currentTooltip}${SORT_NONE}</span>`;
|
|
328
|
-
} else {
|
|
329
|
-
th.setAttribute("data-sort", next);
|
|
330
|
-
const newIcon = next === "asc" ? SORT_ASC : SORT_DESC;
|
|
331
|
-
th.innerHTML = `<span class="bv-th-content">${currentLabel}${currentTooltip}${newIcon}</span>`;
|
|
453
|
+
if (!th.hasAttribute("data-sortable")) return;
|
|
454
|
+
this._cycleSort(th);
|
|
455
|
+
});
|
|
456
|
+
th.addEventListener("keydown", (e) => {
|
|
457
|
+
if (!th.hasAttribute("data-sortable")) return;
|
|
458
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
459
|
+
e.preventDefault();
|
|
460
|
+
this._cycleSort(th);
|
|
332
461
|
}
|
|
333
|
-
this.dispatchEvent(
|
|
334
|
-
new CustomEvent("bv-sort", {
|
|
335
|
-
bubbles: true,
|
|
336
|
-
composed: true,
|
|
337
|
-
detail: { column: th.getAttribute("data-column"), direction: next }
|
|
338
|
-
})
|
|
339
|
-
);
|
|
340
462
|
});
|
|
341
463
|
}
|
|
464
|
+
th.setAttribute("tabindex", "0");
|
|
465
|
+
this._syncSortableTh(th);
|
|
466
|
+
} else {
|
|
467
|
+
const label = this._extractHeaderLabel(th);
|
|
468
|
+
const tip = escapeAttr(th.getAttribute("data-tooltip") ?? "");
|
|
469
|
+
th.innerHTML = `<span class="bv-th-content">${label}<bv-tooltip text="${tip}" position="bottom"></bv-tooltip></span>`;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// Reads the plain-text label from a th, whether it's still raw markup
|
|
473
|
+
// (initial enhance pass) or has already been wrapped in `.bv-th-content`
|
|
474
|
+
// (re-enhance from an observer callback). textContent skips SVG icons and
|
|
475
|
+
// slotless bv-tooltips, so what comes back is the visible column name.
|
|
476
|
+
_extractHeaderLabel(th) {
|
|
477
|
+
var _a, _b;
|
|
478
|
+
const content = th.querySelector(".bv-th-content");
|
|
479
|
+
if (content) {
|
|
480
|
+
const firstText = content.childNodes[0];
|
|
481
|
+
if ((firstText == null ? void 0 : firstText.nodeType) === Node.TEXT_NODE) return ((_a = firstText.textContent) == null ? void 0 : _a.trim()) ?? "";
|
|
482
|
+
}
|
|
483
|
+
return ((_b = th.textContent) == null ? void 0 : _b.trim()) ?? "";
|
|
484
|
+
}
|
|
485
|
+
// BTN-10755 — idempotent visual sync for a single sortable th. Reads
|
|
486
|
+
// `data-sort`, renders the matching icon into `.bv-th-content`, and
|
|
487
|
+
// mirrors the sort direction into `aria-sort` for screen readers. Also
|
|
488
|
+
// used by `_cycleSort` (to reflect the just-computed state) and by the
|
|
489
|
+
// `_headerObserver` (to reflect a consumer's external data-sort write).
|
|
490
|
+
_syncSortableTh(th) {
|
|
491
|
+
const sortDir = th.getAttribute("data-sort") ?? "none";
|
|
492
|
+
const label = this._extractHeaderLabel(th);
|
|
493
|
+
const tooltip = th.hasAttribute("data-tooltip") ? `<bv-tooltip text="${escapeAttr(th.getAttribute("data-tooltip") ?? "")}" position="bottom"></bv-tooltip>` : "";
|
|
494
|
+
const icon = sortDir === "asc" ? SORT_ASC : sortDir === "desc" ? SORT_DESC : SORT_NONE;
|
|
495
|
+
th.innerHTML = `<span class="bv-th-content">${label}${tooltip}${icon}</span>`;
|
|
496
|
+
th.setAttribute(
|
|
497
|
+
"aria-sort",
|
|
498
|
+
sortDir === "asc" ? "ascending" : sortDir === "desc" ? "descending" : "none"
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
// BTN-10755 — click and Enter/Space both route through here. Cycles the
|
|
502
|
+
// tri-state, clears sibling sortable ths (UX for user interaction — the
|
|
503
|
+
// observer path deliberately does NOT clear siblings, since external
|
|
504
|
+
// writes are the consumer speaking and they own sibling state), and
|
|
505
|
+
// dispatches `bv-sort` with the resolved column + direction.
|
|
506
|
+
_cycleSort(th) {
|
|
507
|
+
const current = th.getAttribute("data-sort") ?? "none";
|
|
508
|
+
const next = current === "none" ? "asc" : current === "asc" ? "desc" : "none";
|
|
509
|
+
this.querySelectorAll("thead th[data-sortable]").forEach((other) => {
|
|
510
|
+
if (other !== th && other.hasAttribute("data-sort")) {
|
|
511
|
+
other.removeAttribute("data-sort");
|
|
512
|
+
this._syncSortableTh(other);
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
if (next === "none") th.removeAttribute("data-sort");
|
|
516
|
+
else th.setAttribute("data-sort", next);
|
|
517
|
+
this._syncSortableTh(th);
|
|
518
|
+
this.dispatchEvent(
|
|
519
|
+
new CustomEvent("bv-sort", {
|
|
520
|
+
bubbles: true,
|
|
521
|
+
composed: true,
|
|
522
|
+
// `!` is safe: `_enhanceOneHeader` guards on `data-column`
|
|
523
|
+
// presence before wiring the listeners that reach this dispatch.
|
|
524
|
+
detail: { column: th.getAttribute("data-column"), direction: next }
|
|
525
|
+
})
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
// BTN-10755 — mirrors the body-cells observer pattern but scoped to header
|
|
529
|
+
// attribute mutations (data-sort, data-sortable) inside <thead>. Two use
|
|
530
|
+
// cases:
|
|
531
|
+
// 1. Consumers that set a default sort at mount, or rewrite data-sort
|
|
532
|
+
// when a filter changes, or revert it after an API error. The observer
|
|
533
|
+
// re-renders the icon + aria-sort without echoing bv-sort (that would
|
|
534
|
+
// loop back into the same consumer).
|
|
535
|
+
// 2. Consumers that flip a plain th into a sortable one at runtime
|
|
536
|
+
// (e.g. a feature-flagged "sortable everything" toggle). The observer
|
|
537
|
+
// runs the initial enhance for that th.
|
|
538
|
+
// The observer never clears siblings — sibling sanitation is UX for the
|
|
539
|
+
// user-driven click / keydown paths.
|
|
540
|
+
_observeHeaderMutations() {
|
|
541
|
+
this._headerObserver = new MutationObserver((mutations) => {
|
|
542
|
+
for (const m of mutations) {
|
|
543
|
+
if (m.type !== "attributes") continue;
|
|
544
|
+
const target = m.target;
|
|
545
|
+
if (!(target instanceof HTMLElement)) continue;
|
|
546
|
+
if (target.tagName !== "TH" || !target.closest("thead")) continue;
|
|
547
|
+
if (m.attributeName === "data-sort") {
|
|
548
|
+
if (target.hasAttribute("data-sortable")) this._syncSortableTh(target);
|
|
549
|
+
} else if (m.attributeName === "data-sortable") {
|
|
550
|
+
if (target.hasAttribute("data-sortable")) this._enhanceOneHeader(target);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
this._headerObserver.observe(this, {
|
|
555
|
+
attributes: true,
|
|
556
|
+
subtree: true,
|
|
557
|
+
attributeFilter: ["data-sort", "data-sortable"]
|
|
342
558
|
});
|
|
343
559
|
}
|
|
344
560
|
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { FilterDropdownOption } from '../../../components/bv-filter-dropdown/bv-filter-dropdown';
|
|
2
2
|
export declare const CATEGORY_OPTIONS: FilterDropdownOption[];
|
|
3
3
|
export declare const COURSE_OPTIONS: FilterDropdownOption[];
|
|
4
|
-
export declare const COURSE_SEAT_TIMES: Record<string, number>;
|
|
5
4
|
export declare const GROUP_OPTIONS: FilterDropdownOption[];
|
|
6
5
|
export declare const USER_OPTIONS: FilterDropdownOption[];
|
|
7
6
|
export type EnrollmentProgress = 'complete' | 'on-schedule' | 'behind-schedule' | 'pending-approval' | 'ready-to-pay' | 'enrolled';
|
|
@@ -166,47 +166,6 @@ var COURSE_OPTIONS = [
|
|
|
166
166
|
depth: 0
|
|
167
167
|
}
|
|
168
168
|
];
|
|
169
|
-
var COURSE_SEAT_TIMES = {
|
|
170
|
-
"35697": 45,
|
|
171
|
-
"43588": 60,
|
|
172
|
-
"111430": 30,
|
|
173
|
-
"118402": 45,
|
|
174
|
-
"163061": 25,
|
|
175
|
-
"223929": 90,
|
|
176
|
-
"230765": 50,
|
|
177
|
-
"230818": 60,
|
|
178
|
-
"309214": 40,
|
|
179
|
-
"331980": 75,
|
|
180
|
-
"341235": 60,
|
|
181
|
-
"392536": 55,
|
|
182
|
-
"412765": 35,
|
|
183
|
-
"428390": 65,
|
|
184
|
-
"467233": 70,
|
|
185
|
-
"142859": 120,
|
|
186
|
-
"501234": 90,
|
|
187
|
-
"502567": 120,
|
|
188
|
-
"503891": 75,
|
|
189
|
-
"701001": 20,
|
|
190
|
-
"701002": 45,
|
|
191
|
-
"701003": 90,
|
|
192
|
-
"701004": 60,
|
|
193
|
-
"701005": 75,
|
|
194
|
-
"701006": 90,
|
|
195
|
-
"701007": 60,
|
|
196
|
-
"701008": 45,
|
|
197
|
-
"701009": 105,
|
|
198
|
-
"701010": 60,
|
|
199
|
-
"701011": 75,
|
|
200
|
-
"701012": 60,
|
|
201
|
-
"701013": 90,
|
|
202
|
-
"701014": 60,
|
|
203
|
-
"701015": 45,
|
|
204
|
-
"701016": 180,
|
|
205
|
-
"701017": 90,
|
|
206
|
-
"701018": 75,
|
|
207
|
-
"701019": 45,
|
|
208
|
-
"701020": 60
|
|
209
|
-
};
|
|
210
169
|
var GROUP_OPTIONS = [
|
|
211
170
|
{ label: "Ungrouped", value: "ungrouped", depth: 0, italic: true },
|
|
212
171
|
{ label: "Legrand University", value: "legrand-university", depth: 0 },
|
|
@@ -4485,7 +4444,6 @@ export {
|
|
|
4485
4444
|
CATEGORY_COURSES,
|
|
4486
4445
|
CATEGORY_OPTIONS,
|
|
4487
4446
|
COURSE_OPTIONS,
|
|
4488
|
-
COURSE_SEAT_TIMES,
|
|
4489
4447
|
ENROLLMENTS,
|
|
4490
4448
|
ENROLLMENT_TYPE_BADGE,
|
|
4491
4449
|
GROUP_OPTIONS,
|
package/dist/themes/base.css
CHANGED
package/package.json
CHANGED