bitboss-ui 3.0.0-beta.20 → 3.0.0-beta.21

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 (38) hide show
  1. package/dist/ai/BbTable.md +206 -98
  2. package/dist/ai/changelog.json +21 -3
  3. package/dist/ai/components.json +40 -15
  4. package/dist/ai/guides/component-picker.md +18 -18
  5. package/dist/ai/guides/icons-policy.md +7 -3
  6. package/dist/ai/guides/installation-and-plugin-setup.md +11 -14
  7. package/dist/ai/guides/migration/components/bb-table.md +131 -17
  8. package/dist/ai/recipes/inertia/approvals-inbox.md +0 -1
  9. package/dist/ai/recipes/inertia/inline-edit-workspace.md +0 -1
  10. package/dist/ai/recipes/inertia/ownership-atlas.md +8 -9
  11. package/dist/ai/recipes/inertia/record-form.md +0 -1
  12. package/dist/ai/recipes/inertia/records-workspace.md +0 -1
  13. package/dist/ai/recipes/inertia/upload-center.md +0 -1
  14. package/dist/ai/recipes/nuxt/approvals-inbox.md +0 -1
  15. package/dist/ai/recipes/nuxt/inline-edit-workspace.md +0 -1
  16. package/dist/ai/recipes/nuxt/record-form.md +0 -1
  17. package/dist/ai/recipes/nuxt/records-workspace.md +0 -1
  18. package/dist/ai/recipes/nuxt/upload-center.md +0 -1
  19. package/dist/ai/recipes/vue/approvals-inbox.md +0 -1
  20. package/dist/ai/recipes/vue/inline-edit-workspace.md +0 -1
  21. package/dist/ai/recipes/vue/records-workspace.md +0 -1
  22. package/dist/ai/recipes/vue/upload-center.md +0 -1
  23. package/dist/ai/source/BbTable.md +564 -173
  24. package/dist/components/BbTable/BbTable.vue.d.ts +2 -0
  25. package/dist/components/BbTable/BbTable.vue_vue_type_script_setup_true_lang.js +865 -735
  26. package/dist/components/BbTable/types.d.ts +98 -46
  27. package/dist/components/BbTable/utils.d.ts +21 -16
  28. package/dist/components/BbTable/utils.js +8 -8
  29. package/dist/composables/useTableWidthContext.d.ts +24 -4
  30. package/dist/composables/useTableWidthContext.js +31 -28
  31. package/dist/deprecation/ai-deprecations.json.d.ts +22 -0
  32. package/dist/deprecation/ai-deprecations.json.js +1 -1
  33. package/dist/llms-full.txt +381 -173
  34. package/dist/llms-medium.txt +29 -32
  35. package/dist/vite.js +1 -1
  36. package/package.json +1 -1
  37. package/scripts/lib/eslint-plugin.mjs +9 -14
  38. package/scripts/lib/validate-bb-markup.mjs +0 -7
@@ -233,6 +233,7 @@
233
233
  <!-- @vue-ignore -->
234
234
  <slot
235
235
  :classes="header.classes"
236
+ :hide-column="() => hideColumn(header.key)"
236
237
  :items="internalItems"
237
238
  :label="header.label"
238
239
  :name="header.slotName"
@@ -295,7 +296,7 @@
295
296
  />
296
297
  </div>
297
298
  <div
298
- v-if="actions"
299
+ v-if="hasActions"
299
300
  class="bb-table-header bb-table-header--actions"
300
301
  :class="thClass"
301
302
  role="columnheader"
@@ -405,7 +406,7 @@
405
406
  />
406
407
  </div>
407
408
  <div
408
- v-if="actions"
409
+ v-if="hasActions"
409
410
  class="bb-table-skeleton__cell bb-table-skeleton__cell--actions"
410
411
  role="cell"
411
412
  >
@@ -525,12 +526,14 @@ import {
525
526
  getCurrentInstance,
526
527
  nextTick,
527
528
  onBeforeUnmount,
529
+ onBeforeUpdate,
528
530
  onMounted,
529
531
  reactive,
530
532
  ref,
531
533
  shallowRef,
532
534
  toRef,
533
535
  watch,
536
+ watchEffect,
534
537
  } from 'vue';
535
538
  import {
536
539
  defaultRangeExtractor,
@@ -598,6 +601,7 @@ import {
598
601
  readResolvedTracks,
599
602
  sameColumnOrder,
600
603
  } from './utils';
604
+ import type { FixedColumnConfig } from './utils';
601
605
  import { useBbTableContextHost } from '../../composables/useBbTableContext';
602
606
  import { useLogger } from '@/composables/useLogger';
603
607
  import {
@@ -643,7 +647,6 @@ const props = withDefaults(defineProps<InternalProps<T>>(), {
643
647
  columns: () => [],
644
648
  dependencies: () => [],
645
649
  depsDebounceTime: 0,
646
- fixedColumns: () => [],
647
650
  items: () => [],
648
651
  max: Infinity,
649
652
  multiple: true,
@@ -685,6 +688,7 @@ const slots = defineSlots<
685
688
  sortable?: boolean;
686
689
  sortOrder?: 'asc' | 'desc' | null;
687
690
  toggleSort?: () => void;
691
+ hideColumn?: () => void;
688
692
  }) => any
689
693
  >
690
694
  > &
@@ -699,6 +703,21 @@ const slots = defineSlots<
699
703
  if (process.env.NODE_ENV !== 'production')
700
704
  warnRemovedAttrs('BbTable', removedPropsFor('BbTable'));
701
705
 
706
+ /**
707
+ * The actions column exists exactly when the `#actions` slot is provided —
708
+ * there is no prop. Slot presence is not reactive state (a `computed` reading
709
+ * `slots.actions` never re-runs when the parent toggles the template with a
710
+ * `v-if`), so it is sampled into a ref before every update: the parent's
711
+ * re-render delivers the new slots to this instance before `beforeUpdate`
712
+ * fires, and a conditional slot makes the child's children dynamic, so the
713
+ * child does update. The track computeds and the row context read the ref.
714
+ */
715
+ const hasActions = ref(!!slots.actions);
716
+ onBeforeUpdate(() => {
717
+ const present = !!slots.actions;
718
+ if (present !== hasActions.value) hasActions.value = present;
719
+ });
720
+
702
721
  const { getItemValue } = useItemValue();
703
722
 
704
723
  /** Temporary name for selection inputs; hidden inputs handle real submission. */
@@ -758,7 +777,7 @@ if (process.env.NODE_ENV !== 'production') {
758
777
  useLogger().warn(
759
778
  `BbTable: \`{ key: '${String(column.key)}' }\` is a reserved column and must not appear in \`columns\`. ` +
760
779
  (column.key === 'actions'
761
- ? 'Set the `actions` prop and fill the `#actions` slot instead.'
780
+ ? 'Fill the `#actions` slot instead the slot creates the column.'
762
781
  : 'Selection is controlled by the `selectable` prop.')
763
782
  );
764
783
  }
@@ -810,6 +829,81 @@ const headerInstructionsId = computed(
810
829
  /** Header cells are keyboard targets when either header interaction is on. */
811
830
  const headerKeyboard = computed(() => !!(props.reorderable || props.resizable));
812
831
 
832
+ /* --- Column visibility ---------------------------------------------------------
833
+ * `column.hidden` is the single declared truth (the `width` shape, not the
834
+ * `sort` one): a hidden column renders nothing and owns no track but stays
835
+ * DECLARED, so the order write-back completes around it and it returns to its
836
+ * slot when the flag clears. A header's `hideColumn()` sets a per-mount OVERRIDE the
837
+ * table keeps until that column's flag changes to anything but `true` — the
838
+ * consumer persisting the reported key writes `hidden: true`, which matches
839
+ * the override and keeps it; a panel showing the column writes `false`, which
840
+ * drops it. There is no "show" path on the table: a hidden column has no
841
+ * header to call it from. */
842
+
843
+ /** Keys hidden from their header (`hideColumn()`), pending the definition's word. */
844
+ const hiddenOverrides = ref<ReadonlySet<string>>(new Set());
845
+
846
+ /** Every key that renders nothing right now: the flag or a live override. */
847
+ const hiddenColumnKeys = computed(() => {
848
+ const keys = new Set(hiddenOverrides.value);
849
+ for (const column of props.columns) {
850
+ if (column.hidden) keys.add(String(column.key));
851
+ }
852
+ return keys;
853
+ });
854
+
855
+ /**
856
+ * The declared columns that render, in declaration order. Returns
857
+ * `props.columns` BY IDENTITY when nothing is hidden, so the identity
858
+ * shortcut in `orderedColumns` (and every "feature off" byte-parity
859
+ * guarantee behind it) survives untouched.
860
+ */
861
+ const visibleColumns = computed(() => {
862
+ const hidden = hiddenColumnKeys.value;
863
+ if (!hidden.size) return props.columns;
864
+ return props.columns.filter((column) => !hidden.has(String(column.key)));
865
+ });
866
+
867
+ /** `hideColumn()` from a header slot: an override, reported once. */
868
+ const hideColumn = (key: string) => {
869
+ if (hiddenColumnKeys.value.has(key)) return;
870
+ // A hidden header unmounts under the keyboard user: keep focus on the
871
+ // header row (the same rendered index, i.e. the column that slides in).
872
+ const cell = headerCellFor(key);
873
+ const index = orderedColumnKeys.value.indexOf(key);
874
+ const refocus =
875
+ !!cell && cell.contains(document.activeElement) && headerKeyboard.value;
876
+ hiddenOverrides.value = new Set([...hiddenOverrides.value, key]);
877
+ emit('hide:column', key);
878
+ if (refocus) nextTick(() => focusHeaderAt(index));
879
+ };
880
+
881
+ // The definition wins whenever it changes to something ELSE than the override
882
+ // (the `resizedWidths` rule): `hidden: true` written back is the consumer
883
+ // persisting the hide and keeps the override; any other change drops it.
884
+ watch(
885
+ () =>
886
+ props.columns.map(
887
+ (column) => [String(column.key), !!column.hidden] as const
888
+ ),
889
+ (now, before) => {
890
+ if (!before || !hiddenOverrides.value.size) return;
891
+ const previous = new Map(before);
892
+ const overridden = hiddenOverrides.value;
893
+ const cleared = now.filter(
894
+ ([key, hidden]) =>
895
+ previous.has(key) &&
896
+ previous.get(key) !== hidden &&
897
+ overridden.has(key) &&
898
+ !hidden
899
+ );
900
+ if (!cleared.length) return;
901
+ const rest = new Set(overridden);
902
+ for (const [key] of cleared) rest.delete(key);
903
+ hiddenOverrides.value = rest;
904
+ }
905
+ );
906
+
813
907
  /* --- Column order -----------------------------------------------------------
814
908
  * `columns` is the declaration, `order` is the state (the sort split).
815
909
  * ONE ordered list feeds every positional computation below; everything keyed
@@ -817,6 +911,12 @@ const headerKeyboard = computed(() => !!(props.reorderable || props.resizable));
817
911
  * (`projectedItemAt`) maps `orderedColumns` directly — a reorder drops its memo
818
912
  * and re-runs the formatters, but only for the rows on screen. */
819
913
 
914
+ /**
915
+ * EVERY declared key, hidden ones included — the set the order model is
916
+ * completed against. Built from the declaration and never from the rendered
917
+ * columns: that is what keeps a hidden key in the write-back even when the
918
+ * consumer's persisted model never listed it.
919
+ */
820
920
  const declaredKeys = computed(() => props.columns.map((c) => String(c.key)));
821
921
 
822
922
  /**
@@ -827,18 +927,22 @@ const declaredKeys = computed(() => props.columns.map((c) => String(c.key)));
827
927
  const dragOrder = ref<string[] | null>(null);
828
928
 
829
929
  /**
830
- * The declared columns in render order. Returns `props.columns` BY IDENTITY
831
- * whenever the resolved order is the declaration order (no model, or a model
832
- * that changes nothing): every downstream computed then sees the same
833
- * reference, which is what keeps the DOM, class strings and publish sequence
834
- * byte-identical when the feature is off.
930
+ * The RENDERED columns in render order: the visible declaration, ordered.
931
+ * Returns `visibleColumns` BY IDENTITY (which is `props.columns` itself when
932
+ * nothing is hidden) whenever the resolved order is the declaration order
933
+ * (no model, or a model that changes nothing): every downstream computed
934
+ * then sees the same reference, which is what keeps the DOM, class strings
935
+ * and publish sequence byte-identical when the features are off. Ordering
936
+ * the visible subset is exact: a hidden key in the model is just a key that
937
+ * matches no rendered column, preserved in place like any other.
835
938
  */
836
939
  const orderedColumns = computed(() => {
940
+ const columns = visibleColumns.value;
837
941
  const model = dragOrder.value ?? order.value;
838
- if (!model.length) return props.columns;
839
- const ordered = orderColumns(props.columns, model);
840
- return ordered.every((column, index) => column === props.columns[index])
841
- ? props.columns
942
+ if (!model.length) return columns;
943
+ const ordered = orderColumns(columns, model);
944
+ return ordered.every((column, index) => column === columns[index])
945
+ ? columns
842
946
  : ordered;
843
947
  });
844
948
 
@@ -861,7 +965,13 @@ const orderedColumnKeys = computed(() =>
861
965
  */
862
966
  const nextColumnOrder = (key: string, toIndex: number): string[] | null => {
863
967
  const current = completeColumnOrder(order.value, declaredKeys.value);
864
- const next = moveColumnKey(order.value, declaredKeys.value, key, toIndex);
968
+ const next = moveColumnKey(
969
+ order.value,
970
+ declaredKeys.value,
971
+ key,
972
+ toIndex,
973
+ hiddenColumnKeys.value
974
+ );
865
975
  return sameColumnOrder(next, current) ? null : next;
866
976
  };
867
977
 
@@ -1054,7 +1164,7 @@ if (process.env.NODE_ENV !== 'production') {
1054
1164
  (conflict) => {
1055
1165
  if (conflict) {
1056
1166
  useLogger().warn(
1057
- "BbTable: `snap` is positional in the parent's rendered order and does not compose with `order` / `reorderable` on the same table order the parent (a snap-mapped child follows its slots), or drop the snaps and share the parent's `order` with a same-key child."
1167
+ 'BbTable: `snap` names a PARENT column and the child renders in its own order it does not compose with `order` / `reorderable` / a `hidden` column on the snapped table itself. Order and hide on the parent (a key-snapped child follows it), or drop the snaps.'
1058
1168
  );
1059
1169
  }
1060
1170
  },
@@ -1580,7 +1690,13 @@ const placeDraggedColumn = (rawX: number) => {
1580
1690
  const after = toIndex > from;
1581
1691
  if (drag.target?.key === targetKey && drag.target.after === after) return;
1582
1692
 
1583
- const next = moveColumnKey(drag.base, declaredKeys.value, drag.key, toIndex);
1693
+ const next = moveColumnKey(
1694
+ drag.base,
1695
+ declaredKeys.value,
1696
+ drag.key,
1697
+ toIndex,
1698
+ hiddenColumnKeys.value
1699
+ );
1584
1700
  const current = dragOrder.value ?? drag.base;
1585
1701
  if (sameColumnOrder(next, current)) {
1586
1702
  drag.target = { key: targetKey, after };
@@ -1725,7 +1841,12 @@ const onDragPointerUp = (event: PointerEvent) => {
1725
1841
  if (!drag || event.pointerId !== drag.pointerId) return;
1726
1842
  const { key } = drag;
1727
1843
  const from = drag.base
1728
- ? drag.base.filter((k) => declaredKeys.value.includes(k)).indexOf(key)
1844
+ ? drag.base
1845
+ .filter(
1846
+ (k) =>
1847
+ declaredKeys.value.includes(k) && !hiddenColumnKeys.value.has(k)
1848
+ )
1849
+ .indexOf(key)
1729
1850
  : -1;
1730
1851
  const committed =
1731
1852
  dragPhase.value === 'dragging' &&
@@ -2078,7 +2199,7 @@ const fixedLayout = computed(
2078
2199
  () =>
2079
2200
  props.fixed ||
2080
2201
  hasSnap.value ||
2081
- props.columns.some((column) => column.width != null) ||
2202
+ visibleColumns.value.some((column) => column.width != null) ||
2082
2203
  hasResizedWidths.value ||
2083
2204
  (inheritColumnWidths.value && !!parentId.value)
2084
2205
  );
@@ -2088,6 +2209,12 @@ const isInheriting = computed(
2088
2209
  () => inheritColumnWidths.value && !!parentId.value
2089
2210
  );
2090
2211
 
2212
+ // Tell descendants whether THIS table's columns can move at runtime: a child's
2213
+ // key snaps are only sound against a parent whose order is static.
2214
+ watchEffect(() => {
2215
+ node.ordered = !!props.reorderable || order.value.length > 0;
2216
+ });
2217
+
2091
2218
  /**
2092
2219
  * `virtual` windows the body (see "Virtual rows" below). Root tables only —
2093
2220
  * a nested table's scroller is its parent's and it inherits its tracks — and
@@ -2166,37 +2293,28 @@ const toLength = (width: number | string) => {
2166
2293
  return /^-?\d*\.?\d+$/.test(trimmed) ? `${trimmed}px` : trimmed;
2167
2294
  };
2168
2295
 
2169
- /** Number of data columns the parent renders (numeric track keys). */
2170
- const parentDataColumns = computed(() => {
2171
- const tracks = parentNode.value?.tracks;
2172
- return tracks
2173
- ? Object.keys(tracks).filter((key) => /^\d+$/.test(key)).length
2174
- : 0;
2175
- });
2176
-
2177
2296
  /**
2178
- * The parent's rendered columns in order — `select` (if any), data columns
2179
- * `0…N-1`, then `actions` (if any) — form a gridline coordinate, one cell per
2180
- * column. Cell `c` is the range `[c, c + 1]`. So a no-select parent starts its
2181
- * data at cell 0, while a selectable parent starts it at cell 1.
2297
+ * The parent's rendered columns in order — `select` (if any), its data
2298
+ * columns by KEY, then `actions` (if any) — form a gridline coordinate, one
2299
+ * cell per column. Cell `c` is the range `[c, c + 1]`. So a no-select parent
2300
+ * starts its data at cell 0, while a selectable parent starts it at cell 1.
2301
+ * Published by the parent with its tracks (`node.keys`): a record cannot
2302
+ * carry the order a child relies on.
2182
2303
  */
2183
- const parentColumnKeys = computed(() => {
2184
- const tracks = parentNode.value?.tracks ?? {};
2185
- const keys: string[] = [];
2186
- if ('select' in tracks) keys.push('select');
2187
- for (let k = 0; k < parentDataColumns.value; k++) keys.push(String(k));
2188
- if ('actions' in tracks) keys.push('actions');
2189
- return keys;
2190
- });
2304
+ const parentColumnKeys = computed(() => parentNode.value?.keys ?? []);
2191
2305
 
2192
2306
  /** Gridline cells where the parent's data region begins/ends and the table ends. */
2193
2307
  const firstDataCell = computed(() =>
2194
- 'select' in (parentNode.value?.tracks ?? {}) ? 1 : 0
2195
- );
2196
- const dataEndCell = computed(
2197
- () => firstDataCell.value + parentDataColumns.value
2308
+ parentColumnKeys.value[0] === 'select' ? 1 : 0
2198
2309
  );
2199
2310
  const tableEndCell = computed(() => parentColumnKeys.value.length);
2311
+ const dataEndCell = computed(() => {
2312
+ const keys = parentColumnKeys.value;
2313
+ return keys[keys.length - 1] === 'actions' ? keys.length - 1 : keys.length;
2314
+ });
2315
+
2316
+ /** The parent's gridline cell for a track key, `-1` when it renders none. */
2317
+ const parentCellOf = (key: string) => parentColumnKeys.value.indexOf(key);
2200
2318
 
2201
2319
  /** Parent track custom property for the integer cell `[cell, cell + 1]`. */
2202
2320
  const cellTrackVar = (cell: number) => {
@@ -2224,57 +2342,231 @@ const gridRangeWidth = (a: number, b: number): string | null => {
2224
2342
  };
2225
2343
 
2226
2344
  /**
2227
- * Resolved parent snap interval for each data column.
2228
- * - A number snaps to that start point; the column then runs up to the next
2229
- * column's snap point (or one whole track if the next is unset). So a single
2230
- * snap that skips ahead widens this column to cover the gap — `snap: 4` on the
2231
- * second column makes the first effectively `[start, 4]`.
2232
- * - A `[start, end]` pair is taken verbatim (`end` of `-1` runs to the end of
2233
- * the data region).
2234
- * - Omitted, the column chains from the previous column's end.
2235
- * - The LAST column runs to the end of the parent's data region (as if
2236
- * `[start, -1]`): a table with fewer columns than its parent widens its last
2237
- * content cell across the parent's trailing tracks — trailing tracks are
2238
- * never structural, the actions track inherits only the parent's actions
2239
- * region. A `width` (the column pins its own extent) or an explicit
2240
- * `[start, end]` pair declines them; they stay in the end region.
2345
+ * A snap point: a parent column key plus a fraction of that track. The
2346
+ * fraction is a trailing `.digits` on the raw string (`'amount.5'`); the key
2347
+ * part is matched RAW against the parent's published keys, like every other
2348
+ * key-taking field. A raw key that itself ends in `.digits` is ambiguous here
2349
+ * and is written in its slot-name spelling instead (documented).
2350
+ */
2351
+ const parseSnapPoint = (raw: string): { key: string; fraction: number } => {
2352
+ const match = /^(.+)\.(\d+)$/.exec(raw);
2353
+ if (match) return { key: match[1], fraction: Number(`0.${match[2]}`) };
2354
+ return { key: raw, fraction: 0 };
2355
+ };
2356
+
2357
+ /**
2358
+ * A column's explicit `snap`, resolved to a parent gridline interval, or
2359
+ * `null` when any key it names is not a rendered parent column (hidden, or
2360
+ * absent) — the column then flows like an unsnapped one. `explicit` marks a
2361
+ * pair, which declines the trailing tracks the last column would otherwise
2362
+ * run across.
2363
+ */
2364
+ const resolveSnap = (
2365
+ snap: string | [string, string]
2366
+ ): { start: number; end: number; explicit: boolean } | null => {
2367
+ if (Array.isArray(snap)) {
2368
+ const a = parseSnapPoint(snap[0]);
2369
+ const b = parseSnapPoint(snap[1]);
2370
+ const cellA = parentCellOf(a.key);
2371
+ const cellB = parentCellOf(b.key);
2372
+ if (cellA < 0 || cellB < 0) return null;
2373
+ // Inclusive end: a whole key runs THROUGH its track, a fraction stops
2374
+ // partway into it.
2375
+ const start = cellA + a.fraction;
2376
+ const end = b.fraction > 0 ? cellB + b.fraction : cellB + 1;
2377
+ return end > start ? { start, end, explicit: true } : null;
2378
+ }
2379
+ const point = parseSnapPoint(snap);
2380
+ // A fraction on a single key has no second edge to apply to: ignored.
2381
+ const cell = parentCellOf(point.key);
2382
+ if (cell < 0) {
2383
+ // `.digits` may have been part of the key itself.
2384
+ const whole = parentCellOf(snap);
2385
+ return whole < 0 ? null : { start: whole, end: whole + 1, explicit: false };
2386
+ }
2387
+ return { start: cell, end: cell + 1, explicit: false };
2388
+ };
2389
+
2390
+ /**
2391
+ * True when the parent can reorder its data columns at runtime. A snap names
2392
+ * a parent column, but the child renders its own cells in its own order, so
2393
+ * a parent whose order can change under it can only be inherited by
2394
+ * POSITION — key targets are ignored there (dev builds warn; see the watcher
2395
+ * below). Snaps on the ROOT are a misconfiguration handled by `hasSnap`'s
2396
+ * implicit inherit + no parent → every interval is positional anyway.
2397
+ */
2398
+ const parentOrdered = computed(() => !!parentNode.value?.ordered);
2399
+
2400
+ /**
2401
+ * Resolved parent interval for each data column, in render order. Every
2402
+ * interval TILES the parent's data region from left to right — cell `i`'s
2403
+ * left edge is the sum of the widths before it, so the intervals must be
2404
+ * contiguous and monotonic or the child drifts off the parent's rails.
2405
+ *
2406
+ * 1. Claims. An explicit `snap` claims its interval (unless the parent is
2407
+ * ordered, where snaps are ignored). An unsnapped column whose KEY the
2408
+ * parent renders claims that track (not under an ordered parent either —
2409
+ * pairing by key needs the parent's order to be stable, so there the
2410
+ * child inherits by position, exactly as before keys existed).
2411
+ * 2. Flow. Walking the columns in order with a cursor at the previous end,
2412
+ * an unclaimed column takes the first free parent data cell at or after
2413
+ * the cursor; with none left it OVERFLOWS (content-sized, past the
2414
+ * parent's edge). A claim that starts BEFORE the cursor cannot tile — the
2415
+ * child's order disagrees with the parent's — and is demoted to flow
2416
+ * (dev builds warn).
2417
+ * 3. Tiling. Each interval then runs up to the next one's start, so a free
2418
+ * parent cell between two claims widens the column before it rather than
2419
+ * opening a hole. The LAST column runs to the end of the parent's data
2420
+ * region — trailing tracks are never structural, the actions track
2421
+ * inherits only the parent's actions region — unless it declares a
2422
+ * `width` (the column pins its own extent) or an explicit pair, which
2423
+ * both decline them; they stay in the end region.
2241
2424
  */
2242
2425
  const columnIntervals = computed(() => {
2243
2426
  const cols = orderedColumns.value;
2244
- // Explicit start points; a later column's snap sets the previous column's end.
2245
- const starts = cols.map((column) =>
2246
- typeof column.snap === 'number'
2247
- ? column.snap
2248
- : Array.isArray(column.snap)
2249
- ? column.snap[0]
2250
- : null
2251
- );
2252
- let cursor = firstDataCell.value;
2253
- return cols.map((column, i) => {
2254
- const start = starts[i] ?? cursor;
2255
- let end: number;
2256
- if (Array.isArray(column.snap)) {
2257
- end = column.snap[1] === -1 ? dataEndCell.value : column.snap[1];
2258
- } else {
2259
- const next = starts[i + 1];
2260
- end = next != null && next > start ? next : Math.floor(start) + 1;
2261
- if (i === cols.length - 1 && column.width == null) {
2262
- end = Math.max(end, dataEndCell.value);
2427
+ const dataStart = firstDataCell.value;
2428
+ const dataEnd = dataEndCell.value;
2429
+ const tableEnd = tableEndCell.value;
2430
+ const ordered = parentOrdered.value;
2431
+
2432
+ type Interval = {
2433
+ start: number;
2434
+ end: number;
2435
+ explicit: boolean;
2436
+ overflow: boolean;
2437
+ /** A claim that could not tile (out of the parent's order), flowed instead. */
2438
+ demoted?: string;
2439
+ };
2440
+ const claims: Array<Interval | null> = cols.map(() => null);
2441
+ const claimed = new Set<number>();
2442
+ const claim = (interval: Interval) => {
2443
+ for (let c = Math.floor(interval.start); c < interval.end - 1e-6; c++) {
2444
+ claimed.add(c);
2445
+ }
2446
+ };
2447
+
2448
+ // 1. Claims — explicit snaps first (they may span several cells), then keys.
2449
+ if (!ordered) {
2450
+ cols.forEach((column, i) => {
2451
+ if (column.snap == null) return;
2452
+ const resolved = resolveSnap(column.snap);
2453
+ if (!resolved) return;
2454
+ claims[i] = { ...resolved, overflow: false };
2455
+ claim(claims[i]!);
2456
+ });
2457
+ cols.forEach((column, i) => {
2458
+ if (claims[i] || column.snap != null) return;
2459
+ const cell = parentCellOf(String(column.key));
2460
+ if (cell < dataStart || cell >= dataEnd || claimed.has(cell)) return;
2461
+ claims[i] = {
2462
+ start: cell,
2463
+ end: cell + 1,
2464
+ explicit: false,
2465
+ overflow: false,
2466
+ };
2467
+ claimed.add(cell);
2468
+ });
2469
+ }
2470
+
2471
+ // 2. Flow. The cursor starts at the TABLE's edge (a `select` target is a
2472
+ // legitimate claim), but a flowing column only ever takes data cells.
2473
+ let cursor = 0;
2474
+ let overflowCount = 0;
2475
+ const intervals: Interval[] = cols.map((column, i) => {
2476
+ let interval = claims[i];
2477
+ let demoted: string | undefined;
2478
+ if (interval && interval.start + 1e-6 < cursor) {
2479
+ // Out of the parent's order: cannot tile. Demoted to flow.
2480
+ claims[i] = null;
2481
+ interval = null;
2482
+ demoted = String(column.key);
2483
+ }
2484
+ if (!interval) {
2485
+ // Chains from the previous column's end — a fraction included, so
2486
+ // the half of a track an explicit pair left behind is picked up
2487
+ // here — up to (never over) the next column's claim.
2488
+ const start = Math.max(cursor, dataStart);
2489
+ let limit = dataEnd;
2490
+ for (let j = i + 1; j < cols.length; j++) {
2491
+ const later = claims[j];
2492
+ if (later) limit = Math.min(limit, later.start);
2263
2493
  }
2494
+ let cell = Math.max(Math.floor(start), dataStart);
2495
+ while (cell < limit && claimed.has(cell)) cell++;
2496
+ if (cell < limit && cell + 1e-6 >= start) {
2497
+ claimed.add(cell);
2498
+ interval = {
2499
+ start,
2500
+ end: cell + 1,
2501
+ explicit: false,
2502
+ overflow: false,
2503
+ };
2504
+ } else if (cell < limit) {
2505
+ // The free cell is the one the fraction sits in.
2506
+ claimed.add(cell);
2507
+ interval = { start, end: cell + 1, explicit: false, overflow: false };
2508
+ } else {
2509
+ // Past the parent's data region — or a root table, where the region
2510
+ // is empty and every column sits here: sized to its own content.
2511
+ const start = Math.max(tableEnd, dataEnd) + overflowCount++;
2512
+ interval = { start, end: start + 1, explicit: false, overflow: true };
2513
+ }
2514
+ if (demoted) interval.demoted = demoted;
2264
2515
  }
2265
- cursor = end;
2266
- return { start, end };
2516
+ cursor = Math.max(cursor, interval.end);
2517
+ return interval;
2267
2518
  });
2519
+
2520
+ // 3. Tiling. An explicit pair pins its own extent and is never widened —
2521
+ // a gap after one is the consumer's (the old numeric pairs behaved the
2522
+ // same); everything else runs up to the next column's start.
2523
+ intervals.forEach((interval, i) => {
2524
+ if (interval.overflow || interval.explicit) return;
2525
+ const next = intervals[i + 1];
2526
+ if (next && !next.overflow && next.start > interval.end) {
2527
+ interval.end = next.start;
2528
+ }
2529
+ if (i === cols.length - 1 && cols[i].width == null) {
2530
+ interval.end = Math.max(interval.end, dataEnd);
2531
+ }
2532
+ });
2533
+ return intervals;
2268
2534
  });
2269
2535
 
2270
2536
  /**
2271
- * A column whose interval begins at or beyond the parent's last data cell has no
2272
- * matching parent column to inherit from the table has more columns than its
2273
- * parent. Such columns are sized to their own measured content and scroll within
2274
- * the expand row instead of inheriting a track.
2537
+ * A column with no matching parent track to inherit from the table has more
2538
+ * columns than its parent, or nothing free was left for a displaced one. Such
2539
+ * columns are sized to their own measured content and scroll within the
2540
+ * expand row instead of inheriting a track.
2275
2541
  */
2276
- const isOverflowColumn = (interval: { start: number }) =>
2277
- isInheriting.value && interval.start >= dataEndCell.value;
2542
+ const isOverflowColumn = (interval: { overflow: boolean }) =>
2543
+ isInheriting.value && interval.overflow;
2544
+
2545
+ if (process.env.NODE_ENV !== 'production') {
2546
+ // A snap under a parent that can reorder: ignored (the child inherits by
2547
+ // position, as it did before keys). Warned once per parent, not per render.
2548
+ watch(
2549
+ () => hasSnap.value && parentOrdered.value,
2550
+ (ignored) => {
2551
+ if (ignored) {
2552
+ useLogger().warn(
2553
+ "BbTable: `snap` targets are ignored under a parent that uses `order` / `reorderable` — the parent's columns can move, so the child inherits by position. Keep the parent's order static (no `reorderable`, an empty `order`) to snap by key."
2554
+ );
2555
+ }
2556
+ },
2557
+ { immediate: true }
2558
+ );
2559
+ watch(
2560
+ () => columnIntervals.value.find((i) => i.demoted)?.demoted ?? null,
2561
+ (key) => {
2562
+ if (key === null) return;
2563
+ useLogger().warn(
2564
+ `BbTable: column '${key}' snaps to a parent column that sits BEFORE the column preceding it — the child's order disagrees with the parent's, so the snap cannot tile and the column flows into the next free parent track instead. Declare the child's columns in the parent's order.`
2565
+ );
2566
+ },
2567
+ { immediate: true }
2568
+ );
2569
+ }
2278
2570
 
2279
2571
  /** True when any column overflows the parent (more columns than the parent). */
2280
2572
  const overflowsParent = computed(() =>
@@ -2402,8 +2694,8 @@ const offsetEndValue = computed(() =>
2402
2694
  ? null
2403
2695
  : edgeFold(
2404
2696
  endRegion.value,
2405
- props.actions ? ownStructuralTrack('actions') : null,
2406
- !!props.actions
2697
+ hasActions.value ? ownStructuralTrack('actions') : null,
2698
+ hasActions.value
2407
2699
  )
2408
2700
  );
2409
2701
  const offsetStartActive = computed(() => offsetStartValue.value !== null);
@@ -2508,10 +2800,10 @@ const columnTracks = computed(() => {
2508
2800
  // `isResized`); the declared-only columns keep filling around it.
2509
2801
  const allFrozen =
2510
2802
  !isInheriting.value &&
2511
- props.columns.length > 0 &&
2512
- props.columns.every((column) => column.width != null);
2803
+ columns.length > 0 &&
2804
+ columns.every((column) => column.width != null);
2513
2805
  columns.forEach((column, index) => {
2514
- const key = String(index);
2806
+ const key = String(column.key);
2515
2807
  const declared = effectiveWidth(column);
2516
2808
  const frozen = declared != null ? toLength(declared) : null;
2517
2809
  const interval = columnIntervals.value[index];
@@ -2591,7 +2883,7 @@ const columnTracks = computed(() => {
2591
2883
  tracks.push({ key, width, contentSized: false });
2592
2884
  });
2593
2885
 
2594
- if (props.actions) {
2886
+ if (hasActions.value) {
2595
2887
  tracks.push({
2596
2888
  key: 'actions',
2597
2889
  width: structuralTrack(endRegion.value, 'actions'),
@@ -2618,8 +2910,9 @@ const clippedKeyList = computed(() => {
2618
2910
  const keys: string[] = [];
2619
2911
  for (const track of columnTracks.value) {
2620
2912
  if (track.contentSized) continue;
2621
- const column = columns[Number(track.key)];
2622
- if (column) keys.push(String(column.key));
2913
+ // Data tracks carry the column key itself; skip the structural two.
2914
+ if (columns.some((column) => String(column.key) === track.key))
2915
+ keys.push(track.key);
2623
2916
  }
2624
2917
  return keys.join(' ');
2625
2918
  });
@@ -2864,18 +3157,29 @@ const measureTracks = () => {
2864
3157
  // published unfolded — the pure content rail — and the region goes out
2865
3158
  // under the structural key even though no cell renders it, so the next
2866
3159
  // level reserves it exactly as this one did.
2867
- const lastKey = String(props.columns.length - 1);
3160
+ // Track keys are COLUMN KEYS in render order, so the edge data tracks are
3161
+ // the first and last RENDERED columns, not the first and last declared.
3162
+ const dataKeys = orderedColumnKeys.value;
3163
+ const firstKey = dataKeys[0];
3164
+ const lastKey = dataKeys[dataKeys.length - 1];
2868
3165
  const startFold = offsetStartValue.value;
2869
3166
  const endFold = offsetEndValue.value;
2870
- if (startFold && tracks['0']) {
2871
- tracks['0'] = `calc(${tracks['0']} - (${startFold}))`;
3167
+ if (startFold && firstKey && tracks[firstKey]) {
3168
+ tracks[firstKey] = `calc(${tracks[firstKey]} - (${startFold}))`;
2872
3169
  if (!props.selectable && startRegion.value)
2873
3170
  tracks.select = startRegion.value;
2874
3171
  }
2875
- if (endFold && tracks[lastKey]) {
3172
+ if (endFold && lastKey && tracks[lastKey]) {
2876
3173
  tracks[lastKey] = `calc(${tracks[lastKey]} - (${endFold}))`;
2877
- if (!props.actions && endRegion.value) tracks.actions = endRegion.value;
3174
+ if (!hasActions.value && endRegion.value) tracks.actions = endRegion.value;
2878
3175
  }
3176
+ // The order a child turns keys into gridlines with: `select` region first
3177
+ // (own or reserved), the rendered data keys, the `actions` region last.
3178
+ const publishedKeys = [
3179
+ ...('select' in tracks ? ['select'] : []),
3180
+ ...dataKeys,
3181
+ ...('actions' in tracks ? ['actions'] : []),
3182
+ ];
2879
3183
  freezeVirtualWidths(resolved);
2880
3184
  syncPlaceholderBars(resolved);
2881
3185
  // FLAT ROWS: the resolved list IS what a body row uses as its own tracks.
@@ -2883,10 +3187,10 @@ const measureTracks = () => {
2883
3187
  // (the header cells' observer) carries the body along for free.
2884
3188
  if (virtualActive.value) virtualRowTracks.value = joined;
2885
3189
  lastResolvedTracks = resolved;
2886
- setTracks(tracks);
3190
+ setTracks(tracks, publishedKeys);
2887
3191
  // Sticky column offsets are prefix sums of these tracks: keep them current.
2888
- if (props.fixedColumns?.length) {
2889
- applyFixedTableColumns(root, props.fixedColumns, resolved);
3192
+ if (resolvedPins.value.length) {
3193
+ applyFixedTableColumns(root, resolvedPins.value, resolved);
2890
3194
  }
2891
3195
  };
2892
3196
 
@@ -3043,7 +3347,9 @@ const containerClass = computed(() => ({
3043
3347
 
3044
3348
  const replacementContentSpan = computed(
3045
3349
  () =>
3046
- props.columns.length + Number(!!props.selectable) + Number(!!props.actions)
3350
+ visibleColumns.value.length +
3351
+ Number(!!props.selectable) +
3352
+ Number(hasActions.value)
3047
3353
  );
3048
3354
 
3049
3355
  const slotNamesByKey = computed(() =>
@@ -3278,6 +3584,7 @@ const headerAffixScope = (
3278
3584
  sortable: header.sortable,
3279
3585
  sortOrder: header.sortOrder,
3280
3586
  toggleSort: () => toggleSortFor(header.key),
3587
+ hideColumn: () => hideColumn(header.key),
3281
3588
  });
3282
3589
 
3283
3590
  /**
@@ -4365,7 +4672,7 @@ const freezeVirtualWidths = (resolved: string[]) => {
4365
4672
  structural;
4366
4673
  return;
4367
4674
  }
4368
- const column = columns[Number(track.key)];
4675
+ const column = columns.find((c) => String(c.key) === track.key);
4369
4676
  if (!column || column.width != null) return;
4370
4677
  const key = String(column.key);
4371
4678
  if (key in virtualWidths.value || key in (next ?? {})) return;
@@ -4786,7 +5093,7 @@ const dataRowContext = computed<DataRowContext>(() => ({
4786
5093
  selectable: props.selectable,
4787
5094
  multiple: !!props.multiple,
4788
5095
  readonly: !!props.readonly,
4789
- actions: !!props.actions,
5096
+ actions: hasActions.value,
4790
5097
  keyboardNavigation: !!props.keyboardNavigation,
4791
5098
  contentInert: contentInert.value,
4792
5099
  hasRowIndex: hasProvidedAccessibilityData.value,
@@ -4803,23 +5110,49 @@ const dataRowContext = computed<DataRowContext>(() => ({
4803
5110
  }));
4804
5111
 
4805
5112
  /**
4806
- * The pins as a VALUE: `[0, 1, { index: 8, position: 'right' }]` written inline
4807
- * in a template is a fresh array on every render of the parent, and the parent
4808
- * of a table re-renders for reasons of its own all the time. Watching the prop
4809
- * itself made every one of those renders look like "the pins changed" — which
4810
- * ran a full reconcile pass AND, because the reuse guard compared identities,
4811
- * threw away the tracks and read `grid-template-columns` back off the grid
4812
- * root. That read is a forced style+layout of a 5,000-row scrollport (~15ms
4813
- * measured on the bench playground), taken up to once per scrolled frame, for
4814
- * a prop whose VALUE never moved. The signature makes an identity-only change
4815
- * invisible here.
5113
+ * The pins, resolved from keys to RENDERED positions for the sticky pass:
5114
+ * `column.fixed` on each rendered data column (hidden ones pin nothing), plus
5115
+ * `fixed-select` at slot 0 and `fixed-actions` at the last slot when those
5116
+ * structural columns exist. The pass itself stays positional offsets are
5117
+ * prefix sums of the rendered tracks but the CONFIG follows the column, so
5118
+ * a reorder moves the pin with it instead of leaving it in the slot.
5119
+ */
5120
+ const resolvedPins = computed<FixedColumnConfig[]>(() => {
5121
+ const pins: FixedColumnConfig[] = [];
5122
+ const offset = props.selectable ? 1 : 0;
5123
+ if (props.selectable && props.fixedSelect) {
5124
+ pins.push({ index: 0, position: 'left' });
5125
+ }
5126
+ orderedColumns.value.forEach((column, index) => {
5127
+ if (column.fixed) {
5128
+ pins.push({ index: index + offset, position: column.fixed });
5129
+ }
5130
+ });
5131
+ if (hasActions.value && props.fixedActions) {
5132
+ pins.push({
5133
+ index: orderedColumns.value.length + offset,
5134
+ position: 'right',
5135
+ });
5136
+ }
5137
+ return pins;
5138
+ });
5139
+
5140
+ /**
5141
+ * The pins as a VALUE. `:columns` written inline in a template is a fresh
5142
+ * array on every render of the parent, and the parent of a table re-renders
5143
+ * for reasons of its own all the time; `resolvedPins` is a fresh array on
5144
+ * every one of those too. Watching it by identity made every such render look
5145
+ * like "the pins changed" — which ran a full reconcile pass AND, because the
5146
+ * reuse guard compared identities, threw away the tracks and read
5147
+ * `grid-template-columns` back off the grid root. That read is a forced
5148
+ * style+layout of a 5,000-row scrollport (~15ms measured on the bench
5149
+ * playground), taken up to once per scrolled frame, for pins whose VALUE
5150
+ * never moved. The signature makes an identity-only change invisible here.
4816
5151
  */
4817
5152
  const fixedColumnsKey = computed(() =>
4818
- (props.fixedColumns ?? [])
4819
- .map((column) =>
4820
- typeof column === 'number'
4821
- ? `${column}:left`
4822
- : `${column.index}:${column.position}`
5153
+ resolvedPins.value
5154
+ .map((pin) =>
5155
+ typeof pin === 'number' ? `${pin}:left` : `${pin.index}:${pin.position}`
4823
5156
  )
4824
5157
  .join(',')
4825
5158
  );
@@ -4842,18 +5175,19 @@ watch(
4842
5175
  () => [bodyRows.value, fixedColumnsKey.value, trackKeys.value] as const,
4843
5176
  ([, pins, tracks], previous) => {
4844
5177
  const previousPins = previous?.[1];
4845
- const fixedColumns = props.fixedColumns;
5178
+ const fixedColumns = resolvedPins.value;
4846
5179
  if (tableRef.value && (pins.length || previousPins?.length)) {
4847
5180
  // A `virtual` window move mounts rows but moves no track: reuse the
4848
5181
  // last publish instead of forcing a style read.
4849
5182
  //
4850
- // The reuse test used to also require `fixedColumns === previousFixed`,
4851
- // which asks a question about the CONSUMER's render rather than about
4852
- // this table's geometry. `:fixed-columns="[0, 1, { index: n, position:
4853
- // 'right' }]"` — the documented form, and what the bench playground
4854
- // writes — allocates a fresh array every time the parent renders, so
4855
- // the test failed on essentially every window move and the pass fell
4856
- // back to `getComputedStyle(root).gridTemplateColumns`. That read
5183
+ // The reuse test used to also require the pin config to be the SAME
5184
+ // array as last time, which asks a question about the CONSUMER's render
5185
+ // rather than about this table's geometry. An inline `:columns` array
5186
+ // — the documented form, and what the bench playground writes —
5187
+ // allocates a fresh array every time the parent renders (and so does
5188
+ // `resolvedPins`), so the test failed on essentially every window move
5189
+ // and the pass fell back to `getComputedStyle(root).gridTemplateColumns`.
5190
+ // That read
4857
5191
  // forces style and layout across the whole subgrid: on the 5,000-row
4858
5192
  // playground it measured ELEVEN MILLISECONDS each, once per window
4859
5193
  // move, in the middle of the scroll — the largest single item on a
@@ -4894,7 +5228,7 @@ watch(
4894
5228
  // just moved onto — ~500 cell touches per window move down to ~50.
4895
5229
  applyFixedTableColumns(
4896
5230
  tableRef.value,
4897
- fixedColumns ?? [],
5231
+ fixedColumns,
4898
5232
  reuse ? lastResolvedTracks : null,
4899
5233
  reuse
4900
5234
  );
@@ -5048,6 +5382,33 @@ export type BbTableColumn<Item = any> = BaseColumn<Item> & {
5048
5382
  */
5049
5383
  thClass?: Classes;
5050
5384
 
5385
+ /**
5386
+ * Hides the column: it renders no header and no cells and contributes no
5387
+ * grid track, but stays DECLARED — its key keeps its slot in `order`, the
5388
+ * header reorder writes the model back complete around it, and it comes
5389
+ * back exactly where it was when the flag clears. This is how a column
5390
+ * panel or a narrow-screen breakpoint hides a column; filtering `columns`
5391
+ * is for a column that does not exist at all (a field this user does not
5392
+ * have). Like `width`, the definition is the single declared truth: a
5393
+ * header's `hideColumn()` sets a per-mount override that `hide:column` reports
5394
+ * and that clears the moment this flag changes to anything but `true`
5395
+ * (write the reported key back as `hidden: true` to persist it).
5396
+ */
5397
+ hidden?: boolean;
5398
+
5399
+ /**
5400
+ * Pins the column: its cells become `position: sticky` on the given side,
5401
+ * in place — the column keeps its render slot and sticks there as the rest
5402
+ * scrolls under it (spreadsheet freeze panes, not a pinned region). The
5403
+ * offset accumulates the pinned columns before it on the same side, so
5404
+ * pin a contiguous run from the edge: a lone `left` column in the middle
5405
+ * sticks at `left: 0` and slides over everything before it, by design.
5406
+ * The pin travels with the column through `order` and `reorderable`, and
5407
+ * a `hidden` pinned column pins nothing. The structural columns pin
5408
+ * through `fixed-select` / `fixed-actions` on the table.
5409
+ */
5410
+ fixed?: 'left' | 'right';
5411
+
5051
5412
  /**
5052
5413
  * Freezes the column to a fixed width (numbers are treated as px). When
5053
5414
  * omitted the column syncs its width to the parent table's matching track.
@@ -5055,26 +5416,36 @@ export type BbTableColumn<Item = any> = BaseColumn<Item> & {
5055
5416
  width?: number | string;
5056
5417
 
5057
5418
  /**
5058
- * For a nested table, where this column snaps onto the parent's grid. The
5059
- * parent's columns form snap points where `select` occupies `[0, 1]`, data
5060
- * column `k` occupies `[k + 1, k + 2]` and `actions` occupies `[N + 1, N + 2]`.
5419
+ * For a nested table, which parent column this column snaps onto by the
5420
+ * parent's column KEY, as written (`'client'`, not a position), plus the
5421
+ * reserved names `'select'` and `'actions'` for the parent's structural
5422
+ * columns.
5423
+ *
5424
+ * - A single key takes that parent track: `snap: 'client'`.
5425
+ * - A `[start, end]` pair spans from the start of `start` through the END
5426
+ * of `end`, inclusive: `['client', 'amount']`, or `['name', 'actions']`
5427
+ * to run across the parent's actions column too.
5428
+ * - A trailing `.digits` on a key is a fraction of that track — `'amount.5'`
5429
+ * as an end means "through half of `amount`", as a start "from halfway
5430
+ * into it". Pairs only; a key that itself ends in `.digits` is written
5431
+ * in its slot-name spelling (`line_1`).
5061
5432
  *
5062
- * - A single number is a start snap point that takes one parent track, e.g.
5063
- * `2` `[2, 3]`. It may skip ahead (`5` jumps straight to the sixth track),
5064
- * and the next column resumes from where it ends.
5065
- * - A `[start, end]` pair snaps between two points; the width is the sum of
5066
- * the fractional parent tracks it covers, so `[3, 4.5]` is the full fourth
5067
- * track plus half of the fifth. `end` of `-1` runs to the end of the data
5068
- * region.
5433
+ * When omitted the column pairs with the parent column of the SAME key when
5434
+ * there is one, and otherwise flows into the next parent track no other
5435
+ * column claimed. A column whose target the parent does not render (a
5436
+ * `hidden` parent column, a key it lacks) flows the same way; with no free
5437
+ * track left it content-sizes and overflows. The last column runs to the end
5438
+ * of the parent's data region unless it declares a `width` or an explicit
5439
+ * pair, so a table with fewer columns than its parent widens its last
5440
+ * content cell across the parent's trailing tracks rather than its actions
5441
+ * cell.
5069
5442
  *
5070
- * When omitted the column chains from the previous column's end to the next
5071
- * whole snap point (a one-to-one inheritance by index). The last column
5072
- * always runs to the end of the parent's data region (as if `[start, -1]`)
5073
- * unless it declares a `width` or an explicit pair, so a table with fewer
5074
- * columns than its parent widens its last content cell across the parent's
5075
- * trailing tracks rather than its actions cell.
5443
+ * Snaps resolve against the parent's RENDERED columns, so they are ignored
5444
+ * (dev builds warn) under a parent that reorders `reorderable`, or a
5445
+ * non-empty `order` model where the child falls back to positional
5446
+ * inheritance. Never reorder the snapped child itself.
5076
5447
  */
5077
- snap?: number | [start: number, end: number];
5448
+ snap?: string | [start: string, end: string];
5078
5449
  };
5079
5450
 
5080
5451
  export type BbTableProps<Item = any> = {
@@ -5085,10 +5456,6 @@ export type BbTableProps<Item = any> = {
5085
5456
  * label reads like the row) — look a cell up by `key`, never by position.
5086
5457
  */
5087
5458
  accessibleLabel?: (columns: MappedCell[], item: any) => string;
5088
- /**
5089
- * Displays the actions column.
5090
- */
5091
- actions?: boolean;
5092
5459
  /**
5093
5460
  * Label used in the header of the actions column.
5094
5461
  */
@@ -5175,22 +5542,27 @@ export type BbTableProps<Item = any> = {
5175
5542
  */
5176
5543
  fixed?: boolean;
5177
5544
  /**
5178
- * Definition for which column should be fixed.
5179
- * It can be an array of index of the column to fix on the left side of the table or
5180
- * an array of objects indicating the index and the position `left` or `right` where to affix the columns.
5545
+ * Pins the actions column (the one the `#actions` slot creates) to the
5546
+ * right edge, `position: sticky`, in place. Data columns pin through
5547
+ * `column.fixed`; this is the same feature for the structural column that
5548
+ * has no definition to carry it. No effect without an `#actions` slot.
5181
5549
  *
5182
- * Indices are POSITIONS in the rendered column list (a selection column is
5183
- * index 0), like a spreadsheet's freeze panes — not per-column pins. With
5184
- * `order` the pinned slot holds and whichever column lands in it is
5185
- * pinned; dragging a column out of a pinned slot unpins it.
5186
- *
5187
- * @defaultValue `[]`
5550
+ * @defaultValue `false`
5188
5551
  */
5189
- fixedColumns?: Array<number | { index: number; position: 'left' | 'right' }>;
5552
+ fixedActions?: boolean;
5190
5553
  /**
5191
5554
  * Boolean that sets the headers as sticky to the top of the table.
5192
5555
  */
5193
5556
  fixedHeaders?: boolean;
5557
+ /**
5558
+ * Pins the selection column to the left edge, `position: sticky`, in
5559
+ * place. Data columns pin through `column.fixed`; this is the same feature
5560
+ * for the structural column that has no definition to carry it. No effect
5561
+ * unless the table is `selectable`.
5562
+ *
5563
+ * @defaultValue `false`
5564
+ */
5565
+ fixedSelect?: boolean;
5194
5566
  /**
5195
5567
  * Defines the classes to be passed to the header row.
5196
5568
  */
@@ -5317,16 +5689,15 @@ export type BbTableProps<Item = any> = {
5317
5689
  * Used by `v-model:order`. Ordered array of column keys — the render
5318
5690
  * order of the data columns. Keys listed here render first, in this order;
5319
5691
  * declared columns it omits follow, in declaration order (so a column added
5320
- * to `columns` later renders last); keys that match no declared column render
5321
- * nothing but are PRESERVED in place on every write — a column you hide by
5322
- * removing it from `columns` (a column panel) keeps its slot and comes back
5323
- * where it was. The header drag / keyboard reorder of `reorderable`
5324
- * writes it back complete (every declared key present, duplicates dropped —
5325
- * first occurrence wins) with exactly the moved column relocated: every other
5326
- * key, hidden ones included, keeps its relative order, and a drop that
5327
- * changes nothing emits nothing. Works uncontrolled when unbound, exactly
5328
- * like `sort`. The `select` and `actions` columns are structural and never
5329
- * part of it.
5692
+ * to `columns` later renders last); a `hidden` column keeps its slot
5693
+ * without rendering, and keys that match no declared column render
5694
+ * nothing but are PRESERVED in place on every write. The header drag /
5695
+ * keyboard reorder of `reorderable` writes it back complete (every
5696
+ * declared key present, hidden ones included, duplicates dropped — first
5697
+ * occurrence wins) with exactly the moved column relocated: every other
5698
+ * key keeps its relative order, and a drop that changes nothing emits
5699
+ * nothing. Works uncontrolled when unbound, exactly like `sort`. The
5700
+ * `select` and `actions` columns are structural and never part of it.
5330
5701
  *
5331
5702
  * @defaultValue `[]`
5332
5703
  */
@@ -5564,6 +5935,14 @@ export type BbTableEvents = {
5564
5935
  * its declared width.
5565
5936
  */
5566
5937
  (e: 'resize:column', key: string, width: number | null): void;
5938
+ /**
5939
+ * A column was hidden from its header (a `#header:<key>` /
5940
+ * `#header:<key>:append` control calling the slot's `hideColumn()`). Fired once;
5941
+ * the table keeps the column hidden for the mount, or until the column's
5942
+ * `hidden` flag changes — write `hidden: true` into the definition to
5943
+ * persist it, `hidden: false` (a column panel) to show it again.
5944
+ */
5945
+ (e: 'hide:column', key: string): void;
5567
5946
  (e: 'update:expandedItems', value: any[]): void;
5568
5947
  (e: 'update:unselectedItems', value: any[]): void;
5569
5948
  (e: 'item:selected', value: any): void;
@@ -5593,6 +5972,12 @@ export type BbTableHeaderAffixSlotProps<Item = any> = {
5593
5972
  sortOrder?: 'asc' | 'desc' | null;
5594
5973
  /** Cycles this column's sort: unsorted → asc → desc → unsorted. */
5595
5974
  toggleSort?: () => void;
5975
+ /**
5976
+ * Hides this column (fires `hide:column`). One-way by nature — a hidden
5977
+ * column has no header to call it from; showing it again is the
5978
+ * definition's job (`hidden: false`).
5979
+ */
5980
+ hideColumn?: () => void;
5596
5981
  };
5597
5982
 
5598
5983
  export type BbTableSlots<Item = any> = {
@@ -5668,7 +6053,11 @@ export type BbTableSlots<Item = any> = {
5668
6053
  */
5669
6054
  'no-data'?: (props: object) => any;
5670
6055
  /**
5671
- * Content rendered in the actions cell for each row.
6056
+ * Content rendered in the actions cell for each row. Providing this slot
6057
+ * is what CREATES the actions column (there is no prop): the header cell
6058
+ * (labelled by `actions-text`, replaceable through `#header:actions`) and
6059
+ * one cell per row appear with it, and go away with it. Pin it with
6060
+ * `fixed-actions`.
5672
6061
  * @param expanded - Whether this row is currently expanded.
5673
6062
  * @param expandProps - Bind these onto an expand-toggle button to wire up aria attributes and keyboard handling.
5674
6063
  * @param toggleExpanded - Toggles the expanded state for this row.
@@ -5752,6 +6141,8 @@ export type BbTableSlots<Item = any> = {
5752
6141
  sortable?: boolean;
5753
6142
  sortOrder?: 'asc' | 'desc' | null;
5754
6143
  toggleSort?: () => void;
6144
+ /** Header slots only: hides this column (fires `hide:column`). */
6145
+ hideColumn?: () => void;
5755
6146
  /**
5756
6147
  * Optional because this catch-all also types the `head:*` /
5757
6148
  * `header:*` column slots, which have no row and never receive one.
@@ -5770,7 +6161,7 @@ export type BbTableSlots<Item = any> = {
5770
6161
  .bb-table {
5771
6162
  /* The table's surface colour. Cells are transparent at rest so the row can
5772
6163
  tint them, but anything that sticks over scrolled content — the stuck
5773
- head band and `fixed-columns` cells — must be opaque, and paints this.
6164
+ head band and pinned (`column.fixed`) cells — must be opaque, and paints this.
5774
6165
  Set it on the table (`.my-table { --bg: … }`) when the table sits on a
5775
6166
  surface that is not `--bb-panel`; a nested table re-declares its own. */
5776
6167
  --bg: var(--bb-panel);
@@ -6010,7 +6401,7 @@ export type BbTableSlots<Item = any> = {
6010
6401
  is bounded by its own grid container — the one-row-tall row). Descendant
6011
6402
  form on purpose, so a nested table's head sticks along, as it always did.
6012
6403
  The stuck band paints `--bg` so scrolled rows never show through it.
6013
- Sticky-column header cells (inline sticky from `fixed-columns`) sit above
6404
+ Sticky-column header cells (inline sticky from `column.fixed`) sit above
6014
6405
  their scrolled siblings within the band. */
6015
6406
  /* Own head only (`> .bb-table__table >`): a descendant selector would also
6016
6407
  stick every NESTED table's head — a nested head then painted over the
@@ -6048,7 +6439,7 @@ export type BbTableSlots<Item = any> = {
6048
6439
  }
6049
6440
  }
6050
6441
 
6051
- /* A `fixed-columns` cell (inline `position: sticky` from the component)
6442
+ /* A pinned cell (`column.fixed`, inline `position: sticky` from the component)
6052
6443
  slides over its scrolled siblings, so it cannot stay transparent. It
6053
6444
  paints the row's current tint when the row has one (`--row-bg`, set by
6054
6445
  the hover / highlighted states below), else the table surface — so a
@@ -6090,7 +6481,7 @@ export type BbTableSlots<Item = any> = {
6090
6481
  A child that inherits its parent's tracks is sized to the parent's
6091
6482
  CONTENT width, so a scrollport here would have its right edge off in
6092
6483
  the overflow where nobody can see it, and `position: sticky` from
6093
- `fixed-columns` would affix to that instead of to the visible edge:
6484
+ `column.fixed` would affix to that instead of to the visible edge:
6094
6485
  the parent's actions pin correctly while the child's sit hundreds of
6095
6486
  pixels off-screen until you scroll all the way right.
6096
6487
 
@@ -6280,7 +6671,7 @@ export type BbTableSlots<Item = any> = {
6280
6671
  user-select: none;
6281
6672
  width: 9px;
6282
6673
  /* Above the neighbour cell it overhangs (positioned, z auto) and below a
6283
- pinned cell (z 2), so a handle scrolled under a `fixed-columns` header
6674
+ pinned cell (z 2), so a handle scrolled under a pinned header
6284
6675
  never slides over it. A pinned header's own handle lives inside that
6285
6676
  cell's stacking context and keeps its overhang. */
6286
6677
  z-index: 1;