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
@@ -27026,7 +27026,7 @@ needs is declared here, not in markup:
27026
27026
  `(content, key, item) => Classes` for state-dependent styling. Table-level
27027
27027
  `thClass` / `tdClass` / `rowClass` / `headerRowClass` apply everywhere; a
27028
27028
  row's classes accumulate the table `rowClass` plus every column's.
27029
- - **`snap`** maps a nested table's column onto its parent's tracks — see
27029
+ - **`snap`** names the parent column a nested table's column sits under — see
27030
27030
  Nested tables.
27031
27031
 
27032
27032
  **Columns, formatters and a per-row class**
@@ -27155,11 +27155,11 @@ pre-mapping your items:
27155
27155
  ```
27156
27156
 
27157
27157
  **`actions` and `select` are not columns.** Both are rendered by the table
27158
- itself, from the `actions` and `selectable` props. Putting
27159
- `{ key: 'actions' }` in `columns` adds an ordinary data column that reads
27160
- `item.actions`, so you get a stray empty cell _and_ the `#actions` slot never
27161
- renders which looks like a broken slot and is really a bad column list. Dev
27162
- builds warn.
27158
+ itself: the selection column from the `selectable` prop, the actions column
27159
+ from the `#actions` slot providing the slot is what creates the column,
27160
+ there is no prop. Putting `{ key: 'actions' }` in `columns` adds an ordinary
27161
+ data column that reads `item.actions`, so you get a stray empty cell next to
27162
+ the real one. Dev builds warn.
27163
27163
 
27164
27164
  ### Custom cells and headers
27165
27165
 
@@ -27182,7 +27182,8 @@ Body cell scope: `content` (the formatted value), `item` (raw row), `value`
27182
27182
  one-way state; the `toggle*` callbacks are the write path.
27183
27183
 
27184
27184
  Header scope: `label`, `items` (the full list — handy for counts), `sortable`,
27185
- `sortOrder`, `toggleSort`, `classes`.
27185
+ `sortOrder`, `toggleSort`, `hideColumn` (hides the column, see "Column
27186
+ visibility"), `classes`.
27186
27187
 
27187
27188
  **Add beside a header instead of replacing it** with
27188
27189
  `#header:<key>:prepend` / `#header:<key>:append` — rendered before / after the
@@ -27794,12 +27795,14 @@ The model's contract is what makes a column panel trivial:
27794
27795
 
27795
27796
  - Keys listed first render first; declared columns the array omits follow, in
27796
27797
  declaration order (a column added to `columns` later renders last).
27797
- - Keys that match no declared column render nothing but are **preserved in
27798
- place**. So hide a column by filtering it out of `columns` — never out of
27799
- the order and it comes back exactly where it was.
27798
+ - A `hidden` column (next section) keeps its slot without rendering, and keys
27799
+ that match no declared column render nothing but are **preserved in
27800
+ place**a column that exists only for some users comes back exactly where
27801
+ it was.
27800
27802
  - A user move relocates **exactly one key**; everything else, hidden keys
27801
27803
  included, keeps its relative order. The first move on a partial (or empty)
27802
- model emits the completed array — persist that.
27804
+ model emits the completed array — every declared key, hidden ones included
27805
+ — so persist that and the model is complete from then on.
27803
27806
  - A drop that changes nothing emits nothing; unbound, the table keeps the
27804
27807
  order as per-mount state like `sort`.
27805
27808
 
@@ -27810,8 +27813,9 @@ The model's contract is what makes a column panel trivial:
27810
27813
  <div class="flex max-w-2xl flex-col gap-3">
27811
27814
  <!-- ONE order, two writers: the panel's buttons and the table's own
27812
27815
  header drag / keyboard handles both write `v-model:order`.
27813
- Hiding a column only filters `columns` its key stays in the order,
27814
- so it comes back where it was. -->
27816
+ Visibility is a flag ON the definition (`hidden`), so a hidden
27817
+ column stays declared, keeps its slot in the order, and comes back
27818
+ where it was. -->
27815
27819
  <ul
27816
27820
  aria-label="Columns"
27817
27821
  class="flex flex-wrap gap-2 text-xs text-[color:var(--bb-text-muted)]"
@@ -27824,7 +27828,7 @@ The model's contract is what makes a column panel trivial:
27824
27828
  <BbCheckbox
27825
27829
  :label="column.label"
27826
27830
  :model-value="!hidden.has(column.key)"
27827
- @update:model-value="toggleVisible(column.key)"
27831
+ @update:model-value="setHidden(column.key, !$event)"
27828
27832
  />
27829
27833
  <BbButton
27830
27834
  :aria-label="`Move ${column.label} left`"
@@ -27850,15 +27854,30 @@ The model's contract is what makes a column panel trivial:
27850
27854
  <BbTable
27851
27855
  v-model:order="order"
27852
27856
  caption="Invoices"
27853
- :columns="visibleColumns"
27857
+ :columns="columns"
27854
27858
  compact
27855
27859
  item-value="id"
27856
27860
  :items="invoices"
27857
27861
  reorderable
27858
- />
27862
+ @hide:column="(key) => setHidden(key, true)"
27863
+ >
27864
+ <!-- A "hide" control beside every header: the slot's `hideColumn()` is
27865
+ one-way (a hidden column has no header), so the panel above
27866
+ is what brings a column back. -->
27867
+ <template #header:append="{ hideColumn, label }">
27868
+ <BbButton
27869
+ :aria-label="`Hide ${label}`"
27870
+ icon="lucide:eye-off"
27871
+ size="xs"
27872
+ variant="ghost"
27873
+ @click="hideColumn"
27874
+ />
27875
+ </template>
27876
+ </BbTable>
27859
27877
  </div>
27860
27878
  <p class="text-xs text-[color:var(--bb-text-muted)]">
27861
- Order model: <code>{{ JSON.stringify(order) }}</code>
27879
+ Order model: <code>{{ JSON.stringify(order) }}</code> · hidden:
27880
+ <code>{{ JSON.stringify([...hidden]) }}</code>
27862
27881
  </p>
27863
27882
  </div>
27864
27883
  </template>
@@ -27880,8 +27899,8 @@ const euro = new Intl.NumberFormat('en-IE', {
27880
27899
  currency: 'EUR',
27881
27900
  });
27882
27901
 
27883
- // The declaration: what a column IS. Never reordered.
27884
- const columns: Array<BbTableColumn<Invoice>> = [
27902
+ // The declaration: what a column IS. Never reordered, never filtered.
27903
+ const definitions: Array<BbTableColumn<Invoice>> = [
27885
27904
  { key: 'number', label: 'Invoice', width: 110 },
27886
27905
  { key: 'client', label: 'Client' },
27887
27906
  { key: 'issued', label: 'Issued', width: 110 },
@@ -27925,33 +27944,41 @@ const invoices: Invoice[] = [
27925
27944
  },
27926
27945
  ];
27927
27946
 
27928
- // The state: render order (keys). Persist THIS. Seed it with the full key
27929
- // list it also completes itself on the first move.
27930
- const order = ref<string[]>(columns.map((column) => column.key));
27947
+ // The state, both persistable as-is: render order (keys) and the hidden set.
27948
+ // A partial or empty order is fine the first move writes back the
27949
+ // complete array, hidden keys in their slots, because every column stays
27950
+ // declared below.
27951
+ const order = ref<string[]>([]);
27931
27952
  const hidden = ref(new Set<string>());
27932
27953
 
27933
- // Visibility is just filtering the declaration; the hidden key keeps its
27934
- // slot in `order`.
27935
- const visibleColumns = computed(() =>
27936
- columns.filter((column) => !hidden.value.has(column.key))
27954
+ // The definitions, with the persisted visibility mapped in. Same shape you
27955
+ // would use for persisted widths (`width`) — the definition is the truth.
27956
+ const columns = computed(() =>
27957
+ definitions.map((column) => ({
27958
+ ...column,
27959
+ hidden: hidden.value.has(column.key),
27960
+ }))
27937
27961
  );
27938
27962
 
27939
- // The panel lists every declared column in the order the model has them.
27940
- const panelColumns = computed(() =>
27941
- [...columns].sort(
27942
- (a, b) => order.value.indexOf(a.key) - order.value.indexOf(b.key)
27943
- )
27944
- );
27963
+ // The panel lists every declared column in the order the model has them
27964
+ // (omitted keys follow in declaration order, exactly as the table renders).
27965
+ const panelColumns = computed(() => {
27966
+ const rank = (key: string) => {
27967
+ const index = order.value.indexOf(key);
27968
+ return index < 0 ? order.value.length : index;
27969
+ };
27970
+ return [...definitions].sort((a, b) => rank(a.key) - rank(b.key));
27971
+ });
27945
27972
 
27946
- const toggleVisible = (key: string) => {
27973
+ const setHidden = (key: string, value: boolean) => {
27947
27974
  const next = new Set(hidden.value);
27948
- if (next.has(key)) next.delete(key);
27949
- else next.add(key);
27975
+ if (value) next.add(key);
27976
+ else next.delete(key);
27950
27977
  hidden.value = next;
27951
27978
  };
27952
27979
 
27953
27980
  const move = (key: string, delta: 1 | -1) => {
27954
- const next = [...order.value];
27981
+ const next = panelColumns.value.map((column) => column.key);
27955
27982
  const from = next.indexOf(key);
27956
27983
  const to = from + delta;
27957
27984
  if (from < 0 || to < 0 || to >= next.length) return;
@@ -27962,22 +27989,50 @@ const move = (key: string, delta: 1 | -1) => {
27962
27989
  </script>
27963
27990
  ```
27964
27991
 
27965
- **Pinned slots are positions, not columns.** `fixed-columns` indexes rendered
27966
- slots like a spreadsheet's freeze panes (a selection column is slot 0). With
27967
- column order the slot holds: whichever column lands in it is pinned, and
27968
- dragging the identity column out of its pinned slot (slot 1 when
27969
- `selectable`, slot 0 otherwise) unpins it (AG Grid pins columns; BbTable
27970
- pins positions). Keep the reorder + pin combination for slots that
27971
- never move `select`, `actions`, or a first column you also lock by
27972
- normalizing the model in your `@update:order` handler.
27973
-
27974
- **Nested tables.** An inheriting child aligns to the parent's rails by
27975
- **position**, so a reordered parent re-pairs the child's columns unless the
27976
- child follows. Give both tables the same `v-model:order` when they
27977
- share keys (and the child declares no `snap`) and they stay paired by key.
27978
- `snap` points are positional in the parent's rendered order: reorder the
27979
- parent and a snap-mapped child follows its slots, but never reorder (or share
27980
- the order with) the snap-mapped child itself dev builds warn.
27992
+ ### Column visibility
27993
+
27994
+ `hidden: true` on a column definition hides it: no header, no cells, no grid
27995
+ track. The column stays **declared**, and that is the whole point — the order
27996
+ model is completed against every declared key, so a hidden column keeps its
27997
+ slot through any number of header drags, and it returns to that slot the
27998
+ moment the flag clears. Two rules follow:
27999
+
28000
+ - **Hidden means "exists, not shown".** A column panel, a persisted
28001
+ preference, a narrow-screen breakpoint all write `hidden` into the
28002
+ definition (a `computed` over your persisted set — the example above does
28003
+ exactly that). Filter `columns` only for a column that does **not exist**
28004
+ for this user (a custom field they don't have); its key, if a saved order
28005
+ still lists it, is preserved in place like any unknown key.
28006
+ - **There is no visibility model, on purpose** the `width` shape, not the
28007
+ `sort` one. The definition is the single declared truth. Header slots
28008
+ (`#header:<key>`, `#header:<key>:append`, the table-wide `#header:append`)
28009
+ receive a `hideColumn()` callback for a "Hide column" control of your own; calling
28010
+ it hides the column for the mount and fires `@hide:column="(key) => …"`
28011
+ once. Persist by writing `hidden: true` back into the definition — the
28012
+ override matches it and stays; write `hidden: false` (the panel showing the
28013
+ column again) and the definition wins. Unbound, the hide lives for the
28014
+ mount, like an unbound resize. A hidden column has no header, so `hideColumn()`
28015
+ is one-way by nature: if you offer it, own the persistence and give the
28016
+ user a panel that shows columns again.
28017
+
28018
+ A nested table that shares its parent's keys shares its `hidden` flags too,
28019
+ the same way it shares `order` — a hidden parent column has no track for a
28020
+ child cell to land on.
28021
+
28022
+ **Pins travel with the column.** `fixed` is a field on the column
28023
+ definition (see "Pinned columns"), so a reorder moves the pin with the cell:
28024
+ drag the identity column to slot 3 and it is still sticky, now at slot 3.
28025
+ The offset is still positional — the pinned columns before it on the same
28026
+ side — so a pinned column dragged away from the edge sticks at `left: 0` and
28027
+ slides over the columns before it. Lock the pinned columns in place by
28028
+ normalizing the model in your `@update:order` handler when that matters.
28029
+
28030
+ **Nested tables.** A parent that uses `order` or `reorderable` is inherited
28031
+ by **position**: its columns can move, so the child's `snap` targets and
28032
+ same-key pairing are ignored there (dev builds warn) and the child aligns to
28033
+ the parent's rails slot by slot. Give both tables the same `v-model:order`
28034
+ when they share keys and they stay paired. Never reorder (or share the order
28035
+ with) a snapped child itself — dev builds warn.
27981
28036
 
27982
28037
  Apply the model synchronously in your handler and persist afterwards: an
27983
28038
  `await` before the write moves the cell in a later flush, after the table
@@ -28349,7 +28404,7 @@ fields.
28349
28404
  Track open rows with `v-model:expanded-items` (an array of row values — set
28350
28405
  `item-value` so expansion is stable across refetches) and render their detail
28351
28406
  through the `#expand` slot, a full-width row below the main one. Trigger it
28352
- from the `#actions` slot (enable the column with `actions`, label it with
28407
+ from the `#actions` slot (the slot creates the actions column; label it with
28353
28408
  `actions-text`): the scope hands you `toggleExpanded` — a **callback** — plus
28354
28409
  the read-only `expanded` flag. Drive open/close through the callback; the
28355
28410
  `expanded` prop is one-way state, not something to mutate.
@@ -28366,7 +28421,6 @@ the read-only `expanded` flag. Drive open/close through the callback; the
28366
28421
  >
28367
28422
  <BbTable
28368
28423
  v-model:expanded-items="expanded"
28369
- actions
28370
28424
  actions-text="Details"
28371
28425
  caption="Invoices"
28372
28426
  :columns="columns"
@@ -28590,11 +28644,19 @@ above it. The mechanics:
28590
28644
  - `inherit-column-widths` — `true` inherits from the nearest ancestor table;
28591
28645
  a string targets a specific ancestor's `id` (useful when an intermediate
28592
28646
  table has a different column count).
28593
- - By default child columns map one-to-one onto the parent's data columns. A
28594
- column's `snap` remaps it: a number is a start track (`2` occupies the
28595
- parent's third data region), `[start, end]` spans a range (fractions
28596
- allowed; `end: -1` runs to the end of the data region). Structural columns
28597
- (`select`, actions) are accounted for automatically.
28647
+ - By default a child column pairs with the parent column of the **same key**,
28648
+ and a column the parent lacks flows into the next parent track no other
28649
+ column claimed. A column's `snap` names the parent column instead:
28650
+ `snap: 'client'` takes that track, `['client', 'amount']` spans from the
28651
+ start of one through the end of the other, and the parent's structural
28652
+ columns are targets by name (`'select'`, `'actions'` — `['name',
28653
+ 'actions']` runs a column across the parent's actions cell too). A
28654
+ trailing `.digits` on a key in a pair is a fraction of that track
28655
+ (`['a', 'b.5']` = all of `a` plus half of `b`). Snaps and keys resolve
28656
+ against the parent's **rendered** columns: a target the parent hides (or
28657
+ never had) makes the column flow like an unsnapped one; a snap that lands
28658
+ before the previous column's cannot tile and flows too (dev builds warn);
28659
+ under a parent that reorders, snaps are ignored (see Column order).
28598
28660
  - A child with **more** columns than the parent content-sizes the extras and
28599
28661
  overflows into the parent's scroll (see below); with **fewer**, the leading
28600
28662
  columns stay 1:1, the actions column inherits the parent's actions width,
@@ -28602,15 +28664,16 @@ above it. The mechanics:
28602
28664
  both tables end flush (a right-aligned total lands under the parent's last
28603
28665
  column). Give that last column a `width` or an explicit `[start, end]`
28604
28666
  snap only if you want it to stop short — the leftover then joins the
28605
- actions region instead.
28667
+ actions region instead. A column with nothing free left to flow into is an
28668
+ overflow column too.
28606
28669
  - **An inheriting child does not scroll on its own — the parent's scrollport
28607
28670
  is the only one.** That is what keeps the two grids locked together: a child
28608
28671
  with its own scroll position would fall out of alignment the moment either
28609
28672
  one moved, which is the whole thing `inherit-column-widths` exists to
28610
28673
  prevent. One scrollbar moves both.
28611
28674
  - **The parent's `selectable`/`actions` columns are handled for you** — the
28612
- child's content columns map to the parent's content columns 1:1 (no `snap`
28613
- needed for that), and the parent's select/actions _regions_ are reserved on
28675
+ child's content columns pair with the parent's content columns by key (no
28676
+ `snap` needed for that), and the parent's select/actions _regions_ are reserved on
28614
28677
  each side: a child with its own select or actions column widens that cell
28615
28678
  to the parent's (two equal cells, controls at the row's end, so the ⌃ sits
28616
28679
  under the parent's); a child without one folds the region into its first/
@@ -28642,7 +28705,6 @@ above it. The mechanics:
28642
28705
  >
28643
28706
  <BbTable
28644
28707
  v-model:expanded-items="expanded"
28645
- actions
28646
28708
  actions-text="Line items"
28647
28709
  caption="Invoices"
28648
28710
  :columns="columns"
@@ -28740,27 +28802,29 @@ const expanded = ref<string[]>([]);
28740
28802
 
28741
28803
  #### Affixed actions on both tables
28742
28804
 
28743
- A nested table can carry its own `fixed-columns` — child rows usually need
28744
- their own row actions — and both action columns pin to the same visible edge:
28805
+ A nested table can pin its own actions column with `fixed-actions` — child
28806
+ rows usually need their own row actions — and both action columns pin to the
28807
+ same visible edge:
28745
28808
 
28746
28809
  ```vue
28747
28810
  <BbTable
28748
28811
  id="programmes"
28749
- actions
28750
28812
  :columns="columns"
28751
- :fixed-columns="[{ index: columns.length, position: 'right' }]"
28813
+ fixed-actions
28752
28814
  :items="rows"
28753
28815
  item-value="id"
28754
28816
  >
28817
+ <template #actions="{ item }">…</template>
28755
28818
  <template #expand="{ item }">
28756
28819
  <BbTable
28757
- actions
28758
28820
  :columns="childColumns"
28759
- :fixed-columns="[{ index: childColumns.length, position: 'right' }]"
28821
+ fixed-actions
28760
28822
  inherit-column-widths="programmes"
28761
28823
  :items="item.children"
28762
28824
  item-value="id"
28763
- />
28825
+ >
28826
+ <template #actions="{ item }">…</template>
28827
+ </BbTable>
28764
28828
  </template>
28765
28829
  </BbTable>
28766
28830
  ```
@@ -28769,9 +28833,10 @@ This works because the child shares the parent's scrollport. `position: sticky`
28769
28833
  resolves against the nearest scrolling ancestor, and the child's own box is as
28770
28834
  wide as the parent's **content** — so if it opened a scrollport of its own, the
28771
28835
  child's actions would affix to an edge sitting out in the overflow, hundreds of
28772
- pixels past what anyone can see, while the parent's pinned correctly. Snap the
28773
- child's actions column onto the parent's actions track (see the snapping rule
28774
- above) so the two columns line up rather than merely both being pinned.
28836
+ pixels past what anyone can see, while the parent's pinned correctly. The
28837
+ child's actions column inherits the parent's actions region by itself (the
28838
+ structural rule above), so the two columns line up rather than merely both
28839
+ being pinned.
28775
28840
 
28776
28841
  A pinned cell is opaque — it paints the table's `--bg` surface because it
28777
28842
  slides over its siblings — so a nested table sitting on a tinted expand row
@@ -29059,7 +29124,6 @@ open-in-new-tab for free.
29059
29124
  class="max-w-2xl overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
29060
29125
  >
29061
29126
  <BbTable
29062
- actions
29063
29127
  actions-text="Actions"
29064
29128
  caption="Invoices"
29065
29129
  :columns="columns"
@@ -29395,10 +29459,8 @@ canonical palette lives in the
29395
29459
 
29396
29460
  `compact` tightens row padding. `fixed-headers` makes the header stick to the
29397
29461
  top of the nearest scroll container. `fixed` switches to an equal-width fixed
29398
- layout. `fixed-columns` pins columns while the rest scroll horizontally: an
29399
- array of column indices (left-pinned) or `{ index, position: 'left'|'right' }`
29400
- objects; indices count rendered columns (a selection column is index 0), and
29401
- an out-of-range index logs a console error and is skipped. When content can't
29462
+ layout. Pinned columns (next section) stick while the rest scroll
29463
+ horizontally. When content can't
29402
29464
  fit, the table keeps every column at its measured minimum — its header label
29403
29465
  included — and overflows its container instead of clipping; this is by
29404
29466
  design, not a bug to work around. Note that truncating cells yourself
@@ -29449,13 +29511,51 @@ your wrapper actually sits on before sprinkling `min-width: 0` everywhere.)
29449
29511
  </div>
29450
29512
  ```
29451
29513
 
29514
+ ### Pinned columns
29515
+
29516
+ `fixed: 'left' | 'right'` on a column definition makes its cells
29517
+ `position: sticky` on that side — in place, at the column's own render
29518
+ position, like a spreadsheet's freeze panes. Pin a contiguous run from the
29519
+ edge and the offsets accumulate (the second left-pinned column sticks right
29520
+ after the first). The two structural columns have no definition to carry the
29521
+ field, so they pin through the table: `fixed-select` for the selection
29522
+ column and `fixed-actions` for the actions column (each has one sensible
29523
+ side, so both are booleans).
29524
+
29525
+ ```vue
29526
+ <BbTable :columns="columns" fixed-actions fixed-select :items="rows" selectable>
29527
+ <template #actions="{ item }">…</template>
29528
+ </BbTable>
29529
+ ```
29530
+
29531
+ ```ts
29532
+ const columns = [
29533
+ { key: 'name', label: 'Name', fixed: 'left' },
29534
+ { key: 'region', label: 'Region' },
29535
+ { key: 'amount', label: 'Amount', align: 'right' },
29536
+ ];
29537
+ ```
29538
+
29539
+ Three rules:
29540
+
29541
+ - **The pin is the column's.** Under `order` / `reorderable` it travels with
29542
+ the cell, and a `hidden` pinned column pins nothing. The offset is still
29543
+ positional, so a lone left-pinned column dragged to the middle sticks at
29544
+ `left: 0` and slides over the columns before it — by design; lock the
29545
+ pinned columns in place by normalizing `@update:order` if you need to.
29546
+ - **Pinned cells are opaque** — they paint the table's `--bg` (see Styling
29547
+ hooks), so a table on a tinted surface needs `--bg` set once.
29548
+ - **Nested tables pin against the ancestor scrollport.** A child that
29549
+ inherits the parent's tracks has no scrollport of its own, so both tables'
29550
+ pins land on the same visible edge (see Nested tables).
29551
+
29452
29552
  ### Virtual rows
29453
29553
 
29454
29554
  `virtual` windows the body: only the rows in the vertical scrollport (plus a
29455
29555
  few on each side) are mounted, and two spacer rows hold the scrollport at the
29456
29556
  full list height, so 5,000 or 50,000 rows scroll at the cost of a few dozen.
29457
29557
  Everything else is the same table — the rows stay in flow in the grid, so the
29458
- stuck header, `fixed-columns`, nested tables, selection, highlight and the
29558
+ stuck header, pinned columns, nested tables, selection, highlight and the
29459
29559
  keyboard grammar work untouched. One prop, no knobs.
29460
29560
 
29461
29561
  - **Give it a scroller.** Bound the table's height (`max-height` / `height`
@@ -29513,7 +29613,6 @@ keyboard grammar work untouched. One prop, no knobs.
29513
29613
  class="max-h-80"
29514
29614
  :columns="columns"
29515
29615
  compact
29516
- :fixed-columns="[0]"
29517
29616
  item-value="id"
29518
29617
  :items="rows"
29519
29618
  virtual
@@ -29533,7 +29632,7 @@ type Entry = {
29533
29632
  };
29534
29633
 
29535
29634
  const columns: Array<BbTableColumn<Entry>> = [
29536
- { key: 'number', label: 'Invoice' },
29635
+ { key: 'number', label: 'Invoice', fixed: 'left' },
29537
29636
  { key: 'client', label: 'Client' },
29538
29637
  { key: 'issued', label: 'Issued' },
29539
29638
  { key: 'amount', label: 'Amount', align: 'right' },
@@ -29558,14 +29657,14 @@ leave the screen. Collapse instead of scroll:
29558
29657
 
29559
29658
  - **Rank the columns.** Identity + status + the one number that matters stay;
29560
29659
  everything else is secondary.
29561
- - **Drop the secondary ones from `columns` at the breakpoint** — a `computed`
29562
- over `useMobile()` (or your own `matchMedia`) that filters the secondary keys
29563
- out. Removing the column removes its grid track. Do **not** hide cells with
29564
- `thClass`/`tdClass` + `display: none`: the table is a grid, a hidden cell
29565
- leaves its track in place and every following cell shifts one track over.
29566
- A dropped key keeps its slot in `order` (the write-back never drops
29567
- unknown keys), so the column returns where it was never filter the
29568
- order array.
29660
+ - **Flag the secondary ones `hidden` at the breakpoint** — a `computed`
29661
+ over `useMobile()` (or your own `matchMedia`) that maps the definitions
29662
+ with `hidden: isMobile && secondary.has(key)`. A hidden column has no grid
29663
+ track. Do **not** hide cells with `thClass`/`tdClass` + `display: none`:
29664
+ the table is a grid, a hidden cell leaves its track in place and every
29665
+ following cell shifts one track over. The hidden column stays declared, so
29666
+ it keeps its slot in `order` and returns where it was when the screen
29667
+ widens — never filter the order array.
29569
29668
  - **Stack what survives into the primary cell.** The identity slot renders the
29570
29669
  name plus the hidden facts under it, at `--bb-text-muted`, so nothing is
29571
29670
  actually lost.
@@ -29579,7 +29678,7 @@ records list.
29579
29678
  **The table's surface is `--bg`** (default `var(--bb-panel)`), a local on
29580
29679
  `.bb-table`. Cells are transparent at rest so the row can tint them, but
29581
29680
  anything that slides over scrolled content — the stuck header band with
29582
- `fixed-headers`, and `fixed-columns` cells — is opaque and paints `--bg` (a
29681
+ `fixed-headers`, and pinned (`fixed`) cells — is opaque and paints `--bg` (a
29583
29682
  pinned cell also picks up its row's hover/highlight tint, so it never reads
29584
29683
  as a hole). When a table sits on a surface that is not the panel colour, set
29585
29684
  the one knob and everything pinned follows:
@@ -29726,8 +29825,9 @@ reader.
29726
29825
  - `BbPagination` — shared page ref, or zero-wiring via `id` + `table-id`.
29727
29826
  - `useBbTableContext` (`./composables/useBbTableContext.md`) — bulk-action
29728
29827
  toolbars, detail panels, "reset sort" buttons living outside the table.
29729
- (Column order is not mirrored there: a column panel needs `columns` too,
29730
- so it lives where `v-model:order` is bound.)
29828
+ (Column order and visibility are not mirrored there: a column panel writes
29829
+ `v-model:order` and the `hidden` flags, so it lives where `columns` is
29830
+ built.)
29731
29831
  - `BbButton` — per-row links (`href`/`to`) in cells or `#actions`;
29732
29832
  `BbDropdown` for an overflow menu of row actions.
29733
29833
  - `BbBadge` / `BbAvatar` — status and identity cells via slots.
@@ -29753,10 +29853,17 @@ reader.
29753
29853
  `(columns, item)` with the mapped cells (in **render** order — look them up
29754
29854
  by `key`, never by position) and the raw record, and whatever it returns
29755
29855
  becomes the checkbox's label.
29756
- - `fixed-columns` pins **slots**, not columns: under `order` the
29757
- column that lands in a pinned slot is the pinned one (see Column order).
29856
+ - A pin (`column.fixed`) travels with its column under `order`, but its
29857
+ offset is positional: dragged away from the edge, a pinned column sticks
29858
+ at `left: 0` and slides over the columns before it (see Pinned columns).
29859
+ - The actions column has no prop: the `#actions` slot creates it. A
29860
+ conditional slot (`<template v-if="…" #actions>`) adds and removes the
29861
+ column live.
29758
29862
  - A column panel that holds a partial `order` receives the completed
29759
- array on the user's first move — its persisted list grows to every key.
29863
+ array on the user's first move — its persisted list grows to every declared
29864
+ key, `hidden` ones included. Hide with the flag, not by filtering
29865
+ `columns`: a filtered column is undeclared, so a partial model completes
29866
+ without it and the key is gone from the persisted order.
29760
29867
  - Enabling select-all does not re-emit an empty `modelValue` (see the
29761
29868
  select-all section) — in all-mode, read `select-all` +
29762
29869
  `unselected-items`, not `modelValue`.
@@ -29834,7 +29941,6 @@ reader.
29834
29941
  | Prop | Type | Default | Required | Description |
29835
29942
  | --- | --- | --- | --- | --- |
29836
29943
  | `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`… |
29837
- | `actions` | `boolean \| undefined` | `false` | | Displays the actions column. |
29838
29944
  | `actionsText` | `string \| undefined` | | | Label used in the header of the actions column. |
29839
29945
  | `align` | `"left" \| "center" \| "right" \| undefined` | `"left"` | | Text alignment of the columns. |
29840
29946
  | `caption` | `string \| undefined` | | | Caption that describes the content of the table. Used for accessibility purposes. |
@@ -29848,8 +29954,9 @@ reader.
29848
29954
  | `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… |
29849
29955
  | `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`/… |
29850
29956
  | `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. |
29851
- | `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… |
29957
+ | `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… |
29852
29958
  | `fixedHeaders` | `boolean \| undefined` | `false` | | Boolean that sets the headers as sticky to the top of the table. |
29959
+ | `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… |
29853
29960
  | `headerRowClass` | `Classes \| undefined` | | | Defines the classes to be passed to the header row. |
29854
29961
  | `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… |
29855
29962
  | `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. |
@@ -29890,6 +29997,7 @@ reader.
29890
29997
  - `click:row` — `(e: "click:row", event: MouseEvent, item: any): void`
29891
29998
  - `contextmenu:row` — `(e: "contextmenu:row", event: MouseEvent, item: any): void`
29892
29999
  - `dblclick:row` — `(e: "dblclick:row", event: MouseEvent, item: any): void`
30000
+ - `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…
29893
30001
  - `item:selected` — `(e: "item:selected", value: any): void`
29894
30002
  - `item:unselected` — `(e: "item:unselected", value: any): void`
29895
30003
  - `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.
@@ -29903,7 +30011,7 @@ reader.
29903
30011
 
29904
30012
  ## Slots
29905
30013
 
29906
- - `actions` — scope: `{ expanded?: boolean \| undefined; expandProps?: ExpandControllerProps \| undefined; toggleExpanded?: (() => void) \| undefined; highlighted?: boolean \| undefi…` — Content rendered in the actions cell for each row.
30014
+ - `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…
29907
30015
  - `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.
29908
30016
  - `header:actions` — scope: `{ text: string; }` — Replaces the default actions header cell content.
29909
30017
  - `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…
@@ -39250,24 +39358,24 @@ a composable you want is not in this corpus's `composables/` folder — it is no
39250
39358
  here.** Check this list before inventing a name; if the need is genuinely absent,
39251
39359
  compose it or write it yourself.
39252
39360
 
39253
- | Assumed component or composable | Reality |
39254
- | -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
39255
- | `BbColumnManager` — show/hide/reorder columns | Does not exist as a component. Order is `v-model:order` on `BbTable` (an array of keys any panel can write; `reorderable` adds the built-in header drag + keyboard handle); visibility is still filtering `columns` the hidden key keeps its slot in the order. Compose the panel from `BbPopover` + `BbCheckbox`. |
39256
- | `BbKanban` / drag-and-drop board | Does not exist. There is no reusable DnD primitive in the library (`BbTable`'s header drag is internal to the table) — compose the board from `BbTable` (a table/list view of the same data) or hand-roll the drag-and-drop interaction yourself. |
39257
- | `BbModal` | It is [BbDialog](../BbDialog.md). |
39258
- | `BbDrawer` / `BbSheet` | It is [BbOffCanvas](../BbOffCanvas.md). |
39259
- | `BbCombobox` / `BbAutocomplete` | It is [BbSelect](../BbSelect.md) / [BbSelectPopover](../BbSelectPopover.md) — search is built in, see `disable-writing`. |
39260
- | `BbToggle` / `BbToggleGroup` | It is [BbButton](../BbButton.md) with `v-model` (+ `true-value` / `false-value`), which owns `aria-pressed` and the active class. |
39261
- | `TagChip` / colored, read-only tag chip | It is [BbBadge](../BbBadge.md) — [BbTag](../BbTag.md) is a tags-_input_ only, not a display component. Use `BbBadge` with a registered `soft-*` variant for a fixed palette, or a one-off `--bg`/`--color` override per row for arbitrary/data-driven colors (BbBadge.md § One-off colors) — not a registered variant per distinct color. |
39262
- | `BbTab` (singular) | The consumer API is [BbTabs](../BbTabs.md) / `BbTabsRoot`. `BbTab` appears only as the **v2 "before" side** of the migration diffs. |
39263
- | `BbCard` | Does not exist — a card is a `<div>` with tokens. See [design-language](./design-language.md). |
39264
- | `BbList` / `BbListItem` / `BbDescriptionList` | Do not exist — a list row is an `<li>` with tokens, and [BbBaseButton](../BbBaseButton.md) makes the whole row navigable. See the note under Data Display. |
39265
- | `useLiveFeed` / any polling or cross-tab feed helper | Does not exist, and neither does a cross-tab helper — `useBroadcastChannelInstance` was removed (cross-tab messaging is not a UI concern). Use `useBroadcastChannel` from `@vueuse/core`; the polling and the merge are yours. |
39266
- | `BbStepper` / `BbWizard` | Does not exist as a component; the wizard-form recipe composes one. |
39267
- | `BbSkeleton` (standalone) | Only `BbTable` ships skeletons, via the column `skeleton` field. |
39268
- | `BbCommand` / command palette | Does not exist. Compose `BbDialog` + `BbSelectPopover`. |
39269
- | `BbCalendar` (month / scheduling view) | Does not exist. [BbDatePicker](../BbDatePicker.md) is a picker, not a calendar surface. |
39270
- | `BbChart` / `BbGraph`, `BbCarousel`, `BbTimeline`, `BbDivider` | Do not exist. No charting, carousel or timeline primitive ships here. |
39361
+ | Assumed component or composable | Reality |
39362
+ | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
39363
+ | `BbColumnManager` — show/hide/reorder columns | Does not exist as a component. Order is `v-model:order` on `BbTable` (an array of keys any panel can write; `reorderable` adds the built-in header drag + keyboard handle); visibility is `hidden: true` on the column definition (the column stays declared and keeps its slot in the order; header slots get a `hideColumn()` that fires `hide:column`). Compose the panel from `BbPopover` + `BbCheckbox`. |
39364
+ | `BbKanban` / drag-and-drop board | Does not exist. There is no reusable DnD primitive in the library (`BbTable`'s header drag is internal to the table) — compose the board from `BbTable` (a table/list view of the same data) or hand-roll the drag-and-drop interaction yourself. |
39365
+ | `BbModal` | It is [BbDialog](../BbDialog.md). |
39366
+ | `BbDrawer` / `BbSheet` | It is [BbOffCanvas](../BbOffCanvas.md). |
39367
+ | `BbCombobox` / `BbAutocomplete` | It is [BbSelect](../BbSelect.md) / [BbSelectPopover](../BbSelectPopover.md) — search is built in, see `disable-writing`. |
39368
+ | `BbToggle` / `BbToggleGroup` | It is [BbButton](../BbButton.md) with `v-model` (+ `true-value` / `false-value`), which owns `aria-pressed` and the active class. |
39369
+ | `TagChip` / colored, read-only tag chip | It is [BbBadge](../BbBadge.md) — [BbTag](../BbTag.md) is a tags-_input_ only, not a display component. Use `BbBadge` with a registered `soft-*` variant for a fixed palette, or a one-off `--bg`/`--color` override per row for arbitrary/data-driven colors (BbBadge.md § One-off colors) — not a registered variant per distinct color. |
39370
+ | `BbTab` (singular) | The consumer API is [BbTabs](../BbTabs.md) / `BbTabsRoot`. `BbTab` appears only as the **v2 "before" side** of the migration diffs. |
39371
+ | `BbCard` | Does not exist — a card is a `<div>` with tokens. See [design-language](./design-language.md). |
39372
+ | `BbList` / `BbListItem` / `BbDescriptionList` | Do not exist — a list row is an `<li>` with tokens, and [BbBaseButton](../BbBaseButton.md) makes the whole row navigable. See the note under Data Display. |
39373
+ | `useLiveFeed` / any polling or cross-tab feed helper | Does not exist, and neither does a cross-tab helper — `useBroadcastChannelInstance` was removed (cross-tab messaging is not a UI concern). Use `useBroadcastChannel` from `@vueuse/core`; the polling and the merge are yours. |
39374
+ | `BbStepper` / `BbWizard` | Does not exist as a component; the wizard-form recipe composes one. |
39375
+ | `BbSkeleton` (standalone) | Only `BbTable` ships skeletons, via the column `skeleton` field. |
39376
+ | `BbCommand` / command palette | Does not exist. Compose `BbDialog` + `BbSelectPopover`. |
39377
+ | `BbCalendar` (month / scheduling view) | Does not exist. [BbDatePicker](../BbDatePicker.md) is a picker, not a calendar surface. |
39378
+ | `BbChart` / `BbGraph`, `BbCarousel`, `BbTimeline`, `BbDivider` | Do not exist. No charting, carousel or timeline primitive ships here. |
39271
39379
 
39272
39380
  App chrome — an app shell, sidebar, page header, selection dock — is
39273
39381
  deliberately **not** a component: it is yours to own. The per-platform
@@ -40885,9 +40993,13 @@ The `bitbossUi` Vite plugin (`iconDir` option) — or the Nuxt module (config ke
40885
40993
  `lucide:<name>` available; removing it makes them all errors. Nothing to
40886
40994
  configure.
40887
40995
  - **Dev**: all icons of installed sets are registered. Referencing an
40888
- uninstalled prefix or a non-existent name fails loudly with an actionable
40889
- message (`"lucide:fo" requires "@iconify-json/lucide" run: npm install -D …`
40890
- or `icon "x" does not exist in the lucide icon set`).
40996
+ uninstalled prefix or a non-existent name fails loudly in the Vite error
40997
+ overlay with an actionable message. A typo inside an installed set reads
40998
+ "does not exist in the lucide icon set — did you mean `lucide:…`?"; a set
40999
+ that is not installed reads "no installed icon set has the prefix `mdi`
41000
+ (installed sets: lucide) … run: `npm install -D @iconify-json/mdi`". Read
41001
+ the message: a typo wants a different name, a missing set wants an install,
41002
+ never a guess.
40891
41003
  - **Build**: source files (`.vue`/`.ts`/`.js`/`.tsx`/`.jsx`; tests and stories
40892
41004
  excluded) are scanned for static `provider:name` literals; each referenced
40893
41005
  icon is emitted as its own lazy chunk. This is why rule 4 exists — and why an
@@ -43102,19 +43214,16 @@ goes stable, so never reach for it here.
43102
43214
 
43103
43215
  Peer dependency: `vue ^3.5.12` — the only required one. `@inertiajs/vue3` is an **optional** peer, needed only in Inertia apps.
43104
43216
 
43105
- `@iconify/collections` is an **optional** peer too, and npm never installs
43106
- optional peers automatically install it yourself if you want the dev-time
43107
- provider-icon diagnostics:
43108
-
43109
- ```bash
43110
- npm install -D @iconify/collections
43111
- ```
43112
-
43113
- Without it the plugin has no list of valid Iconify prefixes, so the dev-overlay
43114
- errors below (the "install `@iconify-json/<prefix>`" hint and the "did you
43115
- mean …?" suggestion for a mistyped icon name) never fire — a wrong provider
43116
- icon then surfaces only as `BbIcon`'s generic runtime `Icon "…" does not exist.`
43117
- Nothing else changes: icons that resolve still render, in dev and in production.
43217
+ You do **not** need `@iconify/collections`. It is declared as an optional peer
43218
+ for one reason only: when it happens to be installed, the dev-overlay message
43219
+ for an unknown icon prefix can say for certain whether that prefix is a real
43220
+ Iconify set ("requires `@iconify-json/mdi`, run …") or not ("neither an
43221
+ installed set nor a known Iconify prefix"). Without it the same overlay fires
43222
+ with a hedged wording ("no installed icon set has the prefix `mdi`; if it is an
43223
+ Iconify set, run …"). A mistyped name inside a set you **have** installed
43224
+ (`lucide:eye-misspelled`) is diagnosed from the set's own data in both cases,
43225
+ with a "did you mean …?" suggestion. Nothing else changes: icons that resolve
43226
+ still render, in dev and in production.
43118
43227
 
43119
43228
  **Known-good toolchain:** TypeScript **5.x or 6.x** if your build gate runs
43120
43229
  `vue-tsc`. Both type-check cleanly against the shipped `.d.ts`; the library
@@ -43379,7 +43488,7 @@ utilities. (Same trap as the scoped-`<style>` case above, one level up.)
43379
43488
  ## 6) Icons
43380
43489
 
43381
43490
  - `iconDir` (required) is scanned recursively for `.svg` files; each file becomes `local:<basename>`.
43382
- - Provider icons (`lucide:pencil`, `mdi:home`, …) require the matching `@iconify-json/<prefix>` package installed in the consumer app. Missing packages/icons produce actionable dev-overlay errors **only when the optional `@iconify/collections` peer is installed** (see [Install](#1-install)); otherwise you get `BbIcon`'s generic runtime error instead. Production builds report neither this is a dev-time aid.
43491
+ - Provider icons (`lucide:pencil`, `mdi:home`, …) require the matching `@iconify-json/<prefix>` package installed in the consumer app. In dev, every `prefix:name` the registry does not know produces a Vite error overlay with the fix: a mistyped name inside an installed set gets a "did you mean …?" hint, and an uninstalled prefix gets the list of installed sets plus the `npm install -D @iconify-json/<prefix>` command (the optional `@iconify/collections` peer only sharpens that wording, see [Install](#1-install)). A missing `local:` icon is a missing file in `iconDir` and surfaces as `BbIcon`'s runtime error naming it. Production builds report none of this the scan simply drops what does not resolve, so a wrong name fails at runtime there.
43383
43492
  - Production builds bundle **only statically referenced** provider icons — write icon names as literal strings (`icon="lucide:trash-2"`), never assemble them dynamically.
43384
43493
  - The plugin also writes `node_modules/.bitboss-ui/local-icons.json` and (in dev, unless `vscodeSettings: false`) wires `.vscode/settings.json` so the Iconify editor extension previews local icons.
43385
43494
 
@@ -45658,20 +45767,24 @@ have made the spinner invisible and removed the page's only busy signal.
45658
45767
 
45659
45768
  ## Changes
45660
45769
 
45661
- | v2 | v3 | Kind |
45662
- | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
45663
- | `allowSelectAll?: boolean` (default `true`) | `disableSelectAll?: boolean` (default `false`) | rename, polarity inverted |
45664
- | refetch with rows → skeleton | refetch keeps rows visible (dimmed + progress bar) | **behavior** — the skeleton only shows when there is nothing to display |
45665
- | rows interactive while loading | header + rows go `inert` while loading | **behavior** — opt out with `interactive-while-loading` |
45666
- | `#loading` slot fired on every load | fires on the **first load only** (it replaces the skeleton, so it follows the skeleton's rule) | **⚠ silent** for anyone styling refetches through it |
45667
- | `#no-data` replaced the empty-state `<tr>` | fills the table's own full-width cell (`aria-colspan` counted, centred) | **behavior** — pass content, not a row |
45668
- | `<table>` / `<thead>` / `<tbody>` / `<tr>` / `<th>` / `<td>` markup _(v2 and v3 ≤ beta.5)_ | a CSS grid of `<div>`s carrying `role="table\|rowgroup\|row\|columnheader\|cell"`; `.bb-table__table` / `__head` / `__body` / `__foot` are the new containers, every `.bb-table*` class survives | **⚠ silent** for element selectors, `#thead`/`#tbody`/`#tfoot` markup, copy/paste and print — see below |
45669
- | `--fill` / `--natural-width` published on `.bb-table` _(v3 ≤ beta.5)_ | **removed** — nothing publishes them | **⚠ silent** — see below |
45670
- | — | props `enforceCoherence` (prunes row-keyed models after each load; incompatible with pagination-via-`dependencies`), `expandedItems` + `#expand`, `sort`, `keyboardNavigation`, `highlighted`, `rowClass`, `order` (v-model) + `reorderable` | additive |
45671
- | — | **column** fields `sortable?: boolean`, `rowClass?: ColumnClasses` | additive, **collides** — see below |
45672
- | `useBbTableContext(id).total` _(v3 alphas 4)_ | `useBbTableContext(id).totalItems` | **hard rename** on the composable handle — see below |
45673
- | `@click:row` / `@contextmenu:row` / `@dblclick:row` handlers receive `(event, item, selected)` _(v2 and v3 ≤ beta.5)_ | `(event, item)` — `item` is now your raw record (was the internal row wrapper, record at `row.item`); the trailing `selected` boolean is gone | **payload slimmed** — see below |
45674
- | `--bb-table-*` custom properties | same names without the `bb-` prefix | **⚠ silent** for nested-table layouts — see below |
45770
+ | v2 | v3 | Kind |
45771
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
45772
+ | `allowSelectAll?: boolean` (default `true`) | `disableSelectAll?: boolean` (default `false`) | rename, polarity inverted |
45773
+ | refetch with rows → skeleton | refetch keeps rows visible (dimmed + progress bar) | **behavior** — the skeleton only shows when there is nothing to display |
45774
+ | rows interactive while loading | header + rows go `inert` while loading | **behavior** — opt out with `interactive-while-loading` |
45775
+ | `#loading` slot fired on every load | fires on the **first load only** (it replaces the skeleton, so it follows the skeleton's rule) | **⚠ silent** for anyone styling refetches through it |
45776
+ | `#no-data` replaced the empty-state `<tr>` | fills the table's own full-width cell (`aria-colspan` counted, centred) | **behavior** — pass content, not a row |
45777
+ | `<table>` / `<thead>` / `<tbody>` / `<tr>` / `<th>` / `<td>` markup _(v2 and v3 ≤ beta.5)_ | a CSS grid of `<div>`s carrying `role="table\|rowgroup\|row\|columnheader\|cell"`; `.bb-table__table` / `__head` / `__body` / `__foot` are the new containers, every `.bb-table*` class survives | **⚠ silent** for element selectors, `#thead`/`#tbody`/`#tfoot` markup, copy/paste and print — see below |
45778
+ | `--fill` / `--natural-width` published on `.bb-table` _(v3 ≤ beta.5)_ | **removed** — nothing publishes them | **⚠ silent** — see below |
45779
+ | — | props `enforceCoherence` (prunes row-keyed models after each load; incompatible with pagination-via-`dependencies`), `expandedItems` + `#expand`, `sort`, `keyboardNavigation`, `highlighted`, `rowClass`, `order` (v-model) + `reorderable`, `resizable` + `resize:column`, header-slot `hideColumn()` + `hide:column`, `fixedSelect` / `fixedActions` | additive |
45780
+ | — | **column** fields `sortable?: boolean`, `rowClass?: ColumnClasses`, `hidden?: boolean` (the column stays declared — the way to hide, see § Do not override), `fixed?: 'left' \| 'right'` (replaces `fixedColumns`, see below) | additive, **collides** — see below |
45781
+ | `actions?: boolean` prop _(v2 and v3 ≤ beta.20)_ | **removed** — the `#actions` slot creates the column by itself | **⚠ silent** if you passed the prop without the slot — see below |
45782
+ | `fixedColumns` prop (rendered-slot indices, `[0, { index, position }]`) _(v2 and v3 ≤ beta.20)_ | **removed** — `fixed: 'left' \| 'right'` on the column definition, `fixed-select` / `fixed-actions` for the structural columns | **⚠ silent** — see below |
45783
+ | column `snap?: number \| [number, number]` — gridline indices into the parent's rendered columns (`select` at 0, fractions, `-1` = data end) _(v3 ≤ beta.20)_ | `snap?: string \| [string, string]` — the PARENT's column key, or `'select'` / `'actions'`; a pair is inclusive; `'amount.5'` is a fraction. Same-key columns pair by default. Ignored (dev warning) under a parent that uses `order` / `reorderable` | **⚠ silent** — see below |
45784
+ | `--table-{id}-track-{index}` published per data column _(v3 ≤ beta.20)_ | `--table-{id}-track-{key}` — the column key in its slot-name spelling (`address_city`); `select` / `actions` unchanged | **⚠ silent** for CSS that read the vars by index — see below |
45785
+ | `useBbTableContext(id).total` _(v3 alphas ≤ 4)_ | `useBbTableContext(id).totalItems` | **hard rename** on the composable handle — see below |
45786
+ | `@click:row` / `@contextmenu:row` / `@dblclick:row` handlers receive `(event, item, selected)` _(v2 and v3 ≤ beta.5)_ | `(event, item)` — `item` is now your raw record (was the internal row wrapper, record at `row.item`); the trailing `selected` boolean is gone | **payload slimmed** — see below |
45787
+ | `--bb-table-*` custom properties | same names without the `bb-` prefix | **⚠ silent** for nested-table layouts — see below |
45675
45788
 
45676
45789
  ## Columns gained `sortable` and `rowClass` — check yours first
45677
45790
 
@@ -45778,6 +45891,113 @@ If you used `#loading` to show a custom refetch treatment, that role is gone —
45778
45891
  the built-in dim + progress bar covers refetches; `#loading` is now only the
45779
45892
  first-paint placeholder.
45780
45893
 
45894
+ ### The `#actions` slot creates the actions column — the prop is gone (v3 beta, 2026-09-07)
45895
+
45896
+ `actions` used to be a boolean prop that rendered the column, with the
45897
+ `#actions` slot filling it. The prop carried no information the slot did
45898
+ not, and it had a footgun: a consumer who declared `{ key: 'actions' }` in
45899
+ `columns` instead of setting the prop got a stray empty data cell and a slot
45900
+ that never rendered. Now the slot alone creates the column — header cell
45901
+ (labelled by `actions-text`, replaceable through `#header:actions`), one
45902
+ cell per row, and the `actions` track for nested tables — and removing the
45903
+ slot (or gating it with `v-if`) removes the column live.
45904
+
45905
+ ```diff
45906
+ - <BbTable actions :columns="columns" :items="rows">
45907
+ + <BbTable :columns="columns" :items="rows">
45908
+ <template #actions="{ item }">…</template>
45909
+ </BbTable>
45910
+ ```
45911
+
45912
+ Delete the attribute. The removed-prop warner flags it in dev, the ESLint rule
45913
+ flags it in-editor. The only silent case is a table that set `actions`
45914
+ without ever providing the slot: it had an empty column, and now it does not.
45915
+
45916
+ ### `fixedColumns` → `fixed` on the column, `fixed-select` / `fixed-actions` (v3 beta, 2026-09-07)
45917
+
45918
+ Pins used to be **slot indices** into the rendered column list (a selection
45919
+ column was index 0), like spreadsheet freeze panes: with `order` and
45920
+ `reorderable` the pinned _slot_ held and whichever column landed in it was
45921
+ pinned, which is fragile once users drag headers and hide columns. Pins are
45922
+ now a property of the column, the way `width` and `hidden` are, and follow
45923
+ it through reorders. The sticky behaviour itself is unchanged: in place, at
45924
+ the column's own render position, offset by the pinned columns before it on
45925
+ the same side.
45926
+
45927
+ ```diff
45928
+ - <BbTable
45929
+ - :columns="columns"
45930
+ - :fixed-columns="[0, { index: columns.length + 1, position: 'right' }]"
45931
+ - selectable
45932
+ - >
45933
+ + <BbTable :columns="columns" fixed-select fixed-actions selectable>
45934
+ <template #actions="{ item }">…</template>
45935
+ </BbTable>
45936
+ ```
45937
+
45938
+ ```diff
45939
+ const columns = [
45940
+ - { key: 'name', label: 'Name' },
45941
+ + { key: 'name', label: 'Name', fixed: 'left' },
45942
+ { key: 'amount', label: 'Amount', align: 'right' },
45943
+ ];
45944
+ ```
45945
+
45946
+ Translate each index to the column that sat there: data columns get
45947
+ `fixed: 'left' | 'right'` on their definition, the selection column becomes
45948
+ `fixed-select`, the actions column `fixed-actions` (each has one sensible
45949
+ side, so they are booleans). A nested table that pinned its own actions with
45950
+ `{ index: childColumns.length, position: 'right' }` now says
45951
+ `fixed-actions`. Silent on upgrade: the old prop is ignored (the warner and
45952
+ the ESLint rule flag it), so a table that pinned columns stops pinning until
45953
+ you move the pins onto the definitions.
45954
+
45955
+ ### `snap` names the parent column, not a gridline (v3 beta, 2026-09-07)
45956
+
45957
+ A nested table's `snap` was a number: a gridline index into the parent's
45958
+ rendered columns, with the selection column at `0`, fractions for partial
45959
+ tracks and `-1` for the end of the data region. Index `k` was whatever column
45960
+ happened to render there, so a reorder or a hidden column on the parent
45961
+ moved every snap. The parent now publishes its tracks under **column keys**,
45962
+ and `snap` names the parent column:
45963
+
45964
+ ```diff
45965
+ const holdings = [
45966
+ - { key: 'name', label: 'Resource', snap: 1 },
45967
+ - { key: 'kind', label: 'Type', snap: 2 },
45968
+ - { key: 'monthlyCost', label: '$ / mo', align: 'right', snap: [3, 4.5] },
45969
+ + { key: 'name', label: 'Resource', snap: 'name' },
45970
+ + { key: 'kind', label: 'Type', snap: 'team' },
45971
+ + { key: 'monthlyCost', label: '$ / mo', align: 'right', snap: ['region', 'monthly.5'] },
45972
+ ];
45973
+ ```
45974
+
45975
+ - Translate each index to the parent column that rendered there. The
45976
+ selection column is `'select'`, the actions column `'actions'` — both are
45977
+ targets by name, so `['name', 'actions']` runs a child column across the
45978
+ parent's actions cell.
45979
+ - A pair is **inclusive**: `['client', 'amount']` spans from the start of
45980
+ `client` through the end of `amount` (the old `[a, b]` ended at the start
45981
+ of `b`). A trailing `.digits` on a key is a fraction of that track. `-1`
45982
+ is gone: the last column already runs to the end of the data region unless
45983
+ it declares a `width` or a pair.
45984
+ - A child column with no `snap` now pairs with the parent column of the
45985
+ **same key** when there is one, so most children need no snaps at all; a
45986
+ column the parent lacks (or hides) flows into the next free parent track.
45987
+ - Under a parent that uses `order` or `reorderable`, snaps and key pairing
45988
+ are ignored (dev builds warn) and the child inherits by position, as it
45989
+ always did — a moving parent can only be followed slot by slot.
45990
+
45991
+ Silent on upgrade: a numeric `snap` fails the type check but is ignored at
45992
+ runtime, so the child falls back to key pairing and flow, which for a child
45993
+ sharing the parent's keys is the same alignment as before.
45994
+
45995
+ **The published track variables changed name with it.** `--table-{id}-track-{k}`
45996
+ per data column became `--table-{id}-track-{key}`, the key in its slot-name
45997
+ spelling (`address.city` → `address_city`); `select` and `actions` are
45998
+ unchanged. Consumer CSS that read the numbered variables directly has to
45999
+ switch to the keys. Nothing else about the bridge moved.
46000
+
45781
46001
  ### `#no-data` is content, not a row
45782
46002
 
45783
46003
  v2 handed the slot the whole empty-state `<tr>`, so anything that was not a
@@ -46198,8 +46418,11 @@ grid actually raises is Chrome/Edge, 114 → 117 (123 for centred cells).
46198
46418
  - **Do not hide cells with `display: none`** (a `thClass`/`tdClass` of
46199
46419
  `hidden`, the v2/early-v3 narrow-screen recipe). A hidden grid item leaves
46200
46420
  its track in place: every following cell shifts one track over and the last
46201
- track collapses. Hide a column by removing it from `columns` at the
46202
- breakpoint, which removes the track.
46421
+ track collapses. Hide a column with `hidden: true` on its definition at the
46422
+ breakpoint, which removes the track while the column stays declared (so
46423
+ its slot in `order` survives). Filtering it out of `columns` was the
46424
+ interim advice and is now reserved for a column that does not exist for
46425
+ this user at all.
46203
46426
  - **`contain: inline-size` on `.bb-table-expand__cell`,
46204
46427
  `.bb-table-no-data__cell` and `.bb-table-loading__cell`.** It is the only
46205
46428
  thing keeping a full-width cell's content from resizing the column tracks —
@@ -46220,7 +46443,7 @@ grid actually raises is Chrome/Edge, 114 → 117 (123 for centred cells).
46220
46443
 
46221
46444
  ### Small semantic and visual changes
46222
46445
 
46223
- - **Pinned cells and the stuck header are opaque.** `fixed-columns` cells and
46446
+ - **Pinned cells and the stuck header are opaque.** Pinned (`column.fixed`) cells and
46224
46447
  the `fixed-headers` band used to be transparent, so scrolled content showed
46225
46448
  through them. They now paint the table's surface local `--bg` (default
46226
46449
  `var(--bb-panel)`); a pinned cell also paints its row's hover/highlight
@@ -50070,7 +50293,6 @@ export function useApprovalsInbox(): ApprovalsInboxContext {
50070
50293
  v-model:highlighted="ctx.highlighted.value"
50071
50294
  v-model:select-all="ctx.table.all.value"
50072
50295
  v-model:unselected-items="ctx.table.unselected.value"
50073
- actions
50074
50296
  caption="Expense approvals"
50075
50297
  :columns="columns"
50076
50298
  compact
@@ -52264,7 +52486,6 @@ export function useBacklogWorkspace(): BacklogWorkspaceContext {
52264
52486
  v-model:highlighted="highlightedIssue"
52265
52487
  v-model:select-all="ctx.table.all.value"
52266
52488
  v-model:unselected-items="ctx.table.unselected.value"
52267
- actions
52268
52489
  caption="Sprint 24 backlog"
52269
52490
  :columns="columns"
52270
52491
  compact
@@ -55847,10 +56068,10 @@ OwnerPaginator::make(...), 'tabCounts' => [...], 'ownerOptions' => [...]])` —
55847
56068
  the page.
55848
56069
  - **The expand row is a real nested `BbTable` that inherits the parent's
55849
56070
  column tracks.** `inherit-column-widths` snaps the holdings table onto the
55850
- owners table's grid; because the parent is `selectable`, every child
55851
- column carries an explicit `snap` starting at `1` (`snap: 1…4`), so
55852
- Resource sits exactly under Owner and the right-aligned $/mo columns share
55853
- one edge. This is the recipe's signature move — the full defense is in
56071
+ owners table's grid; every child column carries an explicit `snap` naming
56072
+ the parent column it sits under (`snap: 'name'` `'monthly'` the
56073
+ parent's `select` column is accounted for by name), so Resource sits
56074
+ exactly under Owner and the right-aligned $/mo columns share one edge. This is the recipe's signature move — the full defense is in
55854
56075
  "Why it is built this way" and in the
55855
56076
  [BbTable guide](../../BbTable.md) ("Nested tables that align with the
55856
56077
  parent").
@@ -56355,7 +56576,6 @@ export function useOwnershipAtlas(): OwnershipAtlasContext {
56355
56576
  v-model:highlighted="highlightedOwner"
56356
56577
  v-model:select-all="ctx.table.all.value"
56357
56578
  v-model:unselected-items="ctx.table.unselected.value"
56358
- actions
56359
56579
  actions-text="Holdings"
56360
56580
  caption="Resource owners"
56361
56581
  class="table-flush"
@@ -56686,10 +56906,10 @@ defineProps<{ owner: Owner }>();
56686
56906
  * the parent's first data column, and that inset is what reads as hierarchy.
56687
56907
  */
56688
56908
  const columns: Array<BbTableColumn<Holding>> = [
56689
- { key: 'name', label: 'Resource', snap: 1 },
56690
- { key: 'kind', label: 'Type', snap: 2 },
56691
- { key: 'status', label: 'Status', snap: 3 },
56692
- { key: 'monthlyCost', label: '$ / mo', align: 'right', snap: 4 },
56909
+ { key: 'name', label: 'Resource', snap: 'name' },
56910
+ { key: 'kind', label: 'Type', snap: 'team' },
56911
+ { key: 'status', label: 'Status', snap: 'region' },
56912
+ { key: 'monthlyCost', label: '$ / mo', align: 'right', snap: 'monthly' },
56693
56913
  ];
56694
56914
 
56695
56915
  const money = (value: number) =>
@@ -58012,7 +58232,6 @@ const ctx = useInvoiceForm();
58012
58232
  <!-- Inline-editing table (§6.3): every editable cell type is its own
58013
58233
  leaf. The amount column is virtual — computed from the row. -->
58014
58234
  <BbTable
58015
- actions
58016
58235
  caption="Invoice line items"
58017
58236
  :columns="columns"
58018
58237
  compact
@@ -59216,7 +59435,6 @@ export function useInvoicesWorkspace(): InvoicesWorkspaceContext {
59216
59435
  v-model="ctx.table.selected.value"
59217
59436
  v-model:select-all="ctx.table.all.value"
59218
59437
  v-model:unselected-items="ctx.table.unselected.value"
59219
- actions
59220
59438
  caption="Invoices"
59221
59439
  :columns="columns"
59222
59440
  compact
@@ -61034,7 +61252,6 @@ export function useUploadCenter(): UploadCenterContext {
61034
61252
  v-model="ctx.table.selected.value"
61035
61253
  v-model:select-all="ctx.table.all.value"
61036
61254
  v-model:unselected-items="ctx.table.unselected.value"
61037
- actions
61038
61255
  caption="Expense receipts"
61039
61256
  :columns="columns"
61040
61257
  compact
@@ -64427,7 +64644,6 @@ export function useApprovalsInbox(): ApprovalsInboxContext {
64427
64644
 
64428
64645
  <BbTable
64429
64646
  id="approvals-table"
64430
- actions
64431
64647
  caption="Expense approvals"
64432
64648
  :columns="columns"
64433
64649
  compact
@@ -66554,7 +66770,6 @@ export function useBacklogWorkspace(): BacklogWorkspaceContext {
66554
66770
  <BbTable
66555
66771
  id="backlog-table"
66556
66772
  v-model:highlighted="highlightedIssue"
66557
- actions
66558
66773
  caption="Sprint 24 backlog"
66559
66774
  :columns="columns"
66560
66775
  compact
@@ -70817,7 +71032,6 @@ const ctx = useInvoiceForm();
70817
71032
  <!-- Inline-editing table (§6.3): every editable cell type is its own
70818
71033
  leaf. The amount column is virtual — computed from the row. -->
70819
71034
  <BbTable
70820
- actions
70821
71035
  caption="Invoice line items"
70822
71036
  :columns="columns"
70823
71037
  compact
@@ -72008,7 +72222,6 @@ export function useInvoicesWorkspace(): InvoicesWorkspaceContext {
72008
72222
 
72009
72223
  <BbTable
72010
72224
  id="invoices-table"
72011
- actions
72012
72225
  caption="Invoices"
72013
72226
  :columns="columns"
72014
72227
  compact
@@ -73718,7 +73931,6 @@ export function useUploadCenter(): UploadCenterContext {
73718
73931
 
73719
73932
  <BbTable
73720
73933
  id="receipts-table"
73721
- actions
73722
73934
  caption="Expense receipts"
73723
73935
  :columns="columns"
73724
73936
  compact
@@ -76961,7 +77173,6 @@ export function useApprovalsInbox(): ApprovalsInboxContext {
76961
77173
 
76962
77174
  <BbTable
76963
77175
  id="approvals-table"
76964
- actions
76965
77176
  caption="Expense approvals"
76966
77177
  :columns="columns"
76967
77178
  compact
@@ -78970,7 +79181,6 @@ export function useBacklogWorkspace(): BacklogWorkspaceContext {
78970
79181
  <BbTable
78971
79182
  id="backlog-table"
78972
79183
  v-model:highlighted="highlightedIssue"
78973
- actions
78974
79184
  caption="Sprint 24 backlog"
78975
79185
  :columns="columns"
78976
79186
  compact
@@ -84380,7 +84590,6 @@ export function useInvoicesWorkspace(): InvoicesWorkspaceContext {
84380
84590
 
84381
84591
  <BbTable
84382
84592
  id="invoices-table"
84383
- actions
84384
84593
  caption="Invoices"
84385
84594
  :columns="columns"
84386
84595
  compact
@@ -86018,7 +86227,6 @@ export function useUploadCenter(): UploadCenterContext {
86018
86227
 
86019
86228
  <BbTable
86020
86229
  id="receipts-table"
86021
- actions
86022
86230
  caption="Expense receipts"
86023
86231
  :columns="columns"
86024
86232
  compact