@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.
@@ -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._query.length < this.minQueryLength) {
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._query.length < this.minQueryLength) return;
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 = ["striped", "bordered", "loading", "loading-rows"];
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
- if (td.children.length > 0) return;
251
- const text = td.textContent;
252
- if (!text || !text.trim()) return;
253
- const span = document.createElement("span");
254
- span.className = "bv-cell-text";
255
- span.textContent = text;
256
- td.replaceChildren(span);
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
- var _a;
292
- const hasSortable = th.hasAttribute("data-sortable");
293
- const hasTooltip = th.hasAttribute("data-tooltip");
294
- if (!hasSortable && !hasTooltip) return;
295
- const label = ((_a = th.textContent) == null ? void 0 : _a.trim()) ?? "";
296
- let inner = label;
297
- if (hasTooltip) {
298
- const tip = escapeAttr(th.getAttribute("data-tooltip") ?? "");
299
- inner += `<bv-tooltip text="${tip}" position="bottom"></bv-tooltip>`;
300
- }
301
- if (hasSortable) {
302
- const sortDir = th.getAttribute("data-sort") ?? "none";
303
- const icon = sortDir === "asc" ? SORT_ASC : sortDir === "desc" ? SORT_DESC : SORT_NONE;
304
- inner = `<span class="bv-th-content">${inner}${icon}</span>`;
305
- } else {
306
- inner = `<span class="bv-th-content">${inner}</span>`;
307
- }
308
- th.innerHTML = inner;
309
- if (hasSortable) {
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
- var _a2, _b;
312
- const current = th.getAttribute("data-sort") ?? "none";
313
- const next = current === "none" ? "asc" : current === "asc" ? "desc" : "none";
314
- this.querySelectorAll("thead th[data-sortable]").forEach((other) => {
315
- var _a3, _b2;
316
- if (other !== th) {
317
- other.removeAttribute("data-sort");
318
- const otherLabel = ((_b2 = (_a3 = other.querySelector(".bv-th-content")) == null ? void 0 : _a3.childNodes[0]) == null ? void 0 : _b2.textContent) ?? "";
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,
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Wed, 05 Aug 2026 02:04:47 GMT
3
+ * Generated on Wed, 19 Aug 2026 01:38:34 GMT
4
4
  */
5
5
 
6
6
  :root {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bluevolt-tech/lumen",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org",
package/themes/base.css CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Wed, 05 Aug 2026 02:04:47 GMT
3
+ * Generated on Wed, 19 Aug 2026 01:38:34 GMT
4
4
  */
5
5
 
6
6
  :root {