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
@@ -69,7 +69,7 @@ needs is declared here, not in markup:
69
69
  `(content, key, item) => Classes` for state-dependent styling. Table-level
70
70
  `thClass` / `tdClass` / `rowClass` / `headerRowClass` apply everywhere; a
71
71
  row's classes accumulate the table `rowClass` plus every column's.
72
- - **`snap`** maps a nested table's column onto its parent's tracks — see
72
+ - **`snap`** names the parent column a nested table's column sits under — see
73
73
  Nested tables.
74
74
 
75
75
  **Columns, formatters and a per-row class**
@@ -198,11 +198,11 @@ pre-mapping your items:
198
198
  ```
199
199
 
200
200
  **`actions` and `select` are not columns.** Both are rendered by the table
201
- itself, from the `actions` and `selectable` props. Putting
202
- `{ key: 'actions' }` in `columns` adds an ordinary data column that reads
203
- `item.actions`, so you get a stray empty cell _and_ the `#actions` slot never
204
- renders which looks like a broken slot and is really a bad column list. Dev
205
- builds warn.
201
+ itself: the selection column from the `selectable` prop, the actions column
202
+ from the `#actions` slot providing the slot is what creates the column,
203
+ there is no prop. Putting `{ key: 'actions' }` in `columns` adds an ordinary
204
+ data column that reads `item.actions`, so you get a stray empty cell next to
205
+ the real one. Dev builds warn.
206
206
 
207
207
  ### Custom cells and headers
208
208
 
@@ -225,7 +225,8 @@ Body cell scope: `content` (the formatted value), `item` (raw row), `value`
225
225
  one-way state; the `toggle*` callbacks are the write path.
226
226
 
227
227
  Header scope: `label`, `items` (the full list — handy for counts), `sortable`,
228
- `sortOrder`, `toggleSort`, `classes`.
228
+ `sortOrder`, `toggleSort`, `hideColumn` (hides the column, see "Column
229
+ visibility"), `classes`.
229
230
 
230
231
  **Add beside a header instead of replacing it** with
231
232
  `#header:<key>:prepend` / `#header:<key>:append` — rendered before / after the
@@ -837,12 +838,14 @@ The model's contract is what makes a column panel trivial:
837
838
 
838
839
  - Keys listed first render first; declared columns the array omits follow, in
839
840
  declaration order (a column added to `columns` later renders last).
840
- - Keys that match no declared column render nothing but are **preserved in
841
- place**. So hide a column by filtering it out of `columns` — never out of
842
- the order and it comes back exactly where it was.
841
+ - A `hidden` column (next section) keeps its slot without rendering, and keys
842
+ that match no declared column render nothing but are **preserved in
843
+ place**a column that exists only for some users comes back exactly where
844
+ it was.
843
845
  - A user move relocates **exactly one key**; everything else, hidden keys
844
846
  included, keeps its relative order. The first move on a partial (or empty)
845
- model emits the completed array — persist that.
847
+ model emits the completed array — every declared key, hidden ones included
848
+ — so persist that and the model is complete from then on.
846
849
  - A drop that changes nothing emits nothing; unbound, the table keeps the
847
850
  order as per-mount state like `sort`.
848
851
 
@@ -853,8 +856,9 @@ The model's contract is what makes a column panel trivial:
853
856
  <div class="flex max-w-2xl flex-col gap-3">
854
857
  <!-- ONE order, two writers: the panel's buttons and the table's own
855
858
  header drag / keyboard handles both write `v-model:order`.
856
- Hiding a column only filters `columns` its key stays in the order,
857
- so it comes back where it was. -->
859
+ Visibility is a flag ON the definition (`hidden`), so a hidden
860
+ column stays declared, keeps its slot in the order, and comes back
861
+ where it was. -->
858
862
  <ul
859
863
  aria-label="Columns"
860
864
  class="flex flex-wrap gap-2 text-xs text-[color:var(--bb-text-muted)]"
@@ -867,7 +871,7 @@ The model's contract is what makes a column panel trivial:
867
871
  <BbCheckbox
868
872
  :label="column.label"
869
873
  :model-value="!hidden.has(column.key)"
870
- @update:model-value="toggleVisible(column.key)"
874
+ @update:model-value="setHidden(column.key, !$event)"
871
875
  />
872
876
  <BbButton
873
877
  :aria-label="`Move ${column.label} left`"
@@ -893,15 +897,30 @@ The model's contract is what makes a column panel trivial:
893
897
  <BbTable
894
898
  v-model:order="order"
895
899
  caption="Invoices"
896
- :columns="visibleColumns"
900
+ :columns="columns"
897
901
  compact
898
902
  item-value="id"
899
903
  :items="invoices"
900
904
  reorderable
901
- />
905
+ @hide:column="(key) => setHidden(key, true)"
906
+ >
907
+ <!-- A "hide" control beside every header: the slot's `hideColumn()` is
908
+ one-way (a hidden column has no header), so the panel above
909
+ is what brings a column back. -->
910
+ <template #header:append="{ hideColumn, label }">
911
+ <BbButton
912
+ :aria-label="`Hide ${label}`"
913
+ icon="lucide:eye-off"
914
+ size="xs"
915
+ variant="ghost"
916
+ @click="hideColumn"
917
+ />
918
+ </template>
919
+ </BbTable>
902
920
  </div>
903
921
  <p class="text-xs text-[color:var(--bb-text-muted)]">
904
- Order model: <code>{{ JSON.stringify(order) }}</code>
922
+ Order model: <code>{{ JSON.stringify(order) }}</code> · hidden:
923
+ <code>{{ JSON.stringify([...hidden]) }}</code>
905
924
  </p>
906
925
  </div>
907
926
  </template>
@@ -923,8 +942,8 @@ const euro = new Intl.NumberFormat('en-IE', {
923
942
  currency: 'EUR',
924
943
  });
925
944
 
926
- // The declaration: what a column IS. Never reordered.
927
- const columns: Array<BbTableColumn<Invoice>> = [
945
+ // The declaration: what a column IS. Never reordered, never filtered.
946
+ const definitions: Array<BbTableColumn<Invoice>> = [
928
947
  { key: 'number', label: 'Invoice', width: 110 },
929
948
  { key: 'client', label: 'Client' },
930
949
  { key: 'issued', label: 'Issued', width: 110 },
@@ -968,33 +987,41 @@ const invoices: Invoice[] = [
968
987
  },
969
988
  ];
970
989
 
971
- // The state: render order (keys). Persist THIS. Seed it with the full key
972
- // list it also completes itself on the first move.
973
- const order = ref<string[]>(columns.map((column) => column.key));
990
+ // The state, both persistable as-is: render order (keys) and the hidden set.
991
+ // A partial or empty order is fine the first move writes back the
992
+ // complete array, hidden keys in their slots, because every column stays
993
+ // declared below.
994
+ const order = ref<string[]>([]);
974
995
  const hidden = ref(new Set<string>());
975
996
 
976
- // Visibility is just filtering the declaration; the hidden key keeps its
977
- // slot in `order`.
978
- const visibleColumns = computed(() =>
979
- columns.filter((column) => !hidden.value.has(column.key))
997
+ // The definitions, with the persisted visibility mapped in. Same shape you
998
+ // would use for persisted widths (`width`) — the definition is the truth.
999
+ const columns = computed(() =>
1000
+ definitions.map((column) => ({
1001
+ ...column,
1002
+ hidden: hidden.value.has(column.key),
1003
+ }))
980
1004
  );
981
1005
 
982
- // The panel lists every declared column in the order the model has them.
983
- const panelColumns = computed(() =>
984
- [...columns].sort(
985
- (a, b) => order.value.indexOf(a.key) - order.value.indexOf(b.key)
986
- )
987
- );
1006
+ // The panel lists every declared column in the order the model has them
1007
+ // (omitted keys follow in declaration order, exactly as the table renders).
1008
+ const panelColumns = computed(() => {
1009
+ const rank = (key: string) => {
1010
+ const index = order.value.indexOf(key);
1011
+ return index < 0 ? order.value.length : index;
1012
+ };
1013
+ return [...definitions].sort((a, b) => rank(a.key) - rank(b.key));
1014
+ });
988
1015
 
989
- const toggleVisible = (key: string) => {
1016
+ const setHidden = (key: string, value: boolean) => {
990
1017
  const next = new Set(hidden.value);
991
- if (next.has(key)) next.delete(key);
992
- else next.add(key);
1018
+ if (value) next.add(key);
1019
+ else next.delete(key);
993
1020
  hidden.value = next;
994
1021
  };
995
1022
 
996
1023
  const move = (key: string, delta: 1 | -1) => {
997
- const next = [...order.value];
1024
+ const next = panelColumns.value.map((column) => column.key);
998
1025
  const from = next.indexOf(key);
999
1026
  const to = from + delta;
1000
1027
  if (from < 0 || to < 0 || to >= next.length) return;
@@ -1005,22 +1032,50 @@ const move = (key: string, delta: 1 | -1) => {
1005
1032
  </script>
1006
1033
  ```
1007
1034
 
1008
- **Pinned slots are positions, not columns.** `fixed-columns` indexes rendered
1009
- slots like a spreadsheet's freeze panes (a selection column is slot 0). With
1010
- column order the slot holds: whichever column lands in it is pinned, and
1011
- dragging the identity column out of its pinned slot (slot 1 when
1012
- `selectable`, slot 0 otherwise) unpins it (AG Grid pins columns; BbTable
1013
- pins positions). Keep the reorder + pin combination for slots that
1014
- never move `select`, `actions`, or a first column you also lock by
1015
- normalizing the model in your `@update:order` handler.
1016
-
1017
- **Nested tables.** An inheriting child aligns to the parent's rails by
1018
- **position**, so a reordered parent re-pairs the child's columns unless the
1019
- child follows. Give both tables the same `v-model:order` when they
1020
- share keys (and the child declares no `snap`) and they stay paired by key.
1021
- `snap` points are positional in the parent's rendered order: reorder the
1022
- parent and a snap-mapped child follows its slots, but never reorder (or share
1023
- the order with) the snap-mapped child itself dev builds warn.
1035
+ ### Column visibility
1036
+
1037
+ `hidden: true` on a column definition hides it: no header, no cells, no grid
1038
+ track. The column stays **declared**, and that is the whole point — the order
1039
+ model is completed against every declared key, so a hidden column keeps its
1040
+ slot through any number of header drags, and it returns to that slot the
1041
+ moment the flag clears. Two rules follow:
1042
+
1043
+ - **Hidden means "exists, not shown".** A column panel, a persisted
1044
+ preference, a narrow-screen breakpoint all write `hidden` into the
1045
+ definition (a `computed` over your persisted set — the example above does
1046
+ exactly that). Filter `columns` only for a column that does **not exist**
1047
+ for this user (a custom field they don't have); its key, if a saved order
1048
+ still lists it, is preserved in place like any unknown key.
1049
+ - **There is no visibility model, on purpose** the `width` shape, not the
1050
+ `sort` one. The definition is the single declared truth. Header slots
1051
+ (`#header:<key>`, `#header:<key>:append`, the table-wide `#header:append`)
1052
+ receive a `hideColumn()` callback for a "Hide column" control of your own; calling
1053
+ it hides the column for the mount and fires `@hide:column="(key) => …"`
1054
+ once. Persist by writing `hidden: true` back into the definition — the
1055
+ override matches it and stays; write `hidden: false` (the panel showing the
1056
+ column again) and the definition wins. Unbound, the hide lives for the
1057
+ mount, like an unbound resize. A hidden column has no header, so `hideColumn()`
1058
+ is one-way by nature: if you offer it, own the persistence and give the
1059
+ user a panel that shows columns again.
1060
+
1061
+ A nested table that shares its parent's keys shares its `hidden` flags too,
1062
+ the same way it shares `order` — a hidden parent column has no track for a
1063
+ child cell to land on.
1064
+
1065
+ **Pins travel with the column.** `fixed` is a field on the column
1066
+ definition (see "Pinned columns"), so a reorder moves the pin with the cell:
1067
+ drag the identity column to slot 3 and it is still sticky, now at slot 3.
1068
+ The offset is still positional — the pinned columns before it on the same
1069
+ side — so a pinned column dragged away from the edge sticks at `left: 0` and
1070
+ slides over the columns before it. Lock the pinned columns in place by
1071
+ normalizing the model in your `@update:order` handler when that matters.
1072
+
1073
+ **Nested tables.** A parent that uses `order` or `reorderable` is inherited
1074
+ by **position**: its columns can move, so the child's `snap` targets and
1075
+ same-key pairing are ignored there (dev builds warn) and the child aligns to
1076
+ the parent's rails slot by slot. Give both tables the same `v-model:order`
1077
+ when they share keys and they stay paired. Never reorder (or share the order
1078
+ with) a snapped child itself — dev builds warn.
1024
1079
 
1025
1080
  Apply the model synchronously in your handler and persist afterwards: an
1026
1081
  `await` before the write moves the cell in a later flush, after the table
@@ -1392,7 +1447,7 @@ fields.
1392
1447
  Track open rows with `v-model:expanded-items` (an array of row values — set
1393
1448
  `item-value` so expansion is stable across refetches) and render their detail
1394
1449
  through the `#expand` slot, a full-width row below the main one. Trigger it
1395
- from the `#actions` slot (enable the column with `actions`, label it with
1450
+ from the `#actions` slot (the slot creates the actions column; label it with
1396
1451
  `actions-text`): the scope hands you `toggleExpanded` — a **callback** — plus
1397
1452
  the read-only `expanded` flag. Drive open/close through the callback; the
1398
1453
  `expanded` prop is one-way state, not something to mutate.
@@ -1409,7 +1464,6 @@ the read-only `expanded` flag. Drive open/close through the callback; the
1409
1464
  >
1410
1465
  <BbTable
1411
1466
  v-model:expanded-items="expanded"
1412
- actions
1413
1467
  actions-text="Details"
1414
1468
  caption="Invoices"
1415
1469
  :columns="columns"
@@ -1633,11 +1687,19 @@ above it. The mechanics:
1633
1687
  - `inherit-column-widths` — `true` inherits from the nearest ancestor table;
1634
1688
  a string targets a specific ancestor's `id` (useful when an intermediate
1635
1689
  table has a different column count).
1636
- - By default child columns map one-to-one onto the parent's data columns. A
1637
- column's `snap` remaps it: a number is a start track (`2` occupies the
1638
- parent's third data region), `[start, end]` spans a range (fractions
1639
- allowed; `end: -1` runs to the end of the data region). Structural columns
1640
- (`select`, actions) are accounted for automatically.
1690
+ - By default a child column pairs with the parent column of the **same key**,
1691
+ and a column the parent lacks flows into the next parent track no other
1692
+ column claimed. A column's `snap` names the parent column instead:
1693
+ `snap: 'client'` takes that track, `['client', 'amount']` spans from the
1694
+ start of one through the end of the other, and the parent's structural
1695
+ columns are targets by name (`'select'`, `'actions'` — `['name',
1696
+ 'actions']` runs a column across the parent's actions cell too). A
1697
+ trailing `.digits` on a key in a pair is a fraction of that track
1698
+ (`['a', 'b.5']` = all of `a` plus half of `b`). Snaps and keys resolve
1699
+ against the parent's **rendered** columns: a target the parent hides (or
1700
+ never had) makes the column flow like an unsnapped one; a snap that lands
1701
+ before the previous column's cannot tile and flows too (dev builds warn);
1702
+ under a parent that reorders, snaps are ignored (see Column order).
1641
1703
  - A child with **more** columns than the parent content-sizes the extras and
1642
1704
  overflows into the parent's scroll (see below); with **fewer**, the leading
1643
1705
  columns stay 1:1, the actions column inherits the parent's actions width,
@@ -1645,15 +1707,16 @@ above it. The mechanics:
1645
1707
  both tables end flush (a right-aligned total lands under the parent's last
1646
1708
  column). Give that last column a `width` or an explicit `[start, end]`
1647
1709
  snap only if you want it to stop short — the leftover then joins the
1648
- actions region instead.
1710
+ actions region instead. A column with nothing free left to flow into is an
1711
+ overflow column too.
1649
1712
  - **An inheriting child does not scroll on its own — the parent's scrollport
1650
1713
  is the only one.** That is what keeps the two grids locked together: a child
1651
1714
  with its own scroll position would fall out of alignment the moment either
1652
1715
  one moved, which is the whole thing `inherit-column-widths` exists to
1653
1716
  prevent. One scrollbar moves both.
1654
1717
  - **The parent's `selectable`/`actions` columns are handled for you** — the
1655
- child's content columns map to the parent's content columns 1:1 (no `snap`
1656
- needed for that), and the parent's select/actions _regions_ are reserved on
1718
+ child's content columns pair with the parent's content columns by key (no
1719
+ `snap` needed for that), and the parent's select/actions _regions_ are reserved on
1657
1720
  each side: a child with its own select or actions column widens that cell
1658
1721
  to the parent's (two equal cells, controls at the row's end, so the ⌃ sits
1659
1722
  under the parent's); a child without one folds the region into its first/
@@ -1685,7 +1748,6 @@ above it. The mechanics:
1685
1748
  >
1686
1749
  <BbTable
1687
1750
  v-model:expanded-items="expanded"
1688
- actions
1689
1751
  actions-text="Line items"
1690
1752
  caption="Invoices"
1691
1753
  :columns="columns"
@@ -1783,27 +1845,29 @@ const expanded = ref<string[]>([]);
1783
1845
 
1784
1846
  #### Affixed actions on both tables
1785
1847
 
1786
- A nested table can carry its own `fixed-columns` — child rows usually need
1787
- their own row actions — and both action columns pin to the same visible edge:
1848
+ A nested table can pin its own actions column with `fixed-actions` — child
1849
+ rows usually need their own row actions — and both action columns pin to the
1850
+ same visible edge:
1788
1851
 
1789
1852
  ```vue
1790
1853
  <BbTable
1791
1854
  id="programmes"
1792
- actions
1793
1855
  :columns="columns"
1794
- :fixed-columns="[{ index: columns.length, position: 'right' }]"
1856
+ fixed-actions
1795
1857
  :items="rows"
1796
1858
  item-value="id"
1797
1859
  >
1860
+ <template #actions="{ item }">…</template>
1798
1861
  <template #expand="{ item }">
1799
1862
  <BbTable
1800
- actions
1801
1863
  :columns="childColumns"
1802
- :fixed-columns="[{ index: childColumns.length, position: 'right' }]"
1864
+ fixed-actions
1803
1865
  inherit-column-widths="programmes"
1804
1866
  :items="item.children"
1805
1867
  item-value="id"
1806
- />
1868
+ >
1869
+ <template #actions="{ item }">…</template>
1870
+ </BbTable>
1807
1871
  </template>
1808
1872
  </BbTable>
1809
1873
  ```
@@ -1812,9 +1876,10 @@ This works because the child shares the parent's scrollport. `position: sticky`
1812
1876
  resolves against the nearest scrolling ancestor, and the child's own box is as
1813
1877
  wide as the parent's **content** — so if it opened a scrollport of its own, the
1814
1878
  child's actions would affix to an edge sitting out in the overflow, hundreds of
1815
- pixels past what anyone can see, while the parent's pinned correctly. Snap the
1816
- child's actions column onto the parent's actions track (see the snapping rule
1817
- above) so the two columns line up rather than merely both being pinned.
1879
+ pixels past what anyone can see, while the parent's pinned correctly. The
1880
+ child's actions column inherits the parent's actions region by itself (the
1881
+ structural rule above), so the two columns line up rather than merely both
1882
+ being pinned.
1818
1883
 
1819
1884
  A pinned cell is opaque — it paints the table's `--bg` surface because it
1820
1885
  slides over its siblings — so a nested table sitting on a tinted expand row
@@ -2102,7 +2167,6 @@ open-in-new-tab for free.
2102
2167
  class="max-w-2xl overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
2103
2168
  >
2104
2169
  <BbTable
2105
- actions
2106
2170
  actions-text="Actions"
2107
2171
  caption="Invoices"
2108
2172
  :columns="columns"
@@ -2438,10 +2502,8 @@ canonical palette lives in the
2438
2502
 
2439
2503
  `compact` tightens row padding. `fixed-headers` makes the header stick to the
2440
2504
  top of the nearest scroll container. `fixed` switches to an equal-width fixed
2441
- layout. `fixed-columns` pins columns while the rest scroll horizontally: an
2442
- array of column indices (left-pinned) or `{ index, position: 'left'|'right' }`
2443
- objects; indices count rendered columns (a selection column is index 0), and
2444
- an out-of-range index logs a console error and is skipped. When content can't
2505
+ layout. Pinned columns (next section) stick while the rest scroll
2506
+ horizontally. When content can't
2445
2507
  fit, the table keeps every column at its measured minimum — its header label
2446
2508
  included — and overflows its container instead of clipping; this is by
2447
2509
  design, not a bug to work around. Note that truncating cells yourself
@@ -2492,13 +2554,51 @@ your wrapper actually sits on before sprinkling `min-width: 0` everywhere.)
2492
2554
  </div>
2493
2555
  ```
2494
2556
 
2557
+ ### Pinned columns
2558
+
2559
+ `fixed: 'left' | 'right'` on a column definition makes its cells
2560
+ `position: sticky` on that side — in place, at the column's own render
2561
+ position, like a spreadsheet's freeze panes. Pin a contiguous run from the
2562
+ edge and the offsets accumulate (the second left-pinned column sticks right
2563
+ after the first). The two structural columns have no definition to carry the
2564
+ field, so they pin through the table: `fixed-select` for the selection
2565
+ column and `fixed-actions` for the actions column (each has one sensible
2566
+ side, so both are booleans).
2567
+
2568
+ ```vue
2569
+ <BbTable :columns="columns" fixed-actions fixed-select :items="rows" selectable>
2570
+ <template #actions="{ item }">…</template>
2571
+ </BbTable>
2572
+ ```
2573
+
2574
+ ```ts
2575
+ const columns = [
2576
+ { key: 'name', label: 'Name', fixed: 'left' },
2577
+ { key: 'region', label: 'Region' },
2578
+ { key: 'amount', label: 'Amount', align: 'right' },
2579
+ ];
2580
+ ```
2581
+
2582
+ Three rules:
2583
+
2584
+ - **The pin is the column's.** Under `order` / `reorderable` it travels with
2585
+ the cell, and a `hidden` pinned column pins nothing. The offset is still
2586
+ positional, so a lone left-pinned column dragged to the middle sticks at
2587
+ `left: 0` and slides over the columns before it — by design; lock the
2588
+ pinned columns in place by normalizing `@update:order` if you need to.
2589
+ - **Pinned cells are opaque** — they paint the table's `--bg` (see Styling
2590
+ hooks), so a table on a tinted surface needs `--bg` set once.
2591
+ - **Nested tables pin against the ancestor scrollport.** A child that
2592
+ inherits the parent's tracks has no scrollport of its own, so both tables'
2593
+ pins land on the same visible edge (see Nested tables).
2594
+
2495
2595
  ### Virtual rows
2496
2596
 
2497
2597
  `virtual` windows the body: only the rows in the vertical scrollport (plus a
2498
2598
  few on each side) are mounted, and two spacer rows hold the scrollport at the
2499
2599
  full list height, so 5,000 or 50,000 rows scroll at the cost of a few dozen.
2500
2600
  Everything else is the same table — the rows stay in flow in the grid, so the
2501
- stuck header, `fixed-columns`, nested tables, selection, highlight and the
2601
+ stuck header, pinned columns, nested tables, selection, highlight and the
2502
2602
  keyboard grammar work untouched. One prop, no knobs.
2503
2603
 
2504
2604
  - **Give it a scroller.** Bound the table's height (`max-height` / `height`
@@ -2556,7 +2656,6 @@ keyboard grammar work untouched. One prop, no knobs.
2556
2656
  class="max-h-80"
2557
2657
  :columns="columns"
2558
2658
  compact
2559
- :fixed-columns="[0]"
2560
2659
  item-value="id"
2561
2660
  :items="rows"
2562
2661
  virtual
@@ -2576,7 +2675,7 @@ type Entry = {
2576
2675
  };
2577
2676
 
2578
2677
  const columns: Array<BbTableColumn<Entry>> = [
2579
- { key: 'number', label: 'Invoice' },
2678
+ { key: 'number', label: 'Invoice', fixed: 'left' },
2580
2679
  { key: 'client', label: 'Client' },
2581
2680
  { key: 'issued', label: 'Issued' },
2582
2681
  { key: 'amount', label: 'Amount', align: 'right' },
@@ -2601,14 +2700,14 @@ leave the screen. Collapse instead of scroll:
2601
2700
 
2602
2701
  - **Rank the columns.** Identity + status + the one number that matters stay;
2603
2702
  everything else is secondary.
2604
- - **Drop the secondary ones from `columns` at the breakpoint** — a `computed`
2605
- over `useMobile()` (or your own `matchMedia`) that filters the secondary keys
2606
- out. Removing the column removes its grid track. Do **not** hide cells with
2607
- `thClass`/`tdClass` + `display: none`: the table is a grid, a hidden cell
2608
- leaves its track in place and every following cell shifts one track over.
2609
- A dropped key keeps its slot in `order` (the write-back never drops
2610
- unknown keys), so the column returns where it was never filter the
2611
- order array.
2703
+ - **Flag the secondary ones `hidden` at the breakpoint** — a `computed`
2704
+ over `useMobile()` (or your own `matchMedia`) that maps the definitions
2705
+ with `hidden: isMobile && secondary.has(key)`. A hidden column has no grid
2706
+ track. Do **not** hide cells with `thClass`/`tdClass` + `display: none`:
2707
+ the table is a grid, a hidden cell leaves its track in place and every
2708
+ following cell shifts one track over. The hidden column stays declared, so
2709
+ it keeps its slot in `order` and returns where it was when the screen
2710
+ widens — never filter the order array.
2612
2711
  - **Stack what survives into the primary cell.** The identity slot renders the
2613
2712
  name plus the hidden facts under it, at `--bb-text-muted`, so nothing is
2614
2713
  actually lost.
@@ -2622,7 +2721,7 @@ records list.
2622
2721
  **The table's surface is `--bg`** (default `var(--bb-panel)`), a local on
2623
2722
  `.bb-table`. Cells are transparent at rest so the row can tint them, but
2624
2723
  anything that slides over scrolled content — the stuck header band with
2625
- `fixed-headers`, and `fixed-columns` cells — is opaque and paints `--bg` (a
2724
+ `fixed-headers`, and pinned (`fixed`) cells — is opaque and paints `--bg` (a
2626
2725
  pinned cell also picks up its row's hover/highlight tint, so it never reads
2627
2726
  as a hole). When a table sits on a surface that is not the panel colour, set
2628
2727
  the one knob and everything pinned follows:
@@ -2769,8 +2868,9 @@ reader.
2769
2868
  - `BbPagination` — shared page ref, or zero-wiring via `id` + `table-id`.
2770
2869
  - `useBbTableContext` (`./composables/useBbTableContext.md`) — bulk-action
2771
2870
  toolbars, detail panels, "reset sort" buttons living outside the table.
2772
- (Column order is not mirrored there: a column panel needs `columns` too,
2773
- so it lives where `v-model:order` is bound.)
2871
+ (Column order and visibility are not mirrored there: a column panel writes
2872
+ `v-model:order` and the `hidden` flags, so it lives where `columns` is
2873
+ built.)
2774
2874
  - `BbButton` — per-row links (`href`/`to`) in cells or `#actions`;
2775
2875
  `BbDropdown` for an overflow menu of row actions.
2776
2876
  - `BbBadge` / `BbAvatar` — status and identity cells via slots.
@@ -2796,10 +2896,17 @@ reader.
2796
2896
  `(columns, item)` with the mapped cells (in **render** order — look them up
2797
2897
  by `key`, never by position) and the raw record, and whatever it returns
2798
2898
  becomes the checkbox's label.
2799
- - `fixed-columns` pins **slots**, not columns: under `order` the
2800
- column that lands in a pinned slot is the pinned one (see Column order).
2899
+ - A pin (`column.fixed`) travels with its column under `order`, but its
2900
+ offset is positional: dragged away from the edge, a pinned column sticks
2901
+ at `left: 0` and slides over the columns before it (see Pinned columns).
2902
+ - The actions column has no prop: the `#actions` slot creates it. A
2903
+ conditional slot (`<template v-if="…" #actions>`) adds and removes the
2904
+ column live.
2801
2905
  - A column panel that holds a partial `order` receives the completed
2802
- array on the user's first move — its persisted list grows to every key.
2906
+ array on the user's first move — its persisted list grows to every declared
2907
+ key, `hidden` ones included. Hide with the flag, not by filtering
2908
+ `columns`: a filtered column is undeclared, so a partial model completes
2909
+ without it and the key is gone from the persisted order.
2803
2910
  - Enabling select-all does not re-emit an empty `modelValue` (see the
2804
2911
  select-all section) — in all-mode, read `select-all` +
2805
2912
  `unselected-items`, not `modelValue`.
@@ -2877,7 +2984,6 @@ reader.
2877
2984
  | Prop | Type | Default | Required | Description |
2878
2985
  | --- | --- | --- | --- | --- |
2879
2986
  | `accessibleLabel` | `((columns: MappedCell[], item: any) => string) \| undefined` | | | Function that accepts the columns and the current item as arguments and returns a label to be used for accessibility purposes. The cells arrive in RENDER order (they follow `order`, so the label reads like the row) — look a cell up by `key`… |
2880
- | `actions` | `boolean \| undefined` | `false` | | Displays the actions column. |
2881
2987
  | `actionsText` | `string \| undefined` | | | Label used in the header of the actions column. |
2882
2988
  | `align` | `"left" \| "center" \| "right" \| undefined` | `"left"` | | Text alignment of the columns. |
2883
2989
  | `caption` | `string \| undefined` | | | Caption that describes the content of the table. Used for accessibility purposes. |
@@ -2891,8 +2997,9 @@ reader.
2891
2997
  | `enforceCoherence` | `boolean \| undefined` | `false` | | After every load (initial, `dependencies` refetch or a change of the `items` array) prunes the row-keyed models of values whose row is no longer in the result set: `modelValue`, `unselected-items`, `highlighted` and `expanded-items`. Values… |
2892
2998
  | `expandedItems` | `any[] \| undefined` | `[]` | | Used by `v-model:expandedItems`. Array of the currently expanded items — toggled by the row expand control and by external writes (mirrors the `highlighted`/`sort` v-model surfaces). Drives the `#expand` slot rows and their `aria-expanded`/… |
2893
2999
  | `fixed` | `boolean \| undefined` | `false` | | Splits the width equally among the columns that declare no `width` (each undeclared column becomes a `minmax(0, 1fr)` track) making each column take up the same amount of space. |
2894
- | `fixedColumns` | `(number \| { index: number; position: "left" \| "right"; })[] \| undefined` | `[]` | | Definition for which column should be fixed. It can be an array of index of the column to fix on the left side of the table or an array of objects indicating the index and the position `left` or `right` where to affix the columns. Indices a… |
3000
+ | `fixedActions` | `boolean \| undefined` | `false` | | Pins the actions column (the one the `#actions` slot creates) to the right edge, `position: sticky`, in place. Data columns pin through `column.fixed`; this is the same feature for the structural column that has no definition to carry it. N… |
2895
3001
  | `fixedHeaders` | `boolean \| undefined` | `false` | | Boolean that sets the headers as sticky to the top of the table. |
3002
+ | `fixedSelect` | `boolean \| undefined` | `false` | | Pins the selection column to the left edge, `position: sticky`, in place. Data columns pin through `column.fixed`; this is the same feature for the structural column that has no definition to carry it. No effect unless the table is `selecta… |
2896
3003
  | `headerRowClass` | `Classes \| undefined` | | | Defines the classes to be passed to the header row. |
2897
3004
  | `highlighted` | `any` | `undefined` | | Used by `v-model:highlighted`. A parallel, UI-intent state to selection — typically "the row whose details are open". A single item value (or `null`) — highlight is single by design. **Opt-in, and `undefined` is the opt-out.** Leave it unbo… |
2898
3005
  | `id` | `string \| undefined` | | | Stable id for this table's width context. When omitted a unique id is generated. Nested tables use the nearest ancestor id to inherit widths. |
@@ -2933,6 +3040,7 @@ reader.
2933
3040
  - `click:row` — `(e: "click:row", event: MouseEvent, item: any): void`
2934
3041
  - `contextmenu:row` — `(e: "contextmenu:row", event: MouseEvent, item: any): void`
2935
3042
  - `dblclick:row` — `(e: "dblclick:row", event: MouseEvent, item: any): void`
3043
+ - `hide:column` — `(e: "hide:column", key: string): void` — A column was hidden from its header (a `#header:<key>` / `#header:<key>:append` control calling the slot's `hideColumn()`). Fired once; the table keeps the column hidden for the mount, or until the column's `hidden` flag changes — write `hi…
2936
3044
  - `item:selected` — `(e: "item:selected", value: any): void`
2937
3045
  - `item:unselected` — `(e: "item:unselected", value: any): void`
2938
3046
  - `resize:column` — `(e: "resize:column", key: string, width: number \| null): void` — A column was resized by the user (`resizable`): the new width in px, or `null` when the handle was double-clicked to reset the column to its declared width.
@@ -2946,7 +3054,7 @@ reader.
2946
3054
 
2947
3055
  ## Slots
2948
3056
 
2949
- - `actions` — scope: `{ expanded?: boolean \| undefined; expandProps?: ExpandControllerProps \| undefined; toggleExpanded?: (() => void) \| undefined; highlighted?: boolean \| undefi…` — Content rendered in the actions cell for each row.
3057
+ - `actions` — scope: `{ expanded?: boolean \| undefined; expandProps?: ExpandControllerProps \| undefined; toggleExpanded?: (() => void) \| undefined; highlighted?: boolean \| undefi…` — Content rendered in the actions cell for each row. Providing this slot is what CREATES the actions column (there is no prop): the header cell (labelled by `actions-text`, replaceable through `#header:actions`) and one cell per row appear wi…
2950
3058
  - `expand` — scope: `{ expanded: boolean; expandProps?: ExpandControllerProps \| undefined; toggleExpanded: () => void; highlighted?: boolean \| undefined; toggleHighlighted?: (() =…` — Full-width expandable content row rendered below each main row when expanded.
2951
3059
  - `header:actions` — scope: `{ text: string; }` — Replaces the default actions header cell content.
2952
3060
  - `header:append` — scope: `BbTableHeaderAffixSlotProps<Item>` — Content rendered after every data column's header cell content — the label + sort control, or the column's own `header:<key>` replacement. A column's `header:<key>:append` slot wins over this one for that column; they never stack. Branch on…
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "library": "bitboss-ui",
4
- "version": "3.0.0-beta.20",
4
+ "version": "3.0.0-beta.21",
5
5
  "upgrade": "v2-to-v3",
6
6
  "guide": "ai/guides/migration/v2-to-v3.md",
7
7
  "summary": {
8
8
  "renames": 9,
9
- "behaviourBreaks": 47
9
+ "behaviourBreaks": 50
10
10
  },
11
11
  "breaking": [
12
12
  {
@@ -306,10 +306,28 @@
306
306
  },
307
307
  {
308
308
  "kind": "behaviour",
309
- "description": "BbTable: hiding a column with thClass/tdClass + display: none (the v2/early-v3 narrow-screen recipe) no longer works — a hidden grid item keeps its track and shifts the following cells; remove the column from columns instead",
309
+ "description": "BbTable: hiding a column with thClass/tdClass + display: none (the v2/early-v3 narrow-screen recipe) no longer works — a hidden grid item keeps its track and shifts the following cells; set hidden: true on the column instead (filtering it out of columns was the beta.20 doctrine and lost the key from a partial order — DECISIONS § Column visibility, 2026-09-07)",
310
310
  "ruling": "grid substrate, plans/BBTABLE-GRID.md",
311
311
  "guide": "components/bb-table.md § Do not override; guide § Narrow screens"
312
312
  },
313
+ {
314
+ "kind": "behaviour",
315
+ "description": "BbTable actions prop removed — the #actions slot creates the column (a prop without the slot rendered an empty column; now it renders nothing)",
316
+ "ruling": "DECISIONS § Column pins, 2026-09-07",
317
+ "guide": "components/bb-table.md § The #actions slot creates the actions column"
318
+ },
319
+ {
320
+ "kind": "behaviour",
321
+ "description": "BbTable fixedColumns (rendered-slot indices) removed — fixed: 'left' \\",
322
+ "ruling": "'right' on the column definition + fixed-select / fixed-actions; pins follow the column through a reorder, sticky in place as before",
323
+ "guide": "DECISIONS § Column pins, 2026-09-07"
324
+ },
325
+ {
326
+ "kind": "behaviour",
327
+ "description": "BbTable column snap takes the PARENT's column key ('select' / 'actions' by name, inclusive pairs, 'key.5' fractions) instead of a gridline index; --table-{id}-track-{index} became --table-{id}-track-{key} (slot-name spelling). Numeric snaps are ignored at runtime",
328
+ "ruling": "DECISIONS § Keyed snap, 2026-09-07",
329
+ "guide": "components/bb-table.md § snap names the parent column"
330
+ },
313
331
  {
314
332
  "kind": "behaviour",
315
333
  "description": "BbTable highlight is opt-in: with highlighted unbound (or bound as undefined) clicking a row no longer applies bb-table-data__row--highlighted / aria-current — the table used to drive defineModel's local fallback, an unreadable state the app could not clear. Bind ref(null), not ref(); a context seed counts. click:row is unaffected",