bitboss-ui 2.1.128 → 2.1.129

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.
@@ -84,11 +84,18 @@
84
84
  </slot>
85
85
  </th>
86
86
  <th
87
- v-for="header in mappedHeaders"
87
+ v-for="(header, index) in mappedHeaders"
88
88
  :key="header.key"
89
89
  :aria-sort="header.sortDirection"
90
90
  class="bb-table-header"
91
- :class="header.classes"
91
+ :class="[
92
+ header.classes,
93
+ {
94
+ 'bb-table-header--edge-start': index === 0,
95
+ 'bb-table-header--edge-end':
96
+ index === mappedHeaders.length - 1,
97
+ },
98
+ ]"
92
99
  scope="col"
93
100
  >
94
101
  <!-- @vue-ignore -->
@@ -307,10 +314,17 @@
307
314
  </slot>
308
315
  </td>
309
316
  <td
310
- v-for="col in item.cols"
317
+ v-for="(col, colIndex) in item.cols"
311
318
  :key="col.key"
312
319
  class="bb-table-data__cell"
313
- :class="col.classes"
320
+ :class="[
321
+ col.classes,
322
+ {
323
+ 'bb-table-data__cell--edge-start': colIndex === 0,
324
+ 'bb-table-data__cell--edge-end':
325
+ colIndex === item.cols.length - 1,
326
+ },
327
+ ]"
314
328
  >
315
329
  <!-- @vue-ignore -->
316
330
  <slot
@@ -509,6 +523,10 @@ const {
509
523
  } = useTableWidthContext({
510
524
  id: () => props.id,
511
525
  container: () => containerRef.value,
526
+ inheritFrom: () =>
527
+ typeof props.inheritColumnWidths === 'string'
528
+ ? props.inheritColumnWidths
529
+ : undefined,
512
530
  });
513
531
 
514
532
  const inheritColumnWidths = computed(() => props.inheritColumnWidths !== false);
@@ -533,10 +551,13 @@ const fixedLayout = computed(
533
551
  * parent. Tables with the same column count already sum to the full width, so
534
552
  * this is a no-op for them and only narrows tables with fewer columns.
535
553
  */
536
- const sizeToColumns = computed(
554
+ /** True for a nested table that inherits its column widths from a parent. */
555
+ const isInheriting = computed(
537
556
  () => inheritColumnWidths.value && !!parentId.value
538
557
  );
539
558
 
559
+ const sizeToColumns = isInheriting;
560
+
540
561
  /** Numbers and bare numeric strings (e.g. `400`, `'400'`) default to px; values
541
562
  * that already carry a unit/keyword (`'50%'`, `'10rem'`, `'auto'`) pass through. */
542
563
  const toLength = (width: number | string) => {
@@ -637,25 +658,95 @@ const columnIntervals = computed(() => {
637
658
  });
638
659
  });
639
660
 
661
+ /**
662
+ * A column whose interval begins at or beyond the parent's last data cell has no
663
+ * matching parent column to inherit from — the table has more columns than its
664
+ * parent. Such columns are sized to their own measured content and scroll within
665
+ * the expand row instead of inheriting a track.
666
+ */
667
+ const isOverflowColumn = (interval: { start: number }) =>
668
+ isInheriting.value && interval.start >= dataEndCell.value;
669
+
670
+ /** True when any column overflows the parent (more columns than the parent). */
671
+ const overflowsParent = computed(() =>
672
+ columnIntervals.value.some(isOverflowColumn)
673
+ );
674
+
675
+ /**
676
+ * A "flex" column shares a root table's leftover space (min-content floor)
677
+ * proportionally via the probe. Only root tables flex this way; an inheriting
678
+ * table instead snaps its in-range columns to the parent, content-sizes its
679
+ * overflow columns, and lets a single fill column take up any slack.
680
+ */
681
+ const isFlexColumn = (index: number) =>
682
+ props.columns[index]?.width == null && !isInheriting.value;
683
+
684
+ /**
685
+ * The last column of an inheriting table whose width is left undefined — neither
686
+ * a `width` nor a `snap` (a `snap` *is* a width definition: the track range it
687
+ * spans). That column absorbs the leftover space so the table fills its parent
688
+ * when a frozen/snapped column narrower than the parent's would otherwise leave a
689
+ * gap. `-1` for a root table, or when every column defines its own width.
690
+ */
691
+ const fillColumnIndex = computed(() => {
692
+ if (!isInheriting.value) return -1;
693
+ for (let i = props.columns.length - 1; i >= 0; i--) {
694
+ const column = props.columns[i];
695
+ if (column.width == null && column.snap == null) return i;
696
+ }
697
+ return -1;
698
+ });
699
+
640
700
  // Parent columns left of the first column (e.g. a select this table doesn't
641
701
  // render, or columns skipped by the first snap) and those right of the last are
642
702
  // folded into the edge columns as the structural offset, keeping the table
643
703
  // aligned to the parent. `offsetStartValue` becomes the first column's padding.
644
704
  const offsetStartValue = computed(() => {
645
705
  const first = columnIntervals.value[0];
646
- if (!inheritColumnWidths.value || !first) return null;
647
- // Skip the parent's select cell only when this table renders its own.
648
- const lead = props.selectable && firstDataCell.value === 1 ? 1 : 0;
649
- return gridRangeWidth(lead, first.start);
706
+ // No parent to align to (a root table) means no offset at all — without this
707
+ // the structural reclaim below would still fold a negative `-ownSelect` in.
708
+ if (!inheritColumnWidths.value || !parentId.value || !first) return null;
709
+ // The fill runs from the parent's left edge to this table's first data
710
+ // column — the parent's select cell, if any, plus any skipped data columns.
711
+ const head = gridRangeWidth(0, first.start);
712
+ // This table's own select column sits flush at the left edge and takes its
713
+ // measured width, so reclaim it from the fill (mirror of the actions side):
714
+ // the first data column then lines the *next* column up flush with the
715
+ // parent. When the parent has a select cell it is part of `head`, so only the
716
+ // surplus over it is reclaimed; when the parent has none the whole select
717
+ // width comes out of the first column, which shrinks to make room for it.
718
+ const ownSelect = props.selectable ? structuralFit.select : null;
719
+ if (!ownSelect) return head;
720
+ return `calc(${head ?? '0px'} - ${ownSelect})`;
650
721
  });
651
722
  const offsetEndValue = computed(() => {
652
723
  const intervals = columnIntervals.value;
653
724
  const last = intervals[intervals.length - 1];
654
- if (!inheritColumnWidths.value || !last) return null;
655
- const hasActions = 'actions' in (parentNode.value?.tracks ?? {});
656
- const end =
657
- props.actions && hasActions ? tableEndCell.value - 1 : tableEndCell.value;
658
- return gridRangeWidth(last.end, end);
725
+ // No parent to align to (a root table) means no offset at all — without this
726
+ // the structural reclaim below would still fold a negative `-ownActions` in.
727
+ if (!inheritColumnWidths.value || !parentId.value || !last) return null;
728
+ // With more columns than the parent, the table runs past the parent's right
729
+ // edge: there is no trailing parent region to fold, and the last column is a
730
+ // content-sized overflow column, so there is no end fill.
731
+ if (overflowsParent.value) return null;
732
+ // When the last column is the (auto) fill column it already absorbs the whole
733
+ // trailing region via fixed layout, so folding the offset in too would double
734
+ // it — and skew its right-aligned content inward.
735
+ if (fillColumnIndex.value === props.columns.length - 1) return null;
736
+ // The fill runs from the last data column's end all the way to the parent's
737
+ // right edge — any unmapped data columns plus the parent's actions cell, if
738
+ // it has one.
739
+ const tail = gridRangeWidth(last.end, tableEndCell.value);
740
+ // This table's own actions column sits flush against that right edge and
741
+ // takes its measured content width, so reclaim it from the fill: the table
742
+ // then ends exactly at the parent's edge instead of overflowing by the
743
+ // actions width. When the parent has an actions cell it is part of `tail`, so
744
+ // only the surplus over it is effectively reclaimed; when the parent has none
745
+ // the whole actions width comes out of the data fill. The result may be
746
+ // negative — the last data column then shrinks to make room for the actions.
747
+ const ownActions = props.actions ? structuralFit.actions : null;
748
+ if (!ownActions) return tail;
749
+ return `calc(${tail ?? '0px'} - ${ownActions})`;
659
750
  });
660
751
  const offsetStartActive = computed(() => offsetStartValue.value !== null);
661
752
  const offsetEndActive = computed(() => offsetEndValue.value !== null);
@@ -695,18 +786,33 @@ const columnTracks = computed(() => {
695
786
  props.columns.forEach((column, index) => {
696
787
  const key = String(index);
697
788
  const frozen = column.width != null ? toLength(column.width) : null;
698
- // For auto columns in fixed layout, use the proportional width derived
699
- // from the offscreen probe so each column is pinned to a precise value
700
- // rather than relying on equal-share distribution (which ignores natural
701
- // content widths and can under-allocate to wider auto columns).
702
- const probeWidth =
703
- frozen == null && fixedLayout.value
704
- ? (probeColWidths.value[index] ?? null)
705
- : null;
706
- // The column's own width is the gridline range it spans of the parent.
707
789
  const interval = columnIntervals.value[index];
708
- const span =
709
- frozen ?? probeWidth ?? gridRangeWidth(interval.start, interval.end);
790
+
791
+ // The fill column is left `auto` so fixed layout (the inheriting table has
792
+ // a definite `width: 100%`) hands it the leftover space — its own inherited
793
+ // track plus the folded edge region plus any deficit. Skipped when the
794
+ // table overflows: there is no leftover then, and it must content-size.
795
+ if (index === fillColumnIndex.value && !overflowsParent.value) {
796
+ tracks.push({ key, width: 'auto' });
797
+ return;
798
+ }
799
+
800
+ let span: string | null;
801
+ if (frozen != null) {
802
+ span = frozen;
803
+ } else if (isFlexColumn(index)) {
804
+ // Root table auto column: a precise share of the leftover space from the
805
+ // proportional probe rather than fixed layout's equal split. `auto` is
806
+ // the fallback before the probe runs.
807
+ span = (fixedLayout.value && probeColWidths.value[index]) || null;
808
+ } else if (isOverflowColumn(interval)) {
809
+ // Inheriting table, past the parent's last column: no track to snap to,
810
+ // so size to its own content and let the table overflow (and scroll).
811
+ span = columnMinWidths.value[index] ?? null;
812
+ } else {
813
+ // In range: snap to the parent's matching column.
814
+ span = gridRangeWidth(interval.start, interval.end);
815
+ }
710
816
 
711
817
  const offsets: string[] = [];
712
818
  if (index === 0 && offsetStartActive.value) {
@@ -758,6 +864,10 @@ const containerStyle = computed(() => {
758
864
  vars['--bb-table-natural-width'] = naturalTotalWidth.value;
759
865
  }
760
866
 
867
+ if (fillValue.value > 0) {
868
+ vars['--bb-table-fill'] = `${fillValue.value}px`;
869
+ }
870
+
761
871
  return vars;
762
872
  });
763
873
 
@@ -794,10 +904,19 @@ const measureStructuralFit = () => {
794
904
  const fit = (selector: string) => {
795
905
  let max = 0;
796
906
  table.querySelectorAll<HTMLElement>(selector).forEach((cell) => {
797
- let content = 0;
907
+ // Combined horizontal extent of the children, not the widest one: an
908
+ // actions cell holds several side-by-side controls whose total width is
909
+ // what the column must fit. Measured from edges so it stays correct even
910
+ // when the cell is narrower than its content (controls overflow on
911
+ // `nowrap`), which keeps the pin-to-content convergence stable.
912
+ let left = Infinity;
913
+ let right = -Infinity;
798
914
  cell.querySelectorAll<HTMLElement>(':scope > *').forEach((child) => {
799
- content = Math.max(content, child.getBoundingClientRect().width);
915
+ const rect = child.getBoundingClientRect();
916
+ left = Math.min(left, rect.left);
917
+ right = Math.max(right, rect.right);
800
918
  });
919
+ const content = right > left ? right - left : 0;
801
920
  const style = getComputedStyle(cell);
802
921
  const padding =
803
922
  parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
@@ -806,11 +925,18 @@ const measureStructuralFit = () => {
806
925
  return max > 0 ? `${Math.ceil(max)}px` : null;
807
926
  };
808
927
 
928
+ // Scoped to this table's own header/body rows: an unscoped descendant query
929
+ // would also match the structural cells of a nested table living in an expand
930
+ // row, letting a child's actions/select width hijack this table's column.
809
931
  structuralFit.select = props.selectable
810
- ? fit('.bb-table-header--select, .bb-table-data__cell--select')
932
+ ? fit(
933
+ ':scope > thead > tr > th.bb-table-header--select, :scope > tbody > tr > td.bb-table-data__cell--select'
934
+ )
811
935
  : null;
812
936
  structuralFit.actions = props.actions
813
- ? fit('.bb-table-header--actions, .bb-table__cell--actions')
937
+ ? fit(
938
+ ':scope > thead > tr > th.bb-table-header--actions, :scope > tbody > tr > td.bb-table__cell--actions'
939
+ )
814
940
  : null;
815
941
  };
816
942
 
@@ -824,14 +950,17 @@ const measure = () => {
824
950
  * data column (`null` for frozen columns — they keep their declared width).
825
951
  *
826
952
  * For each auto column the formula is:
827
- * width = max(natural, available × (natural / autoTotal))
953
+ * width = max(minimum, available × (minimum / minimumTotal))
828
954
  *
829
- * where `available` = containerWidth frozenColumnWidths structuralColumnWidths
830
- * and `autoTotal` = sum of auto-column natural widths.
955
+ * where `minimum` is the column's `min-content` width (what it shrinks to in a
956
+ * very narrow container, i.e. the floor below which content would be clipped),
957
+ * `available` = containerWidth − frozenColumnWidths − structuralColumnWidths
958
+ * and `minimumTotal` = sum of the auto columns' minimums.
831
959
  *
832
- * This gives proportional growth when the container is wide (auto columns scale
833
- * relative to each other) and a hard natural-width floor when it is narrow (the
834
- * table-level `min-width` then triggers horizontal overflow instead of squishing).
960
+ * So when the container has room (`available minimumTotal`) the slack is shared
961
+ * out proportionally to each column's minimum, scaling them up together; when it
962
+ * does not, every column drops to its minimum and the table-level `min-width`
963
+ * lets it overflow (the container scrolls) rather than squashing content further.
835
964
  *
836
965
  * Re-measured after any DOM mutation (debounced 200 ms) and after any container
837
966
  * resize (via a dedicated ResizeObserver on the container element).
@@ -839,17 +968,38 @@ const measure = () => {
839
968
  const probeColWidths = ref<(string | null)[]>([]);
840
969
 
841
970
  /**
842
- * Total natural minimum width of the table (all columns, including structural).
843
- * Published as `--bb-table-natural-width` and used as `min-width` on the
844
- * `<table>` element so fixed-layout compression never squeezes content below its
845
- * natural size.
971
+ * Each unfrozen column's measured `min-content` width (`null` for frozen). Used
972
+ * to content-size the overflow columns of an inheriting table — those past the
973
+ * parent's last column, with no track to snap to.
974
+ */
975
+ const columnMinWidths = ref<(string | null)[]>([]);
976
+
977
+ /**
978
+ * Leftover px the inheriting fill column adds so the table reaches its parent's
979
+ * width. Published as `--bb-table-fill`; `0` when the table already fills or
980
+ * overflows. Derived self-correctingly from the live total (see the probe).
981
+ */
982
+ const fillValue = ref(0);
983
+
984
+ /**
985
+ * Total minimum width of the table (all columns at their `min-content`, plus the
986
+ * structural columns). Published as `--bb-table-natural-width` and used as
987
+ * `min-width` on the `<table>` so the table scrolls horizontally only once the
988
+ * container is narrower than this true minimum.
846
989
  */
847
990
  const naturalTotalWidth = ref<string | null>(null);
848
991
 
849
992
  const runNaturalWidthProbe = () => {
850
993
  const container = containerRef.value;
851
994
  if (!container || !fixedLayout.value) return;
852
- if (!props.columns.some((col) => col.width != null)) return;
995
+ // Runs for a frozen-column table (to share space), or any inheriting table
996
+ // (to content-size overflow columns and drive the fill column's slack).
997
+ if (
998
+ !props.columns.some((col) => col.width != null) &&
999
+ !overflowsParent.value &&
1000
+ fillColumnIndex.value < 0
1001
+ )
1002
+ return;
853
1003
 
854
1004
  const containerWidth = container.offsetWidth;
855
1005
  const dataOffset = props.selectable ? 1 : 0;
@@ -863,22 +1013,26 @@ const runNaturalWidthProbe = () => {
863
1013
  ? parseFloat(structuralFit.actions)
864
1014
  : 0;
865
1015
 
866
- // Frozen column widths from the live fixed-layout table getBoundingClientRect
867
- // returns the exact px value the browser computed from the declared CSS length,
868
- // so no unit parsing is needed and viewport-relative units work correctly.
1016
+ // Live rendered widths from the fixed-layout table. `frozenTotal` (declared
1017
+ // widths) feeds the stable min-width floor; `nonFlexTotal` (frozen + inherited)
1018
+ // is what the flex columns share the remainder of. The inherited part is only
1019
+ // used for that `available` figure — never fed back into min-width, since those
1020
+ // widths stretch with the table and would otherwise lock min-width above
1021
+ // max-content and force a spurious uniform stretch on every column.
869
1022
  const realThs = headerCells();
870
1023
  let frozenTotal = 0;
1024
+ let nonFlexTotal = 0;
871
1025
  props.columns.forEach((col, i) => {
872
- if (col.width != null) {
873
- frozenTotal +=
874
- realThs[dataOffset + i]?.getBoundingClientRect().width ?? 0;
875
- }
1026
+ const width = realThs[dataOffset + i]?.getBoundingClientRect().width ?? 0;
1027
+ if (col.width != null) frozenTotal += width;
1028
+ if (!isFlexColumn(i)) nonFlexTotal += width;
876
1029
  });
877
1030
 
878
- // ── Offscreen probe for natural (max-content) auto-column widths ───────
879
- // The clone's inner table is forced to `width: max-content` and all <col>
880
- // hints are stripped so each <th> reports its true content-driven width,
881
- // independent of the container size or previous probe cycles.
1031
+ // ── Offscreen probe for the auto columns' minimum (min-content) widths ──
1032
+ // The clone's inner table is forced to `width: min-content` and the auto <col>
1033
+ // hints are stripped so each <th> reports the narrowest width it can take
1034
+ // before content would be clipped, independent of the container size or
1035
+ // previous probe cycles.
882
1036
  const probe = document.createElement('div');
883
1037
  probe.style.cssText = [
884
1038
  'position:absolute',
@@ -894,11 +1048,16 @@ const runNaturalWidthProbe = () => {
894
1048
  const clone = container.cloneNode(true) as HTMLElement;
895
1049
  clone.classList.remove('bb-table--fixed');
896
1050
 
897
- // Frozen <col> hints are kept intact so auto columns are probed in the
898
- // correct spatial context (a 400 px frozen column still occupies 400 px,
899
- // giving neighbouring auto columns the right available space to size against).
900
- // Only auto column hints are cleared so previous probe cycles cannot inflate
901
- // their measured natural widths.
1051
+ // Expand rows are full-width (colspan) and hold nested tables; under the
1052
+ // auto-layout probe their content would back-pressure this table's own
1053
+ // columns, so drop them and measure this table's columns in isolation.
1054
+ clone
1055
+ .querySelectorAll('.bb-table-expand__row')
1056
+ .forEach((row) => row.remove());
1057
+
1058
+ // Auto <col> hints are cleared so a previous cycle's computed width cannot
1059
+ // act as a minimum and inflate the measurement; frozen hints are left intact
1060
+ // since their declared width is a genuine floor for that column.
902
1061
  const probeColEls = Array.from(clone.querySelectorAll<HTMLElement>('col'));
903
1062
  props.columns.forEach((col, i) => {
904
1063
  if (col.width == null) {
@@ -909,7 +1068,7 @@ const runNaturalWidthProbe = () => {
909
1068
 
910
1069
  const innerTable = clone.querySelector<HTMLElement>('table');
911
1070
  if (innerTable) {
912
- innerTable.style.width = 'max-content';
1071
+ innerTable.style.width = 'min-content';
913
1072
  innerTable.style.minWidth = '0';
914
1073
  }
915
1074
 
@@ -923,8 +1082,10 @@ const runNaturalWidthProbe = () => {
923
1082
  ? Array.from(headerRow.querySelectorAll<HTMLElement>(':scope > th'))
924
1083
  : [];
925
1084
 
926
- // Only auto columns frozen columns use their declared widths from above.
927
- const autoNaturals = props.columns.map((col, i) => {
1085
+ // Min-content of every unfrozen column (0 for frozen). Independent of the live
1086
+ // table's width, so it is a stable basis for both the flex share and the
1087
+ // min-width floor — unlike the rendered inherited widths.
1088
+ const minContents = props.columns.map((col, i) => {
928
1089
  if (col.width != null) return 0;
929
1090
  const th = ths[dataOffset + i];
930
1091
  return th ? th.getBoundingClientRect().width : 0;
@@ -933,24 +1094,62 @@ const runNaturalWidthProbe = () => {
933
1094
  document.body.removeChild(probe);
934
1095
 
935
1096
  // ── Proportional <col> widths ──────────────────────────────────────────
936
- const autoNaturalTotal = autoNaturals.reduce((s, w) => s + w, 0);
937
- const available = containerWidth - frozenTotal - selectWidth - actionsWidth;
938
-
939
- // Scale each auto column proportionally to available space, but never
940
- // below its natural content width so content is never squished.
941
- probeColWidths.value = props.columns.map((col, i) => {
942
- if (col.width != null) return null;
943
- const natural = autoNaturals[i];
944
- if (autoNaturalTotal <= 0) return `${Math.ceil(natural)}px`;
945
- const share = available > 0 ? available * (natural / autoNaturalTotal) : 0;
946
- return `${Math.ceil(Math.max(natural, share))}px`;
1097
+ // Only the flex columns share `available`; the inherited columns are already
1098
+ // laid out and excluded via `nonFlexTotal`.
1099
+ const flexMinTotal = props.columns.reduce(
1100
+ (s, _, i) => s + (isFlexColumn(i) ? minContents[i] : 0),
1101
+ 0
1102
+ );
1103
+ const available = containerWidth - nonFlexTotal - selectWidth - actionsWidth;
1104
+
1105
+ // Share the available space proportionally to each flex column's minimum, but
1106
+ // never drop below that minimum when space runs out the table overflows
1107
+ // (and the container scrolls) instead of clipping content.
1108
+ //
1109
+ // Widths are kept sub-pixel (not rounded up): the raw shares sum to exactly
1110
+ // `available`, whereas ceiling each column would over-allocate by up to a
1111
+ // pixel apiece and, under `table-layout: fixed`, push the table a few px past
1112
+ // the container and trip the horizontal scrollbar. `toFixed` only trims the
1113
+ // float tail, well below the 1px a scrollbar needs.
1114
+ const px = (n: number) => `${Number(n.toFixed(3))}px`;
1115
+ probeColWidths.value = props.columns.map((_, i) => {
1116
+ if (!isFlexColumn(i)) return null;
1117
+ const minimum = minContents[i];
1118
+ if (flexMinTotal <= 0) return px(minimum);
1119
+ const share = available > 0 ? available * (minimum / flexMinTotal) : 0;
1120
+ return px(Math.max(minimum, share));
947
1121
  });
948
1122
 
949
- // Table min-width = real frozen widths + natural auto widths + structural.
950
- // Using real frozen widths (not probe) ensures declared px/rem/% lengths
951
- // are respected rather than the probe's stripped-hint approximation.
1123
+ // Min-content per unfrozen column, for content-sizing an inheriting table's
1124
+ // overflow columns (no parent track to snap to).
1125
+ columnMinWidths.value = minContents.map((w) => (w > 0 ? px(w) : null));
1126
+
1127
+ // Leftover the inheriting fill column must absorb to reach the parent's width.
1128
+ // `parentTotal` is the parent table's full width (the expand cell this table
1129
+ // sits in); the container itself shrink-wraps to the table, so it can't be the
1130
+ // reference. Self-correcting: the live total already includes last cycle's
1131
+ // fill, so adding it back recovers the intrinsic total and the result is the
1132
+ // same once the table fills (0 when it already fills or overflows).
1133
+ if (fillColumnIndex.value >= 0) {
1134
+ const parentTotal = Object.values(parentNode.value?.tracks ?? {}).reduce(
1135
+ (s, w) => s + (parseFloat(w) || 0),
1136
+ 0
1137
+ );
1138
+ const liveTotal = realThs.reduce(
1139
+ (s, th) => s + th.getBoundingClientRect().width,
1140
+ 0
1141
+ );
1142
+ fillValue.value = Math.max(0, parentTotal - liveTotal + fillValue.value);
1143
+ } else {
1144
+ fillValue.value = 0;
1145
+ }
1146
+
1147
+ // Table min-width = frozen (declared) widths + every unfrozen column's
1148
+ // min-content + structural. Built only from values that do not depend on the
1149
+ // table's own width, so it can never exceed max-content and force a stretch.
1150
+ const minContentTotal = minContents.reduce((s, w) => s + w, 0);
952
1151
  const naturalTotal =
953
- frozenTotal + autoNaturalTotal + selectWidth + actionsWidth;
1152
+ frozenTotal + minContentTotal + selectWidth + actionsWidth;
954
1153
  naturalTotalWidth.value =
955
1154
  naturalTotal > 0 ? `${Math.ceil(naturalTotal)}px` : null;
956
1155
  };
@@ -1038,6 +1237,7 @@ const containerClass = computed(() => ({
1038
1237
  'bb-table--loading': loading.value,
1039
1238
  'bb-table--empty': !options.value.length,
1040
1239
  'bb-table--selectable': !!props.selectable,
1240
+ 'scrollbar-border': true,
1041
1241
  }));
1042
1242
 
1043
1243
  const replacementContentSpan = computed(
@@ -1515,10 +1715,16 @@ export type BbTableProps<Item = any> = {
1515
1715
  id?: string;
1516
1716
 
1517
1717
  /**
1518
- * When `true` (the default) unfrozen columns inherit their width from the
1519
- * parent table's matching track, accounting for `select`/`actions` columns.
1718
+ * Controls whether unfrozen columns inherit their width from an ancestor
1719
+ * table's matching track, accounting for `select`/`actions` columns.
1720
+ * - `true` (the default): inherit from the nearest ancestor table.
1721
+ * - `false`: do not inherit.
1722
+ * - a table id (string): inherit from the specific ancestor in the chain with
1723
+ * that id rather than the immediate parent. Use this when an intermediate
1724
+ * table has a different column count, so the extra columns would otherwise
1725
+ * misalign against it.
1520
1726
  */
1521
- inheritColumnWidths?: boolean;
1727
+ inheritColumnWidths?: boolean | string;
1522
1728
 
1523
1729
  /**
1524
1730
  * Boolean that defines whether to display a "Select all" checkbox.
@@ -1880,9 +2086,17 @@ export type BbTableSlots<Item = any> = {
1880
2086
  --bb-table-offset-internal-end: var(--bb-table-offset-end);
1881
2087
  --bb-table-offset-external-start: 0px;
1882
2088
  --bb-table-offset-external-end: 0px;
2089
+ /* Slack the fill column adds to reach the parent's width. Reset per table so a
2090
+ nested table never inherits an ancestor's value through the cascade. */
2091
+ --bb-table-fill: 0px;
1883
2092
  border-collapse: separate;
1884
2093
  border-spacing: 0;
1885
2094
  display: grid;
2095
+ /* Override the UA `min-inline-size: min-content` that a <fieldset> container
2096
+ (rendered for selectable tables) carries — without this the container can't
2097
+ shrink below the table's content width, so it overflows its parent and
2098
+ pushes the page instead of scrolling inside its own `overflow-x: auto`. */
2099
+ min-width: 0;
1886
2100
  overflow-x: auto;
1887
2101
  padding-inline-end: var(--bb-table-offset-external-end);
1888
2102
  padding-inline-start: var(--bb-table-offset-external-start);
@@ -1963,9 +2177,21 @@ export type BbTableSlots<Item = any> = {
1963
2177
  them out of alignment. Declared after `--fixed` and the base table rule so
1964
2178
  it overrides their width/min-width at equal specificity. */
1965
2179
  &--inherit-widths {
2180
+ /* Never grow past the parent's expand cell: a child with more columns
2181
+ than the parent overflows the cell, so cap the container here and let its
2182
+ own `overflow-x: auto` scroll the surplus columns inside the expand row
2183
+ rather than stretching the ancestor. */
2184
+ max-width: 100%;
2185
+ min-width: 0;
2186
+
1966
2187
  table {
1967
2188
  min-width: 0;
1968
- width: max-content;
2189
+ /* A *definite* width (not `max-content`) so `table-layout: fixed` stays
2190
+ content-independent: inherited columns keep their colgroup widths and
2191
+ align with the parent instead of growing to fit content. The fill
2192
+ column is `auto`, so fixed layout hands it the leftover; when columns
2193
+ overflow, the table grows past 100% and scrolls. */
2194
+ width: 100%;
1969
2195
  }
1970
2196
  }
1971
2197
 
@@ -2029,28 +2255,38 @@ export type BbTableSlots<Item = any> = {
2029
2255
  }
2030
2256
 
2031
2257
  td.bb-table-expand__cell {
2258
+ /* Collapse to the content's height instead of inheriting the data-row
2259
+ cell height, so an expand row with no slot content takes no space. */
2260
+ height: auto;
2032
2261
  padding: 0;
2262
+
2263
+ &:empty {
2264
+ display: none;
2265
+ }
2033
2266
  }
2034
2267
  }
2035
2268
  }
2036
2269
 
2037
- /* Apply the structural edge offsets to the first/last cell of every data
2038
- row. The full-width expand and loading cells are excluded. */
2039
- thead tr > th:first-child,
2040
- tbody
2041
- tr
2042
- > td:first-child:not(.bb-table-expand__cell):not(.bb-table-loading__cell) {
2270
+ /* Apply the structural edge offsets to the first/last *data* column. They
2271
+ must land on the data cells, not the absolute first/last child: a select
2272
+ or actions column would otherwise absorb the fill and leave the data
2273
+ columns misaligned with the parent (and bloat the actions cell).
2274
+
2275
+ The offset is clamped at 0 here: a positive fill widens the cell and is
2276
+ reserved as extra edge padding, but a *negative* offset (a select/actions
2277
+ column reclaimed from this column's width) must not eat into the normal
2278
+ `--padding-x` — that only shrinks the column width, not its inner padding. */
2279
+ thead tr > th.bb-table-header--edge-start,
2280
+ tbody tr > td.bb-table-data__cell--edge-start {
2043
2281
  padding-inline-start: calc(
2044
- var(--padding-x) + var(--bb-table-offset-internal-start)
2282
+ var(--padding-x) + max(0px, var(--bb-table-offset-internal-start))
2045
2283
  );
2046
2284
  }
2047
2285
 
2048
- thead tr > th:last-child,
2049
- tbody
2050
- tr
2051
- > td:last-child:not(.bb-table-expand__cell):not(.bb-table-loading__cell) {
2286
+ thead tr > th.bb-table-header--edge-end,
2287
+ tbody tr > td.bb-table-data__cell--edge-end {
2052
2288
  padding-inline-end: calc(
2053
- var(--padding-x) + var(--bb-table-offset-internal-end)
2289
+ var(--padding-x) + max(0px, var(--bb-table-offset-internal-end))
2054
2290
  );
2055
2291
  }
2056
2292
 
@@ -118,10 +118,16 @@ export type BbTableProps<Item = any> = {
118
118
  */
119
119
  id?: string;
120
120
  /**
121
- * When `true` (the default) unfrozen columns inherit their width from the
122
- * parent table's matching track, accounting for `select`/`actions` columns.
123
- */
124
- inheritColumnWidths?: boolean;
121
+ * Controls whether unfrozen columns inherit their width from an ancestor
122
+ * table's matching track, accounting for `select`/`actions` columns.
123
+ * - `true` (the default): inherit from the nearest ancestor table.
124
+ * - `false`: do not inherit.
125
+ * - a table id (string): inherit from the specific ancestor in the chain with
126
+ * that id rather than the immediate parent. Use this when an intermediate
127
+ * table has a different column count, so the extra columns would otherwise
128
+ * misalign against it.
129
+ */
130
+ inheritColumnWidths?: boolean | string;
125
131
  /**
126
132
  * Boolean that defines whether to display a "Select all" checkbox.
127
133
  */
@@ -17,6 +17,13 @@ export interface UseTableWidthContextOptions {
17
17
  id?: MaybeRefOrGetter<string | undefined>;
18
18
  /** This table's container element; used to find the parent table in the DOM. */
19
19
  container: MaybeRefOrGetter<HTMLElement | null | undefined>;
20
+ /**
21
+ * Id of a specific ancestor table to inherit from. When set, parent discovery
22
+ * walks the whole DOM ancestor chain for a table with this id rather than
23
+ * stopping at the nearest one. When omitted, the nearest ancestor table is
24
+ * used. A requested id that is not found in the chain yields no parent.
25
+ */
26
+ inheritFrom?: MaybeRefOrGetter<string | undefined>;
20
27
  }
21
28
  export declare function useTableWidthContext(options: UseTableWidthContextOptions): {
22
29
  id: import('vue').ComputedRef<string>;