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

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 (42) hide show
  1. package/README.md +36 -29
  2. package/dist/ai/BbBadge.md +10 -5
  3. package/dist/ai/BbSelect.md +8 -0
  4. package/dist/ai/BbTable.md +257 -51
  5. package/dist/ai/changelog.json +86 -2
  6. package/dist/ai/components.json +25 -15
  7. package/dist/ai/guides/design-language.md +14 -14
  8. package/dist/ai/guides/design-tokens.md +25 -18
  9. package/dist/ai/guides/installation-and-plugin-setup.md +1 -1
  10. package/dist/ai/guides/migration/components/bb-dialog.md +35 -29
  11. package/dist/ai/guides/migration/components/bb-table.md +406 -28
  12. package/dist/ai/guides/migration/v2-to-v3.md +175 -75
  13. package/dist/ai/index.md +1 -1
  14. package/dist/ai/recipes/inertia/layout-scaffold.md +5 -1
  15. package/dist/ai/recipes/inertia/ownership-atlas.md +41 -20
  16. package/dist/ai/recipes/inertia/records-workspace.md +26 -7
  17. package/dist/ai/recipes/nuxt/layout-scaffold.md +5 -1
  18. package/dist/ai/recipes/nuxt/records-workspace.md +26 -7
  19. package/dist/ai/recipes/vue/layout-scaffold.md +5 -1
  20. package/dist/ai/recipes/vue/records-workspace.md +26 -7
  21. package/dist/ai/source/BbBaseInputContainer.md +26 -0
  22. package/dist/ai/source/BbBreadcrumbs.md +31 -0
  23. package/dist/ai/source/BbDialog.md +15 -9
  24. package/dist/ai/source/BbOffCanvas.md +17 -11
  25. package/dist/ai/source/BbProgress.md +2 -2
  26. package/dist/ai/source/BbTable.md +837 -787
  27. package/dist/ai/source/CommonInputWrapper.md +4 -0
  28. package/dist/components/BbPagination/BbPagination.vue.d.ts +1 -1
  29. package/dist/components/BbTable/BbTable.vue.d.ts +5 -2
  30. package/dist/components/BbTable/BbTable.vue_vue_type_script_setup_true_lang.js +439 -484
  31. package/dist/components/BbTable/types.d.ts +83 -21
  32. package/dist/components/BbTable/utils.d.ts +18 -3
  33. package/dist/components/BbTable/utils.js +36 -28
  34. package/dist/index.d.ts +5 -1
  35. package/dist/index.js +68 -67
  36. package/dist/llms-full.txt +1065 -265
  37. package/dist/llms-medium.txt +15 -15
  38. package/dist/llms.txt +1 -1
  39. package/dist/styles.css +1 -1
  40. package/llms.txt +1 -1
  41. package/package.json +1 -1
  42. package/dist/assets/svgs/sort.svg_raw.js +0 -4
package/README.md CHANGED
@@ -36,46 +36,53 @@ Full detail: `ai/guides/installation-and-plugin-setup.md`.
36
36
 
37
37
  ### Composables
38
38
 
39
- | Composable | Role |
40
- | ----------------------------- | ---------------------------------------------------------- |
41
- | `useBbConfig` | Global options for the kit (locale, defaults, etc.). |
42
- | `useBroadcastChannelInstance` | Shared `BroadcastChannel` helper for cross-tab messaging. |
43
- | `useConfirm` | Programmatic confirm / modal flows. |
44
- | `useCountdown` | Countdown timer state. |
45
- | `useMobile` | Mobile / viewport-oriented behaviour. |
46
- | `useQueue` | Simple async task queue. |
47
- | `useSafeArea` | Claim a screen edge for a docked panel so toasts avoid it. |
48
- | `useToast` | Toast notifications API. |
49
-
50
- ### Base\* components (primitives) _(reference-only - prefer `Bb*` equivalents for consumer code)_
51
-
52
- Headless or low-level pieces used inside **Bb\*** wrappers or your own compositions:
53
-
54
- - **Actions:** `BbBaseButton`
55
- - **Text & numbers:** `BaseTextInput`, `BaseTextarea`, `BaseNumberInput`, `BaseTag`
56
- - **Choice:** `BaseCheckbox`, `BaseCheckboxGroup`, `BaseRadio`, `BaseRadioGroup`, `BaseSwitch`, `BaseSwitchGroup`, `BaseSlider`, `BaseRating`
57
- - **Choice glyphs (presentational SVGs):** `BbBaseCheckboxIcon`, `BbBaseRadioIcon`, `BbBaseSwitchIcon`
58
- - **Rich inputs:** `BaseSelect`, `BaseColorInput`, `BaseDatePicker`, `BaseDatePickerInput`
59
- - **Layout / chrome:** `BaseInputContainer`, `BaseDialog`
39
+ | Composable | Role |
40
+ | ---------------------- | --------------------------------------------------------------------- |
41
+ | `useBbConfig` | Global options for the kit (locale, defaults, etc.). |
42
+ | `useBbDropdownContext` | Read a `BbDropdown`'s resolved groups/selections from anywhere by id. |
43
+ | `useBbTableContext` | Imperative handle onto a `BbTable` by id. |
44
+ | `useBbTabsContext` | Programmatic tab selection for a `BbTabs` by id. |
45
+ | `useConfirm` | Programmatic confirm / modal flows. |
46
+ | `useCountdown` | Countdown timer state. |
47
+ | `useId` | SSR-safe unique ids. |
48
+ | `useMobile` | Mobile / viewport-oriented behaviour. |
49
+ | `useSafeArea` | Claim a screen edge for a docked panel so toasts avoid it. |
50
+ | `useToast` | Toast notifications API. |
51
+
52
+ ### Utilities
53
+
54
+ - **`slotKey`** — the normalizer behind every dynamic per-item named slot (`BbDropdown`, `BbTable`, `BbTabs`, `BbTree`, `BbBreadcrumbs`). Import it when building slot names programmatically instead of restating the rule.
55
+
56
+ ### Directives
57
+
58
+ `vBbTooltip`, `vBbDropdown`, `vBbColor`, `vBbDate`, `vBbTime` — plus a
59
+ `Bb*DirectivePlugin` for each, for app-level registration. Prefer the directive
60
+ over the component for simple no-slot cases.
61
+
62
+ ### Base\* primitives
63
+
64
+ The styled `Base*` input controls and the input chrome (`BbBaseInputContainer`,
65
+ `CommonInputWrapper` — label, prepend/append, prefix/suffix, clear button,
66
+ spacing) are **internal composition layers and are not exported**. Use the full
67
+ public inputs (`BbTextInput`, `BbSelect`, …); pass `hideLabel` for a
68
+ bare/inline control. The exported primitives are **`BbBaseButton`** and the
69
+ presentational glyphs **`BbBaseCheckboxIcon`**, **`BbBaseRadioIcon`**,
70
+ **`BbBaseSwitchIcon`**.
60
71
 
61
72
  ### Bb\* components (composed)
62
73
 
63
74
  Product-ready components with labels, hints, errors, and consistent styling:
64
75
 
65
- - **Forms & actions:** `BbTextInput`, `BbTextarea`, `BbNumberInput`, `BbSelect`, `BbCheckbox`, `BbCheckboxGroup`, `BbRadio`, `BbRadioGroup`, `BbSwitch`, `BbSwitchGroup`, `BbSlider`, `BbRating`, `BbColorInput`, `BbDatePickerInput`, `BbButton`, `BbBadge`, `BbIndicator`, `BbTag`, `BbDropdown`, `BbDropdownButton`
66
- - **Feedback:** `BbAlert`, `BbProgress`, `BbSpinner`, `BbToast`, `BbTooltip`
67
- - **Layout & navigation:** `BbAccordion`, `BbCollapsible`, `BbTab`, `BbBreadcrumbs`, `BbPagination`,`BbSmoothHeight`
76
+ - **Forms & actions:** `BbTextInput`, `BbTextarea`, `BbNumberInput`, `BbSelect`, `BbSelectPopover`, `BbCheckbox`, `BbCheckboxGroup`, `BbRadio`, `BbRadioGroup`, `BbSwitch`, `BbSwitchGroup`, `BbSlider`, `BbRating`, `BbColorInput`, `BbColorPalette`, `BbDatePicker`, `BbDatePickerInput`, `BbTimePicker`, `BbTimePickerInput`, `BbButton`, `BbBadge`, `BbBadgeButton`, `BbIndicator`, `BbTag`, `BbDropdown`, `BbDropdownGroup`, `BbDropdownButton`, `BbAsterisk`
77
+ - **Feedback:** `BbAlert`, `BbProgress`, `BbSpinner`, `BbToast`, `BbToastPortal`, `BbTooltip`
78
+ - **Layout & navigation:** `BbAccordion`, `BbCollapsible`, `BbTabs` (+ `BbTabsRoot`, `BbTabsList`, `BbTabsPanes`), `BbBreadcrumbs`, `BbPagination`, `BbSmoothHeight`
68
79
  - **Overlays & panels:** `BbDialog`, `BbConfirm`, `BbConfirmPortal`, `BbOffCanvas`, `BbPopover`
69
80
  - **Data:** `BbTable`, `BbTree`
70
81
  - **Media & files:** `BbAvatar`, `BbDropzone`, `BbIcon`
71
82
 
72
- ### Shared input shells _(internal layout primitives - not for direct consumer use)_
73
-
74
- - **`CommonInputWrapper`** — the internal input-chrome primitive (label, prepend/append, prefix/suffix, clear button, spacing) that every `Bb*` input composes. Not exported; use the full public inputs (`BbTextInput`, `BbSelect`, …).
75
-
76
83
  ### Exported component types
77
84
 
78
- For most **Base\*** and **Bb\*** components, the package exports matching **props**, **events**, and (where relevant) **slots** types (for example `BbSelectProps`, `BaseSelectEvents`). Use them for typed wrappers and design-system layers; the docs list the full set per component.
85
+ For the public **Bb\*** components, the package exports matching **props**, **events**, and (where relevant) **slots** types (for example `BbSelectProps`, `BbTableEvents`), plus supporting shapes like `BbDropdownItem` and `DropdownGroupItemsGetter`. Use them for typed wrappers and design-system layers; the docs list the full set per component.
79
86
 
80
87
  ---
81
88
 
@@ -309,9 +309,12 @@ state in the handler. Prefer `secondary` / `outline` for filter chips; reserve
309
309
  ```
310
310
 
311
311
  The clear button inherits the variant's foreground, so it reads on every surface
312
- — light on a filled `primary`, dark on a `secondary`/`outline`. Set
313
- `clearable-label` for the button's accessible name (defaults to the localized
314
- `badge.clearLabel`).
312
+ — light on a filled `primary`, dark on a `secondary`/`outline`. By default its
313
+ accessible name composes the localized verb with the badge's own text a chip
314
+ reading "Italy" announces "Remove Italy", with no per-chip wiring. Set
315
+ `clearable-label` only when that composition is wrong for the context (it
316
+ replaces the whole name). With a `BbBadgeButton` body the composition is
317
+ skipped and the button falls back to the bare localized `badge.clearLabel`.
315
318
 
316
319
  Icons combine with `clearable`: a **`prepend:icon`** stays (it leads the label);
317
320
  an **`append:icon`** shares the trailing slot with the clear button — the icon
@@ -695,8 +698,10 @@ selectors like `.bb-badge > .bb-badge__content`, which no longer match.
695
698
  - An icon-only badge (`icon`) has no visible text; the default slot is rendered
696
699
  as a screen-reader-only label — never leave it empty. In button mode that
697
700
  label is also the **button's accessible name**, so it is doubly required.
698
- - The `clearable` button has an accessible name from `clearable-label` (or the
699
- localized `badge.clearLabel`).
701
+ - The `clearable` button's accessible name is composed by default: the
702
+ localized `badge.clearLabel` verb plus the badge body's text ("Remove
703
+ Italy"). `clearable-label` replaces the whole name; with a `BbBadgeButton`
704
+ body the composition is skipped (bare verb only).
700
705
  - A clickable **and** clearable badge is two tab stops (body, then clear). That
701
706
  is intended: two actions, two controls.
702
707
  - Keyboard focus on either control rings the whole pill, so the focused target
@@ -1212,6 +1212,14 @@ Shipped behavior — never reimplement it:
1212
1212
  library default) — `direction="horizontal"` gives a label-beside-field row.
1213
1213
  Only `floating`/`inside` label modes force the vertical layout, since they
1214
1214
  embed the label in the field itself.
1215
+ - **A long `placeholder` or a long selected label never widens the control's
1216
+ track.** The trigger is a button, not an `<input>`, so the placeholder is
1217
+ real text rather than a paint-only `::placeholder` — but the field's minimum
1218
+ size is capped so that text cannot become the floor of a grid or flex track.
1219
+ Put a select with a paragraph-length placeholder in
1220
+ `grid-template-columns: repeat(3, 1fr)` and you still get three equal
1221
+ columns, with the text ellipsised inside its field. You do **not** need
1222
+ `min-w-0` on the item or `minmax(0, 1fr)` on the track to get this.
1215
1223
 
1216
1224
  ### Adaptive mobile sheet
1217
1225
 
@@ -55,13 +55,15 @@ needs is declared here, not in markup:
55
55
  `align` prop sets the default for the whole table (`left`).
56
56
  - **`width`** freezes the column: numbers and bare numeric strings are px
57
57
  (`130`, `'130'`); anything carrying a unit passes through (`'12rem'`,
58
- `'20%'`). Declaring any `width` switches the table to `table-layout: fixed`
59
- (widths are only authoritative there); unfrozen columns then share the
60
- leftover space proportionally to their content.
58
+ `'20%'`). A declared width is the column's grid track and holds exactly;
59
+ unfrozen columns size to their content and share any leftover space equally
60
+ (the table is a CSS grid — the tracks are the width API, there is no
61
+ `table-layout`). When _every_ column declares a width, the surplus is shared
62
+ in proportion to the declared widths so the table still fills its container.
61
63
  - **`skeleton`** picks the loading placeholder shape (see Loading below).
62
64
  - **`sortable` / `sorted`** — see Sorting.
63
65
  - **`thClass` / `tdClass` / `rowClass`** style the header cell, the data cells,
64
- and the whole `<tr>`. `tdClass` and `rowClass` may be functions
66
+ and the whole row. `tdClass` and `rowClass` may be functions
65
67
  `(content, key, item) => Classes` for state-dependent styling. Table-level
66
68
  `thClass` / `tdClass` / `rowClass` / `headerRowClass` apply everywhere; a
67
69
  row's classes accumulate the table `rowClass` plus every column's.
@@ -155,7 +157,7 @@ const invoices: Invoice[] = [
155
157
  },
156
158
  ];
157
159
 
158
- // `row-class` is applied to each `<tr>`; use it to flag rows by state. Soft amber
160
+ // `row-class` is applied to each row (`.bb-table-data__row`); use it to flag rows by state. Soft amber
159
161
  // wash matches the design-language attention accent — public API, no internals.
160
162
  const rowClass = (invoice: Invoice) =>
161
163
  invoice.status === 'overdue'
@@ -207,7 +209,11 @@ Override any column's body with a `#<key>` slot and its header with a
207
209
  characters turned into `_` (dashes are kept as-is), **lowercased**:
208
210
  `address.city` → `#address_city`, `first_name` → `#first_name`, `issued-at` →
209
211
  `#issued-at`, but also `issuedAt` → `#issuedat` — prefer snake_case keys so
210
- slot names read naturally. Keep the column in `columns`
212
+ slot names read naturally. Building slot names in code (columns from config, a
213
+ dynamic `<template>` loop)? Import the normalizer itself — `import { slotKey }
214
+ from 'bitboss-ui'` — instead of restating the rule: a hand copy that differs by
215
+ one character (`\W+` collapses dashes; the real rule keeps them) makes the slot
216
+ silently never match. Keep the column in `columns`
211
217
  even when you fully slot it: label, alignment and width still come from there.
212
218
 
213
219
  Body cell scope: `content` (the formatted value), `item` (raw row), `value`
@@ -219,6 +225,22 @@ one-way state; the `toggle*` callbacks are the write path.
219
225
  Header scope: `label`, `items` (the full list — handy for counts), `sortable`,
220
226
  `sortOrder`, `toggleSort`, `classes`.
221
227
 
228
+ **Add beside a header instead of replacing it** with
229
+ `#header:<key>:prepend` / `#header:<key>:append` — rendered before / after the
230
+ column's existing header content (the label + sort control, or your own
231
+ `#header:<key>` replacement), so a resize grip or reorder handle no longer
232
+ costs you the built-in sort button. The table-wide pair `#header:prepend` /
233
+ `#header:append` applies one template to every data column's header (never the
234
+ structural select/actions cells); both forms receive the header scope above
235
+ plus `columnKey` (the raw column key) so a table-wide template can branch per
236
+ column. When both a per-column and the table-wide slot are provided for the
237
+ same side, the per-column one wins for that column — they never stack. Affixes
238
+ render even beside a full `#header:<key>` replacement (the same rule as
239
+ BbDropdown's `#<key>:prepend/append`). One caveat: a column key that
240
+ normalizes to `prepend` or `append` collides with the table-wide slot names;
241
+ the table-wide interpretation wins, per-column header slots are unavailable
242
+ for that column, and dev builds warn.
243
+
222
244
  Below, the status badges carry brand glyphs shipped in the app's `iconDir`,
223
245
  referenced as `local:*` names on the badge's `prepend:icon`. Per the icons
224
246
  policy the dynamic status maps to **full-name literals** (a lookup object) —
@@ -365,7 +387,10 @@ Beyond per-column slots, the structural slots cover every other region:
365
387
  (replace the per-row checkbox/radio; scope: `item`, `value`, `checked`,
366
388
  `disabled`, `readonly`, `inputName`, `toggleSelected`), `#loading`, `#no-data`
367
389
  (both below), and `#thead` / `#tbody` / `#tfoot` to replace an entire region
368
- when you need full control (you then own its markup and semantics).
390
+ when you need full control (you then own its markup and semantics: the table
391
+ is a CSS grid, so pass `<div role="row">` rows holding `<div role="cell">` /
392
+ `<div role="columnheader">` cells — never `<tr>`/`<td>`; a spanning cell uses
393
+ `grid-column: span N` with `#tfoot`'s `columnCount`, see **Styling hooks**).
369
394
 
370
395
  ### Loading and empty states
371
396
 
@@ -396,8 +421,9 @@ it renders only when there is nothing to keep. (Scope: the current, possibly
396
421
  stale `items` — handy for matching the previous row count.) When `items` resolves
397
422
  empty the table shows its no-data row — localized default text, overridable via
398
423
  `no-data-text` or fully via the `#no-data` slot. That slot fills the table's own
399
- full-width empty-state cell (already centred, colspan already counted), so pass
400
- the content — a message, a "clear filters" button — not a `<tr>` of your own.
424
+ full-width empty-state cell (already centred, already spanning every column),
425
+ so pass the content — a message, a "clear filters" button — not a row of your
426
+ own.
401
427
 
402
428
  `loading-text` is the loading row's `aria-live` announcement. Like
403
429
  `no-data-text` it has a **localized default** (`common.loadingText` —
@@ -693,6 +719,11 @@ sorts first. The table only reports the model; **apply it to your data**
693
719
  (client-side, as below) or forward it to the server as `orderBy` and refetch
694
720
  via `dependencies`.
695
721
 
722
+ The affordance is one arrow: nothing at rest, a faint arrow previewed while
723
+ the header is hovered or focused, solid once the column is sorted (up for
724
+ ascending, down for descending — the AG Grid convention), gone again on the
725
+ third click.
726
+
696
727
  **Sortable headers driving a client-side sort**
697
728
 
698
729
  ```vue
@@ -773,7 +804,10 @@ const sortedInvoices = computed(() => {
773
804
 
774
805
  A custom `#header:<key>` slot replaces the built-in sort button, but its scope
775
806
  hands you `sortOrder` and `toggleSort` so your header can keep sorting alive
776
- (render your own button and call `toggleSort`). The column-level `sorted`
807
+ (render your own button and call `toggleSort`). To merely add something next
808
+ to the control — a resize grip, a filter icon — use
809
+ `#header:<key>:prepend/append` instead and keep the built-in button (see
810
+ "Custom cells and headers"). The column-level `sorted`
777
811
  field only seeds the header's `aria-sort` when no live model entry exists — it
778
812
  does not move the visual indicator; to start pre-sorted, seed `v-model:sort`
779
813
  instead (as the example does).
@@ -1282,7 +1316,7 @@ _detail about the row_ rather than _children of it_.
1282
1316
  A `BbTable` rendered inside another's `#expand` slot is independent by
1283
1317
  default. Add `inherit-column-widths` and it snaps its columns to the parent's
1284
1318
  tracks, so a line-items breakdown reads as a continuation of the invoice row
1285
- above it (this also forces fixed layout). The mechanics:
1319
+ above it. The mechanics:
1286
1320
 
1287
1321
  - `inherit-column-widths` — `true` inherits from the nearest ancestor table;
1288
1322
  a string targets a specific ancestor's `id` (useful when an intermediate
@@ -1293,23 +1327,34 @@ above it (this also forces fixed layout). The mechanics:
1293
1327
  allowed; `end: -1` runs to the end of the data region). Structural columns
1294
1328
  (`select`, actions) are accounted for automatically.
1295
1329
  - A child with **more** columns than the parent content-sizes the extras and
1296
- scrolls; with fewer, the last width-free column absorbs the slack so both
1297
- tables end flush.
1298
- - **When the parent has `selectable`/`actions` columns, snap every child
1299
- column** (`snap: base` `base + n`, where `base` is `1` under a selectable
1300
- parent, `0` otherwise). A width-free last column becomes the fill track and
1301
- absorbs the parent's actions region right-aligned content then sits under
1302
- the expand/actions control instead of under the parent's last data column.
1303
- With every column snapped, the parent's select and actions tracks fold into
1304
- the child's first/last cell as _padding_, so the child's content stays flush
1305
- with the parent's data grid on both edges (a child without its own select
1306
- column starts flush with the parent's first data column the inset itself
1307
- reads as hierarchy).
1330
+ overflows into the parent's scroll (see below); with fewer, the last
1331
+ width-free column absorbs the slack so both tables end flush.
1332
+ - **An inheriting child does not scroll on its own — the parent's scrollport
1333
+ is the only one.** That is what keeps the two grids locked together: a child
1334
+ with its own scroll position would fall out of alignment the moment either
1335
+ one moved, which is the whole thing `inherit-column-widths` exists to
1336
+ prevent. One scrollbar moves both.
1337
+ - **The parent's `selectable`/`actions` columns are handled for you** — the
1338
+ child's content columns map to the parent's content columns 1:1 (no `snap`
1339
+ needed for that), and the parent's select/actions _regions_ are reserved on
1340
+ each side: a child with its own select or actions column widens that cell
1341
+ to the parent's (two equal cells, controls at the row's end, so the ⌃ sits
1342
+ under the parent's); a child without one folds the region into its first/
1343
+ last cell as _padding_, so its content stays flush with the parent's data
1344
+ grid on both edges (the inset reads as hierarchy). Only a child cell
1345
+ _wider_ than the parent's — a select the parent lacks, more buttons — moves
1346
+ anything, and only the adjacent content column: it shrinks by the surplus
1347
+ so every other rail holds. What a child publishes to its own children is
1348
+ the same contract (content rails + regions), so a three-level nest aligns
1349
+ at every depth.
1308
1350
  - **Scope any page-level cell-padding overrides to the parent's own cells.** A
1309
- bare `:deep(tbody td:first-child)` also matches the nested table's cells and
1310
- clobbers the offset padding it computes to stay aligned — use direct-child
1311
- combinators (`:deep(> table > tbody > tr > td:first-child)`) so the child
1312
- keeps its structural padding.
1351
+ bare `:deep([role='cell']:first-child)` also matches the nested table's
1352
+ cells and clobbers the offset padding it computes to stay aligned — use the
1353
+ direct-child chain through the grid's containers
1354
+ (`:deep(> .bb-table__table > .bb-table__body > [role='row'] > :first-child)`)
1355
+ so the child, which lives inside `.bb-table-expand__cell`, keeps its
1356
+ structural padding. The canonical `.table-flush` idiom is under **Styling
1357
+ hooks**.
1313
1358
 
1314
1359
  **Invoice lines aligned under the invoice columns**
1315
1360
 
@@ -1419,6 +1464,41 @@ const expanded = ref<string[]>([]);
1419
1464
  </script>
1420
1465
  ```
1421
1466
 
1467
+ #### Affixed actions on both tables
1468
+
1469
+ A nested table can carry its own `fixed-columns` — child rows usually need
1470
+ their own row actions — and both action columns pin to the same visible edge:
1471
+
1472
+ ```vue
1473
+ <BbTable
1474
+ id="programmes"
1475
+ actions
1476
+ :columns="columns"
1477
+ :fixed-columns="[{ index: columns.length, position: 'right' }]"
1478
+ :items="rows"
1479
+ item-value="id"
1480
+ >
1481
+ <template #expand="{ item }">
1482
+ <BbTable
1483
+ actions
1484
+ :columns="childColumns"
1485
+ :fixed-columns="[{ index: childColumns.length, position: 'right' }]"
1486
+ inherit-column-widths="programmes"
1487
+ :items="item.children"
1488
+ item-value="id"
1489
+ />
1490
+ </template>
1491
+ </BbTable>
1492
+ ```
1493
+
1494
+ This works because the child shares the parent's scrollport. `position: sticky`
1495
+ resolves against the nearest scrolling ancestor, and the child's own box is as
1496
+ wide as the parent's **content** — so if it opened a scrollport of its own, the
1497
+ child's actions would affix to an edge sitting out in the overflow, hundreds of
1498
+ pixels past what anyone can see, while the parent's pinned correctly. Snap the
1499
+ child's actions column onto the parent's actions track (see the snapping rule
1500
+ above) so the two columns line up rather than merely both being pinned.
1501
+
1422
1502
  #### Flush tables + inherited widths: mind the edge padding
1423
1503
 
1424
1504
  `table-flush` removes the outer cell padding so the table reads edge-to-edge in
@@ -1432,9 +1512,10 @@ Two rules keep them honest:
1432
1512
  1. **Snap against the same base you paint.** Measure the parent's columns with
1433
1513
  the same flush/padded state the child renders in — never measure padded and
1434
1514
  render flush.
1435
- 2. **Never add nest padding on the body only.** Padding the child's `tbody` but
1436
- not its header (or vice versa) reintroduces exactly the offset the snap just
1437
- removed. Put the inset on the nest container, where both rows inherit it.
1515
+ 2. **Never add nest padding on the body only.** Padding the child's body
1516
+ (`.bb-table__body`) but not its header (or vice versa) reintroduces exactly
1517
+ the offset the snap just removed. Put the inset on the nest container, where
1518
+ both row groups inherit it.
1438
1519
 
1439
1520
  ### Row highlight (master–detail)
1440
1521
 
@@ -1636,9 +1717,11 @@ const rowClass = (event: AuditEvent) =>
1636
1717
  ```
1637
1718
 
1638
1719
  Row events: `click:row`, `dblclick:row` and `contextmenu:row` fire with
1639
- `(event, row, selected)`. Note that `row` is the table's internal row wrapper,
1640
- not your item: read the raw record from `row.item` and its resolved value from
1641
- `row.value`. Reserve these events for side effects that are not navigation
1720
+ `(event, item)` the native event and **your record exactly as you passed it
1721
+ in `items`**, nothing else. No wrapper to unpack, no third argument; selection
1722
+ state is already yours through the `v-model`, and the row's value resolves
1723
+ from the record through `item-value` exactly as the models do. Reserve these
1724
+ events for side effects that are not navigation —
1642
1725
  logging, opening a drawer (though `v-model:highlighted` usually covers that
1643
1726
  without any handler).
1644
1727
 
@@ -2057,10 +2140,11 @@ leave the screen. Collapse instead of scroll:
2057
2140
 
2058
2141
  - **Rank the columns.** Identity + status + the one number that matters stay;
2059
2142
  everything else is secondary.
2060
- - **Hide the secondary ones at a breakpoint** with the column's `thClass` _and_
2061
- `tdClass` (there is no `classes` field): give both the same
2062
- `'hidden md:table-cell'`, so they are gone rather than off-screen. Both are
2063
- needed — `thClass` hides the header, `tdClass` the body cells.
2143
+ - **Drop the secondary ones from `columns` at the breakpoint** a `computed`
2144
+ over `useMobile()` (or your own `matchMedia`) that filters the secondary keys
2145
+ out. Removing the column removes its grid track. Do **not** hide cells with
2146
+ `thClass`/`tdClass` + `display: none`: the table is a grid, a hidden cell
2147
+ leaves its track in place and every following cell shifts one track over.
2064
2148
  - **Stack what survives into the primary cell.** The identity slot renders the
2065
2149
  name plus the hidden facts under it, at `--bb-text-muted`, so nothing is
2066
2150
  actually lost.
@@ -2069,6 +2153,125 @@ Keep the sticky/pinned identity column for the case where the full grid really
2069
2153
  must stay reachable (a spreadsheet-shaped tool), not as the default for a
2070
2154
  records list.
2071
2155
 
2156
+ ### Styling hooks
2157
+
2158
+ **The table's surface is `--bg`** (default `var(--bb-panel)`), a local on
2159
+ `.bb-table`. Cells are transparent at rest so the row can tint them, but
2160
+ anything that slides over scrolled content — the stuck header band with
2161
+ `fixed-headers`, and `fixed-columns` cells — is opaque and paints `--bg` (a
2162
+ pinned cell also picks up its row's hover/highlight tint, so it never reads
2163
+ as a hole). When a table sits on a surface that is not the panel colour, set
2164
+ the one knob and everything pinned follows:
2165
+
2166
+ ```css
2167
+ .my-card .bb-table {
2168
+ --bg: var(--card-bg);
2169
+ }
2170
+ ```
2171
+
2172
+ The table is a **CSS grid of `<div>`s with ARIA roles, not a `<table>`**: the
2173
+ root `.bb-table__table[role=table]` owns the column tracks and every row group
2174
+ and row is a subgrid of it. Style it through the classes and the roles — never
2175
+ through element names, which match nothing:
2176
+
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`) | — |
2191
+
2192
+ `[role='row']`, `[role='cell']` and `[role='columnheader']` are supported
2193
+ hooks, not incidental markup — use them when you mean "every row kind" or
2194
+ "every cell"; use the classes when you mean one kind. Cells are direct
2195
+ children of their row, so `:first-child` / `:last-child` / `:nth-child(n)`
2196
+ address columns.
2197
+
2198
+ The full-bleed idiom every workspace page reaches for — the table bleeds past
2199
+ the content padding and hands the inset back to its outermost cells — is
2200
+ written with a **direct-child chain** so a nested table inheriting the
2201
+ parent's tracks (it lives inside `.bb-table-expand__cell`) is never touched:
2202
+
2203
+ ```css
2204
+ .table-flush {
2205
+ --page-inset: 1rem;
2206
+ margin-inline: calc(var(--page-inset) * -1);
2207
+ width: calc(100% + var(--page-inset) * 2);
2208
+ }
2209
+
2210
+ .table-flush
2211
+ :deep(> .bb-table__table > .bb-table__head > [role='row'] > :first-child),
2212
+ .table-flush
2213
+ :deep(
2214
+ > .bb-table__table
2215
+ > .bb-table__body
2216
+ > [role='row']
2217
+ > :first-child:not(.bb-table-expand__cell)
2218
+ ) {
2219
+ padding-inline-start: var(--page-inset);
2220
+ }
2221
+
2222
+ .table-flush
2223
+ :deep(> .bb-table__table > .bb-table__head > [role='row'] > :last-child),
2224
+ .table-flush
2225
+ :deep(
2226
+ > .bb-table__table
2227
+ > .bb-table__body
2228
+ > [role='row']
2229
+ > :last-child:not(.bb-table-expand__cell)
2230
+ ) {
2231
+ padding-inline-end: var(--page-inset);
2232
+ }
2233
+
2234
+ /* Expand rows stay edge-to-edge so the nested table can snap onto the parent
2235
+ tracks without an extra inset shifting it sideways. */
2236
+ .table-flush
2237
+ :deep(
2238
+ > .bb-table__table > .bb-table__body > [role='row'] > .bb-table-expand__cell
2239
+ ) {
2240
+ padding-inline: 0;
2241
+ }
2242
+ ```
2243
+
2244
+ Leave the geometry alone: `display: grid` / `gap: 0` on the root, row groups
2245
+ and rows, `grid-column: 1 / -1` **and `contain: inline-size`** on the
2246
+ full-width cells (without the containment, expand-row content resizes the
2247
+ parent's columns), `white-space: nowrap` on the actions cell (it sizes the
2248
+ actions track), and `position: sticky` on `.bb-table__head` under
2249
+ `fixed-headers`.
2250
+
2251
+ When you replace a region with `#thead` / `#tbody` / `#tfoot`, ship the same
2252
+ shape the table does — role-complete rows, spanning with `grid-column` rather
2253
+ than `colspan`:
2254
+
2255
+ ```vue
2256
+ <template #tfoot="{ columnCount }">
2257
+ <div role="row">
2258
+ <div
2259
+ role="cell"
2260
+ :aria-colspan="columnCount - 1"
2261
+ :style="{ gridColumn: `span ${columnCount - 1}` }"
2262
+ >
2263
+ Total
2264
+ </div>
2265
+ <div role="cell" class="text-right">{{ total }}</div>
2266
+ </div>
2267
+ </template>
2268
+ ```
2269
+
2270
+ A `<tr>` inside the grid is not merely unsemantic — the browser wraps it in an
2271
+ implicit `<table>` and the cells never land on the tracks. Nothing lints the
2272
+ roles: a `<div>` row without them renders fine and is invisible to a screen
2273
+ reader.
2274
+
2072
2275
  ### Config and platform
2073
2276
 
2074
2277
  - **Locale.** The plugin's `locale` (`it`/`en`) drives the built-in strings:
@@ -2119,7 +2322,8 @@ records list.
2119
2322
  - Enabling select-all does not re-emit an empty `modelValue` (see the
2120
2323
  select-all section) — in all-mode, read `select-all` +
2121
2324
  `unselected-items`, not `modelValue`.
2122
- - Row event payloads are the internal row wrapper your record is `row.item`.
2325
+ - Row event payloads are `(event, item)` with `item` your raw record — there is
2326
+ no wrapper to unpack and no `selected` argument.
2123
2327
  - A mistyped context/pagination `id` does not error; it silently syncs with
2124
2328
  nothing. If a toolbar "works" but the table never reacts, check the id.
2125
2329
  - In multiple mode `modelValue` must be an array, or it throws.
@@ -2195,13 +2399,13 @@ records list.
2195
2399
  | `displayCaption` | `boolean \| undefined` | | | Boolean that defines whether to display or hide the caption. By default is true and the caption is hidden. |
2196
2400
  | `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… |
2197
2401
  | `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`/… |
2198
- | `fixed` | `boolean \| undefined` | | | Defines the table layout as fixed making each column take up the same amount of space. |
2402
+ | `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. |
2199
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. |
2200
2404
  | `fixedHeaders` | `boolean \| undefined` | | | Boolean that sets the headers as sticky to the top of the table. |
2201
- | `headerRowClass` | `Classes \| undefined` | | | Defines the classes to be passed to the header `<tr>`. |
2405
+ | `headerRowClass` | `Classes \| undefined` | | | Defines the classes to be passed to the header row. |
2202
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… |
2203
2407
  | `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. |
2204
- | `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 forces `table-layout: fixed`, so it is **off by default** — a nested table renders independ… |
2408
+ | `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… |
2205
2409
  | `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 … |
2206
2410
  | `items` | `Item[] \| ((prefill: boolean, modelValue?: any) => Item[] \| Promise<Item[]>)` | `[]` | yes | Rows to render, or a fetcher that returns them. |
2207
2411
  | `itemValue` | `ItemAccessor<Item> \| undefined` | | | |
@@ -2217,23 +2421,23 @@ records list.
2217
2421
  | `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… |
2218
2422
  | `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… |
2219
2423
  | `readonly` | `boolean \| undefined` | | | Sets the input in a readonly state. |
2220
- | `rowClass` | `RowClasses<Item> \| undefined` | | | Defines the classes to be passed to each data `<tr>`. Can also be a function of the row's item for dynamic values. |
2424
+ | `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. |
2221
2425
  | `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. |
2222
2426
  | `selectAll` | `boolean \| undefined` | `false` | | Defines whether the "Select all" checkbox is checked. |
2223
2427
  | `selectAllLabel` | `string \| undefined` | | | Text of the label used by the "Select all" boolean. |
2224
2428
  | `selectText` | `string \| undefined` | | | Text of the header used for the radio inputs when the table is not `multiple`. |
2225
2429
  | `sort` | `BbTableSortEntry[] \| undefined` | `[]` | | Used by `v-model:sort`. Ordered array of `[columnKey, direction]` entries, one per actively sorted column. Bidirectional: the header buttons of `sortable` columns update it, and external writes update the header indicators. The table does n… |
2226
- | `tdClass` | `ColumnClasses<Item> \| undefined` | | | Defines the classes to be passed to the `<td>` elements. Can also be a function for dynamic values. |
2227
- | `thClass` | `Classes \| undefined` | | | Defines the classes to be passed to the `<th>`. |
2430
+ | `tdClass` | `ColumnClasses<Item> \| undefined` | | | Defines the classes to be passed to every data cell. Can also be a function for dynamic values. |
2431
+ | `thClass` | `Classes \| undefined` | | | Defines the classes to be passed to every header cell. |
2228
2432
  | `totalItems` | `string \| number \| undefined` | | | Total number of items there is. Also flows into the shared table context, where a `BbPagination` paired through `id` derives its page count from it. When omitted, an array `items` table infers it from `items.length`; an explicit value (or a… |
2229
2433
  | `totalPages` | `string \| number \| undefined` | | | Seeds the page count shown by a `BbPagination` paired through `id`. A shortcut for when the server reports a page count rather than a row count: the table renders nothing from it, it only flows into the shared table context. A known total a… |
2230
2434
  | `unselectedItems` | `any[] \| undefined` | `[]` | | Items that are not selected. |
2231
2435
 
2232
2436
  ## Events
2233
2437
 
2234
- - `click:row` — `(e: "click:row", event: MouseEvent, item: any, selected: boolean): void`
2235
- - `contextmenu:row` — `(e: "contextmenu:row", event: MouseEvent, item: any, selected: boolean): void`
2236
- - `dblclick:row` — `(e: "dblclick:row", event: MouseEvent, item: any, selected: boolean): void`
2438
+ - `click:row` — `(e: "click:row", event: MouseEvent, item: any): void`
2439
+ - `contextmenu:row` — `(e: "contextmenu:row", event: MouseEvent, item: any): void`
2440
+ - `dblclick:row` — `(e: "dblclick:row", event: MouseEvent, item: any): void`
2237
2441
  - `item:selected` — `(e: "item:selected", value: any): void`
2238
2442
  - `item:unselected` — `(e: "item:unselected", value: any): void`
2239
2443
  - `update:expandedItems` — `(e: "update:expandedItems", value: any[]): void`
@@ -2248,13 +2452,15 @@ records list.
2248
2452
  - `actions` — scope: `{ expanded?: boolean \| undefined; expandProps?: ExpandControllerProps \| undefined; toggleExpanded?: (() => void) \| undefined; highlighted?: boolean \| undefi…` — Content rendered in the actions cell for each row.
2249
2453
  - `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.
2250
2454
  - `header:actions` — scope: `{ text: string; }` — Replaces the default actions header cell content.
2455
+ - `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…
2456
+ - `header:prepend` — scope: `BbTableHeaderAffixSlotProps<Item>` — Content rendered before every data column's header cell content — the label + sort control, or the column's own `header:<key>` replacement. A column's `header:<key>:prepend` slot wins over this one for that column; they never stack. Branch …
2251
2457
  - `header:select` — scope: `{ multiple: boolean; selectAll: boolean; text: string; }` — Replaces the default selection header cell content (checkbox or radio label).
2252
2458
  - `loading` — scope: `{ items: Item[]; }` — Content shown while table rows are loading (replaces the skeleton rows).
2253
- - `no-data` — scope: `object` — Content shown when the items list is empty and not loading. It fills the table's own full-width empty-state cell (the colspan already accounts for the selection/actions columns), so provide the *content* — a message, an illustration, a "cle
2459
+ - `no-data` — scope: `object` — Content shown when the items list is empty and not loading. It fills the table's own full-width empty-state cell (it already spans the selection/actions columns), so provide the *content* — a message, an illustration, a "clear filters" butt
2254
2460
  - `select` — scope: `{ item: Item; value: any; checked: boolean; disabled: boolean; inputName: string; readonly: boolean; toggleSelected?: (() => void) \| undefined; }` — Replaces the default checkbox/radio in the selection cell for each row.
2255
- - `tbody` — scope: `object` — Replaces the entire `<tbody>` element. Use when you need full control over the body markup.
2256
- - `tfoot` — scope: `{ columnCount: number; }` — Replaces the entire `<tfoot>` element. Use when you need full control over the footer markup.
2257
- - `thead` — scope: `object` — Replaces the entire `<thead>` element. Use when you need full control over the header markup.
2461
+ - `tbody` — scope: `object` — Replaces the content of the body row group (`.bb-table__body`). Use when you need full control over the body markup. Provide role-complete rows — `<div role="row">` holding one `<div role="cell">` per column — never a `<tr>`/`<td>`. A full-…
2462
+ - `tfoot` — scope: `{ columnCount: number; }` — Fills the footer row group (`.bb-table__foot`). Use when you need a footer. Provide role-complete rows — `<div role="row">` holding `<div role="cell">` elements — never a `<tr>`/`<td>`.
2463
+ - `thead` — scope: `object` — Replaces the content of the header row group (`.bb-table__head`). Use when you need full control over the header markup. The table is a CSS grid, not a `<table>`: provide role-complete rows — `<div role="row">` holding one `<div role="colum…
2258
2464
 
2259
2465
  ## See Also
2260
2466