bitboss-ui 3.0.0-beta.8 → 3.0.0-beta.9

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 +391 -26
  2. package/dist/ai/changelog.json +8 -2
  3. package/dist/ai/components.json +37 -5
  4. package/dist/ai/guides/component-picker.md +2 -2
  5. package/dist/ai/guides/migration/components/bb-table.md +54 -14
  6. package/dist/ai/source/BbTable.md +1612 -155
  7. package/dist/components/BbTable/BbTable.vue.d.ts +3 -2
  8. package/dist/components/BbTable/BbTable.vue_vue_type_script_setup_true_lang.js +829 -408
  9. package/dist/components/BbTable/types.d.ts +135 -66
  10. package/dist/components/BbTable/utils.d.ts +53 -1
  11. package/dist/components/BbTable/utils.js +43 -20
  12. package/dist/i18n/locales/cs.json +4 -1
  13. package/dist/i18n/locales/da.json +4 -1
  14. package/dist/i18n/locales/de.json +4 -1
  15. package/dist/i18n/locales/el.json +4 -1
  16. package/dist/i18n/locales/en.json +4 -1
  17. package/dist/i18n/locales/es.json +4 -1
  18. package/dist/i18n/locales/fi.json +4 -1
  19. package/dist/i18n/locales/fr.json +4 -1
  20. package/dist/i18n/locales/hu.json +4 -1
  21. package/dist/i18n/locales/it.json +4 -1
  22. package/dist/i18n/locales/ja.json +4 -1
  23. package/dist/i18n/locales/ko.json +4 -1
  24. package/dist/i18n/locales/nb.json +4 -1
  25. package/dist/i18n/locales/nl.json +4 -1
  26. package/dist/i18n/locales/pl.json +4 -1
  27. package/dist/i18n/locales/pt.json +4 -1
  28. package/dist/i18n/locales/ro.json +4 -1
  29. package/dist/i18n/locales/ru.json +4 -1
  30. package/dist/i18n/locales/sv.json +4 -1
  31. package/dist/i18n/locales/tr.json +4 -1
  32. package/dist/i18n/locales/uk.json +4 -1
  33. package/dist/i18n/locales/zh-cn.json +4 -1
  34. package/dist/llms-full.txt +447 -42
  35. package/dist/llms-medium.txt +2 -2
  36. package/dist/locale-blueprint.json +4 -1
  37. package/dist/styles.css +1 -1
  38. package/package.json +1 -1
@@ -25,8 +25,8 @@ keyboard navigation, so your markup stays a column definition plus a few slots.
25
25
  domain objects straight to `items` and set `item-value` for stable row
26
26
  identity — never pre-map rows into `{ label, value }` shapes.
27
27
 
28
- The table **reports** state (sort, selection, page, expansion, highlight)
29
- through v-models; it never reorders, slices, or fetches on its own. You apply
28
+ The table **reports** state (sort, column order, selection, page, expansion,
29
+ highlight) through v-models; it never sorts, slices, or fetches on its own. You apply
30
30
  that state to your data source (client-side or via `dependencies`-driven
31
31
  refetch). This keeps it equally at home in a client-only screen and a
32
32
  server-paginated one.
@@ -711,8 +711,10 @@ scriptable from anywhere via `useBbTableContext`
711
711
  ### Sorting
712
712
 
713
713
  Mark columns `sortable` and bind `v-model:sort` — an ordered
714
- `BbTableSortEntry[]` (`[columnKey, 'asc' | 'desc']`). Header buttons cycle a
715
- column unsorted asc desc removed; a direction flip updates the entry in
714
+ `BbTableSortEntry[]` (`[columnKey, 'asc' | 'desc']`). A click anywhere on a
715
+ sortable header (its whitespace included AG Grid parity; the built-in
716
+ button is the keyboard/AT control) cycles the column unsorted → asc → desc →
717
+ removed; a direction flip updates the entry in
716
718
  place, so the column keeps its priority in a multi-column sort and other
717
719
  entries are never touched. The array order **is** the priority: first entry
718
720
  sorts first. The table only reports the model; **apply it to your data**
@@ -812,6 +814,217 @@ field only seeds the header's `aria-sort` when no live model entry exists — it
812
814
  does not move the visual indicator; to start pre-sorted, seed `v-model:sort`
813
815
  instead (as the example does).
814
816
 
817
+ ### Column order
818
+
819
+ `columns` declares what a column **is**; `v-model:order` is the order
820
+ it **renders** in — an array of column keys, the same split as `sortable` +
821
+ `v-model:sort`. Two things write it:
822
+
823
+ - **Your own control** — a column panel, a settings drawer, a persisted
824
+ preference. Bind the model and write the array; the table follows. No flag
825
+ needed for this.
826
+ - **The user, on the table** — with `reorderable`, every data header
827
+ is a grab surface: drag it and the column moves live under the pointer (AG
828
+ Grid style, a chip with the label follows), release to commit. The same
829
+ header row is also one keyboard tab stop (the AG Grid grammar): ←/→ move
830
+ focus between headers, **Shift+←/→** move the column one slot
831
+ (Shift+Home/End to the ends), Enter sorts a sortable header.
832
+ Each committed move writes the model once and is announced.
833
+
834
+ The model's contract is what makes a column panel trivial:
835
+
836
+ - Keys listed first render first; declared columns the array omits follow, in
837
+ declaration order (a column added to `columns` later renders last).
838
+ - Keys that match no declared column render nothing but are **preserved in
839
+ place**. So hide a column by filtering it out of `columns` — never out of
840
+ the order — and it comes back exactly where it was.
841
+ - A user move relocates **exactly one key**; everything else, hidden keys
842
+ included, keeps its relative order. The first move on a partial (or empty)
843
+ model emits the completed array — persist that.
844
+ - A drop that changes nothing emits nothing; unbound, the table keeps the
845
+ order as per-mount state like `sort`.
846
+
847
+ **A column panel and the header drag sharing one order**
848
+
849
+ ```vue
850
+ <template>
851
+ <div class="flex max-w-2xl flex-col gap-3">
852
+ <!-- ONE order, two writers: the panel's buttons and the table's own
853
+ header drag / keyboard handles both write `v-model:order`.
854
+ Hiding a column only filters `columns` — its key stays in the order,
855
+ so it comes back where it was. -->
856
+ <ul
857
+ aria-label="Columns"
858
+ class="flex flex-wrap gap-2 text-xs text-[color:var(--bb-text-muted)]"
859
+ >
860
+ <li
861
+ v-for="(column, index) in panelColumns"
862
+ :key="column.key"
863
+ class="flex items-center gap-1 rounded-[var(--bb-radius-sm)] border border-[color:var(--bb-border)] py-0.5 pl-2 pr-1"
864
+ >
865
+ <BbCheckbox
866
+ :label="column.label"
867
+ :model-value="!hidden.has(column.key)"
868
+ @update:model-value="toggleVisible(column.key)"
869
+ />
870
+ <BbButton
871
+ :aria-label="`Move ${column.label} left`"
872
+ :disabled="index === 0"
873
+ icon="lucide:chevron-left"
874
+ size="xs"
875
+ variant="ghost"
876
+ @click="move(column.key, -1)"
877
+ />
878
+ <BbButton
879
+ :aria-label="`Move ${column.label} right`"
880
+ :disabled="index === panelColumns.length - 1"
881
+ icon="lucide:chevron-right"
882
+ size="xs"
883
+ variant="ghost"
884
+ @click="move(column.key, 1)"
885
+ />
886
+ </li>
887
+ </ul>
888
+ <div
889
+ class="overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
890
+ >
891
+ <BbTable
892
+ v-model:order="order"
893
+ caption="Invoices"
894
+ :columns="visibleColumns"
895
+ compact
896
+ item-value="id"
897
+ :items="invoices"
898
+ reorderable
899
+ />
900
+ </div>
901
+ <p class="text-xs text-[color:var(--bb-text-muted)]">
902
+ Order model: <code>{{ JSON.stringify(order) }}</code>
903
+ </p>
904
+ </div>
905
+ </template>
906
+ <script setup lang="ts">
907
+ import { computed, ref } from 'vue';
908
+ import { BbButton, BbCheckbox, BbTable } from 'bitboss-ui';
909
+ import type { BbTableColumn } from 'bitboss-ui';
910
+
911
+ type Invoice = {
912
+ id: string;
913
+ number: string;
914
+ client: string;
915
+ issued: string;
916
+ amount: number;
917
+ };
918
+
919
+ const euro = new Intl.NumberFormat('en-IE', {
920
+ style: 'currency',
921
+ currency: 'EUR',
922
+ });
923
+
924
+ // The declaration: what a column IS. Never reordered.
925
+ const columns: Array<BbTableColumn<Invoice>> = [
926
+ { key: 'number', label: 'Invoice', width: 110 },
927
+ { key: 'client', label: 'Client' },
928
+ { key: 'issued', label: 'Issued', width: 110 },
929
+ {
930
+ key: 'amount',
931
+ label: 'Amount',
932
+ align: 'right',
933
+ width: 110,
934
+ formatter: euro.format,
935
+ },
936
+ ];
937
+
938
+ const invoices: Invoice[] = [
939
+ {
940
+ id: '1',
941
+ number: 'INV-1041',
942
+ client: 'Northwind Traders',
943
+ issued: '2026-08-02',
944
+ amount: 4200,
945
+ },
946
+ {
947
+ id: '2',
948
+ number: 'INV-1042',
949
+ client: 'Globex',
950
+ issued: '2026-08-05',
951
+ amount: 980,
952
+ },
953
+ {
954
+ id: '3',
955
+ number: 'INV-1043',
956
+ client: 'Initech',
957
+ issued: '2026-08-11',
958
+ amount: 15600,
959
+ },
960
+ {
961
+ id: '4',
962
+ number: 'INV-1044',
963
+ client: 'Umbrella',
964
+ issued: '2026-08-19',
965
+ amount: 2750,
966
+ },
967
+ ];
968
+
969
+ // The state: render order (keys). Persist THIS. Seed it with the full key
970
+ // list — it also completes itself on the first move.
971
+ const order = ref<string[]>(columns.map((column) => column.key));
972
+ const hidden = ref(new Set<string>());
973
+
974
+ // Visibility is just filtering the declaration; the hidden key keeps its
975
+ // slot in `order`.
976
+ const visibleColumns = computed(() =>
977
+ columns.filter((column) => !hidden.value.has(column.key))
978
+ );
979
+
980
+ // The panel lists every declared column in the order the model has them.
981
+ const panelColumns = computed(() =>
982
+ [...columns].sort(
983
+ (a, b) => order.value.indexOf(a.key) - order.value.indexOf(b.key)
984
+ )
985
+ );
986
+
987
+ const toggleVisible = (key: string) => {
988
+ const next = new Set(hidden.value);
989
+ if (next.has(key)) next.delete(key);
990
+ else next.add(key);
991
+ hidden.value = next;
992
+ };
993
+
994
+ const move = (key: string, delta: 1 | -1) => {
995
+ const next = [...order.value];
996
+ const from = next.indexOf(key);
997
+ const to = from + delta;
998
+ if (from < 0 || to < 0 || to >= next.length) return;
999
+ next.splice(from, 1);
1000
+ next.splice(to, 0, key);
1001
+ order.value = next;
1002
+ };
1003
+ </script>
1004
+ ```
1005
+
1006
+ **Pinned slots are positions, not columns.** `fixed-columns` indexes rendered
1007
+ slots like a spreadsheet's freeze panes (a selection column is slot 0). With
1008
+ column order the slot holds: whichever column lands in it is pinned, and
1009
+ dragging the identity column out of its pinned slot (slot 1 when
1010
+ `selectable`, slot 0 otherwise) unpins it (AG Grid pins columns; BbTable
1011
+ pins positions). Keep the reorder + pin combination for slots that
1012
+ never move — `select`, `actions`, or a first column you also lock by
1013
+ normalizing the model in your `@update:order` handler.
1014
+
1015
+ **Nested tables.** An inheriting child aligns to the parent's rails by
1016
+ **position**, so a reordered parent re-pairs the child's columns unless the
1017
+ child follows. Give both tables the same `v-model:order` when they
1018
+ share keys (and the child declares no `snap`) and they stay paired by key.
1019
+ `snap` points are positional in the parent's rendered order: reorder the
1020
+ parent and a snap-mapped child follows its slots, but never reorder (or share
1021
+ the order with) the snap-mapped child itself — dev builds warn.
1022
+
1023
+ Apply the model synchronously in your handler and persist afterwards: an
1024
+ `await` before the write moves the cell in a later flush, after the table
1025
+ has re-focused the handle, and keyboard focus is usually lost (whether it
1026
+ survives depends on which cell Vue's keyed patch detaches).
1027
+
815
1028
  ### Selection and bulk actions
816
1029
 
817
1030
  Add `selectable` for multi-select (`multiple` is the default) bound with
@@ -1311,6 +1524,99 @@ fixed first track).
1311
1524
  The actions-column toggle remains valid — prefer it when expansion reveals
1312
1525
  _detail about the row_ rather than _children of it_.
1313
1526
 
1527
+ ### Column resize
1528
+
1529
+ `resizable` puts a handle on the trailing edge of every data header:
1530
+ drag it and the column follows live, release to commit; double-click it to go
1531
+ back to the declared width. From the keyboard, **Alt+←/→** resizes the
1532
+ focused header in 10px steps (the header row is a tab stop whenever columns
1533
+ are resizable or reorderable).
1534
+
1535
+ There is no width model on purpose. `width` on the column definition stays
1536
+ the single declared truth, and a user resize is an **override** the table
1537
+ keeps until that column's `width` changes:
1538
+
1539
+ - Each commit fires `@resize:column="(key, width) => …"` **once** with the new width
1540
+ in px — or `null` after a double-click reset.
1541
+ - To persist, write the reported width back into your column definition (a
1542
+ `computed` over your persisted widths). The definition wins whenever it
1543
+ changes, so a programmatic `width` change always takes effect.
1544
+ - Unbound, the override lives for the mount, like an unbound `sort`.
1545
+ - A resized width is always px; `width` still accepts `'20%'` / `'12rem'`
1546
+ for what you declare yourself.
1547
+
1548
+ Headers never wrap and never floor their column: squeezed, a header label
1549
+ ellipsizes and the sort arrow stays; at the narrowest only the arrow (and
1550
+ the handle) survive, and the column is still resizable. The data cells of a
1551
+ resized column clip with an ellipsis instead of spilling into the neighbour
1552
+ (a squeezed track cannot wrap a single word); every other cell keeps
1553
+ wrapping. While a resize runs the whole page shows the resize cursor.
1554
+
1555
+ A resized column becomes an **exact** track; the other columns keep doing
1556
+ what they did — unfrozen ones share the surplus, and a table whose columns
1557
+ are all declared with a `width` keeps sharing it among the un-resized ones —
1558
+ so a filling table stays filled and nothing collapses. **Shift+drag** moves
1559
+ the border instead: the right neighbour absorbs the delta, the total stays
1560
+ put, and both columns are reported. A table that inherits its widths from a
1561
+ parent (`inherit-column-widths`) renders no handles — its tracks are the
1562
+ parent's.
1563
+
1564
+ **Resizable columns reporting widths back into the definition**
1565
+
1566
+ ```vue
1567
+ <template>
1568
+ <div class="flex max-w-2xl flex-col gap-1.5">
1569
+ <div
1570
+ class="overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
1571
+ >
1572
+ <BbTable
1573
+ caption="Invoices"
1574
+ :columns="columns"
1575
+ compact
1576
+ item-value="id"
1577
+ :items="invoices"
1578
+ resizable
1579
+ @resize="onResize"
1580
+ />
1581
+ </div>
1582
+ <p class="text-xs text-[color:var(--bb-text-muted)]">
1583
+ Widths: <code>{{ JSON.stringify(widths) }}</code>
1584
+ </p>
1585
+ </div>
1586
+ </template>
1587
+ <script setup lang="ts">
1588
+ import { computed, ref } from 'vue';
1589
+ import { BbTable } from 'bitboss-ui';
1590
+ import type { BbTableColumn } from 'bitboss-ui';
1591
+
1592
+ type Invoice = { id: string; number: string; client: string; amount: number };
1593
+
1594
+ const base: Array<BbTableColumn<Invoice>> = [
1595
+ { key: 'number', label: 'Invoice' },
1596
+ { key: 'client', label: 'Client' },
1597
+ { key: 'amount', label: 'Amount', align: 'right' },
1598
+ ];
1599
+ // Persist THIS (localStorage, a user preference…): key → px.
1600
+ const widths = ref<Record<string, number>>({});
1601
+ // The definition carries the persisted widths; the table treats them as the
1602
+ // declared truth, so nothing jumps after a reload.
1603
+ const columns = computed(() =>
1604
+ base.map((column) => ({ ...column, width: widths.value[column.key] }))
1605
+ );
1606
+ const onResize = (key: string, width: number | null) => {
1607
+ const next = { ...widths.value };
1608
+ if (width === null) delete next[key];
1609
+ else next[key] = width;
1610
+ widths.value = next;
1611
+ };
1612
+ const invoices: Invoice[] = [
1613
+ { id: '1', number: 'INV-1041', client: 'Northwind Traders', amount: 4200 },
1614
+ { id: '2', number: 'INV-1042', client: 'Globex', amount: 980 },
1615
+ { id: '3', number: 'INV-1043', client: 'Initech', amount: 15600 },
1616
+ ];
1617
+ </script>
1618
+ ```
1619
+
1314
1620
  ### Nested tables that align with the parent
1315
1621
 
1316
1622
  A `BbTable` rendered inside another's `#expand` slot is independent by
@@ -1528,11 +1834,42 @@ clicks are ignored on purpose: those inside interactive elements (links,
1528
1834
  buttons, inputs, labels keep their own semantics) and those ending a text
1529
1835
  selection (dragging to copy a cell must not mutate state).
1530
1836
 
1837
+ **Highlight is opt-in, and the model's value is the switch.** Bind nothing and
1838
+ the mechanic is entirely off — a row click emits `click:row` and does nothing
1839
+ else: no class, no `aria-current`, no state. That matters because the table has
1840
+ nowhere to put a highlight it invents; a table that highlighted itself would
1841
+ drive a cursor the app can neither read nor clear.
1842
+
1843
+ The switch reads the value, not the binding: **`undefined` means "never
1844
+ initialised" and keeps highlight off; `null` means "initialised and currently
1845
+ empty" and turns it on.** So initialise the model:
1846
+
1847
+ ```ts
1848
+ const highlighted = ref(null); // ✅ connected
1849
+ const highlighted = ref(); // ❌ undefined — highlight stays off (warns in dev)
1850
+ ```
1851
+
1852
+ A value seeded through the shared context counts exactly the same, and binds
1853
+ two ways just like the prop:
1854
+
1855
+ ```ts
1856
+ const highlighted = ref(null);
1857
+ useBbTableContext('invoices-table', { highlighted });
1858
+ ```
1859
+
1860
+ `click:row` is never gated — it fires on every row click whether or not
1861
+ highlight is connected, so listening for clicks costs you nothing. Calling
1862
+ `toggleHighlighted` from a cell slot on an unconnected table does nothing and
1863
+ warns once in dev.
1864
+
1531
1865
  Highlight ships with a **default cursor**: the row gets `aria-current="true"`
1532
1866
  and the `bb-table-data__row--highlighted` class, and the shipped stylesheet
1533
- paints that class with a subtle primary-tinted fill plus a left accent bar
1534
- distinct from hover and from a plain focus ring, and readable in both light
1535
- and dark. Screen-reader users get the `aria-current` announcement either way;
1867
+ paints that class with a quiet neutral fill plus a left accent bar in your
1868
+ brand primary — distinct from hover and from a plain focus ring, and readable
1869
+ in both light and dark. The fill is `--bb-muted` (the shared "selected/active"
1870
+ stop on the neutral surface ladder, one step above the `--bb-surface-hover`
1871
+ used for row hover) and steps to `--bb-pressed` while hovered, so it never
1872
+ depends on what your `--bb-primary` happens to be — only the accent bar does. Screen-reader users get the `aria-current` announcement either way;
1536
1873
  sighted users now get a visible cursor out of the box too, including once
1537
1874
  `keyboard-navigation` is on and the roving tabindex needs to be seen. Override
1538
1875
  it with `row-class` (or equivalent visible styling) whenever the default
@@ -2145,6 +2482,9 @@ leave the screen. Collapse instead of scroll:
2145
2482
  out. Removing the column removes its grid track. Do **not** hide cells with
2146
2483
  `thClass`/`tdClass` + `display: none`: the table is a grid, a hidden cell
2147
2484
  leaves its track in place and every following cell shifts one track over.
2485
+ A dropped key keeps its slot in `order` (the write-back never drops
2486
+ unknown keys), so the column returns where it was — never filter the
2487
+ order array.
2148
2488
  - **Stack what survives into the primary cell.** The identity slot renders the
2149
2489
  name plus the hidden facts under it, at `--bb-text-muted`, so nothing is
2150
2490
  actually lost.
@@ -2174,20 +2514,23 @@ root `.bb-table__table[role=table]` owns the column tracks and every row group
2174
2514
  and row is a subgrid of it. Style it through the classes and the roles — never
2175
2515
  through element names, which match nothing:
2176
2516
 
2177
- | Region | Class | Role |
2178
- | ------------ | -------------------------------------------------------- | ----------------------------------------- |
2179
- | grid root | `.bb-table__table` | `table` |
2180
- | header group | `.bb-table__head` | `rowgroup` |
2181
- | header row | `.bb-table-header-row` | `row` |
2182
- | header cell | `.bb-table-header` (+ `--select`, `--actions`) | `columnheader` |
2183
- | body group | `.bb-table__body` | `rowgroup` |
2184
- | data row | `.bb-table-data__row` (+ `--highlighted`) | `row` |
2185
- | data cell | `.bb-table-data__cell` (+ `--select`) | `cell` |
2186
- | actions cell | `.bb-table__cell--actions` | `cell` |
2187
- | expand row | `.bb-table-expand__row` > `.bb-table-expand__cell` | `row` > `cell` |
2188
- | empty state | `.bb-table-no-data__row` > `.bb-table-no-data__cell` | `row` > `cell` |
2189
- | footer group | `.bb-table__foot` (`#tfoot` content) | `rowgroup` only when `#tfoot` is provided |
2190
- | caption | `.bb-table-caption` (before the root, `aria-labelledby`) | |
2517
+ | Region | Class | Role |
2518
+ | -------------- | ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
2519
+ | grid root | `.bb-table__table` | `table` |
2520
+ | header group | `.bb-table__head` | `rowgroup` |
2521
+ | header row | `.bb-table-header-row` | `row` |
2522
+ | header cell | `.bb-table-header` (+ `--select`, `--actions`) | `columnheader` |
2523
+ | body group | `.bb-table__body` | `rowgroup` |
2524
+ | data row | `.bb-table-data__row` (+ `--highlighted`) | `row` |
2525
+ | data cell | `.bb-table-data__cell` (+ `--select`) | `cell` |
2526
+ | actions cell | `.bb-table__cell--actions` | `cell` |
2527
+ | expand row | `.bb-table-expand__row` > `.bb-table-expand__cell` | `row` > `cell` |
2528
+ | empty state | `.bb-table-no-data__row` > `.bb-table-no-data__cell` | `row` > `cell` |
2529
+ | footer group | `.bb-table__foot` (`#tfoot` content) | `rowgroup` only when `#tfoot` is provided |
2530
+ | focused header | `.bb-table-header[data-column-key]:focus-visible` (`reorderable` only; inset ring, the header row is one roving tab stop) | `columnheader` |
2531
+ | dragged column | `.bb-table-header--dragging` on the header; `.bb-table__head--dragging` on the head group during a drag | — |
2532
+ | drag chip | `.bb-table-column-ghost` (a `popover="manual"` in the top layer; paints `--bb-panel`, not the table's `--bg`) | — (`aria-hidden`) |
2533
+ | caption | `.bb-table-caption` (before the root, `aria-labelledby`) | — |
2191
2534
 
2192
2535
  `[role='row']`, `[role='cell']` and `[role='columnheader']` are supported
2193
2536
  hooks, not incidental markup — use them when you mean "every row kind" or
@@ -2295,6 +2638,8 @@ reader.
2295
2638
  - `BbPagination` — shared page ref, or zero-wiring via `id` + `table-id`.
2296
2639
  - `useBbTableContext` (`./composables/useBbTableContext.md`) — bulk-action
2297
2640
  toolbars, detail panels, "reset sort" buttons living outside the table.
2641
+ (Column order is not mirrored there: a column panel needs `columns` too,
2642
+ so it lives where `v-model:order` is bound.)
2298
2643
  - `BbButton` — per-row links (`href`/`to`) in cells or `#actions`;
2299
2644
  `BbDropdown` for an overflow menu of row actions.
2300
2645
  - `BbBadge` / `BbAvatar` — status and identity cells via slots.
@@ -2317,8 +2662,13 @@ reader.
2317
2662
  localized `common.loadingText`, so an unset value still announces.
2318
2663
  - Row selection labels default to the localized "select" word plus every
2319
2664
  cell's content. `accessible-label` overrides that per row — it is called
2320
- `(columns, item)` with the mapped cells and the raw record, and whatever it
2321
- returns becomes the checkbox's label.
2665
+ `(columns, item)` with the mapped cells (in **render** order look them up
2666
+ by `key`, never by position) and the raw record, and whatever it returns
2667
+ becomes the checkbox's label.
2668
+ - `fixed-columns` pins **slots**, not columns: under `order` the
2669
+ column that lands in a pinned slot is the pinned one (see Column order).
2670
+ - A column panel that holds a partial `order` receives the completed
2671
+ array on the user's first move — its persisted list grows to every key.
2322
2672
  - Enabling select-all does not re-emit an empty `modelValue` (see the
2323
2673
  select-all section) — in all-mode, read `select-all` +
2324
2674
  `unselected-items`, not `modelValue`.
@@ -2358,6 +2708,15 @@ reader.
2358
2708
  identity rather than object reference.
2359
2709
  - Sortable headers set `aria-sort` and render real buttons; custom header
2360
2710
  slots should keep a button calling `toggleSort`.
2711
+ - With `reorderable` the header row becomes one roving tab stop
2712
+ (the AG Grid grammar): ←/→ move focus between headers, Home/End to the
2713
+ ends, **Shift+←/→** move the focused column one slot (Shift+Home/End to
2714
+ the ends), Enter/Space sorts a sortable header — every move committed
2715
+ immediately and announced through a polite status region ("X moved to
2716
+ position 2 of 5"); a one-line sr-only instruction describes each header.
2717
+ The sort button leaves the tab order (the header is the target; it still
2718
+ works for the mouse). NVDA/JAWS browse mode intercepts arrows on a focused
2719
+ element — the same focus-mode step as row keyboard navigation.
2361
2720
  - When you paginate, pass `page` / `per-page` / `total-items` so the table
2362
2721
  announces the full result count (`aria-rowcount`) and each row's absolute
2363
2722
  index (`aria-rowindex`, expansion-aware), not just the current page.
@@ -2377,6 +2736,7 @@ reader.
2377
2736
  - `v-model:expandedItems` — type: `any[] \| undefined`
2378
2737
  - `v-model:highlighted` — type: `any`
2379
2738
  - `v-model` — type: `any`
2739
+ - `v-model:order` — type: `string[] \| undefined`
2380
2740
  - `v-model:selectAll` — type: `boolean \| undefined`
2381
2741
  - `v-model:sort` — type: `BbTableSortEntry[] \| undefined`
2382
2742
  - `v-model:unselectedItems` — type: `any[] \| undefined`
@@ -2385,7 +2745,7 @@ reader.
2385
2745
 
2386
2746
  | Prop | Type | Default | Required | Description |
2387
2747
  | --- | --- | --- | --- | --- |
2388
- | `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. |
2748
+ | `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`… |
2389
2749
  | `actions` | `boolean \| undefined` | | | Displays the actions column. |
2390
2750
  | `actionsText` | `string \| undefined` | | | Label used in the header of the actions column. |
2391
2751
  | `align` | `"left" \| "center" \| "right" \| undefined` | `"left"` | | Text alignment of the columns. |
@@ -2400,10 +2760,10 @@ reader.
2400
2760
  | `enforceCoherence` | `boolean \| undefined` | | | 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… |
2401
2761
  | `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`/… |
2402
2762
  | `fixed` | `boolean \| undefined` | | | 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. |
2403
- | `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. |
2763
+ | `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… |
2404
2764
  | `fixedHeaders` | `boolean \| undefined` | | | Boolean that sets the headers as sticky to the top of the table. |
2405
2765
  | `headerRowClass` | `Classes \| undefined` | | | Defines the classes to be passed to the header row. |
2406
- | `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. Purely mechanical: highlighted rows only get the `bb-tabl… |
2766
+ | `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… |
2407
2767
  | `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. |
2408
2768
  | `inheritColumnWidths` | `string \| boolean \| undefined` | | | Opt a nested table into inheriting its column widths from an ancestor table's matching tracks (accounting for `select`/`actions` columns). This also makes the parent's tracks authoritative, so it is **off by default** — a nested table rende… |
2409
2769
  | `interactiveWhileLoading` | `boolean \| undefined` | | | Keeps the table interactive while it is loading. The header and the rows are `inert` while loading by default — a refetch is about to replace the rows, so editing, selecting or sorting them races the incoming data. This escape hatch exists … |
@@ -2418,9 +2778,12 @@ reader.
2418
2778
  | `multiple` | `boolean \| undefined` | `true` | | Allows the selection of multiple items. |
2419
2779
  | `name` | `string \| undefined` | | | Defines the name of the input. |
2420
2780
  | `noDataText` | `string \| undefined` | | | String displayed when there are no items to display. |
2781
+ | `order` | `string[] \| undefined` | `[]` | | Used by `v-model:order`. Ordered array of column keys — the render order of the data columns. Keys listed here render first, in this order; declared columns it omits follow, in declaration order (so a column added to `columns` later renders… |
2421
2782
  | `page` | `string \| number \| undefined` | | | Current page number starting from 1, used for accessibility purposes. Also flows into the shared table context, so a `BbPagination` paired through `id` reads it as the current page. Numeric strings are coerced (server pagination fields ofte… |
2422
2783
  | `perPage` | `string \| number \| undefined` | | | Number of items per page, used for accessibility purposes. Also flows into the shared table context read by a `BbPagination` paired through `id`. When omitted, an array `items` table infers it from `items.length`; an explicit value (or a co… |
2423
2784
  | `readonly` | `boolean \| undefined` | | | Sets the input in a readonly state. |
2785
+ | `reorderable` | `boolean \| undefined` | | | Lets the user reorder the data columns: drag a header cell (AG Grid style — the column moves live under the pointer and a chip with its label follows) or, from the keyboard, use each header's reorder handle (ArrowLeft / ArrowRight move the … |
2786
+ | `resizable` | `boolean \| undefined` | | | Lets the user resize the data columns of a root table: drag the handle on a header's trailing edge (the column follows live, the width commits on release), or press Alt + ArrowLeft / ArrowRight on the focused header; double-click the handle… |
2424
2787
  | `rowClass` | `RowClasses<Item> \| undefined` | | | Defines the classes to be passed to each data row. Can also be a function of the row's item for dynamic values. |
2425
2788
  | `selectable` | `boolean \| ((item: Item) => boolean) \| undefined` | | | Defines whether the table is selectable. Can be a global boolean that affects all rows or a function that accepts an item and returns a boolean that only affects that item. |
2426
2789
  | `selectAll` | `boolean \| undefined` | `false` | | Defines whether the "Select all" checkbox is checked. |
@@ -2440,9 +2803,11 @@ reader.
2440
2803
  - `dblclick:row` — `(e: "dblclick:row", event: MouseEvent, item: any): void`
2441
2804
  - `item:selected` — `(e: "item:selected", value: any): void`
2442
2805
  - `item:unselected` — `(e: "item:unselected", value: any): void`
2806
+ - `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.
2443
2807
  - `update:expandedItems` — `(e: "update:expandedItems", value: any[]): void`
2444
2808
  - `update:highlighted` — `(e: "update:highlighted", value: any): void`
2445
2809
  - `update:modelValue` — `(e: "update:modelValue", value: any): void`
2810
+ - `update:order` — `(e: "update:order", value: string[]): void`
2446
2811
  - `update:selectAll` — `(e: "update:selectAll", value: boolean): void`
2447
2812
  - `update:sort` — `(e: "update:sort", value: BbTableSortEntry[]): void`
2448
2813
  - `update:unselectedItems` — `(e: "update:unselectedItems", value: any[]): void`
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "library": "bitboss-ui",
4
- "version": "3.0.0-beta.8",
4
+ "version": "3.0.0-beta.9",
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": 43
9
+ "behaviourBreaks": 44
10
10
  },
11
11
  "breaking": [
12
12
  {
@@ -292,6 +292,12 @@
292
292
  "ruling": "grid substrate, plans/BBTABLE-GRID.md",
293
293
  "guide": "components/bb-table.md § Do not override; guide § Narrow screens"
294
294
  },
295
+ {
296
+ "kind": "behaviour",
297
+ "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",
298
+ "ruling": "owner 2026-08-27",
299
+ "guide": "components/bb-table.md § Highlight is opt-in"
300
+ },
295
301
  {
296
302
  "kind": "behaviour",
297
303
  "description": "BbTable renders a CSS grid of divs with ARIA roles, not a <table>: every element selector under .bb-table (table/thead/tbody/tfoot/caption/tr/th/td/colgroup/col) matches nothing. Every .bb-table* class survives; .bb-table__table/__head/__body/__foot + [role=row]/[role=columnheader]/[role=cell] are the blessed translation (no data-* hooks, no shim classes)",