@bluevolt-tech/lumen 2.3.1 → 2.6.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.
Files changed (50) hide show
  1. package/README.md +1 -1
  2. package/dist/components/_shared/attribute-dispatch.d.ts +44 -0
  3. package/dist/components/_shared/tree-selection.d.ts +67 -0
  4. package/dist/components/bv-alert-banner/bv-alert-banner.js +1 -1
  5. package/dist/components/bv-avatar/bv-avatar.js +1 -1
  6. package/dist/components/bv-badge/bv-badge.js +1 -1
  7. package/dist/components/bv-button/bv-button.js +8 -6
  8. package/dist/components/bv-category-pill/bv-category-pill.js +1 -1
  9. package/dist/components/bv-checkbox/bv-checkbox.d.ts +15 -1
  10. package/dist/components/bv-checkbox/bv-checkbox.js +50 -5
  11. package/dist/components/bv-chip/bv-chip.js +1 -1
  12. package/dist/components/bv-course-detail/bv-course-detail.js +2 -2
  13. package/dist/components/bv-course-detail-modal/bv-course-detail-modal.js +26 -22
  14. package/dist/components/bv-course-grid-card/bv-course-grid-card.js +28 -21
  15. package/dist/components/bv-course-list-card/bv-course-list-card.js +28 -21
  16. package/dist/components/bv-course-outline/bv-course-outline.js +14 -12
  17. package/dist/components/bv-date-range-input/bv-date-range-input.js +3 -3
  18. package/dist/components/bv-date-range-picker/bv-date-range-picker.js +2 -2
  19. package/dist/components/bv-empty-state/bv-empty-state.js +21 -15
  20. package/dist/components/bv-filter-dropdown/bv-filter-dropdown.js +1 -1
  21. package/dist/components/bv-filter-panel/bv-filter-panel.d.ts +14 -0
  22. package/dist/components/bv-filter-panel/bv-filter-panel.js +87 -6
  23. package/dist/components/bv-icon/bv-icon.js +9 -7
  24. package/dist/components/bv-input/bv-input.js +1 -1
  25. package/dist/components/bv-input-dropdown/bv-input-dropdown.d.ts +123 -1
  26. package/dist/components/bv-input-dropdown/bv-input-dropdown.js +418 -14
  27. package/dist/components/bv-modal/bv-modal.js +1 -1
  28. package/dist/components/bv-page-title/bv-page-title.js +1 -1
  29. package/dist/components/bv-pagination/bv-pagination.js +1 -1
  30. package/dist/components/bv-portal-header/bv-portal-header.js +43 -37
  31. package/dist/components/bv-portal-layout/bv-portal-layout.js +1 -1
  32. package/dist/components/bv-progress-bar/bv-progress-bar.js +1 -1
  33. package/dist/components/bv-promo-banner/bv-promo-banner.js +1 -1
  34. package/dist/components/bv-radio/bv-radio.js +1 -1
  35. package/dist/components/bv-search-picker/bv-search-picker.d.ts +59 -0
  36. package/dist/components/bv-search-picker/bv-search-picker.js +175 -23
  37. package/dist/components/bv-segmented-control/bv-segmented-control.d.ts +13 -0
  38. package/dist/components/bv-segmented-control/bv-segmented-control.js +1 -1
  39. package/dist/components/bv-select/bv-select.js +1 -1
  40. package/dist/components/bv-split-button/bv-split-button.js +8 -6
  41. package/dist/components/bv-star-rating/bv-star-rating.js +1 -1
  42. package/dist/components/bv-table/bv-table.d.ts +50 -0
  43. package/dist/components/bv-table/bv-table.js +275 -34
  44. package/dist/components/bv-toggle/bv-toggle.d.ts +15 -0
  45. package/dist/components/bv-toggle/bv-toggle.js +40 -3
  46. package/dist/components/bv-user-row/bv-user-row.js +22 -15
  47. package/dist/components/bv-view-switcher/bv-view-switcher.js +1 -1
  48. package/dist/themes/base.css +0 -5
  49. package/package.json +11 -8
  50. package/themes/base.css +0 -5
@@ -22,6 +22,21 @@ export declare class BvTable extends HTMLElement {
22
22
  private _mutationObserver;
23
23
  private _bodyObserver;
24
24
  private _headerObserver;
25
+ /**
26
+ * BTN-11914 — the `<thead>` the header observer currently watches for
27
+ * `characterData`. Held so the re-attach below can tell "already watching
28
+ * this one" from "the consumer swapped the header out", and so
29
+ * `disconnectedCallback` can drop it: `disconnect()` clears the
30
+ * registration, and a stale ref would make the re-attach skip itself.
31
+ */
32
+ private _observedThead;
33
+ /**
34
+ * BTN-11914 — dedicated to the `<thead>` `characterData` watch. Separate from
35
+ * `_headerObserver` so a replaced header can be dropped with `disconnect()`
36
+ * without discarding the host observer's queued childList records. See
37
+ * `_observeTheadCharacterData`.
38
+ */
39
+ private _theadObserver;
25
40
  get loading(): boolean;
26
41
  set loading(v: boolean);
27
42
  get fitViewport(): boolean;
@@ -31,6 +46,7 @@ export declare class BvTable extends HTMLElement {
31
46
  attributeChangedCallback(name: string, _old: string | null, val: string | null): void;
32
47
  private _renderSkeleton;
33
48
  private _removeSkeleton;
49
+ private _syncSkeletonLabel;
34
50
  private _setupDrag;
35
51
  private _teardownDrag;
36
52
  private _setupStickyColumns;
@@ -46,8 +62,42 @@ export declare class BvTable extends HTMLElement {
46
62
  private _isOverText;
47
63
  private _enhanceHeaders;
48
64
  private _enhanceOneHeader;
65
+ private _renderTooltipOnlyTh;
66
+ private _buildThContent;
49
67
  private _extractHeaderLabel;
50
68
  private _syncSortableTh;
51
69
  private _cycleSort;
52
70
  private _observeHeaderMutations;
71
+ /**
72
+ * The header observer's record handling, shared by both observers that feed
73
+ * it: the host-wide one above (childList + attributes) and the `<thead>`
74
+ * characterData one below. One body so a branch added to either cannot drift
75
+ * from the other.
76
+ */
77
+ private _handleHeaderMutations;
78
+ /**
79
+ * BTN-11914 — watches `characterData` on the current `<thead>`, and nothing
80
+ * else. Cheap to call repeatedly: it returns immediately while the same
81
+ * element is already being watched.
82
+ *
83
+ * `characterData` must keep reaching the header (BTN-11247): Angular's Ivy
84
+ * creates the header's text node empty and fills it on the update pass, which
85
+ * arrives as a characterData mutation, not a childList one.
86
+ *
87
+ * **Why its own MutationObserver instead of a second `observe()` on the
88
+ * header one.** A replaced `<thead>` has to stop being watched, or a consumer
89
+ * that keeps and mutates the detached subtree still reaches
90
+ * `_enhanceFilledHeader` — records for stale DOM, which is the narrow scope
91
+ * leaking back open (raised by Copilot on PR #50). There is no
92
+ * `unobserve(node)`, so the only way to drop one registration is
93
+ * `disconnect()`, and that also **discards records already queued and not yet
94
+ * delivered**. On the shared observer that would throw away pending childList
95
+ * and attribute records mid-batch — the first version of this fix accepted the
96
+ * stale registration for exactly that reason. A dedicated observer removes the
97
+ * trade entirely: the records it discards are the old `<thead>`'s, which are
98
+ * the ones being dropped on purpose.
99
+ */
100
+ private _observeTheadCharacterData;
101
+ private _enhanceFilledHeader;
102
+ private _enhanceAddedHeaders;
53
103
  }
@@ -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/* \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";
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\n/* Light DOM by design (the CSS is injected into document.head so it can reach\n nested th/td), so this is the element selector, not `:host`. The rule above\n sets `display: block` from the author origin, which beats the user agent's\n `[hidden] { display: none }` regardless of specificity. */\nbv-table[hidden] {\n display: none;\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";
@@ -156,8 +156,8 @@ var SORT_ASC = `<svg class="bv-sort-icon" viewBox="0 0 16 16" fill="none" xmlns=
156
156
  var SORT_DESC = `<svg class="bv-sort-icon" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
157
157
  <path d="M8 3v10M5 10l3 3 3-3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
158
158
  </svg>`;
159
- function escapeAttr(s) {
160
- return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
159
+ function normalizeLoadingLabel(label) {
160
+ return (label == null ? void 0 : label.trim()) || "Loading";
161
161
  }
162
162
  function injectTableStyles() {
163
163
  if (document.getElementById("bv-table-styles")) return;
@@ -172,6 +172,7 @@ var BvTable = class extends HTMLElement {
172
172
  "bordered",
173
173
  "loading",
174
174
  "loading-rows",
175
+ "loading-label",
175
176
  "fit-viewport"
176
177
  ];
177
178
  _isDragging = false;
@@ -185,6 +186,21 @@ var BvTable = class extends HTMLElement {
185
186
  _mutationObserver = null;
186
187
  _bodyObserver = null;
187
188
  _headerObserver = null;
189
+ /**
190
+ * BTN-11914 — the `<thead>` the header observer currently watches for
191
+ * `characterData`. Held so the re-attach below can tell "already watching
192
+ * this one" from "the consumer swapped the header out", and so
193
+ * `disconnectedCallback` can drop it: `disconnect()` clears the
194
+ * registration, and a stale ref would make the re-attach skip itself.
195
+ */
196
+ _observedThead = null;
197
+ /**
198
+ * BTN-11914 — dedicated to the `<thead>` `characterData` watch. Separate from
199
+ * `_headerObserver` so a replaced header can be dropped with `disconnect()`
200
+ * without discarding the host observer's queued childList records. See
201
+ * `_observeTheadCharacterData`.
202
+ */
203
+ _theadObserver = null;
188
204
  get loading() {
189
205
  return this.hasAttribute("loading");
190
206
  }
@@ -210,13 +226,16 @@ var BvTable = class extends HTMLElement {
210
226
  if (this.hasAttribute("loading")) this._renderSkeleton();
211
227
  }
212
228
  disconnectedCallback() {
213
- var _a, _b;
229
+ var _a, _b, _c;
214
230
  this._teardownDrag();
215
231
  this._teardownStickyColumns();
216
232
  (_a = this._bodyObserver) == null ? void 0 : _a.disconnect();
217
233
  this._bodyObserver = null;
218
234
  (_b = this._headerObserver) == null ? void 0 : _b.disconnect();
219
235
  this._headerObserver = null;
236
+ (_c = this._theadObserver) == null ? void 0 : _c.disconnect();
237
+ this._theadObserver = null;
238
+ this._observedThead = null;
220
239
  }
221
240
  attributeChangedCallback(name, _old, val) {
222
241
  if (!this.isConnected) return;
@@ -228,6 +247,7 @@ var BvTable = class extends HTMLElement {
228
247
  this._removeSkeleton();
229
248
  this._renderSkeleton();
230
249
  }
250
+ if (name === "loading-label") this._syncSkeletonLabel(val);
231
251
  if (name === "fit-viewport") {
232
252
  this._teardownStickyColumns();
233
253
  this._setupStickyColumns();
@@ -245,7 +265,7 @@ var BvTable = class extends HTMLElement {
245
265
  skeleton.className = "bv-table-skeleton";
246
266
  skeleton.setAttribute("role", "status");
247
267
  skeleton.setAttribute("aria-live", "polite");
248
- skeleton.setAttribute("aria-label", this.getAttribute("loading-label") ?? "Loading");
268
+ skeleton.setAttribute("aria-label", normalizeLoadingLabel(this.getAttribute("loading-label")));
249
269
  for (let r = 0; r < rows; r++) {
250
270
  const row = document.createElement("div");
251
271
  row.className = "bv-table-skeleton-row";
@@ -262,6 +282,19 @@ var BvTable = class extends HTMLElement {
262
282
  var _a;
263
283
  (_a = this.querySelector(":scope > .bv-table-skeleton")) == null ? void 0 : _a.remove();
264
284
  }
285
+ // BTN-11343 — patches the skeleton's accessible name in place. Extracted
286
+ // rather than inlined in `attributeChangedCallback` for the same reason
287
+ // `_renderSkeleton` and `_removeSkeleton` are: `wc/no-child-traversal-in-
288
+ // attributechangedcallback` rejects traversing children from there, and the
289
+ // rule is right — the skeleton only exists while loading, so the caller
290
+ // cannot assume it is there. No-op when it is absent.
291
+ _syncSkeletonLabel(label) {
292
+ var _a;
293
+ (_a = this.querySelector(":scope > .bv-table-skeleton")) == null ? void 0 : _a.setAttribute(
294
+ "aria-label",
295
+ normalizeLoadingLabel(label)
296
+ );
297
+ }
265
298
  _setupDrag() {
266
299
  this.addEventListener("mousedown", this._boundMouseDown);
267
300
  window.addEventListener("mousemove", this._boundMouseMove);
@@ -367,10 +400,22 @@ var BvTable = class extends HTMLElement {
367
400
  if (td.children.length > 0) return;
368
401
  const text = td.textContent;
369
402
  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);
403
+ const children = Array.from(td.childNodes);
404
+ let run = [];
405
+ const wrapRun = () => {
406
+ if (run.length === 0) return;
407
+ const span = document.createElement("span");
408
+ span.className = "bv-cell-text";
409
+ span.textContent = run.map((n) => n.textContent ?? "").join("");
410
+ td.insertBefore(span, run[0] ?? null);
411
+ for (const n of run) td.removeChild(n);
412
+ run = [];
413
+ };
414
+ for (const node of children) {
415
+ if (node.nodeType === Node.TEXT_NODE) run.push(node);
416
+ else wrapRun();
417
+ }
418
+ wrapRun();
374
419
  }
375
420
  // Watches the host for TDs materialized after the initial `_enhanceBodyCells`
376
421
  // pass — Angular *ngFor, React lists, or any consumer that mounts <bv-table>
@@ -436,14 +481,15 @@ var BvTable = class extends HTMLElement {
436
481
  const hasTooltip = th.hasAttribute("data-tooltip");
437
482
  if (!hasSortable && !hasTooltip) return;
438
483
  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
- );
484
+ if (!th.dataset["bvColumnWarned"]) {
485
+ th.dataset["bvColumnWarned"] = "true";
486
+ console.warn(
487
+ "[bv-table] <th data-sortable> requires data-column; skipping sort enhancement for:",
488
+ th
489
+ );
490
+ }
443
491
  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>`;
492
+ this._renderTooltipOnlyTh(th);
447
493
  return;
448
494
  }
449
495
  if (hasSortable) {
@@ -464,10 +510,57 @@ var BvTable = class extends HTMLElement {
464
510
  th.setAttribute("tabindex", "0");
465
511
  this._syncSortableTh(th);
466
512
  } 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>`;
513
+ this._renderTooltipOnlyTh(th);
514
+ }
515
+ }
516
+ // BTN-11212 — tooltip-only header render. Extracted from `_enhanceOneHeader`
517
+ // because two call sites need it and both are now re-entrant: the header
518
+ // observer watches childList, so this runs again every time the consumer's
519
+ // framework mutates the header's children.
520
+ //
521
+ // Two guards keep the re-entry harmless:
522
+ // 1. An already-enhanced th short-circuits on the bv-tooltip probe, which
523
+ // preserves the single-shot property the previous inline version had.
524
+ // 2. An empty label means the framework has created the th but has not
525
+ // written its text yet — rewriting now would delete the text node it
526
+ // is about to fill. Skip; the childList branch of the observer calls
527
+ // back once the text lands.
528
+ _renderTooltipOnlyTh(th) {
529
+ if (th.querySelector("bv-tooltip")) return;
530
+ const label = this._extractHeaderLabel(th);
531
+ if (!label) return;
532
+ this._buildThContent(th, label);
533
+ }
534
+ // Builds the `.bv-th-content` wrapper without ever interpolating consumer
535
+ // text into markup (Copilot review). The label arrives from
536
+ // `_extractHeaderLabel`, i.e. from `textContent` — decoded text, not
537
+ // markup — so interpolating it into `innerHTML` re-parsed it: a header
538
+ // whose label was the plain string "<img src=x onerror=…>" became a real
539
+ // <img> element. Reachable in practice, because a header label is routinely
540
+ // data (a category name, an admin-configured column title) rendered through
541
+ // a framework binding.
542
+ //
543
+ // The label goes in as a text node and the tooltip text through
544
+ // `setAttribute`, so neither can be parsed as HTML and neither needs
545
+ // escaping. `icon` is the only raw markup and it is always one of the three
546
+ // module-level SVG constants — never anything a consumer supplies.
547
+ //
548
+ // `replaceChildren` reports the wrapper as the single added node, same as
549
+ // the `innerHTML` write it replaces, so the observer's re-entry guard is
550
+ // unaffected.
551
+ _buildThContent(th, label, icon) {
552
+ const content = document.createElement("span");
553
+ content.className = "bv-th-content";
554
+ content.appendChild(document.createTextNode(label));
555
+ const tip = th.getAttribute("data-tooltip");
556
+ if (tip !== null) {
557
+ const tooltip = document.createElement("bv-tooltip");
558
+ tooltip.setAttribute("text", tip);
559
+ tooltip.setAttribute("position", "bottom");
560
+ content.appendChild(tooltip);
470
561
  }
562
+ if (icon) content.insertAdjacentHTML("beforeend", icon);
563
+ th.replaceChildren(content);
471
564
  }
472
565
  // Reads the plain-text label from a th, whether it's still raw markup
473
566
  // (initial enhance pass) or has already been wrapped in `.bv-th-content`
@@ -489,14 +582,14 @@ var BvTable = class extends HTMLElement {
489
582
  // `_headerObserver` (to reflect a consumer's external data-sort write).
490
583
  _syncSortableTh(th) {
491
584
  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
585
  th.setAttribute(
497
586
  "aria-sort",
498
587
  sortDir === "asc" ? "ascending" : sortDir === "desc" ? "descending" : "none"
499
588
  );
589
+ const label = this._extractHeaderLabel(th);
590
+ if (!label) return;
591
+ const icon = sortDir === "asc" ? SORT_ASC : sortDir === "desc" ? SORT_DESC : SORT_NONE;
592
+ this._buildThContent(th, label, icon);
500
593
  }
501
594
  // BTN-10755 — click and Enter/Space both route through here. Cycles the
502
595
  // tri-state, clears sibling sortable ths (UX for user interaction — the
@@ -537,25 +630,173 @@ var BvTable = class extends HTMLElement {
537
630
  // runs the initial enhance for that th.
538
631
  // The observer never clears siblings — sibling sanitation is UX for the
539
632
  // user-driven click / keydown paths.
633
+ //
634
+ // BTN-11212 — the observer also watches `childList`. Before that, headers
635
+ // were enhanced from `connectedCallback` and nowhere else, which never runs
636
+ // against real headers for a framework that appends <bv-table> before
637
+ // creating its children (Angular does). The initial pass saw an empty
638
+ // <thead>, and a <th> appended afterwards — already carrying data-sortable
639
+ // or data-tooltip — is a childList mutation the attribute filter cannot
640
+ // see. Tooltips therefore never rendered at all, and sortable headers were
641
+ // enhanced only by the accident of a later `data-sort` write, at a moment
642
+ // when their text had not been written yet. `_bodyObserver` has had the
643
+ // equivalent childList branch for <td> all along; this is the same thing
644
+ // for <th>.
540
645
  _observeHeaderMutations() {
541
646
  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
- }
647
+ this._observeTheadCharacterData();
648
+ this._handleHeaderMutations(mutations);
553
649
  });
554
650
  this._headerObserver.observe(this, {
555
651
  attributes: true,
652
+ childList: true,
556
653
  subtree: true,
557
654
  attributeFilter: ["data-sort", "data-sortable"]
558
655
  });
656
+ this._observeTheadCharacterData();
657
+ }
658
+ /**
659
+ * The header observer's record handling, shared by both observers that feed
660
+ * it: the host-wide one above (childList + attributes) and the `<thead>`
661
+ * characterData one below. One body so a branch added to either cannot drift
662
+ * from the other.
663
+ */
664
+ _handleHeaderMutations(mutations) {
665
+ for (const m of mutations) {
666
+ if (m.target.nodeType === Node.COMMENT_NODE) continue;
667
+ if (m.type === "childList") {
668
+ this._enhanceAddedHeaders(m);
669
+ continue;
670
+ }
671
+ if (m.type === "characterData") {
672
+ this._enhanceFilledHeader(m);
673
+ continue;
674
+ }
675
+ if (m.type !== "attributes") continue;
676
+ const target = m.target;
677
+ if (!(target instanceof HTMLElement)) continue;
678
+ if (target.tagName !== "TH" || !target.closest("thead")) continue;
679
+ if (m.attributeName === "data-sort") {
680
+ if (target.hasAttribute("data-sortable")) this._syncSortableTh(target);
681
+ } else if (m.attributeName === "data-sortable") {
682
+ if (target.hasAttribute("data-sortable")) this._enhanceOneHeader(target);
683
+ }
684
+ }
685
+ }
686
+ /**
687
+ * BTN-11914 — watches `characterData` on the current `<thead>`, and nothing
688
+ * else. Cheap to call repeatedly: it returns immediately while the same
689
+ * element is already being watched.
690
+ *
691
+ * `characterData` must keep reaching the header (BTN-11247): Angular's Ivy
692
+ * creates the header's text node empty and fills it on the update pass, which
693
+ * arrives as a characterData mutation, not a childList one.
694
+ *
695
+ * **Why its own MutationObserver instead of a second `observe()` on the
696
+ * header one.** A replaced `<thead>` has to stop being watched, or a consumer
697
+ * that keeps and mutates the detached subtree still reaches
698
+ * `_enhanceFilledHeader` — records for stale DOM, which is the narrow scope
699
+ * leaking back open (raised by Copilot on PR #50). There is no
700
+ * `unobserve(node)`, so the only way to drop one registration is
701
+ * `disconnect()`, and that also **discards records already queued and not yet
702
+ * delivered**. On the shared observer that would throw away pending childList
703
+ * and attribute records mid-batch — the first version of this fix accepted the
704
+ * stale registration for exactly that reason. A dedicated observer removes the
705
+ * trade entirely: the records it discards are the old `<thead>`'s, which are
706
+ * the ones being dropped on purpose.
707
+ */
708
+ _observeTheadCharacterData() {
709
+ const thead = this.querySelector("thead");
710
+ if (thead === this._observedThead) return;
711
+ this._observedThead = thead;
712
+ if (!this._theadObserver) {
713
+ this._theadObserver = new MutationObserver(
714
+ (mutations) => this._handleHeaderMutations(mutations)
715
+ );
716
+ }
717
+ this._theadObserver.disconnect();
718
+ if (thead) this._theadObserver.observe(thead, { characterData: true, subtree: true });
719
+ }
720
+ // BTN-11247 — characterData half of the header observer. BTN-11212 covered
721
+ // the two childList shapes (a <th> appended, and a text node appended into
722
+ // a <th>) and assumed those were all the ways a label can arrive late.
723
+ // They are not: Angular's Ivy creates the header's text node **empty**
724
+ // during the creation pass and fills it during the update pass. Filling an
725
+ // existing text node is neither of those shapes — it is a characterData
726
+ // mutation, and nothing was listening.
727
+ //
728
+ // The consequence was not a missing observer callback but a half-rendered
729
+ // header, which is why it read as working: `_enhanceOneHeader` did run off
730
+ // the empty-text-node append, wired the listeners and set tabindex /
731
+ // aria-sort, then returned on the empty-label guard exactly as intended.
732
+ // Nothing called it back, so sorting ended up fully accessible and
733
+ // completely invisible, and tooltip-only headers rendered nothing at all.
734
+ //
735
+ // Re-entry is safe for the same two reasons the childList branch documents
736
+ // above, and the second one holds here specifically: `_buildThContent`
737
+ // writes a **new** text node via `replaceChildren`, which the observer
738
+ // reports as a childList mutation on the wrapper — never characterData on
739
+ // a <th>'s own child. So our own writes cannot reach this branch at all.
740
+ //
741
+ // BTN-11850 — that last sentence is true and was not enough, which is the
742
+ // part worth carrying forward. It reasons about the nodes *we* write, and
743
+ // the loop that shipped came from nodes we do not write: a framework's own
744
+ // children living inside the same <th>, mutated by that framework, landing
745
+ // here, and being destroyed by the rebuild this function triggers — so the
746
+ // framework wrote them again. The feedback edge does not need our writes to
747
+ // reach this branch; it only needs our writes to disturb something whose
748
+ // owner will restore it.
749
+ //
750
+ // The guard for that specific shape is in `_observeHeaderMutations`, which
751
+ // drops comment targets before this function is reached. The general rule
752
+ // it stands for: an enhancer that rebuilds a subtree it does not fully own
753
+ // must be judged on what else lives in that subtree, not only on what it
754
+ // writes into it.
755
+ _enhanceFilledHeader(m) {
756
+ var _a;
757
+ const el = (_a = m.target.parentElement) == null ? void 0 : _a.closest("th, tbody");
758
+ if (!(el instanceof HTMLElement) || el.tagName !== "TH") return;
759
+ if (el.closest("thead")) this._enhanceOneHeader(el);
760
+ }
761
+ // BTN-11212 — childList half of the header observer. Covers the two ways a
762
+ // header can materialize as a *structural* change: the <th> itself being
763
+ // appended (directly, or as part of a <thead> / <table> subtree), and the
764
+ // label text landing in a <th> that was appended empty. The third way — an
765
+ // already-appended empty text node being filled in place — is not a
766
+ // childList mutation at all; see `_enhanceFilledHeader` (BTN-11247).
767
+ //
768
+ // This runs inside the subtree the enhancers rewrite, so re-entry matters.
769
+ // Two independent things make it safe, in order of strength:
770
+ //
771
+ // 1. Both render paths are idempotent. `_syncSortableTh` re-reads the
772
+ // label out of the `.bv-th-content` it wrote and produces the same
773
+ // markup; `_renderTooltipOnlyTh` short-circuits on the bv-tooltip
774
+ // probe. So the worst case of an unexpected re-entry is one redundant
775
+ // rewrite that converges — not a loop. This is the guarantee to
776
+ // preserve if the rewrite below is ever changed, and it is pinned by
777
+ // the "stray text appended to an already-enhanced th" test.
778
+ // 2. In practice re-entry does not happen for our own writes at all:
779
+ // `_buildThContent`'s `replaceChildren` reports the `.bv-th-content`
780
+ // wrapper as the added node, never a bare text node with a <th> parent
781
+ // and never a <th>, so both branches below skip it.
782
+ //
783
+ // Point 2 is an observation about what MutationObserver reports; point 1 is
784
+ // structural. Do not let a future rewrite rely on point 2 alone.
785
+ _enhanceAddedHeaders(m) {
786
+ m.addedNodes.forEach((node) => {
787
+ if (node.nodeType === Node.TEXT_NODE) {
788
+ const parent = node.parentElement;
789
+ if ((parent == null ? void 0 : parent.tagName) === "TH" && parent.closest("thead")) this._enhanceOneHeader(parent);
790
+ return;
791
+ }
792
+ if (!(node instanceof HTMLElement)) return;
793
+ if (node.classList.contains("bv-th-content")) return;
794
+ if (node.closest("tbody")) return;
795
+ if (node.matches("th") && node.closest("thead")) this._enhanceOneHeader(node);
796
+ node.querySelectorAll("th").forEach((th) => {
797
+ if (th.closest("thead")) this._enhanceOneHeader(th);
798
+ });
799
+ });
559
800
  }
560
801
  };
561
802
  if (!customElements.get("bv-table")) {
@@ -2,11 +2,26 @@ export type ToggleSize = 'sm' | 'md';
2
2
  export declare class BvToggle extends HTMLElement {
3
3
  static readonly observedAttributes: readonly ["checked", "disabled", "label", "size"];
4
4
  private _rendered;
5
+ private _wrapEl;
5
6
  private _labelEl;
6
7
  constructor();
7
8
  connectedCallback(): void;
8
9
  attributeChangedCallback(name: string, _old: string | null, val: string | null): void;
9
10
  get checked(): boolean;
10
11
  set checked(v: boolean);
12
+ /**
13
+ * Focus the toggle. The focusable element is the internal `.wrap`, not the
14
+ * host, so `hostEl.focus()` would otherwise be a silent no-op. Consumers that
15
+ * rebuild the panel a toggle lives in (bv-input-dropdown's `toggle-label`,
16
+ * bv-search-picker's `inactive-toggle-label`) need this to restore focus
17
+ * after the rebuild without reaching into this component's shadow root.
18
+ *
19
+ * No-op before the first render, and when disabled — `tabindex="-1"` only
20
+ * removes the element from Tab order, it does not block programmatic focus,
21
+ * so a restore pass could otherwise land focus on a control that was
22
+ * disabled mid-rebuild.
23
+ */
24
+ focus(options?: FocusOptions): void;
11
25
  private _render;
26
+ private _syncAria;
12
27
  }