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
@@ -1,23 +1,26 @@
1
1
  ---
2
2
  title: 'Migration v2→v3: BbTable'
3
- summary: 'allowSelectAll inverted to disableSelectAll; loading no longer blanks populated tables and now inerts rows; #loading is first-load-only; #no-data fills a full-width table cell instead of replacing the row; columns gained sortable + rowClass, which collide with same-named consumer fields.'
3
+ summary: 'The table renders as a CSS grid of divs with ARIA roles, not a <table> — every element selector (table/thead/tbody/tr/th/td) under .bb-table matches nothing, #thead/#tbody/#tfoot take role-complete div rows, --fill/--natural-width are gone, column widths drift within documented bounds, copy-to-spreadsheet and print header repetition degrade; allowSelectAll inverted to disableSelectAll; loading no longer blanks populated tables and now inerts rows; #loading is first-load-only; #no-data fills a full-width cell instead of replacing the row; row events drop the selected argument; columns gained sortable + rowClass, which collide with same-named consumer fields.'
4
4
  ---
5
5
 
6
6
  # BbTable — v2 → v3
7
7
 
8
8
  ## Changes
9
9
 
10
- | v2 | v3 | Kind |
11
- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
12
- | `allowSelectAll?: boolean` (default `true`) | `disableSelectAll?: boolean` (default `false`) | rename, polarity inverted |
13
- | refetch with rows → skeleton | refetch keeps rows visible (dimmed + progress bar) | **behavior** — the skeleton only shows when there is nothing to display |
14
- | rows interactive while loading | header + rows go `inert` while loading | **behavior** — opt out with `interactive-while-loading` |
15
- | `#loading` slot fired on every load | fires on the **first load only** (it replaces the skeleton, so it follows the skeleton's rule) | **⚠ silent** for anyone styling refetches through it |
16
- | `#no-data` replaced the empty-state `<tr>` | fills the table's own full-width `<td>` (colspan counted, centred) | **behavior** — pass content, not a row |
17
- | — | props `enforceCoherence` (prunes row-keyed models after each load; incompatible with pagination-via-`dependencies`), `expandedItems` + `#expand`, `sort`, `keyboardNavigation`, `highlighted`, `rowClass` | additive |
18
- | — | **column** fields `sortable?: boolean`, `rowClass?: ColumnClasses` | additive, **collides** — see below |
19
- | `useBbTableContext(id).total` _(v3 alphas ≤ 4)_ | `useBbTableContext(id).totalItems` | **hard rename** on the composable handle — see below |
20
- | `--bb-table-*` custom properties | same names without the `bb-` prefix | **⚠ silent** for nested-table layouts — see below |
10
+ | v2 | v3 | Kind |
11
+ | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
12
+ | `allowSelectAll?: boolean` (default `true`) | `disableSelectAll?: boolean` (default `false`) | rename, polarity inverted |
13
+ | refetch with rows → skeleton | refetch keeps rows visible (dimmed + progress bar) | **behavior** — the skeleton only shows when there is nothing to display |
14
+ | rows interactive while loading | header + rows go `inert` while loading | **behavior** — opt out with `interactive-while-loading` |
15
+ | `#loading` slot fired on every load | fires on the **first load only** (it replaces the skeleton, so it follows the skeleton's rule) | **⚠ silent** for anyone styling refetches through it |
16
+ | `#no-data` replaced the empty-state `<tr>` | fills the table's own full-width cell (`aria-colspan` counted, centred) | **behavior** — pass content, not a row |
17
+ | `<table>` / `<thead>` / `<tbody>` / `<tr>` / `<th>` / `<td>` markup _(v2 and v3 ≤ beta.5)_ | a CSS grid of `<div>`s carrying `role="table\|rowgroup\|row\|columnheader\|cell"`; `.bb-table__table` / `__head` / `__body` / `__foot` are the new containers, every `.bb-table*` class survives | **⚠ silent** for element selectors, `#thead`/`#tbody`/`#tfoot` markup, copy/paste and print — see below |
18
+ | `--fill` / `--natural-width` published on `.bb-table` _(v3 ≤ beta.5)_ | **removed** — nothing publishes them | **⚠ silent** — see below |
19
+ | — | props `enforceCoherence` (prunes row-keyed models after each load; incompatible with pagination-via-`dependencies`), `expandedItems` + `#expand`, `sort`, `keyboardNavigation`, `highlighted`, `rowClass` | additive |
20
+ | — | **column** fields `sortable?: boolean`, `rowClass?: ColumnClasses` | additive, **collides** — see below |
21
+ | `useBbTableContext(id).total` _(v3 alphas ≤ 4)_ | `useBbTableContext(id).totalItems` | **hard rename** on the composable handle — see below |
22
+ | `@click:row` / `@contextmenu:row` / `@dblclick:row` handlers receive `(event, item, selected)` _(v2 and v3 ≤ beta.5)_ | `(event, item)` — `item` is now your raw record (was the internal row wrapper, record at `row.item`); the trailing `selected` boolean is gone | **payload slimmed** — see below |
23
+ | `--bb-table-*` custom properties | same names without the `bb-` prefix | **⚠ silent** for nested-table layouts — see below |
21
24
 
22
25
  ## Columns gained `sortable` and `rowClass` — check yours first
23
26
 
@@ -70,7 +73,8 @@ migration. Fix it at the type, not at the call sites:
70
73
  `BbTable` coerces the field (`sortable: !!column.sortable` in `BbTable.vue`), so
71
74
  **any** truthy value counts — including a sort-key string like
72
75
  `'patients.created_at'`. The header stops rendering a plain label and renders a
73
- `.bb-table-sort` button instead, and the `<th>` gains `aria-sort="none"`. An app
76
+ `.bb-table-sort` button instead, and the header cell (`.bb-table-header`,
77
+ `role="columnheader"`) gains `aria-sort="none"`. An app
74
78
  that already ships its own sortable-header UI now draws two.
75
79
 
76
80
  The damage is bounded: `sort` is a controlled model (`v-model:sort`), so the
@@ -88,8 +92,9 @@ const tableColumns = computed(() =>
88
92
 
89
93
  Column `rowClass` is the same shape of trap without the type error, because
90
94
  `ColumnClasses` already accepts `string`: a leftover `rowClass: 'w-40'` you
91
- meant for the column typechecks fine and is now merged onto the whole row's
92
- `<tr>`, alongside the table-level `rowClass` prop. A non-`Classes` shape
95
+ meant for the column typechecks fine and is now merged onto the whole row
96
+ (`.bb-table-data__row`), alongside the table-level `rowClass` prop. A
97
+ non-`Classes` shape
93
98
  (`rowClass?: { colspan: number }`) does error, with the same `undefined`
94
99
  wording.
95
100
 
@@ -127,9 +132,13 @@ first-paint placeholder.
127
132
  v2 handed the slot the whole empty-state `<tr>`, so anything that was not a
128
133
  row — the usual `<div>` with a message and a "clear filters" button — landed
129
134
  loose inside `<tbody>` and rendered left-aligned at content width instead of
130
- spanning the table. v3 keeps the `<tr>`/`<td>` shell (with the colspan already
131
- counting the selection and actions columns) and puts the slot inside it, so
132
- that same markup now centres across the full width.
135
+ spanning the table. v3 owns the row/cell shell itself
136
+ (`.bb-table-no-data__row[role=row]` > `.bb-table-no-data__cell[role=cell]`,
137
+ spanning every column with `grid-column: 1 / -1` and an `aria-colspan` that
138
+ already counts the selection and actions columns) and puts the slot inside it,
139
+ so that same markup now centres across the full width. A v2 `<tr>`/`<td>`
140
+ passed here is not just redundant any more — it is table markup inside a
141
+ grid, which is why the diff below deletes it rather than keeping it.
133
142
 
134
143
  ```diff
135
144
  <BbTable :items="rows" :columns="columns" selectable actions>
@@ -184,6 +193,372 @@ unrelated and does not change.
184
193
  `BbPagination` consumers see the same rename through `table-id`; see
185
194
  [BbPagination](./bb-pagination.md).
186
195
 
196
+ ### Row events carry `(event, item)` — your raw record (v3 beta, 2026-08-26)
197
+
198
+ `click:row`, `contextmenu:row` and `dblclick:row` used to pass **the table's
199
+ internal row wrapper** (cells, classes, selection/highlight/expansion flags,
200
+ value hash — your record buried at `row.item`) plus a third `selected`
201
+ argument. Both are gone: the payload is the native event and **the record
202
+ exactly as you passed it in `items`**, nothing else. Selection state is
203
+ already yours through the `v-model` / `unselected-items` models, so the
204
+ handler read it from a second source of truth — and the owner wants row events
205
+ to carry the row, not a state dump.
206
+
207
+ ```diff
208
+ - <BbTable @click:row="(event, row, selected) => open(row.item, selected)" />
209
+ + <BbTable @click:row="(event, item) => open(item, selectedIds.includes(item.id))" />
210
+ ```
211
+
212
+ Two silent JavaScript failures and one loud one: `row.item` is now
213
+ `undefined` (the record has no `.item` field unless yours does), the third
214
+ parameter is simply `undefined`, and any handler typed against `BbTableEvents`
215
+ with three parameters is a **type error** under `vue-tsc` 3. Grep:
216
+ `rg '(click|contextmenu|dblclick):row' --type vue` and inspect every handler
217
+ for `.item` reads and a third parameter.
218
+
219
+ ## The table is a CSS grid, not a `<table>` (v3 beta, 2026-08-26)
220
+
221
+ `BbTable` no longer renders a single `<table>` element. The substrate is a CSS
222
+ grid of `<div>`s: the root owns `grid-template-columns` (the column tracks
223
+ _are_ the width API — `columns[].width`, `fixed`, `inherit-column-widths` and
224
+ `snap` all resolve to tracks), and every row group and row is a `subgrid` of
225
+ it, so a cell lands on its column through two levels without a width of its
226
+ own. Nothing about the props, slots, events or models changed for this; what
227
+ changed is everything that touched the **markup** — CSS element selectors,
228
+ the markup you hand to `#thead` / `#tbody` / `#tfoot`, two published custom
229
+ properties, and a handful of things browsers do for real tables that they do
230
+ not do for grids (copy/paste, print, global element CSS).
231
+
232
+ ### What renders now
233
+
234
+ ```
235
+ .bb-table container (div, or fieldset when selectable) — unchanged: [data-bb-table-id], aria-busy, overflow-x: auto
236
+ legend.sr-only (selectable) — unchanged
237
+ .bb-table-caption[id] replaces <caption>; sr-only unless display-caption; sits BEFORE the grid root
238
+ .bb-table__table[role=table] grid root — aria-labelledby → the caption, aria-rowcount
239
+ .bb-table__head[role=rowgroup] replaces <thead>; goes inert while loading; hosts the busy bar
240
+ .bb-table-header-row[role=row]
241
+ .bb-table-header[role=columnheader] (+ --select / --actions / --edge-start / --edge-end, aria-sort)
242
+ .bb-table__body[role=rowgroup] replaces <tbody>
243
+ .bb-table-data__row[role=row] tabindex / aria-rowindex / aria-current / inert / row events — as before
244
+ .bb-table-data__cell[role=cell] (+ --select / --edge-start / --edge-end)
245
+ .bb-table__cell.bb-table__cell--actions[role=cell]
246
+ .bb-table-expand__row[role=row] id="expanded_{hash}" (the aria-controls target) — unchanged
247
+ .bb-table-expand__cell[role=cell] aria-colspan=N, grid-column: 1 / -1
248
+ .bb-table-no-data__row > .bb-table-no-data__cell[role=cell] aria-colspan=N, grid-column: 1 / -1
249
+ .bb-table-skeleton__row > .bb-table-skeleton__cell[role=cell]
250
+ .bb-table-loading__row > .bb-table-loading__cell[role=cell] > span.bb-table-loading__text[role=status][aria-live=polite]
251
+ .bb-table__foot replaces <tfoot>; always rendered, role=rowgroup only when #tfoot is provided
252
+ ```
253
+
254
+ **Every `.bb-table*` class survives on the equivalent node** — including the
255
+ asymmetric `bb-table__cell--actions` and the caption's `bb-table-caption` (no
256
+ `__`). The one exception is the transient `bb-table--measuring` state class,
257
+ which was never documented and existed only to hide the old probe's
258
+ scrollbar flash: it is gone with the probe. Four container classes are new: `.bb-table__table`, `.bb-table__head`,
259
+ `.bb-table__body`, `.bb-table__foot`. Every row carries `role="row"`, every
260
+ header cell `role="columnheader"`, every body/foot cell `role="cell"`. The
261
+ role chain `table → rowgroup → row → columnheader|cell` is what assistive
262
+ technology reads as a table; without it the whole tree is a pile of generic
263
+ `div`s. `aria-colspan` replaces the deleted `colspan` on the three full-width
264
+ cells (expand, no-data, loading).
265
+
266
+ ### Selector translation
267
+
268
+ Element selectors are the break. Nothing throws: a rule on `tbody td` matches
269
+ nothing, the browser applies nothing, and the page renders with the
270
+ override silently missing. Translate every element name under `.bb-table`:
271
+
272
+ | v2 element | v3 selector |
273
+ | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
274
+ | `table` | `.bb-table__table` |
275
+ | `thead` | `.bb-table__head` |
276
+ | `tbody` | `.bb-table__body` |
277
+ | `tfoot` | `.bb-table__foot` |
278
+ | `caption` | `.bb-table-caption` |
279
+ | `tr` | `[role=row]` — or `.bb-table-data__row` for data rows only (the old bare `tr` matched every row kind: header, skeleton, no-data, expand, loading) |
280
+ | `th` | `[role=columnheader]` / `.bb-table-header` |
281
+ | `td` | `[role=cell]` / `.bb-table-data__cell` |
282
+ | `:first-child` / `:last-child` / `:not(.bb-table-expand__cell)` | **verbatim** — cells stay siblings inside their row |
283
+ | `td:nth-of-type(n)` | `:nth-child(n)` — the children are type-uniform now, so `nth-of-type` and `nth-child` mean the same thing |
284
+ | `colgroup` / `col` | **gone** — use `columns[].width` |
285
+
286
+ `[role=row]`, `[role=cell]` and `[role=columnheader]` are **supported styling
287
+ hooks**, not incidental markup: the role tree is pinned by the component's own
288
+ tests, so a future move to `role="grid"` would be a deliberate selector break,
289
+ not an accident. There are no `data-*` hooks and no shim classes — the roles
290
+ plus the four container classes are the whole translation.
291
+
292
+ **Specificity caveat.** `[role=cell]` is an attribute selector, specificity
293
+ (0,1,0); `td` was a type selector, (0,0,1). A translated rule is therefore
294
+ _stronger_ than the one it replaces, and an override that used to lose to a
295
+ library rule (or to another of your own rules) by one type-selector point can
296
+ now win. Diff any page where two rules targeted the same cell, and prefer the
297
+ class forms (`.bb-table-data__cell`) when you need the old ordering back.
298
+
299
+ The library's own body-cell rules are deliberately held at **(0,2,0)** — the
300
+ row step is wrapped in `:where()` — so a scoped override of yours that beat
301
+ the old `.bb-table tbody tr td` (0,1,3) at (0,2,0) or better keeps winning.
302
+ An override written at exactly (0,1,x) (one class plus type selectors) is
303
+ the only band that loses ground: give it one more class.
304
+
305
+ The one idiom that dominates real apps — a full-bleed table that re-adds the
306
+ page inset on its outermost cells, scoped with direct-child combinators so a
307
+ nested table inheriting the parent's tracks is not touched — translates like
308
+ this:
309
+
310
+ ```diff
311
+ - .table-flush :deep(> table > thead > tr > th:first-child),
312
+ - .table-flush :deep(> table > tbody > tr > td:first-child:not(.bb-table-expand__cell)) {
313
+ + .table-flush :deep(> .bb-table__table > .bb-table__head > [role='row'] > :first-child),
314
+ + .table-flush :deep(> .bb-table__table > .bb-table__body > [role='row'] > :first-child:not(.bb-table-expand__cell)) {
315
+ padding-inline-start: var(--page-inset);
316
+ }
317
+
318
+ - .table-flush :deep(> table > tbody > tr > td.bb-table-expand__cell) {
319
+ + .table-flush :deep(> .bb-table__table > .bb-table__body > [role='row'] > .bb-table-expand__cell) {
320
+ padding-inline: 0;
321
+ }
322
+ ```
323
+
324
+ The direct-child chain matters exactly as before: `.table-flush` lands on the
325
+ container, `.bb-table__table` is its direct child, and a nested table lives
326
+ inside `.bb-table-expand__cell`, so `>` at every step keeps the override off
327
+ the child's cells (whose padding is the alignment mechanism).
328
+
329
+ Grep for the hits: element names inside `:deep()`, inside `.bb-table` rules,
330
+ and — easy to miss — any **global** table CSS (`table { … }`, `td { padding }`,
331
+ `.prose table`), which reached the v2 markup and now reaches nothing (see
332
+ "Global element CSS" below).
333
+
334
+ ### `#thead` / `#tbody` / `#tfoot` take role-complete rows
335
+
336
+ These three slots replace a whole row group, and the markup you pass into them
337
+ is now grid markup: a `<div role="row">` per row holding one
338
+ `<div role="columnheader">` (head) or `<div role="cell">` (body, foot) per
339
+ column — **never** a `<tr>` / `<th>` / `<td>`. A `<tr>` inside a grid is not
340
+ just wrong semantically: the `<tr>` is blockified into a single grid item and
341
+ the browser wraps its `<td>`s in an anonymous CSS table (under SSR the parser
342
+ drops the tags outright), so the cells never land on the tracks.
343
+
344
+ Rows are subgrid rows, so a cell takes the matching column track with no
345
+ width of its own. A cell that spans columns uses `grid-column`, not `colspan`:
346
+ the slot's `columnCount` (total rendered columns, selection and actions
347
+ included) is still handed to `#tfoot`, and it now feeds `grid-column: span N`
348
+ / `grid-column: 1 / -1` instead of a `colspan` attribute. Add `aria-colspan`
349
+ on a spanning cell so column math stays right for assistive technology.
350
+
351
+ ```diff
352
+ <template #tfoot="{ columnCount }">
353
+ - <tr>
354
+ - <td :colspan="columnCount - 1">Total</td>
355
+ - <td class="text-right">{{ total }}</td>
356
+ - </tr>
357
+ + <div role="row">
358
+ + <div
359
+ + role="cell"
360
+ + :aria-colspan="columnCount - 1"
361
+ + :style="{ gridColumn: `span ${columnCount - 1}` }"
362
+ + >
363
+ + Total
364
+ + </div>
365
+ + <div role="cell" class="text-right">{{ total }}</div>
366
+ + </div>
367
+ </template>
368
+ ```
369
+
370
+ A full-width row is `grid-column: 1 / -1` on its single cell (plus
371
+ `aria-colspan="columnCount"`). Keep the invariant the component keeps: no
372
+ role-bearing element between the row group and the row, and every child of a
373
+ row is a `cell` or `columnheader`. **Nothing lints this** — a slot ported to
374
+ `<div>`s without roles renders fine and ships a table screen readers cannot
375
+ read; the docs are the only guard, hence the emphasis.
376
+
377
+ Two side effects of replacing the header: the table measures its column
378
+ tracks off the header cells, so with a custom `#thead` it falls back to the
379
+ first row with one child per track (the first data row) — **a custom header
380
+ over an empty table publishes no tracks**, and a nested table cannot inherit
381
+ from it until rows arrive. And under `fixed-headers`, a `#thead` with several
382
+ rows now sticks as a block (all rows stack); the old cell-level sticky stacked
383
+ them on top of each other.
384
+
385
+ ### `--fill` and `--natural-width` are removed, not renamed
386
+
387
+ Both were plumbing for the `table-layout: fixed` self-correction loop (a
388
+ measured natural width the table could never shrink below, and a fill
389
+ correction that compensated fixed-layout arithmetic). The grid has no such
390
+ loop: tracks are sized by the engine from the declarations, in one layout
391
+ pass, with no offscreen probe. Republishing the sum of the resolved tracks
392
+ under the same names would publish the _container_ width under a name
393
+ promising the _natural_ width — wrong by 2× in the panel's probe — so they
394
+ are dropped outright, the same treatment `--available-height` got. No consumer
395
+ outside the component's own stylesheet ever read them; a
396
+ `var(--natural-width)` in your CSS now resolves to its fallback.
397
+
398
+ ### Column widths: what holds and what drifts
399
+
400
+ Frozen widths (`columns[].width`) hold **exactly**: a `120px` column is
401
+ 120px. The overflow threshold (the container width at which the table starts
402
+ scrolling) and the wrap floor (the narrowest a column gets before its text
403
+ wraps) are **pixel-identical** to table auto layout — the panel measured 905px
404
+ against 905px. `fixed` still splits the width equally (`minmax(0, 1fr)` per
405
+ column). Everything else below is documented drift, each with the numbers
406
+ from the panel's probes (`plans/BBTABLE-GRID.md` § Tracks and § v2.1), so
407
+ you know what a screenshot diff is showing you:
408
+
409
+ - **Roomy containers share the surplus equally, not proportionally to
410
+ content.** Two auto columns that the table sized 748px / 52px now come out
411
+ 544px / 256px. Short columns get wider; long ones narrower. Add a `width` to
412
+ any column whose proportion you relied on.
413
+ - **Constrained wrapping distributes differently.** When several wrappable
414
+ columns compete for a narrow container, tracks grow in equal increments up
415
+ to their max-content instead of proportionally — the longest column ends up
416
+ ~22% narrower and its rows taller. The table starts scrolling at the same
417
+ width as before; only the split before that point moved.
418
+ - **Frozen + auto mixes drift ≤ ~19px per column** — the fixed-layout regime
419
+ where declared widths sat next to unfrozen ones.
420
+ - **All-frozen tables inflate their data columns exactly as before, but not
421
+ the structural ones.** When every column declares a width and the sum is
422
+ below the container, the surplus is still shared in proportion to the
423
+ declared widths (`[100, 200, 100]` at 600px → `[150, 300, 150]`, `%` too),
424
+ so the table keeps filling. The selection and actions columns stay at
425
+ content width instead of inflating with the rest.
426
+ - **A cell that is its own scroll container no longer floors the column.**
427
+ Under table auto layout an `overflow: hidden` / `auto` cell still
428
+ contributed its content width to the column. Under grid it does not — which
429
+ means native `text-overflow: ellipsis` finally works in a cell — **and the
430
+ classic table trick `max-width: 0` breaks**: the cell collapses to its
431
+ padding. Delete every `max-width: 0` you added to make ellipsis work.
432
+ - **Percentage widths in an overflowing table resolve against the grid's
433
+ min-content width**, not the container: a `30%` column that the table gave
434
+ 181px gets 303px once the table overflows. Non-overflowing tables are
435
+ identical. (`width: fit-content` on the grid root keeps the root's box
436
+ covering its tracks only without `%` tracks under overflow — a `%`-wide
437
+ overflowing table is the one case where row backgrounds can paint short of
438
+ the scroll width.)
439
+ - **Expand-row content never widens the parent's columns.** A wide nested
440
+ table or a long unbroken string in `#expand` used to push an auto-layout
441
+ parent's columns apart (a 300px track became 500px in the probe); the grid
442
+ contains the full-width cells (`contain: inline-size`), so the parent's
443
+ tracks hold and the content overflows into the scrollport instead. Fixed
444
+ layout was already immune; auto layout was not; the grid always is.
445
+ - **A `snap` on a column whose table has no parent** used to fall into an
446
+ accidental equal split. It now sizes like any other unfrozen column
447
+ (`auto`).
448
+ - **Nested tables align by contract, at any depth.** A child's select/actions
449
+ cell is widened to the parent's region (`minmax(region, max-content)` — two
450
+ equal cells, controls at the row's end); a child without one folds the
451
+ region into its edge cell's padding. A child cell _wider_ than the parent's
452
+ is reclaimed from the adjacent content column only. What each table
453
+ publishes to its own children is the pure content rails plus the two
454
+ regions, so a grandchild aligns with the root — previously the middle
455
+ table's padding compensation was invisible to its children. The fill
456
+ column now only ever grows (`minmax(<its rail>, 1fr)`); it no longer
457
+ shrinks to absorb a wider actions column.
458
+
459
+ ### Copy-to-spreadsheet degrades
460
+
461
+ Selecting rows in a real `<table>` and pasting into a spreadsheet gave one
462
+ cell per column. Chrome's selection serializer keys off the computed
463
+ `display`, not the element or its role, so a grid pastes **one value per
464
+ line** — the columns are lost. Accepted: the substrate is not going to carry a
465
+ hidden `<table>` for the clipboard. The sanctioned path is an app-level
466
+ "copy as TSV" action built from the **items model** (the same rows you gave
467
+ `items`, joined with tabs and newlines and written with `navigator.clipboard`)
468
+ — a recipe for it is queued (FOLLOWUPS Q7.4); do not scrape the DOM for it.
469
+
470
+ ### Print: header repetition is gone
471
+
472
+ Browsers repeat a `<thead>` at the top of every printed page; there is no
473
+ grid equivalent, so a long table prints its header once. Data rows carry
474
+ `break-inside: avoid` so a row is never split across pages, which is the most
475
+ the substrate can do. No `@media print` rule exists in the library, so nothing
476
+ of yours is fighting one — but if a printed report depended on the repeated
477
+ header, that is a real regression to plan for (a print-specific view, or
478
+ paginated sections with their own headers).
479
+
480
+ ### Global element CSS no longer reaches the table
481
+
482
+ Any CSS that reached `BbTable` through element names now misses it: a reset
483
+ (`table { border-collapse: … }`, `td, th { padding: 0 }`), Tailwind's
484
+ `.prose table` typography, a stray `td { vertical-align: top }` in a legacy
485
+ sheet. This cuts both ways. It is an **immunity** — the table no longer
486
+ inherits whatever global table styling the app carries — and it is a **silent
487
+ change** on every page where that global CSS was, knowingly or not, part of
488
+ the look. Diff those pages; then re-express what you actually wanted on
489
+ `.bb-table-data__cell` / `[role=cell]` scoped to a table.
490
+
491
+ ### Browser floor
492
+
493
+ The table requires `grid-template-columns: subgrid`: **Chrome/Edge 117,
494
+ Firefox 71, Safari 16**. There is no `@supports` fallback and none is planned —
495
+ maintaining two substrates defeats the rewrite. Cell content is centred
496
+ vertically with `align-content` on a block container, which lands one notch
497
+ higher: **Chrome 123, Safari 17.4, Firefox 125**. Below that, cells still lay
498
+ out correctly but content sits at the top of the cell instead of the middle.
499
+
500
+ All of this sits inside the library-wide floor — **Chrome/Edge 114, Safari 17,
501
+ Firefox 125**, set by the native Popover API (see
502
+ `installation-and-plugin-setup.md` § Browser support) — so the only engine the
503
+ grid actually raises is Chrome/Edge, 114 → 117 (123 for centred cells).
504
+
505
+ ### Do not override
506
+
507
+ - **Do not hide cells with `display: none`** (a `thClass`/`tdClass` of
508
+ `hidden`, the v2/early-v3 narrow-screen recipe). A hidden grid item leaves
509
+ its track in place: every following cell shifts one track over and the last
510
+ track collapses. Hide a column by removing it from `columns` at the
511
+ breakpoint, which removes the track.
512
+ - **`contain: inline-size` on `.bb-table-expand__cell`,
513
+ `.bb-table-no-data__cell` and `.bb-table-loading__cell`.** It is the only
514
+ thing keeping a full-width cell's content from resizing the column tracks —
515
+ `min-width: 0` and `width: 100%` wrappers do not work here, and with a nested
516
+ inheriting table inside the expand row the result without it is a feedback
517
+ loop. Removing it (or setting `contain: none` on a cell) is not a styling
518
+ choice.
519
+ - **`gap: 0` on `.bb-table__table`.** A gap set on the root cannot be undone
520
+ from a subgrid level; it opens gutters between every column.
521
+ - **`display: grid` on the root, row groups and rows**, and the
522
+ `grid-column: 1 / -1` on the full-width cells — the geometry.
523
+ - **`white-space: nowrap` on `.bb-table__cell--actions`** — the actions track
524
+ is `max-content`, and `nowrap` is what makes that the width of the controls
525
+ side by side rather than a wrapped stack.
526
+ - **`position: sticky` on `.bb-table__head`** under `fixed-headers`: sticky
527
+ lives on the row group, not on cells — a sticky cell inside the subgrid
528
+ cannot stick in any engine.
529
+
530
+ ### Small semantic and visual changes
531
+
532
+ - **Pinned cells and the stuck header are opaque.** `fixed-columns` cells and
533
+ the `fixed-headers` band used to be transparent, so scrolled content showed
534
+ through them. They now paint the table's surface local `--bg` (default
535
+ `var(--bb-panel)`); a pinned cell also paints its row's hover/highlight
536
+ tint. On a non-panel surface set `.bb-table { --bg: … }` once. The
537
+ highlighted row's left accent bar moved from the row onto its first cell
538
+ (same position; an opaque pinned first column would have hidden it).
539
+ - **The sort glyph is one arrow.** The two-chevron `sort.svg` (with its
540
+ inner `sort-icon__asc` / `sort-icon__desc` paths) is gone; the sort button
541
+ renders an inline `<svg class="bb-table-sort__icon">` — a single arrow,
542
+ hidden at rest, faint on hover/focus, solid when sorted: up for ascending,
543
+ rotated down for descending (the AG Grid convention). Apps that copied the old glyph to rebuild
544
+ a header no longer need to (use the header prepend/append slots); any CSS
545
+ on `.sort-icon__asc` / `.sort-icon__desc` matches nothing.
546
+ - **Caption reading order.** The caption is now a `div` _before_ the table
547
+ role, named through `aria-labelledby`, rather than a `<caption>` inside it.
548
+ The accessible name is identical; a screen reader reading linearly meets the
549
+ caption text just before the table instead of as its first child.
550
+ - **The busy bar rides with a stuck header.** With `fixed-headers`, the
551
+ refetch progress bar is anchored to the sticky head row group, so it stays
552
+ at the header/body boundary while you scroll. It used to anchor to the
553
+ in-flow `<thead>` box and scroll away while the header cells stuck — the
554
+ documented intent, now the behavior.
555
+ - **Multi-row `#thead` heads stack when stuck** (above).
556
+ - **`aria-rowcount` now counts the header row**: it is `totalItems + 1`, so
557
+ the last row's `aria-rowindex` no longer exceeds the count. Anything
558
+ asserting the old value must add one.
559
+ - **Row heights change with the wrapping drift** — expect it in screenshot
560
+ diffs of narrow, text-heavy tables.
561
+
187
562
  ## The custom properties the table _publishes_ lost the `bb-` prefix
188
563
 
189
564
  These are not theming inputs — they are values `BbTable` measures at runtime and
@@ -191,18 +566,21 @@ writes onto its own DOM, for the table's CSS and for descendants to read. You
191
566
  reach for them for exactly one reason: aligning a nested table's columns to its
192
567
  parent's. They all dropped the prefix.
193
568
 
194
- | v2 | v3 |
195
- | ------------------------------------------- | ------------------------------------------- |
196
- | `--bb-table-{id}-track-{key}` | `--table-{id}-track-{key}` |
197
- | `--bb-table-offset-start` / `-end` | `--offset-start` / `--offset-end` |
198
- | `--bb-table-offset-internal-start` / `-end` | `--offset-internal-start` / `-internal-end` |
199
- | `--bb-table-offset-external-start` / `-end` | `--offset-external-start` / `-external-end` |
200
- | `--bb-table-fill` | `--fill` |
201
- | `--bb-table-natural-width` | `--natural-width` |
202
- | `--bb-table-cell-h` | `--cell-h` |
569
+ | v2 | v3 |
570
+ | ------------------------------------------- | -------------------------------------------------- |
571
+ | `--bb-table-{id}-track-{key}` | `--table-{id}-track-{key}` |
572
+ | `--bb-table-offset-start` / `-end` | `--offset-start` / `--offset-end` |
573
+ | `--bb-table-offset-internal-start` / `-end` | `--offset-internal-start` / `-internal-end` |
574
+ | `--bb-table-offset-external-start` / `-end` | `--offset-external-start` / `-external-end` |
575
+ | `--bb-table-fill` | **removed** (was `--fill` in v3 ≤ beta.5) |
576
+ | `--bb-table-natural-width` | **removed** (was `--natural-width` in v3 ≤ beta.5) |
577
+ | `--bb-table-cell-h` | `--cell-h` |
203
578
 
204
579
  Only the track bridge keeps a `table-` segment, because it crosses component
205
- boundaries; the rest are plain locals on `.bb-table`. `--padding-x`,
580
+ boundaries; the rest are plain locals on `.bb-table`. `--fill` and
581
+ `--natural-width` were the fixed-layout self-correction values; the grid
582
+ substrate (next section) has no such loop, so they are gone rather than
583
+ republished under a name that would lie about what they hold. `--padding-x`,
206
584
  `--padding-y` and `--actions-spacing` were already unprefixed in v2 and keep
207
585
  their names (their _values_ tightened: `16px`/`8px` → `12px`/`6px`, and the cell
208
586
  height `42px` → `36px`).