@bluevolt-tech/lumen 2.2.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.
@@ -2,12 +2,14 @@ export type SegmentedControlOption = {
2
2
  label: string;
3
3
  value: string;
4
4
  };
5
+ export type SegmentedControlSize = 'sm' | 'md';
6
+ export type SegmentedControlVariant = 'default' | 'primary';
5
7
  export interface SegmentedControlChangeDetail {
6
8
  value: string;
7
9
  label: string;
8
10
  }
9
11
  export declare class BvSegmentedControl extends HTMLElement {
10
- static readonly observedAttributes: readonly ["options", "value", "label", "required"];
12
+ static readonly observedAttributes: readonly ["options", "value", "label", "required", "size", "variant"];
11
13
  private _rendered;
12
14
  private _labelEl;
13
15
  private _containerEl;
@@ -17,6 +19,10 @@ export declare class BvSegmentedControl extends HTMLElement {
17
19
  set value(v: string);
18
20
  get required(): boolean;
19
21
  set required(v: boolean);
22
+ get size(): SegmentedControlSize;
23
+ set size(v: SegmentedControlSize);
24
+ get variant(): SegmentedControlVariant;
25
+ set variant(v: SegmentedControlVariant);
20
26
  connectedCallback(): void;
21
27
  attributeChangedCallback(name: string, _old: string | null, _val: string | null): void;
22
28
  private _render;
@@ -1,5 +1,5 @@
1
1
  // css-inline:/home/runner/work/BV_lumen/BV_lumen/components/bv-segmented-control/bv-segmented-control.css
2
- var bv_segmented_control_default = ":host {\n display: block;\n font-family: var(--bv-font-family-base);\n}\n\n.field-label {\n display: block;\n font-size: var(--bv-font-size-sm);\n line-height: 20px;\n font-weight: var(--bv-font-weight-semibold);\n color: var(--bv-color-neutral-800);\n margin-bottom: 6px;\n}\n\n.field-label[hidden] {\n display: none;\n}\n\n.required-mark {\n color: var(--bv-color-danger);\n margin-left: 2px;\n}\n\n.container {\n display: flex;\n align-items: center;\n height: 44px;\n box-sizing: border-box;\n background: var(--bv-color-neutral-100);\n border: 1px solid var(--bv-color-neutral-200);\n border-radius: var(--bv-radius-lg);\n padding: 3px;\n gap: 2px;\n}\n\n.pill {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n padding: 0 var(--bv-spacing-md);\n border: none;\n border-radius: calc(var(--bv-radius-lg) - 3px);\n background: transparent;\n font-family: inherit;\n font-size: var(--bv-font-size-sm);\n font-weight: var(--bv-font-weight-medium);\n color: var(--bv-color-neutral-500);\n cursor: pointer;\n white-space: nowrap;\n user-select: none;\n transition:\n color var(--bv-transition-fast),\n background var(--bv-transition-fast),\n box-shadow var(--bv-transition-fast);\n}\n\n.pill:hover:not(.selected) {\n color: var(--bv-color-neutral-800);\n}\n\n.pill.selected {\n background: var(--bv-color-neutral-0);\n color: var(--bv-color-neutral-900);\n box-shadow: var(--bv-shadow-sm);\n}\n";
2
+ var bv_segmented_control_default = ":host {\n display: block;\n font-family: var(--bv-font-family-base);\n}\n\n.field-label {\n display: block;\n font-size: var(--bv-font-size-sm);\n line-height: 20px;\n font-weight: var(--bv-font-weight-semibold);\n color: var(--bv-color-neutral-800);\n margin-bottom: 6px;\n}\n\n.field-label[hidden] {\n display: none;\n}\n\n.required-mark {\n color: var(--bv-color-danger);\n margin-left: 2px;\n}\n\n.container {\n display: flex;\n align-items: center;\n height: 44px;\n box-sizing: border-box;\n background: var(--bv-color-neutral-100);\n border: 1px solid var(--bv-color-neutral-200);\n border-radius: var(--bv-radius-lg);\n padding: 3px;\n gap: 2px;\n}\n\n:host([size='sm']) .container {\n height: var(--bv-control-height-md);\n border-radius: var(--bv-radius-md);\n padding: 3px;\n}\n\n.pill {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n padding: 0 var(--bv-spacing-md);\n border: none;\n border-radius: calc(var(--bv-radius-lg) - 3px);\n background: transparent;\n font-family: inherit;\n font-size: var(--bv-font-size-sm);\n font-weight: var(--bv-font-weight-medium);\n color: var(--bv-color-neutral-500);\n cursor: pointer;\n white-space: nowrap;\n user-select: none;\n transition:\n color var(--bv-transition-fast),\n background var(--bv-transition-fast),\n box-shadow var(--bv-transition-fast);\n}\n\n:host([size='sm']) .pill {\n padding: 0 var(--bv-spacing-md-minus);\n font-size: var(--bv-font-size-sm);\n}\n\n.pill:hover:not(.selected) {\n color: var(--bv-color-neutral-800);\n}\n\n.pill.selected {\n background: var(--bv-color-neutral-0);\n color: var(--bv-color-neutral-900);\n box-shadow: var(--bv-shadow-sm);\n}\n\n/* Filled variant \u2014 selected pill uses the primary color instead of white */\n:host([variant='primary']) .pill.selected {\n background: var(--bv-color-primary);\n color: var(--bv-color-neutral-0);\n}\n";
3
3
 
4
4
  // components/_shared/adopt-styles.ts
5
5
  var _sheetCache = /* @__PURE__ */ new Map();
@@ -21,7 +21,14 @@ function adoptStyles(shadow, css) {
21
21
 
22
22
  // components/bv-segmented-control/bv-segmented-control.ts
23
23
  var BvSegmentedControl = class extends HTMLElement {
24
- static observedAttributes = ["options", "value", "label", "required"];
24
+ static observedAttributes = [
25
+ "options",
26
+ "value",
27
+ "label",
28
+ "required",
29
+ "size",
30
+ "variant"
31
+ ];
25
32
  _rendered = false;
26
33
  _labelEl = null;
27
34
  _containerEl = null;
@@ -43,6 +50,18 @@ var BvSegmentedControl = class extends HTMLElement {
43
50
  if (v) this.setAttribute("required", "");
44
51
  else this.removeAttribute("required");
45
52
  }
53
+ get size() {
54
+ return this.getAttribute("size") ?? "md";
55
+ }
56
+ set size(v) {
57
+ this.setAttribute("size", v);
58
+ }
59
+ get variant() {
60
+ return this.getAttribute("variant") ?? "default";
61
+ }
62
+ set variant(v) {
63
+ this.setAttribute("variant", v);
64
+ }
46
65
  connectedCallback() {
47
66
  this._render();
48
67
  }
@@ -42,6 +42,13 @@ function bindDropdownController(host, options = {}) {
42
42
  if (options.canClose && !options.canClose(reason)) return;
43
43
  host.removeAttribute("open");
44
44
  syncExpanded();
45
+ host.dispatchEvent(
46
+ new CustomEvent("bv-dropdown-close", {
47
+ bubbles: true,
48
+ composed: true,
49
+ detail: { reason }
50
+ })
51
+ );
45
52
  (_a = options.onAfterClose) == null ? void 0 : _a.call(options);
46
53
  };
47
54
  const close = () => closeWithReason("programmatic");
@@ -172,9 +179,8 @@ var BvSelect = class extends HTMLElement {
172
179
  chevron.className = "trigger-chevron";
173
180
  chevron.innerHTML = CHEVRON_DOWN;
174
181
  trigger.append(triggerLabel, chevron);
175
- trigger.addEventListener("click", (e) => {
182
+ trigger.addEventListener("click", () => {
176
183
  var _a;
177
- e.stopPropagation();
178
184
  if (!this.hasAttribute("disabled")) (_a = this._dropdown) == null ? void 0 : _a.toggle();
179
185
  });
180
186
  const panel = document.createElement("div");
@@ -42,6 +42,13 @@ function bindDropdownController(host, options = {}) {
42
42
  if (options.canClose && !options.canClose(reason)) return;
43
43
  host.removeAttribute("open");
44
44
  syncExpanded();
45
+ host.dispatchEvent(
46
+ new CustomEvent("bv-dropdown-close", {
47
+ bubbles: true,
48
+ composed: true,
49
+ detail: { reason }
50
+ })
51
+ );
45
52
  (_a = options.onAfterClose) == null ? void 0 : _a.call(options);
46
53
  };
47
54
  const close = () => closeWithReason("programmatic");
@@ -229,9 +236,8 @@ var BvSplitButton = class extends HTMLElement {
229
236
  toggleBtn.setAttribute("aria-expanded", "false");
230
237
  toggleBtn.setAttribute("aria-label", "More options");
231
238
  toggleBtn.innerHTML = CHEVRON_DOWN;
232
- toggleBtn.addEventListener("click", (e) => {
239
+ toggleBtn.addEventListener("click", () => {
233
240
  var _a;
234
- e.stopPropagation();
235
241
  if (!this.disabled) (_a = this._dropdown) == null ? void 0 : _a.toggle();
236
242
  });
237
243
  this._toggleBtn = toggleBtn;
@@ -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';