bitboss-ui 3.0.0-beta.51 → 3.0.0-beta.53

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 (90) hide show
  1. package/bin/bitboss-ui-mcp.mjs +16 -8
  2. package/dist/ai/BbBadge.md +3 -2
  3. package/dist/ai/BbColorPalette.md +4 -3
  4. package/dist/ai/BbDatePicker.md +1 -1
  5. package/dist/ai/BbDropdown.md +6 -2
  6. package/dist/ai/BbForm.md +25 -17
  7. package/dist/ai/BbHoverCard.md +1 -1
  8. package/dist/ai/BbPopover.md +5 -3
  9. package/dist/ai/BbSelectPopover.md +1 -1
  10. package/dist/ai/BbTable.md +176 -51
  11. package/dist/ai/BbTimePicker.md +1 -1
  12. package/dist/ai/BbTooltip.md +1 -1
  13. package/dist/ai/CommonPopover.md +1 -1
  14. package/dist/ai/changelog.json +77 -3
  15. package/dist/ai/components.json +14 -14
  16. package/dist/ai/guides/component-picker.md +3 -3
  17. package/dist/ai/guides/migration/components/bb-table.md +32 -31
  18. package/dist/ai/guides/migration/from-async-validator.md +60 -46
  19. package/dist/ai/guides/migration/v2-to-v3.md +46 -0
  20. package/dist/ai/guides/options-items-playbook.md +5 -5
  21. package/dist/ai/guides/validated-forms.md +19 -8
  22. package/dist/ai/helpers/inertia/manifest.json +1 -1
  23. package/dist/ai/manifest/components/BbBadge.json +1 -1
  24. package/dist/ai/manifest/components/BbColorPalette.json +1 -1
  25. package/dist/ai/manifest/components/BbDatePicker.json +1 -1
  26. package/dist/ai/manifest/components/BbDropdown.json +1 -1
  27. package/dist/ai/manifest/components/BbHoverCard.json +1 -1
  28. package/dist/ai/manifest/components/BbPopover.json +1 -1
  29. package/dist/ai/manifest/components/BbSelectPopover.json +1 -1
  30. package/dist/ai/manifest/components/BbTable.json +6 -5
  31. package/dist/ai/manifest/components/BbTable.tree.json +19 -0
  32. package/dist/ai/manifest/components/BbTimePicker.json +1 -1
  33. package/dist/ai/manifest/components/BbTooltip.json +1 -1
  34. package/dist/ai/manifest/components/CommonPopover.json +1 -1
  35. package/dist/ai/manifest/index.json +2 -2
  36. package/dist/ai/manifest/types.api.json +1 -1
  37. package/dist/ai/recipes/inertia/approvals-inbox.md +1 -1
  38. package/dist/ai/recipes/nuxt/approvals-inbox.md +1 -1
  39. package/dist/ai/recipes/vue/approvals-inbox.md +1 -1
  40. package/dist/ai/source/BbBadge.md +3 -4
  41. package/dist/ai/source/BbBadgeButton.md +3 -4
  42. package/dist/ai/source/BbColorPalette.md +3 -1
  43. package/dist/ai/source/BbDatePicker.md +4 -1
  44. package/dist/ai/source/BbDropdown.md +5 -3
  45. package/dist/ai/source/BbDropdownGroup.md +5 -3
  46. package/dist/ai/source/BbHoverCard.md +3 -2
  47. package/dist/ai/source/BbHoverCardPortal.md +3 -2
  48. package/dist/ai/source/BbPopover.md +3 -2
  49. package/dist/ai/source/BbPopoverPortal.md +3 -2
  50. package/dist/ai/source/BbSelectPopover.md +3 -1
  51. package/dist/ai/source/BbTable.md +405 -141
  52. package/dist/ai/source/BbTimePicker.md +4 -1
  53. package/dist/ai/source/BbTooltip.md +3 -2
  54. package/dist/ai/source/CommonPopover.md +11 -8
  55. package/dist/components/BbBadge/types.d.ts +3 -4
  56. package/dist/components/BbColorPalette/types.d.ts +3 -1
  57. package/dist/components/BbDatePicker/types.d.ts +4 -1
  58. package/dist/components/BbDropdown/types.d.ts +5 -3
  59. package/dist/components/BbForm/bindFormField.js +4 -4
  60. package/dist/components/BbHoverCard/types.d.ts +3 -2
  61. package/dist/components/BbPopover/types.d.ts +3 -2
  62. package/dist/components/BbSelectPopover/types.d.ts +3 -1
  63. package/dist/components/BbTable/BbTable.vue_vue_type_script_setup_true_lang.js +999 -957
  64. package/dist/components/BbTable/BbTableDataRow.js +77 -78
  65. package/dist/components/BbTable/types.d.ts +32 -9
  66. package/dist/components/BbTable/useRowReorder.d.ts +6 -0
  67. package/dist/components/BbTable/useRowReorder.js +217 -195
  68. package/dist/components/BbTable/utils.d.ts +21 -0
  69. package/dist/components/BbTable/utils.js +19 -16
  70. package/dist/components/BbTable/warroom/harness.d.ts +7 -0
  71. package/dist/components/BbTimePicker/types.d.ts +4 -1
  72. package/dist/components/BbTooltip/types.d.ts +3 -2
  73. package/dist/components/CommonPopover/CommonPopover.vue_vue_type_script_setup_true_lang.js +1 -1
  74. package/dist/components/CommonPopover/types.d.ts +4 -4
  75. package/dist/composables/useBaseOptions.d.ts +3 -3
  76. package/dist/composables/useBaseOptions.js +11 -11
  77. package/dist/composables/useBbTableContext.js +1 -0
  78. package/dist/composables/useItemText.d.ts +5 -0
  79. package/dist/composables/useItemText.js +14 -14
  80. package/dist/composables/useItemValue.js +9 -9
  81. package/dist/composables/useItemsGetter.js +55 -51
  82. package/dist/composables/useSegmentedFields.js +6 -1
  83. package/dist/composables/useTableWidthContext.d.ts +6 -0
  84. package/dist/composables/useTableWidthContext.js +2 -1
  85. package/dist/llms-full.txt +390 -177
  86. package/dist/llms-medium.txt +3 -3
  87. package/dist/styles.css +2 -2
  88. package/dist/utils/versionCheck.js +1 -1
  89. package/package.json +2 -1
  90. package/scripts/lib/hand-roll-hints.mjs +11 -2
@@ -373,7 +373,7 @@ scope in `#<key>:header`. With it each name gets its own scope, as on `BbTable`.
373
373
  **Add beside a header instead of replacing it** with
374
374
  `#<key>:header:prepend` / `#<key>:header:append` — rendered before / after the
375
375
  column's existing header content (the label + sort control, or your own
376
- `#<key>:header` replacement), so a resize grip or reorder handle no longer
376
+ `#<key>:header` replacement), so a resize handle or reorder handle no longer
377
377
  costs you the built-in sort button. The table-wide pair `#header:prepend` /
378
378
  `#header:append` applies one template to every data column's header (never the
379
379
  structural select/actions cells); both forms receive the header scope above
@@ -953,6 +953,20 @@ a native `<input>` / `<select>`:
953
953
  <BbTable :items="filteredItems" :columns="columns" caption="Invoices" />
954
954
  ```
955
955
 
956
+ ```ts
957
+ // An emptied (or cleared) BbTextInput writes `null`, not '' — type it so.
958
+ const search = ref<string | null>('');
959
+ const statuses = ref<string[]>([]);
960
+ const filteredItems = computed(() => {
961
+ const q = search.value?.trim().toLowerCase() ?? '';
962
+ return invoices.value.filter(
963
+ (row) =>
964
+ (!q || row.client.toLowerCase().includes(q)) &&
965
+ (!statuses.value.length || statuses.value.includes(row.status))
966
+ );
967
+ });
968
+ ```
969
+
956
970
  Pick the shape by where the rows come from:
957
971
 
958
972
  | Your situation | The filter mechanism |
@@ -1172,7 +1186,7 @@ const sortedInvoices = computed(() => {
1172
1186
  A custom `#<key>:header` slot replaces the built-in sort button, but its scope
1173
1187
  hands you `sortOrder` and `toggleSort` so your header can keep sorting alive
1174
1188
  (render your own button and call `toggleSort`). To merely add something next
1175
- to the control — a resize grip, a filter icon — use
1189
+ to the control — a resize handle, a filter icon — use
1176
1190
  `#<key>:header:prepend/append` instead and keep the built-in button (see
1177
1191
  "Custom cells and headers"). The column-level `sorted`
1178
1192
  field only seeds the header's `aria-sort` when no live model entry exists — it
@@ -1471,11 +1485,17 @@ Add `selectable` for multi-select (`multiple` is the default) bound with
1471
1485
  `v-model` — the model **must** be an array in multiple mode (the table throws
1472
1486
  otherwise). With `item-value` set, the model holds ids rather than whole rows.
1473
1487
  Pass `selectable` a predicate (`(item) => boolean`) to gate specific rows: the
1474
- excluded rows' controls disable and `toggleSelected` refuses them too.
1488
+ excluded rows' controls disable and `toggleSelected` refuses them too. Under
1489
+ `select-all` an excluded row stays unticked (select-all covers the rows the
1490
+ user can select); one the model names explicitly still shows ticked.
1475
1491
 
1476
1492
  To drive a toolbar that lives **outside** the table subtree, give the table an
1477
1493
  `id` and read `useBbTableContext(id).selected` — the handle mirrors the
1478
- table's selection both ways, so a bulk-action bar needs no prop drilling. As
1494
+ table's selection both ways, so a bulk-action bar needs no prop drilling.
1495
+ Count from `all` and `unselected` too: the header checkbox is select-all,
1496
+ which leaves `selected` empty (see [§ Single select, select-all and
1497
+ limits](#single-select-select-all-and-limits)), so a bar that reads
1498
+ `selected` alone vanishes while every row shows ticked. As
1479
1499
  everywhere in the library, the destructive bulk action is gated by
1480
1500
  `useConfirm` before it touches the data.
1481
1501
 
@@ -1564,14 +1584,27 @@ const selected = ref<string[]>([]);
1564
1584
 
1565
1585
  // Same id as the table — the handle mirrors the table's selection both ways.
1566
1586
  const ctx = useBbTableContext('member-selection', { selected: [] });
1567
- const selectedCount = computed(() => ctx.selected.value?.length ?? 0);
1568
- const clear = () => (ctx.selected.value = []);
1587
+ // The header checkbox is "select all": it sets `all` and leaves `selected`
1588
+ // empty (built for "all across pages"), with the rows unticked since in
1589
+ // `unselected`. A client-side table reads the chosen rows from both.
1590
+ const chosenIds = computed<string[]>(() =>
1591
+ ctx.all.value
1592
+ ? members.value
1593
+ .map((member) => member.id)
1594
+ .filter((id) => !ctx.unselected.value.includes(id))
1595
+ : (ctx.selected.value ?? [])
1596
+ );
1597
+ const selectedCount = computed(() => chosenIds.value.length);
1598
+ const clear = () => {
1599
+ ctx.all.value = false;
1600
+ ctx.selected.value = [];
1601
+ };
1569
1602
 
1570
1603
  const { confirm } = useConfirm();
1571
1604
 
1572
1605
  // Destructive bulk action: `await` the decision, act only on a yes.
1573
1606
  const removeSelected = async () => {
1574
- const ids: string[] = ctx.selected.value ?? [];
1607
+ const ids = chosenIds.value;
1575
1608
  const ok = await confirm({
1576
1609
  title: `Delete ${ids.length} ${ids.length === 1 ? 'member' : 'members'}?`,
1577
1610
  text: 'They will lose access to this workspace immediately.',
@@ -1979,7 +2012,7 @@ _detail about the row_ rather than _children of it_.
1979
2012
 
1980
2013
  ### Reordering rows
1981
2014
 
1982
- `reorderable-rows` adds a slim grip at the start of every row: the user drags
2015
+ `reorderable-rows` adds a slim drag handle at the start of every row: the user drags
1983
2016
  a row to a new place and the table reports the move with **`reorder:rows`**.
1984
2017
  The table never reorders `items` itself. Your handler splices the array, and
1985
2018
  saves the order if it should outlive the page:
@@ -1991,8 +2024,8 @@ saves the order if it should outlive the page:
1991
2024
  <div class="flex max-w-2xl flex-col gap-3">
1992
2025
  <!-- The table only REPORTS the move: `reorder:rows` says which item went
1993
2026
  from where to where, and the handler below splices the array and
1994
- saves the order. Drag a row by its grip (or anywhere on it with a
1995
- mouse), or focus a grip and use Space, the arrow keys and Space. -->
2027
+ saves the order. Drag a row by its handle (or anywhere on it with a
2028
+ mouse), or focus a handle and use Space, the arrow keys and Space. -->
1996
2029
  <div
1997
2030
  class="overflow-hidden rounded-(--bb-radius) border border-(--bb-border)"
1998
2031
  >
@@ -2106,11 +2139,11 @@ function move({ from, to }: { from: number; to: number }) {
2106
2139
  `items` (`items.indexOf(item)` finds it); `from` and `to` index the `items`
2107
2140
  the table holds. Remove at `from`, insert at `to`. A drop where the row
2108
2141
  already was emits nothing.
2109
- - **Mouse and pen** drag from the grip, or from anywhere on the row once the
2142
+ - **Mouse and pen** drag from the handle, or from anywhere on the row once the
2110
2143
  pointer has moved 5px. A press on a control inside the row never starts a
2111
2144
  drag (a button, a link, an input, the select checkbox, anything with a
2112
2145
  widget role or `draggable`), and a press that sets off sideways selects text
2113
- instead. **Touch** drags from the grip only, at once (no long-press), so a
2146
+ instead. **Touch** drags from the handle only, at once (no long-press), so a
2114
2147
  finger on the row still scrolls. The click that ends a drag is not a row
2115
2148
  click: no `click:row`, no highlight.
2116
2149
  - **While it drags**, a copy of the row follows the pointer with its real cell
@@ -2119,10 +2152,10 @@ function move({ from, to }: { from: number; to: number }) {
2119
2152
  Near the top or bottom edge the nearest scroll container scrolls, `virtual`
2120
2153
  tables included, and the rows that scroll in are targets; the wheel scrolls
2121
2154
  too.
2122
- - **Keyboard:** the grip is a button. Space or Enter picks the row up, ↑ / ↓ /
2155
+ - **Keyboard:** the drag handle is a button. Space or Enter picks the row up, ↑ / ↓ /
2123
2156
  Home / End move it, Space or Enter drops it, Esc or Tab cancels. Under
2124
- `keyboard-navigation`, Space or Enter on the grip's cell picks it up at once.
2125
- - **Accessibility:** each grip is named "Move row" + the row's label (its
2157
+ `keyboard-navigation`, Space or Enter on the handle's cell picks it up at once.
2158
+ - **Accessibility:** each drag handle is named "Move row" + the row's label (its
2126
2159
  cell in the `rowHeader: true` column, else the first column) and described by the keyboard
2127
2160
  instructions. A polite live region announces the pick-up, each position
2128
2161
  ("Position 3 of 6"), the drop and a cancel.
@@ -2135,10 +2168,16 @@ function move({ from, to }: { from: number; to: number }) {
2135
2168
  - **Server pages:** the indexes are into the page the table holds. For the
2136
2169
  whole list, add the page's offset (`(page - 1) * perPage + from`). Moving a
2137
2170
  row to another page is not supported.
2138
- - **Sorting:** the grips stay enabled while a column sort is active, so a move
2139
- is reported against the sorted rows. What that means for your stored order
2140
- is your app's call (clear the sort first, or hide reordering while sorted).
2141
- - **Pinning and RTL:** the grip column is structural, like the select column:
2171
+ - **Sorting:** the drag handles stay enabled while a column sort is active, and a
2172
+ move is reported against the sorted rows, the order the user sees. This is
2173
+ easy to get wrong: splicing your stored array changes nothing a sort shows,
2174
+ so the row snaps back. Give the drop a meaning the sort can show. Sorted by
2175
+ a rank the drag edits (priority, position), write the new ranks in the order
2176
+ the user now sees and the sort reproduces the drop. Sorted by something the
2177
+ drag cannot change (a name, a date), turn the drag handles off for that sort
2178
+ (`:reorderable-rows="!sort.length || sort[0][0] === 'priority'"`) rather
2179
+ than snapping back or clearing the user's sort behind their back.
2180
+ - **Pinning and RTL:** the drag handle column is structural, like the select column:
2142
2181
  it never appears in `order` and never moves, it pins with the first
2143
2182
  left-pinned column, and in RTL it sits at the logical start (the right
2144
2183
  edge).
@@ -2146,7 +2185,7 @@ function move({ from, to }: { from: number; to: number }) {
2146
2185
  goes on while its row is still there, and cancels cleanly, announced, when
2147
2186
  it is gone. Expanding or collapsing one of the table's own rows cancels it.
2148
2187
  While `loading`, no new drag starts.
2149
- - **No grips** with a custom `#tbody`.
2188
+ - **No drag handles** with a custom `#tbody`.
2150
2189
  - **Styling:** `pt:dragHandle` takes a class list or an object (no per-row
2151
2190
  function).
2152
2191
 
@@ -2194,6 +2233,13 @@ put, and both columns are reported. A table that inherits its widths from a
2194
2233
  parent (`inherit-column-widths`) renders no handles — its tracks are the
2195
2234
  parent's.
2196
2235
 
2236
+ **Lock one column with `resizable: false`** on its definition, the way
2237
+ `reorderable: false` locks its place: its header has no handle, Alt+←/→ on
2238
+ it does nothing, and a neighbour's Shift+drag resizes the neighbour alone. Use
2239
+ it for control columns sized to their content (an expand chevron, a row
2240
+ number at `width: 'max-content'`). The lock stops the user, not your code: a
2241
+ `width` you set still applies.
2242
+
2197
2243
  **Resizable columns reporting widths back into the definition**
2198
2244
 
2199
2245
  ```vue
@@ -2340,7 +2386,7 @@ above it. The mechanics:
2340
2386
  </template>
2341
2387
 
2342
2388
  <template #expand="{ item }">
2343
- <div class="bg-(--bb-muted) px-1 py-1">
2389
+ <div class="bg-(--bb-muted) py-1">
2344
2390
  <BbTable
2345
2391
  :caption="`Line items of ${item.number}`"
2346
2392
  :columns="lineColumns"
@@ -2476,7 +2522,42 @@ Two rules keep them honest:
2476
2522
  2. **Never add nest padding on the body only.** Padding the child's body
2477
2523
  (`.bb-table__body`) but not its header (or vice versa) reintroduces exactly
2478
2524
  the offset the snap just removed. Put the inset on the nest container, where
2479
- both row groups inherit it.
2525
+ both row groups inherit it, and keep it **vertical** (`py-1`): the child
2526
+ inherits the parent's track widths, not their positions, so horizontal
2527
+ padding moves every column off its rail by that much and pushes the child
2528
+ past the parent's edge into its scroll.
2529
+
2530
+ #### Indenting nested levels
2531
+
2532
+ Two CSS tokens on the **root** table step each nested level in, the way a
2533
+ tree reads, while every other column stays on the parent's columns:
2534
+
2535
+ - `--indent` — the step per level (a length; `0px` by default, no indent).
2536
+ - `--indent-at` — where the step goes: `drag-handle`, `select` (the default) or
2537
+ `content`.
2538
+
2539
+ ```css
2540
+ .services-table {
2541
+ --indent: 24px;
2542
+ --indent-at: select;
2543
+ }
2544
+ ```
2545
+
2546
+ | `--indent-at` | Stays in one column at every level | Steps in per level |
2547
+ | ------------- | ---------------------------------- | ---------------------------------------- |
2548
+ | `drag-handle` | nothing before the content | the drag handle, the select, the content |
2549
+ | `select` | the drag handle | the select and the content |
2550
+ | `content` | the drag handle and the select | the first column's content |
2551
+
2552
+ Each nested level adds one step before the anchor and takes it from its
2553
+ first content column, so Status, Total or any other column never moves and
2554
+ level three lands two steps in by itself. A missing anchor falls through: no
2555
+ drag handle column behaves as `select`, no select column as `content`. Set the tokens
2556
+ on the root table (nested tables inherit them), or on one nested table to
2557
+ change the anchor from that level down. `--indent-at` is read when the table
2558
+ measures (mount, resize): set it in CSS, not from an animation. A nested
2559
+ table also carries `--depth` (1, 2, …) for styling by level, e.g. a group bar:
2560
+ `border-inline-start: calc(var(--depth) * 2px) solid var(--bb-primary)`.
2480
2561
 
2481
2562
  ### Row highlight (master–detail)
2482
2563
 
@@ -2485,37 +2566,63 @@ details are open" — independent of selection. Clicking anywhere on a row
2485
2566
  toggles it: a new click replaces the previous highlight, and clicking the
2486
2567
  highlighted row clears it back to `null`. **Highlight is single by design** —
2487
2568
  one row is "the open one" — so bind a single item value, never an array. Two kinds of
2488
- clicks are ignored on purpose: those inside interactive elements (links,
2489
- buttons, inputs, labels keep their own semantics) and those ending a text
2490
- selection (dragging to copy a cell must not mutate state).
2569
+ clicks are not row clicks: those starting on a control inside the row (a link,
2570
+ a button, an input, the select checkbox, any ARIA widget — they keep their own
2571
+ semantics) and those ending a text selection (dragging to copy a cell must not
2572
+ mutate state). Neither highlights the row nor emits `click:row`.
2491
2573
 
2492
- **Highlight is opt-in, and the model's value is the switch.** Bind nothing and
2493
- the mechanic is entirely off — a row click emits `click:row` and does nothing
2574
+ **Highlight is opt-in: it is on when it is bound.** Bind nothing and the
2575
+ mechanic is entirely off — a row click emits `click:row` and does nothing
2494
2576
  else: no class, no `aria-current`, no state. That matters because the table has
2495
2577
  nowhere to put a highlight it invents; a table that highlighted itself would
2496
- drive a cursor the app can neither read nor clear.
2497
-
2498
- The switch reads the value, not the binding: **`undefined` means "never
2499
- initialised" and keeps highlight off; `null` means "initialised and currently
2500
- empty" and turns it on.** So initialise the model:
2578
+ drive a cursor the app can neither read nor clear. Bound means
2579
+ `v-model:highlighted` (whatever its value — `ref()` and `ref(null)` both mean
2580
+ "nothing open yet"), or `highlighted` provided through the shared context,
2581
+ which binds two ways just like the prop:
2501
2582
 
2502
2583
  ```ts
2503
- const highlighted = ref(null); // ✅ connected
2504
- const highlighted = ref(); // ❌ undefined — highlight stays off (warns in dev)
2584
+ const highlighted = ref<number | null>(null);
2585
+ useBbTableContext('invoices-table', { highlighted });
2505
2586
  ```
2506
2587
 
2507
- A value seeded through the shared context counts exactly the same, and binds
2508
- two ways just like the prop:
2588
+ **`highlightable` decides which rows a click may open.** `false` turns the
2589
+ click off for every row (a user without the privilege to open details), and a
2590
+ function of the row's item turns it off for some. A refused row ignores a
2591
+ click, Enter and `toggleHighlighted`, loses the pointer cursor highlightable
2592
+ rows get, and never clears the row that is open. It gates the user only: a
2593
+ value your code puts in `highlighted` (a deep link to a record) shows on a
2594
+ refused row too — the table reflects your state, it does not correct it, just
2595
+ as a `selectable`-refused row the model names stays ticked.
2596
+
2597
+ Fetching the details belongs in a watcher on the model, not in `click:row`
2598
+ (the model also changes on the click that closes, on Enter, and from your own
2599
+ code). Abort the previous fetch when the highlight moves on, so a slow answer
2600
+ never paints over a newer one:
2509
2601
 
2510
2602
  ```ts
2511
- const highlighted = ref(null);
2512
- useBbTableContext('invoices-table', { highlighted });
2603
+ const openTaskId = ref<string | null>(null);
2604
+ watch(openTaskId, (id, _previous, onCleanup) => {
2605
+ if (id === null) return taskStore.closeDetails();
2606
+ const controller = new AbortController();
2607
+ onCleanup(() => controller.abort());
2608
+ void taskStore.showDetails(id, { signal: controller.signal });
2609
+ });
2610
+ ```
2611
+
2612
+ ```vue
2613
+ <BbTable
2614
+ v-model:highlighted="openTaskId"
2615
+ :highlightable="can(Privilege.TASKS_SHOW)"
2616
+ item-value="id"
2617
+ :items="tasks"
2618
+ :columns="columns"
2619
+ />
2513
2620
  ```
2514
2621
 
2515
- `click:row` is never gated — it fires on every row click whether or not
2516
- highlight is connected, so listening for clicks costs you nothing. Calling
2517
- `toggleHighlighted` from a cell slot on an unconnected table does nothing and
2518
- warns once in dev.
2622
+ `click:row` is never gated by highlight — it fires on every row click whether
2623
+ or not highlight is bound or the row highlightable, so listening for clicks
2624
+ costs you nothing. Calling `toggleHighlighted` from a cell slot on an unbound
2625
+ table does nothing and warns once in dev.
2519
2626
 
2520
2627
  Highlight ships with a **default cursor**: the row gets `aria-current="true"`
2521
2628
  and the `bb-table__row--highlighted` class, and the shipped stylesheet
@@ -2743,7 +2850,11 @@ Row events: `click:row`, `dblclick:row` and `contextmenu:row` fire with
2743
2850
  `(event, item)` — the native event and **your record exactly as you passed it
2744
2851
  in `items`**, nothing else. No wrapper to unpack, no third argument; selection
2745
2852
  state is already yours through the `v-model`, and the row's value resolves
2746
- from the record through `item-value` exactly as the models do. Reserve these
2853
+ from the record through `item-value` exactly as the models do. A click that
2854
+ starts on a control inside the row (the select checkbox, a link, a button, any
2855
+ ARIA widget) is the control's, not the row's: none of the three fires, so a
2856
+ cell's button needs no `@click.stop` and a right-click on a cell link keeps
2857
+ the browser's menu. Reserve these
2747
2858
  events for side effects that are not navigation —
2748
2859
  logging, opening a drawer (though `v-model:highlighted` usually covers that
2749
2860
  without any handler).
@@ -3851,8 +3962,13 @@ for a preset that fits any row type (never `<any>`), not an imported
3851
3962
  - `loading-text` is `sr-only` — it is announced, never painted. The visible
3852
3963
  loading signal is the skeleton (or your `#loading` slot). It defaults to the
3853
3964
  localized `common.loadingText`, so an unset value still announces.
3854
- - Row selection labels default to every cell's content inside the locale's
3855
- "select" template (`Select {0}`, `{0} auswählen`). `accessible-label` overrides that per row — it is called
3965
+ - Row selection labels default to the row's **name** inside the locale's
3966
+ "select" template (`Select {0}`, `{0} auswählen`): its `rowHeader` cells,
3967
+ else its first column ("Select INV-1041"), the same name the drag handle
3968
+ uses. Not every cell: a name tells rows apart, and a screen reader reads the
3969
+ other cells with their headers in table navigation. Mark the identity
3970
+ column `rowHeader` when it is not the first (a receipt thumbnail leads).
3971
+ `accessible-label` overrides that per row — it is called
3856
3972
  `(columns, item)` with the mapped cells (in **render** order — look them up
3857
3973
  by `key`, never by position) and the raw record, and whatever it returns
3858
3974
  becomes the checkbox's label.
@@ -3907,8 +4023,9 @@ for a preset that fits any row type (never `<any>`), not an imported
3907
4023
  (`role="rowheader"`), and a screen reader reading across a row, or down
3908
4024
  another column, announces that name with each value — "Ada Lovelace,
3909
4025
  ada@x.io" instead of a bare "ada@x.io". The cells look the same.
3910
- `accessible-label` is a different thing: it only names each row's
3911
- selection checkbox. The column:
4026
+ It also names the row's selection checkbox and drag handle ("Select Ada
4027
+ Lovelace", "Move row Ada Lovelace"); `accessible-label` overrides only the
4028
+ checkbox. The column:
3912
4029
  `{ key: 'name', label: 'Name', rowHeader: true }`.
3913
4030
 
3914
4031
  - Set `item-value` so selection, expansion and highlight track rows by stable
@@ -3972,7 +4089,7 @@ for a preset that fits any row type (never `<any>`), not an imported
3972
4089
  | Prop | Type | Default | Required | Description |
3973
4090
  | --- | --- | --- | --- | --- |
3974
4091
  | `pt:<part>`, `pt:<part>:<state>`, `pt` | `PtValue` | | | Passthrough — a class list, or `{ class, style, attrs }`, bound to one named part of the component; the state form applies only while that state is on. Parts: `body`, `caption`, `cell`, `cellColumnHeader`, `dragHandle`, `dragIcon`, `firstRow`, `foot`, `head`, `headerRow`, `lastRow`, `root`, `row`, `selectIcon`, `sortIcon`, `table`. States: `disabled`, `empty`, `expanded`, `highlighted`, `indeterminate`, `loading`, `selected`, `sorted`, `sortedDesc`. What each one is: _Passthrough parts and states_ below. `pt` is the object form with the same keys minus the prefix (`{ icon: '…', 'icon:loading': '…' }`). See `guides/passthrough.md`. In `<script>` a `pt` value is typed `BbTableProps<Row>['pt']` (`BbTableProps['pt']` for a preset that fits any row type, never `<any>`) (`PtValue` is not exported): § Typing `pt` in `<script>`. |
3975
- | `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`, never by position. |
4092
+ | `accessibleLabel` | `((columns: MappedCell[], item: any) => string) \| undefined` | | | The row's selection checkbox label, from `(columns, item)`. Unset: the row's name in the locale's select template (its `rowHeader` cells, else its first column: "Select INV-1041"). The cells arrive in render order: look one up by `key`, never by position. |
3976
4093
  | `actionsText` | `string \| undefined` | | | Label used in the header of the actions column. |
3977
4094
  | `align` | `"left" \| "center" \| "right" \| undefined` | `"left"` | | Text alignment of the columns. |
3978
4095
  | `caption` | `string \| undefined` | | | Caption that describes the content of the table. Used for accessibility purposes. |
@@ -3994,7 +4111,8 @@ for a preset that fits any row type (never `<any>`), not an imported
3994
4111
  | `fixedLast` | `boolean \| undefined` | `false` | | Sticks the `#<key>:last` row (a totals row) to the bottom of the scrollport. |
3995
4112
  | `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 `selectable`. |
3996
4113
  | `headerRowClass` | `Classes \| undefined` | | | Defines the classes to be passed to the header row. |
3997
- | `highlighted` | `any` | `undefined` | | Used by `v-model:highlighted`: the one row the UI is about ("the row whose details are open"), a single item value or `null`. Opt-in: bind `ref(null)`, not `ref()`; unbound or `undefined`, a row click only emits `click:row`. The row gets `aria-current="true"` and a shipped tint. |
4114
+ | `highlightable` | `boolean \| ((item: Item) => boolean) \| undefined` | `undefined` | | Which rows a click, Enter or `toggleHighlighted` may highlight: `false` for none, or a function of the row's item. Unset: every row, when `highlighted` is bound. A value the app sets still shows on a refused row. |
4115
+ | `highlighted` | `any` | `undefined` | | Used by `v-model:highlighted`: the one row the UI is about ("the row whose details are open"), a single item value or `null`. Opt-in: bound, a row click highlights; unbound, it only emits `click:row`. The row gets `aria-current="true"` and a shipped tint. |
3998
4116
  | `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. |
3999
4117
  | `inheritColumnWidths` | `string \| boolean \| undefined` | | | A nested table snaps its columns to an ancestor table's tracks: `true` = the nearest ancestor, a table `id` = that ancestor (when an intermediate table has a different column count). Off by default: a nested table sizes itself. |
4000
4118
  | `interactiveWhileLoading` | `boolean \| undefined` | `false` | | Keeps the header and rows interactive while loading (by default they are `inert`: a refetch is about to replace them). For background refreshes that must not interrupt an open editor. |
@@ -4013,7 +4131,7 @@ for a preset that fits any row type (never `<any>`), not an imported
4013
4131
  | `perPage` | `string \| number \| undefined` | | | Rows per page, for `aria-rowindex` and a paired `BbPagination`. Doesn't slice `items`. Unset, an array `items` infers it from its length; an explicit value wins. Numeric strings are coerced. |
4014
4132
  | `readonly` | `boolean \| undefined` | `false` | | Sets the input in a readonly state. |
4015
4133
  | `reorderable` | `boolean \| undefined` | `false` | | Lets the user reorder the data columns: drag a header, or Shift+←/→ on a focused header. Every move writes `v-model:order`; a column panel that only writes `order` does not need it. `select` / `actions` and a custom `#thead` never move. |
4016
- | `reorderableRows` | `boolean \| undefined` | `false` | | Adds a grip at the start of each row: the user drags a row, or lifts it with Space / Enter and the arrow keys, and `reorder:rows` reports `{ item, from, to }`. The table never reorders `items`: splice it in the handler. |
4134
+ | `reorderableRows` | `boolean \| undefined` | `false` | | Adds a drag handle at the start of each row: the user drags a row, or lifts it with Space / Enter and the arrow keys, and `reorder:rows` reports `{ item, from, to }`. The table never reorders `items`: splice it in the handler. |
4017
4135
  | `resizable` | `boolean \| undefined` | `false` | | Lets the user resize the data columns (drag a header's trailing edge, or Alt+←/→; double-click resets). Each commit fires `resize:column(key, width)` once: write the width back into the column definition to keep it. Ignored when inheriting widths. |
4018
4136
  | `rowClass` | `BbTableRowClasses<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. |
4019
4137
  | `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. |
@@ -4048,7 +4166,7 @@ for a preset that fits any row type (never `<any>`), not an imported
4048
4166
  | `lastRow` | The row of `#<key>:last` cells, after the data rows (a totals row: `pt:lastRow="font-semibold"`). |
4049
4167
  | `selectIcon` | The selection glyph: each row's checkbox (radio with single selection) and the select-all checkbox — resolved per node. Not rendered when the `select` / `select:header` slots replace them. |
4050
4168
  | `dragIcon` | The move glyph on the chip that follows a column drag (`reorderable`). |
4051
- | `dragHandle` | Each row's grip button (`reorderable-rows`), resolved with its row's states. A class list or an object; no per-row function. |
4169
+ | `dragHandle` | Each row's drag handle button (`reorderable-rows`), resolved with its row's states. A class list or an object; no per-row function. |
4052
4170
  | `foot` | The footer row group (the `tfoot` slot's container). |
4053
4171
 
4054
4172
  States are listed in precedence order: when two are on at once and their entries conflict, the later one wins.
@@ -4117,6 +4235,13 @@ Set these on the element, or on a class you put on it, to retune this component
4117
4235
  | `--actions-spacing` | `.bb-table` | `8px` | |
4118
4236
  | `--offset-start` | `.bb-table` | `0px` | Raw structural edge offset measured by the component (the parent's select/actions track width). Default 0 keeps every calc at its base. |
4119
4237
  | `--offset-end` | `.bb-table` | `0px` | |
4238
+ | `--ig` | `.bb-table` | `0px` | Nested indent locals (see `indentAnchor` in BbTable.vue): the step lands on ONE cell's leading padding. Reset on every table so a parent's step never inherits into a child's cells; the modifier picks the anchor. |
4239
+ | `--is` | `.bb-table` | `0px` | |
4240
+ | `--ic` | `.bb-table` | `0px` | |
4241
+ | `--depth` | `.bb-table` | `0` | Nesting depth (0 at the root) and the parent-region floors of the own structural tracks, both set inline by the component on nested tables; declared here so a parent's values never inherit into a child. |
4242
+ | `--min-g` | `.bb-table` | `0px` | |
4243
+ | `--min-s` | `.bb-table` | `0px` | |
4244
+ | `--min-a` | `.bb-table` | `0px` | |
4120
4245
  | `--offset-internal-start` | `.bb-table` | `var(--offset-start)` | Routing of that offset. By default it is "internal": added to the first/last cell padding and folded into the first/last column width. Swap internal↔external (set internal to 0 and external to the raw offset) to pad the whole table instead of the edge cells. |
4121
4246
  | `--offset-internal-end` | `.bb-table` | `var(--offset-end)` | |
4122
4247
  | `--offset-external-start` | `.bb-table` | `0px` | |
@@ -289,7 +289,7 @@ typeahead. Always pass a `label` when the trigger is an unlabelled cell or icon.
289
289
  | `adaptive` | `boolean \| undefined` | ``config.adaptive` (`true`)` | | On mobile, open in a bottom off-canvas sheet instead of a floating popover. When unset, falls back to the global `config.adaptive`. |
290
290
  | `ampm` | `boolean \| undefined` | `false` | | 12-hour display with an AM/PM column; emitted values stay 24-hour. |
291
291
  | `anchor` | `ElementOrComponentRef \| undefined` | | | Element or component ref the popover positions itself relative to. Defaults to the activator when omitted (anchor to a larger row while a smaller element stays the click target). |
292
- | `boundary` | `ElementOrComponentRefOrSelector \| undefined` | | | Constrains the popover within a bounding element or CSS selector. |
292
+ | `boundary` | `ElementOrComponentRefOrSelector \| undefined` | | | Constrains the popover within a bounding element or CSS selector: sideways only, alongside the trigger; it flips at the viewport. |
293
293
  | `disabled` | `boolean \| undefined` | `false` | | Disables the activator and the columns. |
294
294
  | `disableFlip` | `boolean \| undefined` | `false` | | Disable the automatic flip to the opposite side on overflow. |
295
295
  | `eager` | `boolean \| undefined` | `false` | | Render popover content before it is first shown. |
@@ -597,7 +597,7 @@ A `pt` value written in `<script>` (a constant, a preset, a wrapper's
597
597
  | `pt:<part>`, `pt:<part>:<state>`, `pt` | `PtValue` | | | Passthrough — a class list, or `{ class, style, attrs }`, bound to one named part of the component; the state form applies only while that state is on. Parts: `panel`, `root`. States: `open`. What each one is: _Passthrough parts and states_ below. `pt` is the object form with the same keys minus the prefix (`{ icon: '…', 'icon:loading': '…' }`). See `guides/passthrough.md`. In `<script>` a `pt` value is typed `BbTooltipProps['pt']` (`PtValue` is not exported): § Typing `pt` in `<script>`. |
598
598
  | `activator` | `ElementOrComponentRef \| undefined` | | | External activator element or component ref. When provided, the activator slot is not rendered and event listeners are attached programmatically to the referenced element. |
599
599
  | `arrowPadding` | `number \| undefined` | `10` | | Space the arrow will maintain to the edge of the tooltip bubble. Useful when the floating element has border radius so it won't appear broken |
600
- | `boundary` | `ElementOrComponentRefOrSelector \| undefined` | | | Constrains the tooltip within a bounding element. When provided, the tooltip will not overflow this container's bounds (e.g. a sidebar). Accepts an HTMLElement, a Vue component ref, or a CSS selector string. |
600
+ | `boundary` | `ElementOrComponentRefOrSelector \| undefined` | | | Constrains the tooltip within a bounding element: it stays inside it alongside its trigger (sideways for a tooltip above or below); away from the trigger it may pass the edge, and it flips at the viewport only. Accepts an HTMLElement, a Vue component ref, or a CSS selector string. |
601
601
  | `delay` | `number \| undefined` | `100` | | Milliseconds a pointer rests on the activator before the tooltip shows (`0`: at once). Keyboard focus shows it at once, and so does a pointer arriving while another tooltip shows or within 300 ms of one closing. |
602
602
  | `disabled` | `boolean \| undefined` | `false` | | Disables the component |
603
603
  | `eager` | `boolean \| undefined` | `false` | | Renders the tooltip's popover and content at mount instead of on the first open. The description screen readers hear exists from mount either way. |
@@ -18,7 +18,7 @@
18
18
  | --- | --- | --- | --- | --- |
19
19
  | `anchor` | `any` | | | The element the popover positions itself relative to. Accepts an HTMLElement, a Vue component ref, or a virtual element with a `getBoundingClientRect` method. |
20
20
  | `arrowPadding` | `number \| undefined` | | | Space the arrow will maintain to the edge of the dropdown. Useful when the floating element has border radius so it won't appear broken |
21
- | `boundary` | `string \| Record<string, any> \| HTMLElement \| null \| undefined` | | | Constrains the popover's placement (`flip` / `shift` keep it inside); it never clamps the size, so a short boundary never collapses the bubble: use `overflowBoundary` for that. An HTMLElement, a component ref or a CSS selector. |
21
+ | `boundary` | `string \| Record<string, any> \| HTMLElement \| null \| undefined` | | | Keeps the popover inside this element alongside its anchor (sideways above or below, vertically beside it); away from the anchor it may pass the edge, and only the viewport flips it. It never clamps the size (that is `overflowBoundary`). An HTMLElement, component ref or selector. |
22
22
  | `bubbleAttrs` | `Record<string, unknown> \| ((space: CommonPopoverAvailableSpace) => Record<string, unknown>) \| undefined` | | | Attributes bound on the bubble (`class`, `style`, `inert`, …) after its own class, so the owner can make the bubble its layout container. The function form receives the default slot's free space (for a height cap). |
23
23
  | `dialog` | `boolean \| undefined` | `false` | | Renders the popover as a `<dialog>` element (modal overlay) instead of a `<span>` with the Popover API. |
24
24
  | `disableFlip` | `boolean \| undefined` | `false` | | Disables the automatic flip to the opposite side when the popover would overflow (flipping is on by default). |
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 3,
3
3
  "library": "bitboss-ui",
4
- "version": "3.0.0-beta.51",
4
+ "version": "3.0.0-beta.53",
5
5
  "upgrade": "v2-to-v3",
6
6
  "guide": "ai/guides/migration/v2-to-v3.md",
7
7
  "legend": {
@@ -62,7 +62,9 @@
62
62
  "3.0.0-beta.48",
63
63
  "3.0.0-beta.49",
64
64
  "3.0.0-beta.50",
65
- "3.0.0-beta.51"
65
+ "3.0.0-beta.51",
66
+ "3.0.0-beta.52",
67
+ "3.0.0-beta.53"
66
68
  ],
67
69
  "unpublished": [
68
70
  "3.0.0-alpha.2",
@@ -83,7 +85,7 @@
83
85
  ],
84
86
  "summary": {
85
87
  "renames": 220,
86
- "behaviourBreaks": 188,
88
+ "behaviourBreaks": 193,
87
89
  "bySurface": {
88
90
  "prop": 15,
89
91
  "option": 1,
@@ -5901,6 +5903,78 @@
5901
5903
  "ruling": "owner 2026-10-08 (Q60.31 option B)",
5902
5904
  "guide": "v2-to-v3.md § 6 item 56 + useBbFormField.md + validated-forms.md § validationMode",
5903
5905
  "firstReleasedIn": "3.0.0-beta.51"
5906
+ },
5907
+ {
5908
+ "id": "f328",
5909
+ "kind": "behaviour",
5910
+ "summary": "BbTable row events (`click:row`, `dblclick:row`, `contextmenu:row`) skip clicks on controls inside the row (select checkbox, link, button, ARIA widget); drop `@click.stop` guards",
5911
+ "components": [
5912
+ "BbTable"
5913
+ ],
5914
+ "description": "BbTable: a click that starts on a control inside a row (the select checkbox, a link, a button, an input, any ARIA widget, contenteditable, [draggable]) emits no click:row, dblclick:row or contextmenu:row (they emitted for every click; the highlight already ignored them, now one rule for all, the row drag's); a click ending a text selection emits no click:row either. A cell button needs no @click.stop; a page guard on .bb-table__cell--select can go; a right-click on a cell link keeps the browser menu",
5915
+ "ruling": "owner 2026-10-09 (progetto-32 report)",
5916
+ "guide": "v2-to-v3.md § 6 item 57 + BbTable.guide.md § Row events",
5917
+ "firstReleasedIn": "3.0.0-beta.52"
5918
+ },
5919
+ {
5920
+ "id": "f329",
5921
+ "kind": "behaviour",
5922
+ "summary": "BbTable highlight is on when bound (any value; `undefined` no longer turns it off); new `highlightable` (bool or row function) gates which rows a click may highlight, app-set values still show",
5923
+ "components": [
5924
+ "BbTable"
5925
+ ],
5926
+ "description": "BbTable highlight is on when BOUND (v-model:highlighted with any value, ref() included, or highlighted provided through useBbTableContext); the model's value no longer switches it (undefined was off, null on, with a dev warning for a bound undefined — gone). New highlightable (boolean | (item) => boolean; unset = on exactly when highlight is bound) gates which rows a click / Enter / toggleHighlighted may highlight; a refused row never clears the open one, and a value the app sets still shows on it. Highlightable rows get bb-table__row--highlightable and a pointer cursor. A page that kept highlight off by binding undefined moves to :highlightable=\"false\"",
5927
+ "ruling": "owner 2026-10-09 (progetto-32, panel)",
5928
+ "guide": "v2-to-v3.md § 6 item 58 + BbTable.guide.md § Row highlight + components/bb-table.md",
5929
+ "firstReleasedIn": "3.0.0-beta.52"
5930
+ },
5931
+ {
5932
+ "id": "f330",
5933
+ "kind": "behaviour",
5934
+ "summary": "Option components: a function `item-text` runs on string items (was skipped: raw codes showed); a path on a primitive keeps the primitive as text and value",
5935
+ "components": [
5936
+ "BbSelect",
5937
+ "BbSelectPopover",
5938
+ "BbCheckboxGroup",
5939
+ "BbRadioGroup",
5940
+ "BbSwitchGroup",
5941
+ "BbDropdown"
5942
+ ],
5943
+ "description": "Option surfaces (BbSelect, BbSelectPopover, BbCheckboxGroup, BbRadioGroup, BbSwitchGroup, a BbDropdown pipeline group): on a primitive item (string[], number[]) a FUNCTION item-text now runs (strings skipped it and showed the raw string, numbers ran it), and a path item-text / item-value keeps the primitive (a number blanked its text, every primitive got undefined as its value). :item-text=\"(code) => labels[code]\" over a list of codes shows the labels and keeps the codes as values",
5944
+ "ruling": "owner 2026-10-12 (docs paragraph review)",
5945
+ "guide": "v2-to-v3.md § 6 item 59 + options-items-playbook.md § Primitive items",
5946
+ "firstReleasedIn": "3.0.0-beta.53"
5947
+ },
5948
+ {
5949
+ "id": "f331",
5950
+ "kind": "behaviour",
5951
+ "summary": "Floating panels: `boundary` confines a panel sideways only; it no longer flips at the boundary edge (only the viewport flips), so a table's last-row menu opens below",
5952
+ "components": [
5953
+ "BbDropdown",
5954
+ "BbPopover",
5955
+ "BbTooltip",
5956
+ "BbHoverCard",
5957
+ "BbSelectPopover",
5958
+ "BbColorPalette",
5959
+ "BbDatePicker",
5960
+ "BbTimePicker"
5961
+ ],
5962
+ "description": "Floating panels (BbDropdown + v-bb-dropdown, BbPopover, BbTooltip + v-bb-tooltip, BbHoverCard, BbSelectPopover, BbColorPalette, BbDatePicker, BbTimePicker): boundary keeps the panel inside it ALONGSIDE the trigger only (sideways for a panel above or below); away from the trigger it may pass the boundary, and it flips at the viewport, never at the boundary's edge (it flipped there: a menu in a table's last rows, bounded by the table, opened above its row)",
5963
+ "ruling": "owner 2026-10-12 (table matrix review)",
5964
+ "guide": "v2-to-v3.md § 6 item 60 + BbPopover.guide.md § Placement, offset and boundary",
5965
+ "firstReleasedIn": "3.0.0-beta.53"
5966
+ },
5967
+ {
5968
+ "id": "f332",
5969
+ "kind": "behaviour",
5970
+ "summary": "BbTable row checkbox name is the row's name (rowHeader cells, else first column), not every cell; mark the identity column rowHeader when it is not first",
5971
+ "components": [
5972
+ "BbTable"
5973
+ ],
5974
+ "description": "BbTable: a row's selection checkbox is named by the row's NAME, its rowHeader cells else its first column (\"Select INV-1041\"), the same name the drag grip uses; it read every cell (\"Select INV-1041 Northwind €4,200 Paid 2026-09-01 …\"). A test or e2e finding the checkbox by the long name, or by a prefix with more text after the name, matches the short one now; a table whose first column is not its identity (a thumbnail) marks the identity column rowHeader. accessible-label still overrides",
5975
+ "ruling": "owner 2026-10-12 (table matrix review)",
5976
+ "guide": "v2-to-v3.md § 6 item 61 + BbTable.guide.md § Accessibility",
5977
+ "firstReleasedIn": "3.0.0-beta.53"
5904
5978
  }
5905
5979
  ]
5906
5980
  }