bitboss-ui 3.0.0-beta.2 → 3.0.0-beta.20

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 (337) hide show
  1. package/README.md +72 -33
  2. package/dist/ai/BbAccordion.md +3 -3
  3. package/dist/ai/BbAlert.md +9 -5
  4. package/dist/ai/BbAsterisk.md +17 -0
  5. package/dist/ai/BbAvatar.md +12 -3
  6. package/dist/ai/BbBadge.md +15 -10
  7. package/dist/ai/BbBadgeButton.md +16 -16
  8. package/dist/ai/BbBaseButton.md +16 -16
  9. package/dist/ai/BbBaseCheckbox.md +8 -8
  10. package/dist/ai/BbBaseCheckboxGroup.md +13 -13
  11. package/dist/ai/BbBaseCheckboxIcon.md +26 -9
  12. package/dist/ai/BbBaseColorInput.md +11 -11
  13. package/dist/ai/BbBaseDatePicker.md +6 -6
  14. package/dist/ai/BbBaseDatePickerInput.md +15 -15
  15. package/dist/ai/BbBaseInputContainer.md +12 -12
  16. package/dist/ai/BbBaseNumberInput.md +8 -8
  17. package/dist/ai/BbBaseRadio.md +5 -5
  18. package/dist/ai/BbBaseRadioGroup.md +7 -7
  19. package/dist/ai/BbBaseRadioIcon.md +25 -9
  20. package/dist/ai/BbBaseRating.md +5 -5
  21. package/dist/ai/BbBaseSelect.md +22 -21
  22. package/dist/ai/BbBaseSlider.md +9 -9
  23. package/dist/ai/BbBaseSwitch.md +8 -8
  24. package/dist/ai/BbBaseSwitchGroup.md +13 -13
  25. package/dist/ai/BbBaseSwitchIcon.md +28 -10
  26. package/dist/ai/BbBaseTag.md +10 -10
  27. package/dist/ai/BbBaseTextInput.md +9 -9
  28. package/dist/ai/BbBaseTextarea.md +9 -9
  29. package/dist/ai/BbBaseTimePickerInput.md +13 -13
  30. package/dist/ai/BbBreadcrumbs.md +2 -2
  31. package/dist/ai/BbButton.md +15 -15
  32. package/dist/ai/BbCheckbox.md +18 -17
  33. package/dist/ai/BbCheckboxGroup.md +15 -14
  34. package/dist/ai/BbCollapsible.md +11 -9
  35. package/dist/ai/BbColorInput.md +15 -15
  36. package/dist/ai/BbColorPalette.md +12 -6
  37. package/dist/ai/BbConfirm.md +12 -5
  38. package/dist/ai/BbDatePicker.md +13 -13
  39. package/dist/ai/BbDatePickerInput.md +20 -20
  40. package/dist/ai/BbDialog.md +78 -12
  41. package/dist/ai/BbDropdown.md +132 -22
  42. package/dist/ai/BbDropdownButton.md +12 -12
  43. package/dist/ai/BbDropdownGroup.md +1 -1
  44. package/dist/ai/BbDropzone.md +4 -4
  45. package/dist/ai/BbForm.md +49 -14
  46. package/dist/ai/BbIcon.md +6 -0
  47. package/dist/ai/BbIndicator.md +5 -5
  48. package/dist/ai/BbNumberInput.md +11 -11
  49. package/dist/ai/BbOffCanvas.md +90 -20
  50. package/dist/ai/BbPagination.md +3 -3
  51. package/dist/ai/BbPopover.md +32 -17
  52. package/dist/ai/BbProgress.md +7 -4
  53. package/dist/ai/BbRadio.md +16 -15
  54. package/dist/ai/BbRadioGroup.md +20 -17
  55. package/dist/ai/BbRating.md +11 -11
  56. package/dist/ai/BbSelect.md +138 -37
  57. package/dist/ai/BbSelectPopover.md +40 -41
  58. package/dist/ai/BbSlider.md +37 -22
  59. package/dist/ai/BbSmoothHeight.md +1 -1
  60. package/dist/ai/BbSwitch.md +25 -10
  61. package/dist/ai/BbSwitchGroup.md +17 -14
  62. package/dist/ai/BbTable.md +791 -88
  63. package/dist/ai/BbTabs.md +28 -14
  64. package/dist/ai/BbTabsList.md +1 -1
  65. package/dist/ai/BbTabsPanes.md +1 -1
  66. package/dist/ai/BbTabsRoot.md +10 -10
  67. package/dist/ai/BbTag.md +14 -14
  68. package/dist/ai/BbTextInput.md +16 -15
  69. package/dist/ai/BbTextarea.md +13 -13
  70. package/dist/ai/BbTimePicker.md +10 -10
  71. package/dist/ai/BbTimePickerInput.md +17 -17
  72. package/dist/ai/BbToast.md +1 -1
  73. package/dist/ai/BbToastPortal.md +1 -1
  74. package/dist/ai/BbTooltip.md +12 -10
  75. package/dist/ai/BbTree.md +1 -1
  76. package/dist/ai/ChipsBox.md +1 -1
  77. package/dist/ai/CommonInputWrapper.md +10 -10
  78. package/dist/ai/CommonPopover.md +6 -6
  79. package/dist/ai/FlatListBox.md +5 -5
  80. package/dist/ai/GroupedListBox.md +5 -5
  81. package/dist/ai/ListBox.md +4 -4
  82. package/dist/ai/OptionsContainer.md +4 -4
  83. package/dist/ai/changelog.json +110 -2
  84. package/dist/ai/components.json +882 -90
  85. package/dist/ai/composables/useBbConfig.md +72 -11
  86. package/dist/ai/composables/useConfirm.md +5 -3
  87. package/dist/ai/guides/ai-router.md +9 -4
  88. package/dist/ai/guides/component-picker.md +2 -2
  89. package/dist/ai/guides/design-language.md +14 -14
  90. package/dist/ai/guides/design-tokens.md +25 -18
  91. package/dist/ai/guides/fetch-items-playbook.md +1 -1
  92. package/dist/ai/guides/icons-policy.md +4 -2
  93. package/dist/ai/guides/installation-and-plugin-setup.md +74 -9
  94. package/dist/ai/guides/migration/components/bb-checkbox-group.md +30 -0
  95. package/dist/ai/guides/migration/components/bb-checkbox.md +37 -3
  96. package/dist/ai/guides/migration/components/bb-collapsible.md +5 -5
  97. package/dist/ai/guides/migration/components/bb-dialog.md +62 -34
  98. package/dist/ai/guides/migration/components/bb-offcanvas.md +29 -13
  99. package/dist/ai/guides/migration/components/bb-rating.md +8 -2
  100. package/dist/ai/guides/migration/components/bb-select.md +5 -2
  101. package/dist/ai/guides/migration/components/bb-table.md +446 -28
  102. package/dist/ai/guides/migration/components/bb-text-input.md +6 -6
  103. package/dist/ai/guides/migration/v2-to-v3.md +260 -89
  104. package/dist/ai/guides/ssr-prefill.md +120 -0
  105. package/dist/ai/index.md +10 -9
  106. package/dist/ai/recipes/inertia/layout-scaffold.md +5 -1
  107. package/dist/ai/recipes/inertia/ownership-atlas.md +41 -20
  108. package/dist/ai/recipes/inertia/records-workspace.md +26 -7
  109. package/dist/ai/recipes/nuxt/layout-scaffold.md +5 -1
  110. package/dist/ai/recipes/nuxt/records-workspace.md +26 -7
  111. package/dist/ai/recipes/vue/layout-scaffold.md +5 -1
  112. package/dist/ai/recipes/vue/records-workspace.md +26 -7
  113. package/dist/ai/source/BbAlert.md +60 -2
  114. package/dist/ai/source/BbAvatar.md +3 -3
  115. package/dist/ai/source/BbBadge.md +60 -15
  116. package/dist/ai/source/BbBadgeButton.md +8 -2
  117. package/dist/ai/source/BbBaseButton.md +3 -1
  118. package/dist/ai/source/BbBaseCheckboxGroup.md +6 -2
  119. package/dist/ai/source/BbBaseDatePicker.md +3 -1
  120. package/dist/ai/source/BbBaseDatePickerInput.md +5 -1
  121. package/dist/ai/source/BbBaseInputContainer.md +52 -1
  122. package/dist/ai/source/BbBaseRadioGroup.md +4 -0
  123. package/dist/ai/source/BbBaseRating.md +3 -1
  124. package/dist/ai/source/BbBaseSelect.md +71 -16
  125. package/dist/ai/source/BbBaseSwitchGroup.md +4 -0
  126. package/dist/ai/source/BbBaseTimePickerInput.md +5 -1
  127. package/dist/ai/source/BbBreadcrumbs.md +31 -0
  128. package/dist/ai/source/BbButton.md +51 -24
  129. package/dist/ai/source/BbCheckbox.md +15 -1
  130. package/dist/ai/source/BbCheckboxGroup.md +2 -1
  131. package/dist/ai/source/BbCollapsible.md +7 -1
  132. package/dist/ai/source/BbColorInput.md +3 -4
  133. package/dist/ai/source/BbColorPalette.md +3 -4
  134. package/dist/ai/source/BbDatePicker.md +5 -1
  135. package/dist/ai/source/BbDatePickerInput.md +5 -1
  136. package/dist/ai/source/BbDialog.md +175 -19
  137. package/dist/ai/source/BbDropdown.md +376 -69
  138. package/dist/ai/source/BbDropdownGroup.md +330 -59
  139. package/dist/ai/source/BbForm.md +93 -5
  140. package/dist/ai/source/BbIcon.md +46 -36
  141. package/dist/ai/source/BbNumberInput.md +3 -4
  142. package/dist/ai/source/BbOffCanvas.md +474 -194
  143. package/dist/ai/source/BbPopover.md +24 -17
  144. package/dist/ai/source/BbProgress.md +2 -2
  145. package/dist/ai/source/BbRadio.md +15 -1
  146. package/dist/ai/source/BbRadioGroup.md +3 -1
  147. package/dist/ai/source/BbSelect.md +50 -2
  148. package/dist/ai/source/BbSelectPopover.md +171 -23
  149. package/dist/ai/source/BbSlider.md +3 -4
  150. package/dist/ai/source/BbSwitch.md +15 -1
  151. package/dist/ai/source/BbSwitchGroup.md +8 -5
  152. package/dist/ai/source/BbTable.md +4642 -1387
  153. package/dist/ai/source/BbTabs.md +6 -7
  154. package/dist/ai/source/BbTabsList.md +6 -7
  155. package/dist/ai/source/BbTabsPanes.md +6 -7
  156. package/dist/ai/source/BbTabsRoot.md +6 -7
  157. package/dist/ai/source/BbTag.md +3 -4
  158. package/dist/ai/source/BbTextInput.md +3 -4
  159. package/dist/ai/source/BbTextarea.md +3 -4
  160. package/dist/ai/source/BbTimePicker.md +4 -1
  161. package/dist/ai/source/BbTimePickerInput.md +5 -1
  162. package/dist/ai/source/BbToast.md +3 -1
  163. package/dist/ai/source/BbTooltip.md +5 -1
  164. package/dist/ai/source/CommonInputWrapper.md +4 -0
  165. package/dist/ai/source/CommonPopover.md +6 -0
  166. package/dist/ai/source/FlatListBox.md +11 -5
  167. package/dist/ai/source/GroupedListBox.md +7 -5
  168. package/dist/components/BbAlert/BbAlert.vue_vue_type_script_setup_true_lang.js +4 -4
  169. package/dist/components/BbAlert/types.d.ts +10 -1
  170. package/dist/components/BbAvatar/BbAvatar.vue.d.ts +1 -1
  171. package/dist/components/BbAvatar/BbAvatar.vue_vue_type_script_setup_true_lang.js +33 -33
  172. package/dist/components/BbBadge/BadgeBodyContent.vue_vue_type_script_setup_true_lang.js +22 -16
  173. package/dist/components/BbBadge/BbBadge.vue_vue_type_script_setup_true_lang.js +36 -15
  174. package/dist/components/BbBadge/badgeButtonContext.d.ts +21 -8
  175. package/dist/components/BbBadge/types.d.ts +8 -2
  176. package/dist/components/BbBaseButton/BbBaseButton.vue_vue_type_script_setup_true_lang.js +47 -47
  177. package/dist/components/BbBaseButton/RouterComponent.vue_vue_type_script_setup_true_lang.js +14 -14
  178. package/dist/components/BbBaseCheckboxGroup/BbBaseCheckboxGroup.vue_vue_type_script_setup_true_lang.js +30 -28
  179. package/dist/components/BbBaseCheckboxGroup/types.d.ts +2 -2
  180. package/dist/components/BbBaseDatePicker/BbBaseDatePicker.vue_vue_type_script_setup_true_lang.js +167 -167
  181. package/dist/components/BbBaseDatePicker/BbBaseDatePickerInputDaySelector.vue.d.ts +9 -9
  182. package/dist/components/BbBaseDatePicker/BbBaseDatePickerInputDaySelector.vue_vue_type_script_setup_true_lang.js +78 -78
  183. package/dist/components/BbBaseDatePicker/BbBaseDatePickerMonthSelector.vue_vue_type_script_setup_true_lang.js +64 -64
  184. package/dist/components/BbBaseDatePickerInput/BbBaseDatePickerInput.vue.d.ts +1 -1
  185. package/dist/components/BbBaseDatePickerInput/BbBaseDatePickerInput.vue_vue_type_script_setup_true_lang.js +138 -138
  186. package/dist/components/BbBaseInputContainer/BbBaseInputContainer.vue_vue_type_script_setup_true_lang.js +2 -2
  187. package/dist/components/BbBaseRadioGroup/BbBaseRadioGroup.vue_vue_type_script_setup_true_lang.js +24 -22
  188. package/dist/components/BbBaseRating/BbBaseRating.vue_vue_type_script_setup_true_lang.js +40 -40
  189. package/dist/components/BbBaseSelect/BbBaseSelect.vue_vue_type_script_setup_true_lang.js +74 -65
  190. package/dist/components/BbBaseSelect/types.d.ts +22 -0
  191. package/dist/components/BbBaseSlider/BbBaseSlider.vue_vue_type_script_setup_true_lang.js +5 -5
  192. package/dist/components/BbBaseSwitchGroup/BbBaseSwitchGroup.vue_vue_type_script_setup_true_lang.js +30 -28
  193. package/dist/components/BbBaseTimePickerInput/BbBaseTimePickerInput.vue.d.ts +1 -1
  194. package/dist/components/BbBaseTimePickerInput/BbBaseTimePickerInput.vue_vue_type_script_setup_true_lang.js +104 -104
  195. package/dist/components/BbBreadcrumbs/BbBreadcrumbs.vue_vue_type_script_setup_true_lang.js +35 -35
  196. package/dist/components/BbButton/BbButton.vue_vue_type_script_setup_true_lang.js +85 -81
  197. package/dist/components/BbCheckboxGroup/types.d.ts +2 -1
  198. package/dist/components/BbCollapsible/types.d.ts +7 -1
  199. package/dist/components/BbColorInput/BbColorInput.vue.d.ts +1 -1
  200. package/dist/components/BbColorInput/BbColorInput.vue_vue_type_script_setup_true_lang.js +18 -18
  201. package/dist/components/BbColorPalette/BbColorPalette.vue.d.ts +2 -2
  202. package/dist/components/BbColorPalette/BbColorPalette.vue_vue_type_script_setup_true_lang.js +50 -50
  203. package/dist/components/BbConfirm/BbConfirm.vue_vue_type_script_setup_true_lang.js +17 -17
  204. package/dist/components/BbDatePicker/BbDatePicker.vue.d.ts +1 -1
  205. package/dist/components/BbDatePicker/BbDatePicker.vue_vue_type_script_setup_true_lang.js +100 -100
  206. package/dist/components/BbDatePickerInput/BbDatePickerInput.vue_vue_type_script_setup_true_lang.js +21 -21
  207. package/dist/components/BbDialog/BbDialog.vue.d.ts +1 -1
  208. package/dist/components/BbDialog/BbDialog.vue_vue_type_script_setup_true_lang.js +133 -107
  209. package/dist/components/BbDialog/types.d.ts +29 -2
  210. package/dist/components/BbDropdown/AdaptiveDropdown.vue.d.ts +2 -0
  211. package/dist/components/BbDropdown/AdaptiveDropdown.vue_vue_type_script_setup_true_lang.js +99 -100
  212. package/dist/components/BbDropdown/BbDropdown.vue.d.ts +2 -2
  213. package/dist/components/BbDropdown/BbDropdown.vue_vue_type_script_setup_true_lang.js +208 -201
  214. package/dist/components/BbDropdown/BbDropdownList.vue_vue_type_script_setup_true_lang.js +225 -208
  215. package/dist/components/BbDropdown/DropdownPipelineResolver.vue_vue_type_script_setup_true_lang.js +21 -21
  216. package/dist/components/BbDropdown/types.d.ts +20 -5
  217. package/dist/components/BbDropdownButton/BbDropdownButton.vue_vue_type_script_setup_true_lang.js +5 -5
  218. package/dist/components/BbIcon/BbIcon.vue_vue_type_script_setup_true_lang.js +46 -46
  219. package/dist/components/BbIcon/iconCache.d.ts +33 -0
  220. package/dist/components/BbIcon/iconCache.js +17 -0
  221. package/dist/components/BbNumberInput/BbNumberInput.vue.d.ts +1 -1
  222. package/dist/components/BbNumberInput/BbNumberInput.vue_vue_type_script_setup_true_lang.js +15 -15
  223. package/dist/components/BbOffCanvas/BbOffCanvas.vue.d.ts +1 -1
  224. package/dist/components/BbOffCanvas/BbOffCanvas.vue_vue_type_script_setup_true_lang.js +254 -208
  225. package/dist/components/BbOffCanvas/types.d.ts +30 -5
  226. package/dist/components/BbPagination/BbPagination.vue.d.ts +1 -1
  227. package/dist/components/BbPopover/BbPopover.vue.d.ts +2 -2
  228. package/dist/components/BbPopover/BbPopover.vue_vue_type_script_setup_true_lang.js +149 -146
  229. package/dist/components/BbPopover/types.d.ts +3 -2
  230. package/dist/components/BbRadioGroup/types.d.ts +3 -1
  231. package/dist/components/BbSelect/BbSelect.vue_vue_type_script_setup_true_lang.js +27 -23
  232. package/dist/components/BbSelect/types.d.ts +32 -0
  233. package/dist/components/BbSelectPopover/BbSelectPopover.vue_vue_type_script_setup_true_lang.js +221 -213
  234. package/dist/components/BbSlider/BbSlider.vue.d.ts +1 -1
  235. package/dist/components/BbSlider/BbSlider.vue_vue_type_script_setup_true_lang.js +20 -20
  236. package/dist/components/BbTable/BbTable.vue.d.ts +8 -4
  237. package/dist/components/BbTable/BbTable.vue_vue_type_script_setup_true_lang.js +1319 -616
  238. package/dist/components/BbTable/BbTableDataRow.d.ts +162 -0
  239. package/dist/components/BbTable/BbTableDataRow.js +136 -0
  240. package/dist/components/BbTable/BbTableExpandRow.d.ts +73 -0
  241. package/dist/components/BbTable/BbTableExpandRow.js +52 -0
  242. package/dist/components/BbTable/types.d.ts +247 -86
  243. package/dist/components/BbTable/utils.d.ts +87 -4
  244. package/dist/components/BbTable/utils.js +71 -36
  245. package/dist/components/BbTabs/types.d.ts +0 -8
  246. package/dist/components/BbTag/BbTag.vue.d.ts +1 -1
  247. package/dist/components/BbTag/BbTag.vue_vue_type_script_setup_true_lang.js +18 -18
  248. package/dist/components/BbTextInput/BbTextInput.vue.d.ts +1 -1
  249. package/dist/components/BbTextInput/BbTextInput.vue_vue_type_script_setup_true_lang.js +18 -18
  250. package/dist/components/BbTextarea/BbTextarea.vue.d.ts +1 -1
  251. package/dist/components/BbTextarea/BbTextarea.vue_vue_type_script_setup_true_lang.js +54 -54
  252. package/dist/components/BbTimePicker/BbTimePicker.vue.d.ts +1 -1
  253. package/dist/components/BbTimePicker/BbTimePicker.vue_vue_type_script_setup_true_lang.js +80 -80
  254. package/dist/components/BbTimePickerInput/BbTimePickerInput.vue_vue_type_script_setup_true_lang.js +61 -61
  255. package/dist/components/BbToast/BbToast.vue_vue_type_script_setup_true_lang.js +17 -17
  256. package/dist/components/BbToast/BbToastMessage.vue_vue_type_script_setup_true_lang.js +13 -13
  257. package/dist/components/BbTooltip/types.d.ts +5 -1
  258. package/dist/components/ChipsBox/ChipsBox.vue_vue_type_script_setup_true_lang.js +17 -17
  259. package/dist/components/ClearableButton/ClearableButton.vue_vue_type_script_setup_true_lang.js +5 -5
  260. package/dist/components/CommonPopover/CommonPopover.vue_vue_type_script_setup_true_lang.js +2 -1
  261. package/dist/components/FlatListBox/FlatListBox.vue_vue_type_script_setup_true_lang.js +83 -83
  262. package/dist/components/GroupedListBox/GroupedListBox.vue_vue_type_script_setup_true_lang.js +56 -56
  263. package/dist/composables/useBaseOptions.js +30 -20
  264. package/dist/composables/useBbConfig.d.ts +27 -6
  265. package/dist/composables/useBbConfig.js +8 -2
  266. package/dist/composables/useConfig.d.ts +59 -0
  267. package/dist/composables/useConfig.js +9 -0
  268. package/dist/composables/useItemsGetter.d.ts +11 -0
  269. package/dist/composables/useItemsGetter.js +44 -18
  270. package/dist/composables/useLocale.js +10 -7
  271. package/dist/composables/useMobile.js +5 -5
  272. package/dist/composables/usePrefill.d.ts +21 -2
  273. package/dist/composables/usePrefill.js +25 -19
  274. package/dist/composables/useSsrState.d.ts +20 -0
  275. package/dist/composables/useSsrState.js +13 -0
  276. package/dist/composables/useToast.js +5 -5
  277. package/dist/deprecation/ai-deprecations.json.d.ts +99 -11
  278. package/dist/deprecation/ai-deprecations.json.js +1 -1
  279. package/dist/directives/bbDropdown.d.ts +15 -4
  280. package/dist/directives/bbDropdown.js +31 -9
  281. package/dist/directives/createPopoverDirective.d.ts +62 -0
  282. package/dist/directives/createPopoverDirective.js +63 -23
  283. package/dist/i18n/locales/cs.json +4 -1
  284. package/dist/i18n/locales/da.json +4 -1
  285. package/dist/i18n/locales/de.json +4 -1
  286. package/dist/i18n/locales/el.json +4 -1
  287. package/dist/i18n/locales/en.json +4 -1
  288. package/dist/i18n/locales/es.json +4 -1
  289. package/dist/i18n/locales/fi.json +4 -1
  290. package/dist/i18n/locales/fr.json +4 -1
  291. package/dist/i18n/locales/hu.json +4 -1
  292. package/dist/i18n/locales/it.json +4 -1
  293. package/dist/i18n/locales/ja.json +4 -1
  294. package/dist/i18n/locales/ko.json +4 -1
  295. package/dist/i18n/locales/nb.json +4 -1
  296. package/dist/i18n/locales/nl.json +4 -1
  297. package/dist/i18n/locales/pl.json +4 -1
  298. package/dist/i18n/locales/pt.json +4 -1
  299. package/dist/i18n/locales/ro.json +4 -1
  300. package/dist/i18n/locales/ru.json +4 -1
  301. package/dist/i18n/locales/sv.json +4 -1
  302. package/dist/i18n/locales/tr.json +4 -1
  303. package/dist/i18n/locales/uk.json +4 -1
  304. package/dist/i18n/locales/zh-cn.json +4 -1
  305. package/dist/index.d.ts +7 -2
  306. package/dist/index.js +69 -67
  307. package/dist/llms-full.txt +3712 -1381
  308. package/dist/llms-medium.txt +113 -43
  309. package/dist/llms.txt +7 -6
  310. package/dist/locale-blueprint.json +4 -1
  311. package/dist/nuxt-auto-imports.d.ts +100 -0
  312. package/dist/nuxt-auto-imports.js +55 -0
  313. package/dist/nuxt-module.d.ts +22 -1
  314. package/dist/nuxt.js +22 -8
  315. package/dist/plugin.d.ts +10 -1
  316. package/dist/plugin.js +20 -19
  317. package/dist/runtime/nuxt-plugin.js +28 -21
  318. package/dist/styles.css +1 -1
  319. package/dist/utilities/misc/listboxMetrics.d.ts +3 -3
  320. package/dist/utilities/misc/listboxMetrics.js +2 -2
  321. package/dist/utilities/misc/ssrStateInjectionKey.d.ts +42 -0
  322. package/dist/utilities/misc/ssrStateInjectionKey.js +4 -0
  323. package/dist/validated/BbForm.vue_vue_type_script_setup_true_lang.js +45 -27
  324. package/dist/validated/BbSelect.vue.d.ts +2 -0
  325. package/dist/validated/BbSelect.vue_vue_type_script_setup_true_lang.js +1 -0
  326. package/dist/validated/useValidatedField.js +25 -23
  327. package/dist/vite.js +6 -2
  328. package/llms.txt +8 -7
  329. package/package.json +2 -2
  330. package/scripts/lib/eslint-plugin.d.ts +19 -1
  331. package/scripts/lib/eslint-plugin.mjs +146 -0
  332. package/scripts/lib/html-attributes.mjs +91 -0
  333. package/scripts/lib/validate-bb-markup.mjs +59 -4
  334. package/dist/assets/svgs/sort.svg_raw.js +0 -4
  335. package/dist/components/BbBadge/BadgeAppendIcon.vue.d.ts +0 -3
  336. package/dist/components/BbBadge/BadgeAppendIcon.vue.js +0 -5
  337. package/dist/components/BbBadge/BadgeAppendIcon.vue_vue_type_script_setup_true_lang.js +0 -22
@@ -25,18 +25,20 @@ keyboard navigation, so your markup stays a column definition plus a few slots.
25
25
  domain objects straight to `items` and set `item-value` for stable row
26
26
  identity — never pre-map rows into `{ label, value }` shapes.
27
27
 
28
- The table **reports** state (sort, selection, page, expansion, highlight)
29
- through v-models; it never reorders, slices, or fetches on its own. You apply
28
+ The table **reports** state (sort, column order, selection, page, expansion,
29
+ highlight) through v-models; it never sorts, slices, or fetches on its own. You apply
30
30
  that state to your data source (client-side or via `dependencies`-driven
31
31
  refetch). This keeps it equally at home in a client-only screen and a
32
32
  server-paginated one.
33
33
 
34
- **`BbTable` ships no client-side virtualization or windowing** — every row you
35
- hand it in `items` mounts to the DOM, always. A few hundred rows is fine for a
36
- client-only screen. Past roughly 1,000 rows, don't ship the whole dataset to
37
- the browser: page it — feed `BbTable` the current slice as `items` and pass
38
- `page` / `per-page` / `total-items` (see § Pagination) so the server, not the
39
- DOM, holds the rest.
34
+ **Every row in `items` mounts to the DOM unless you say `virtual`.** A few
35
+ hundred rows is fine for a client-only screen. Past roughly 1,000 rows,
36
+ either page it — feed `BbTable` the current slice as `items` and pass `page`
37
+ / `per-page` / `total-items` (see § Pagination) so the server, not the DOM,
38
+ holds the rest — or, when the whole dataset legitimately lives in the
39
+ browser (local filtering, a spreadsheet-shaped tool), add `virtual` and let
40
+ the table window its rows (see § Virtual rows). The two compose: a large
41
+ page can be windowed.
40
42
 
41
43
  ### Columns: the full grammar
42
44
 
@@ -55,13 +57,15 @@ needs is declared here, not in markup:
55
57
  `align` prop sets the default for the whole table (`left`).
56
58
  - **`width`** freezes the column: numbers and bare numeric strings are px
57
59
  (`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.
60
+ `'20%'`). A declared width is the column's grid track and holds exactly;
61
+ unfrozen columns size to their content and share any leftover space equally
62
+ (the table is a CSS grid — the tracks are the width API, there is no
63
+ `table-layout`). When _every_ column declares a width, the surplus is shared
64
+ in proportion to the declared widths so the table still fills its container.
61
65
  - **`skeleton`** picks the loading placeholder shape (see Loading below).
62
66
  - **`sortable` / `sorted`** — see Sorting.
63
67
  - **`thClass` / `tdClass` / `rowClass`** style the header cell, the data cells,
64
- and the whole `<tr>`. `tdClass` and `rowClass` may be functions
68
+ and the whole row. `tdClass` and `rowClass` may be functions
65
69
  `(content, key, item) => Classes` for state-dependent styling. Table-level
66
70
  `thClass` / `tdClass` / `rowClass` / `headerRowClass` apply everywhere; a
67
71
  row's classes accumulate the table `rowClass` plus every column's.
@@ -155,7 +159,7 @@ const invoices: Invoice[] = [
155
159
  },
156
160
  ];
157
161
 
158
- // `row-class` is applied to each `<tr>`; use it to flag rows by state. Soft amber
162
+ // `row-class` is applied to each row (`.bb-table-data__row`); use it to flag rows by state. Soft amber
159
163
  // wash matches the design-language attention accent — public API, no internals.
160
164
  const rowClass = (invoice: Invoice) =>
161
165
  invoice.status === 'overdue'
@@ -207,7 +211,11 @@ Override any column's body with a `#<key>` slot and its header with a
207
211
  characters turned into `_` (dashes are kept as-is), **lowercased**:
208
212
  `address.city` → `#address_city`, `first_name` → `#first_name`, `issued-at` →
209
213
  `#issued-at`, but also `issuedAt` → `#issuedat` — prefer snake_case keys so
210
- slot names read naturally. Keep the column in `columns`
214
+ slot names read naturally. Building slot names in code (columns from config, a
215
+ dynamic `<template>` loop)? Import the normalizer itself — `import { slotKey }
216
+ from 'bitboss-ui'` — instead of restating the rule: a hand copy that differs by
217
+ one character (`\W+` collapses dashes; the real rule keeps them) makes the slot
218
+ silently never match. Keep the column in `columns`
211
219
  even when you fully slot it: label, alignment and width still come from there.
212
220
 
213
221
  Body cell scope: `content` (the formatted value), `item` (raw row), `value`
@@ -219,6 +227,22 @@ one-way state; the `toggle*` callbacks are the write path.
219
227
  Header scope: `label`, `items` (the full list — handy for counts), `sortable`,
220
228
  `sortOrder`, `toggleSort`, `classes`.
221
229
 
230
+ **Add beside a header instead of replacing it** with
231
+ `#header:<key>:prepend` / `#header:<key>:append` — rendered before / after the
232
+ column's existing header content (the label + sort control, or your own
233
+ `#header:<key>` replacement), so a resize grip or reorder handle no longer
234
+ costs you the built-in sort button. The table-wide pair `#header:prepend` /
235
+ `#header:append` applies one template to every data column's header (never the
236
+ structural select/actions cells); both forms receive the header scope above
237
+ plus `columnKey` (the raw column key) so a table-wide template can branch per
238
+ column. When both a per-column and the table-wide slot are provided for the
239
+ same side, the per-column one wins for that column — they never stack. Affixes
240
+ render even beside a full `#header:<key>` replacement (the same rule as
241
+ BbDropdown's `#<key>:prepend/append`). One caveat: a column key that
242
+ normalizes to `prepend` or `append` collides with the table-wide slot names;
243
+ the table-wide interpretation wins, per-column header slots are unavailable
244
+ for that column, and dev builds warn.
245
+
222
246
  Below, the status badges carry brand glyphs shipped in the app's `iconDir`,
223
247
  referenced as `local:*` names on the badge's `prepend:icon`. Per the icons
224
248
  policy the dynamic status maps to **full-name literals** (a lookup object) —
@@ -365,7 +389,10 @@ Beyond per-column slots, the structural slots cover every other region:
365
389
  (replace the per-row checkbox/radio; scope: `item`, `value`, `checked`,
366
390
  `disabled`, `readonly`, `inputName`, `toggleSelected`), `#loading`, `#no-data`
367
391
  (both below), and `#thead` / `#tbody` / `#tfoot` to replace an entire region
368
- when you need full control (you then own its markup and semantics).
392
+ when you need full control (you then own its markup and semantics: the table
393
+ is a CSS grid, so pass `<div role="row">` rows holding `<div role="cell">` /
394
+ `<div role="columnheader">` cells — never `<tr>`/`<td>`; a spanning cell uses
395
+ `grid-column: span N` with `#tfoot`'s `columnCount`, see **Styling hooks**).
369
396
 
370
397
  ### Loading and empty states
371
398
 
@@ -396,8 +423,9 @@ it renders only when there is nothing to keep. (Scope: the current, possibly
396
423
  stale `items` — handy for matching the previous row count.) When `items` resolves
397
424
  empty the table shows its no-data row — localized default text, overridable via
398
425
  `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.
426
+ full-width empty-state cell (already centred, already spanning every column),
427
+ so pass the content — a message, a "clear filters" button — not a row of your
428
+ own.
401
429
 
402
430
  `loading-text` is the loading row's `aria-live` announcement. Like
403
431
  `no-data-text` it has a **localized default** (`common.loadingText` —
@@ -685,14 +713,21 @@ scriptable from anywhere via `useBbTableContext`
685
713
  ### Sorting
686
714
 
687
715
  Mark columns `sortable` and bind `v-model:sort` — an ordered
688
- `BbTableSortEntry[]` (`[columnKey, 'asc' | 'desc']`). Header buttons cycle a
689
- column unsorted → asc → desc → removed; a direction flip updates the entry in
716
+ `BbTableSortEntry[]` (`[columnKey, 'asc' | 'desc']`). A click anywhere on a
717
+ sortable header (its whitespace included — AG Grid parity; the built-in
718
+ button is the keyboard/AT control) cycles the column unsorted → asc → desc →
719
+ removed; a direction flip updates the entry in
690
720
  place, so the column keeps its priority in a multi-column sort and other
691
721
  entries are never touched. The array order **is** the priority: first entry
692
722
  sorts first. The table only reports the model; **apply it to your data**
693
723
  (client-side, as below) or forward it to the server as `orderBy` and refetch
694
724
  via `dependencies`.
695
725
 
726
+ The affordance is one arrow: nothing at rest, a faint arrow previewed while
727
+ the header is hovered or focused, solid once the column is sorted (up for
728
+ ascending, down for descending — the AG Grid convention), gone again on the
729
+ third click.
730
+
696
731
  **Sortable headers driving a client-side sort**
697
732
 
698
733
  ```vue
@@ -773,11 +808,225 @@ const sortedInvoices = computed(() => {
773
808
 
774
809
  A custom `#header:<key>` slot replaces the built-in sort button, but its scope
775
810
  hands you `sortOrder` and `toggleSort` so your header can keep sorting alive
776
- (render your own button and call `toggleSort`). The column-level `sorted`
811
+ (render your own button and call `toggleSort`). To merely add something next
812
+ to the control — a resize grip, a filter icon — use
813
+ `#header:<key>:prepend/append` instead and keep the built-in button (see
814
+ "Custom cells and headers"). The column-level `sorted`
777
815
  field only seeds the header's `aria-sort` when no live model entry exists — it
778
816
  does not move the visual indicator; to start pre-sorted, seed `v-model:sort`
779
817
  instead (as the example does).
780
818
 
819
+ ### Column order
820
+
821
+ `columns` declares what a column **is**; `v-model:order` is the order
822
+ it **renders** in — an array of column keys, the same split as `sortable` +
823
+ `v-model:sort`. Two things write it:
824
+
825
+ - **Your own control** — a column panel, a settings drawer, a persisted
826
+ preference. Bind the model and write the array; the table follows. No flag
827
+ needed for this.
828
+ - **The user, on the table** — with `reorderable`, every data header
829
+ is a grab surface: drag it and the column moves live under the pointer (AG
830
+ Grid style, a chip with the label follows), release to commit. The same
831
+ header row is also one keyboard tab stop (the AG Grid grammar): ←/→ move
832
+ focus between headers, **Shift+←/→** move the column one slot
833
+ (Shift+Home/End to the ends), Enter sorts a sortable header.
834
+ Each committed move writes the model once and is announced.
835
+
836
+ The model's contract is what makes a column panel trivial:
837
+
838
+ - Keys listed first render first; declared columns the array omits follow, in
839
+ declaration order (a column added to `columns` later renders last).
840
+ - Keys that match no declared column render nothing but are **preserved in
841
+ place**. So hide a column by filtering it out of `columns` — never out of
842
+ the order — and it comes back exactly where it was.
843
+ - A user move relocates **exactly one key**; everything else, hidden keys
844
+ included, keeps its relative order. The first move on a partial (or empty)
845
+ model emits the completed array — persist that.
846
+ - A drop that changes nothing emits nothing; unbound, the table keeps the
847
+ order as per-mount state like `sort`.
848
+
849
+ **A column panel and the header drag sharing one order**
850
+
851
+ ```vue
852
+ <template>
853
+ <div class="flex max-w-2xl flex-col gap-3">
854
+ <!-- ONE order, two writers: the panel's buttons and the table's own
855
+ header drag / keyboard handles both write `v-model:order`.
856
+ Hiding a column only filters `columns` — its key stays in the order,
857
+ so it comes back where it was. -->
858
+ <ul
859
+ aria-label="Columns"
860
+ class="flex flex-wrap gap-2 text-xs text-[color:var(--bb-text-muted)]"
861
+ >
862
+ <li
863
+ v-for="(column, index) in panelColumns"
864
+ :key="column.key"
865
+ class="flex items-center gap-1 rounded-[var(--bb-radius-sm)] border border-[color:var(--bb-border)] py-0.5 pl-2 pr-1"
866
+ >
867
+ <BbCheckbox
868
+ :label="column.label"
869
+ :model-value="!hidden.has(column.key)"
870
+ @update:model-value="toggleVisible(column.key)"
871
+ />
872
+ <BbButton
873
+ :aria-label="`Move ${column.label} left`"
874
+ :disabled="index === 0"
875
+ icon="lucide:chevron-left"
876
+ size="xs"
877
+ variant="ghost"
878
+ @click="move(column.key, -1)"
879
+ />
880
+ <BbButton
881
+ :aria-label="`Move ${column.label} right`"
882
+ :disabled="index === panelColumns.length - 1"
883
+ icon="lucide:chevron-right"
884
+ size="xs"
885
+ variant="ghost"
886
+ @click="move(column.key, 1)"
887
+ />
888
+ </li>
889
+ </ul>
890
+ <div
891
+ class="overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
892
+ >
893
+ <BbTable
894
+ v-model:order="order"
895
+ caption="Invoices"
896
+ :columns="visibleColumns"
897
+ compact
898
+ item-value="id"
899
+ :items="invoices"
900
+ reorderable
901
+ />
902
+ </div>
903
+ <p class="text-xs text-[color:var(--bb-text-muted)]">
904
+ Order model: <code>{{ JSON.stringify(order) }}</code>
905
+ </p>
906
+ </div>
907
+ </template>
908
+ <script setup lang="ts">
909
+ import { computed, ref } from 'vue';
910
+ import { BbButton, BbCheckbox, BbTable } from 'bitboss-ui';
911
+ import type { BbTableColumn } from 'bitboss-ui';
912
+
913
+ type Invoice = {
914
+ id: string;
915
+ number: string;
916
+ client: string;
917
+ issued: string;
918
+ amount: number;
919
+ };
920
+
921
+ const euro = new Intl.NumberFormat('en-IE', {
922
+ style: 'currency',
923
+ currency: 'EUR',
924
+ });
925
+
926
+ // The declaration: what a column IS. Never reordered.
927
+ const columns: Array<BbTableColumn<Invoice>> = [
928
+ { key: 'number', label: 'Invoice', width: 110 },
929
+ { key: 'client', label: 'Client' },
930
+ { key: 'issued', label: 'Issued', width: 110 },
931
+ {
932
+ key: 'amount',
933
+ label: 'Amount',
934
+ align: 'right',
935
+ width: 110,
936
+ formatter: euro.format,
937
+ },
938
+ ];
939
+
940
+ const invoices: Invoice[] = [
941
+ {
942
+ id: '1',
943
+ number: 'INV-1041',
944
+ client: 'Northwind Traders',
945
+ issued: '2026-08-02',
946
+ amount: 4200,
947
+ },
948
+ {
949
+ id: '2',
950
+ number: 'INV-1042',
951
+ client: 'Globex',
952
+ issued: '2026-08-05',
953
+ amount: 980,
954
+ },
955
+ {
956
+ id: '3',
957
+ number: 'INV-1043',
958
+ client: 'Initech',
959
+ issued: '2026-08-11',
960
+ amount: 15600,
961
+ },
962
+ {
963
+ id: '4',
964
+ number: 'INV-1044',
965
+ client: 'Umbrella',
966
+ issued: '2026-08-19',
967
+ amount: 2750,
968
+ },
969
+ ];
970
+
971
+ // The state: render order (keys). Persist THIS. Seed it with the full key
972
+ // list — it also completes itself on the first move.
973
+ const order = ref<string[]>(columns.map((column) => column.key));
974
+ const hidden = ref(new Set<string>());
975
+
976
+ // Visibility is just filtering the declaration; the hidden key keeps its
977
+ // slot in `order`.
978
+ const visibleColumns = computed(() =>
979
+ columns.filter((column) => !hidden.value.has(column.key))
980
+ );
981
+
982
+ // The panel lists every declared column in the order the model has them.
983
+ const panelColumns = computed(() =>
984
+ [...columns].sort(
985
+ (a, b) => order.value.indexOf(a.key) - order.value.indexOf(b.key)
986
+ )
987
+ );
988
+
989
+ const toggleVisible = (key: string) => {
990
+ const next = new Set(hidden.value);
991
+ if (next.has(key)) next.delete(key);
992
+ else next.add(key);
993
+ hidden.value = next;
994
+ };
995
+
996
+ const move = (key: string, delta: 1 | -1) => {
997
+ const next = [...order.value];
998
+ const from = next.indexOf(key);
999
+ const to = from + delta;
1000
+ if (from < 0 || to < 0 || to >= next.length) return;
1001
+ next.splice(from, 1);
1002
+ next.splice(to, 0, key);
1003
+ order.value = next;
1004
+ };
1005
+ </script>
1006
+ ```
1007
+
1008
+ **Pinned slots are positions, not columns.** `fixed-columns` indexes rendered
1009
+ slots like a spreadsheet's freeze panes (a selection column is slot 0). With
1010
+ column order the slot holds: whichever column lands in it is pinned, and
1011
+ dragging the identity column out of its pinned slot (slot 1 when
1012
+ `selectable`, slot 0 otherwise) unpins it (AG Grid pins columns; BbTable
1013
+ pins positions). Keep the reorder + pin combination for slots that
1014
+ never move — `select`, `actions`, or a first column you also lock by
1015
+ normalizing the model in your `@update:order` handler.
1016
+
1017
+ **Nested tables.** An inheriting child aligns to the parent's rails by
1018
+ **position**, so a reordered parent re-pairs the child's columns unless the
1019
+ child follows. Give both tables the same `v-model:order` when they
1020
+ share keys (and the child declares no `snap`) and they stay paired by key.
1021
+ `snap` points are positional in the parent's rendered order: reorder the
1022
+ parent and a snap-mapped child follows its slots, but never reorder (or share
1023
+ the order with) the snap-mapped child itself — dev builds warn.
1024
+
1025
+ Apply the model synchronously in your handler and persist afterwards: an
1026
+ `await` before the write moves the cell in a later flush, after the table
1027
+ has re-focused the handle, and keyboard focus is usually lost (whether it
1028
+ survives depends on which cell Vue's keyed patch detaches).
1029
+
781
1030
  ### Selection and bulk actions
782
1031
 
783
1032
  Add `selectable` for multi-select (`multiple` is the default) bound with
@@ -1277,12 +1526,109 @@ fixed first track).
1277
1526
  The actions-column toggle remains valid — prefer it when expansion reveals
1278
1527
  _detail about the row_ rather than _children of it_.
1279
1528
 
1529
+ ### Column resize
1530
+
1531
+ `resizable` puts a handle on the trailing edge of every data header:
1532
+ drag it and the column follows live, release to commit; double-click it to go
1533
+ back to the declared width. From the keyboard, **Alt+←/→** resizes the
1534
+ focused header in 10px steps (the header row is a tab stop whenever columns
1535
+ are resizable or reorderable).
1536
+
1537
+ There is no width model on purpose. `width` on the column definition stays
1538
+ the single declared truth, and a user resize is an **override** the table
1539
+ keeps until that column's `width` changes:
1540
+
1541
+ - Each commit fires `@resize:column="(key, width) => …"` **once** with the new width
1542
+ in px — or `null` after a double-click reset.
1543
+ - To persist, write the reported width back into your column definition (a
1544
+ `computed` over your persisted widths). The definition wins whenever it
1545
+ changes, so a programmatic `width` change always takes effect.
1546
+ - Unbound, the override lives for the mount, like an unbound `sort`.
1547
+ - A resized width is always px; `width` still accepts `'20%'` / `'12rem'`
1548
+ for what you declare yourself.
1549
+
1550
+ Headers never wrap. A column you have not sized is never narrower than its
1551
+ own heading — the label is the table's size hint, so an empty or slim column
1552
+ with a long heading still reads on a narrow screen (the table overflows
1553
+ instead). Once a track is a length (a declared `width`, a user resize) the
1554
+ label cannot widen it: squeezed, it ellipsizes and the sort arrow stays; at
1555
+ the narrowest only the arrow (and the handle) survive, and the column is
1556
+ still resizable. The data cells of a resized column clip with an ellipsis
1557
+ instead of spilling into the neighbour (a squeezed track cannot wrap a single
1558
+ word); every other cell keeps wrapping. While a resize runs the whole page
1559
+ shows the resize cursor.
1560
+
1561
+ A resized column becomes an **exact** track; the other columns keep doing
1562
+ what they did — unfrozen ones share the surplus, and a table whose columns
1563
+ are all declared with a `width` keeps sharing it among the un-resized ones —
1564
+ so a filling table stays filled and nothing collapses. **Shift+drag** moves
1565
+ the border instead: the right neighbour absorbs the delta, the total stays
1566
+ put, and both columns are reported. A table that inherits its widths from a
1567
+ parent (`inherit-column-widths`) renders no handles — its tracks are the
1568
+ parent's.
1569
+
1570
+ **Resizable columns reporting widths back into the definition**
1571
+
1572
+ ```vue
1573
+ <template>
1574
+ <div class="flex max-w-2xl flex-col gap-1.5">
1575
+ <div
1576
+ class="overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
1577
+ >
1578
+ <BbTable
1579
+ caption="Invoices"
1580
+ :columns="columns"
1581
+ compact
1582
+ item-value="id"
1583
+ :items="invoices"
1584
+ resizable
1585
+ @resize:column="onResize"
1586
+ />
1587
+ </div>
1588
+ <p class="text-xs text-[color:var(--bb-text-muted)]">
1589
+ Widths: <code>{{ JSON.stringify(widths) }}</code>
1590
+ </p>
1591
+ </div>
1592
+ </template>
1593
+ <script setup lang="ts">
1594
+ import { computed, ref } from 'vue';
1595
+ import { BbTable } from 'bitboss-ui';
1596
+ import type { BbTableColumn } from 'bitboss-ui';
1597
+
1598
+ type Invoice = { id: string; number: string; client: string; amount: number };
1599
+
1600
+ const base: Array<BbTableColumn<Invoice>> = [
1601
+ { key: 'number', label: 'Invoice' },
1602
+ { key: 'client', label: 'Client' },
1603
+ { key: 'amount', label: 'Amount', align: 'right' },
1604
+ ];
1605
+ // Persist THIS (localStorage, a user preference…): key → px.
1606
+ const widths = ref<Record<string, number>>({});
1607
+ // The definition carries the persisted widths; the table treats them as the
1608
+ // declared truth, so nothing jumps after a reload.
1609
+ const columns = computed(() =>
1610
+ base.map((column) => ({ ...column, width: widths.value[column.key] }))
1611
+ );
1612
+ const onResize = (key: string, width: number | null) => {
1613
+ const next = { ...widths.value };
1614
+ if (width === null) delete next[key];
1615
+ else next[key] = width;
1616
+ widths.value = next;
1617
+ };
1618
+ const invoices: Invoice[] = [
1619
+ { id: '1', number: 'INV-1041', client: 'Northwind Traders', amount: 4200 },
1620
+ { id: '2', number: 'INV-1042', client: 'Globex', amount: 980 },
1621
+ { id: '3', number: 'INV-1043', client: 'Initech', amount: 15600 },
1622
+ ];
1623
+ </script>
1624
+ ```
1625
+
1280
1626
  ### Nested tables that align with the parent
1281
1627
 
1282
1628
  A `BbTable` rendered inside another's `#expand` slot is independent by
1283
1629
  default. Add `inherit-column-widths` and it snaps its columns to the parent's
1284
1630
  tracks, so a line-items breakdown reads as a continuation of the invoice row
1285
- above it (this also forces fixed layout). The mechanics:
1631
+ above it. The mechanics:
1286
1632
 
1287
1633
  - `inherit-column-widths` — `true` inherits from the nearest ancestor table;
1288
1634
  a string targets a specific ancestor's `id` (useful when an intermediate
@@ -1293,23 +1639,39 @@ above it (this also forces fixed layout). The mechanics:
1293
1639
  allowed; `end: -1` runs to the end of the data region). Structural columns
1294
1640
  (`select`, actions) are accounted for automatically.
1295
1641
  - 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).
1642
+ overflows into the parent's scroll (see below); with **fewer**, the leading
1643
+ columns stay 1:1, the actions column inherits the parent's actions width,
1644
+ and the last content column runs across every remaining parent track so
1645
+ both tables end flush (a right-aligned total lands under the parent's last
1646
+ column). Give that last column a `width` or an explicit `[start, end]`
1647
+ snap only if you want it to stop short — the leftover then joins the
1648
+ actions region instead.
1649
+ - **An inheriting child does not scroll on its own — the parent's scrollport
1650
+ is the only one.** That is what keeps the two grids locked together: a child
1651
+ with its own scroll position would fall out of alignment the moment either
1652
+ one moved, which is the whole thing `inherit-column-widths` exists to
1653
+ prevent. One scrollbar moves both.
1654
+ - **The parent's `selectable`/`actions` columns are handled for you** — the
1655
+ child's content columns map to the parent's content columns 1:1 (no `snap`
1656
+ needed for that), and the parent's select/actions _regions_ are reserved on
1657
+ each side: a child with its own select or actions column widens that cell
1658
+ to the parent's (two equal cells, controls at the row's end, so the ⌃ sits
1659
+ under the parent's); a child without one folds the region into its first/
1660
+ last cell as _padding_, so its content stays flush with the parent's data
1661
+ grid on both edges (the inset reads as hierarchy). Only a child cell
1662
+ _wider_ than the parent's — a select the parent lacks, more buttons — moves
1663
+ anything, and only the adjacent content column: it shrinks by the surplus
1664
+ so every other rail holds. What a child publishes to its own children is
1665
+ the same contract (content rails + regions), so a three-level nest aligns
1666
+ at every depth.
1308
1667
  - **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.
1668
+ bare `:deep([role='cell']:first-child)` also matches the nested table's
1669
+ cells and clobbers the offset padding it computes to stay aligned — use the
1670
+ direct-child chain through the grid's containers
1671
+ (`:deep(> .bb-table__table > .bb-table__body > [role='row'] > :first-child)`)
1672
+ so the child, which lives inside `.bb-table-expand__cell`, keeps its
1673
+ structural padding. The canonical `.table-flush` idiom is under **Styling
1674
+ hooks**.
1313
1675
 
1314
1676
  **Invoice lines aligned under the invoice columns**
1315
1677
 
@@ -1419,6 +1781,47 @@ const expanded = ref<string[]>([]);
1419
1781
  </script>
1420
1782
  ```
1421
1783
 
1784
+ #### Affixed actions on both tables
1785
+
1786
+ A nested table can carry its own `fixed-columns` — child rows usually need
1787
+ their own row actions — and both action columns pin to the same visible edge:
1788
+
1789
+ ```vue
1790
+ <BbTable
1791
+ id="programmes"
1792
+ actions
1793
+ :columns="columns"
1794
+ :fixed-columns="[{ index: columns.length, position: 'right' }]"
1795
+ :items="rows"
1796
+ item-value="id"
1797
+ >
1798
+ <template #expand="{ item }">
1799
+ <BbTable
1800
+ actions
1801
+ :columns="childColumns"
1802
+ :fixed-columns="[{ index: childColumns.length, position: 'right' }]"
1803
+ inherit-column-widths="programmes"
1804
+ :items="item.children"
1805
+ item-value="id"
1806
+ />
1807
+ </template>
1808
+ </BbTable>
1809
+ ```
1810
+
1811
+ This works because the child shares the parent's scrollport. `position: sticky`
1812
+ resolves against the nearest scrolling ancestor, and the child's own box is as
1813
+ wide as the parent's **content** — so if it opened a scrollport of its own, the
1814
+ child's actions would affix to an edge sitting out in the overflow, hundreds of
1815
+ pixels past what anyone can see, while the parent's pinned correctly. Snap the
1816
+ child's actions column onto the parent's actions track (see the snapping rule
1817
+ above) so the two columns line up rather than merely both being pinned.
1818
+
1819
+ A pinned cell is opaque — it paints the table's `--bg` surface because it
1820
+ slides over its siblings — so a nested table sitting on a tinted expand row
1821
+ (`--bb-muted` is the usual choice) shows white pinned actions unless you tell
1822
+ it which surface it is on: `.nest .bb-table { --bg: var(--bb-muted); }`. Same
1823
+ rule as any table on a non-panel surface (see **Styling hooks**).
1824
+
1422
1825
  #### Flush tables + inherited widths: mind the edge padding
1423
1826
 
1424
1827
  `table-flush` removes the outer cell padding so the table reads edge-to-edge in
@@ -1432,9 +1835,10 @@ Two rules keep them honest:
1432
1835
  1. **Snap against the same base you paint.** Measure the parent's columns with
1433
1836
  the same flush/padded state the child renders in — never measure padded and
1434
1837
  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.
1838
+ 2. **Never add nest padding on the body only.** Padding the child's body
1839
+ (`.bb-table__body`) but not its header (or vice versa) reintroduces exactly
1840
+ the offset the snap just removed. Put the inset on the nest container, where
1841
+ both row groups inherit it.
1438
1842
 
1439
1843
  ### Row highlight (master–detail)
1440
1844
 
@@ -1447,11 +1851,42 @@ clicks are ignored on purpose: those inside interactive elements (links,
1447
1851
  buttons, inputs, labels keep their own semantics) and those ending a text
1448
1852
  selection (dragging to copy a cell must not mutate state).
1449
1853
 
1854
+ **Highlight is opt-in, and the model's value is the switch.** Bind nothing and
1855
+ the mechanic is entirely off — a row click emits `click:row` and does nothing
1856
+ else: no class, no `aria-current`, no state. That matters because the table has
1857
+ nowhere to put a highlight it invents; a table that highlighted itself would
1858
+ drive a cursor the app can neither read nor clear.
1859
+
1860
+ The switch reads the value, not the binding: **`undefined` means "never
1861
+ initialised" and keeps highlight off; `null` means "initialised and currently
1862
+ empty" and turns it on.** So initialise the model:
1863
+
1864
+ ```ts
1865
+ const highlighted = ref(null); // ✅ connected
1866
+ const highlighted = ref(); // ❌ undefined — highlight stays off (warns in dev)
1867
+ ```
1868
+
1869
+ A value seeded through the shared context counts exactly the same, and binds
1870
+ two ways just like the prop:
1871
+
1872
+ ```ts
1873
+ const highlighted = ref(null);
1874
+ useBbTableContext('invoices-table', { highlighted });
1875
+ ```
1876
+
1877
+ `click:row` is never gated — it fires on every row click whether or not
1878
+ highlight is connected, so listening for clicks costs you nothing. Calling
1879
+ `toggleHighlighted` from a cell slot on an unconnected table does nothing and
1880
+ warns once in dev.
1881
+
1450
1882
  Highlight ships with a **default cursor**: the row gets `aria-current="true"`
1451
1883
  and the `bb-table-data__row--highlighted` class, and the shipped stylesheet
1452
- paints that class with a subtle primary-tinted fill plus a left accent bar —
1453
- distinct from hover and from a plain focus ring, and readable in both light
1454
- and dark. Screen-reader users get the `aria-current` announcement either way;
1884
+ paints that class with a quiet neutral fill plus a left accent bar in your
1885
+ brand primary — distinct from hover and from a plain focus ring, and readable
1886
+ in both light and dark. The fill is `--bb-muted` (the shared "selected/active"
1887
+ stop on the neutral surface ladder, one step above the `--bb-surface-hover`
1888
+ used for row hover) and steps to `--bb-pressed` while hovered, so it never
1889
+ depends on what your `--bb-primary` happens to be — only the accent bar does. Screen-reader users get the `aria-current` announcement either way;
1455
1890
  sighted users now get a visible cursor out of the box too, including once
1456
1891
  `keyboard-navigation` is on and the roving tabindex needs to be seen. Override
1457
1892
  it with `row-class` (or equivalent visible styling) whenever the default
@@ -1636,9 +2071,11 @@ const rowClass = (event: AuditEvent) =>
1636
2071
  ```
1637
2072
 
1638
2073
  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 —
2074
+ `(event, item)` — the native event and **your record exactly as you passed it
2075
+ in `items`**, nothing else. No wrapper to unpack, no third argument; selection
2076
+ state is already yours through the `v-model`, and the row's value resolves
2077
+ from the record through `item-value` exactly as the models do. Reserve these
2078
+ events for side effects that are not navigation —
1642
2079
  logging, opening a drawer (though `v-model:highlighted` usually covers that
1643
2080
  without any handler).
1644
2081
 
@@ -2005,8 +2442,14 @@ layout. `fixed-columns` pins columns while the rest scroll horizontally: an
2005
2442
  array of column indices (left-pinned) or `{ index, position: 'left'|'right' }`
2006
2443
  objects; indices count rendered columns (a selection column is index 0), and
2007
2444
  an out-of-range index logs a console error and is skipped. When content can't
2008
- fit, the table keeps every column at its measured minimum and overflows its
2009
- container instead of clipping — this is by design, not a bug to work around.
2445
+ fit, the table keeps every column at its measured minimum — its header label
2446
+ included — and overflows its container instead of clipping; this is by
2447
+ design, not a bug to work around. Note that truncating cells yourself
2448
+ (`overflow: hidden` + `text-overflow: ellipsis` via `tdClass`) removes that
2449
+ column's content floor — only the label floors it then, and the grid pads
2450
+ every column equally with whatever is left — so declare a `width` on the
2451
+ columns you truncate, or truncate with `white-space: nowrap` alone and let
2452
+ the table scroll.
2010
2453
 
2011
2454
  **The consumer overflow contract has two parts.** Wrap the table in an
2012
2455
  `overflow-x: auto` element (below), **and** make sure that wrapper is actually
@@ -2049,6 +2492,107 @@ your wrapper actually sits on before sprinkling `min-width: 0` everywhere.)
2049
2492
  </div>
2050
2493
  ```
2051
2494
 
2495
+ ### Virtual rows
2496
+
2497
+ `virtual` windows the body: only the rows in the vertical scrollport (plus a
2498
+ few on each side) are mounted, and two spacer rows hold the scrollport at the
2499
+ full list height, so 5,000 or 50,000 rows scroll at the cost of a few dozen.
2500
+ Everything else is the same table — the rows stay in flow in the grid, so the
2501
+ stuck header, `fixed-columns`, nested tables, selection, highlight and the
2502
+ keyboard grammar work untouched. One prop, no knobs.
2503
+
2504
+ - **Give it a scroller.** Bound the table's height (`max-height` / `height`
2505
+ on the table, or a flex item with `min-height: 0`) and the table itself
2506
+ scrolls its rows; left unbounded it grows with the page and the page (or
2507
+ the nearest scrolling ancestor) is the scroller. `virtual` implies a stuck
2508
+ header — a windowed body without column names is a scroller, not a table.
2509
+ - **Columns are sized once.** A windowed body only ever holds a few rows, so
2510
+ `auto` tracks would move as rows come and go. The first rendered batch is
2511
+ measured and its widths become floors (`minmax(px, 1fr)`): they still share
2512
+ surplus, later rows never widen them — and the cells of those columns are
2513
+ single-line and **clip with an ellipsis** (like a resized column) so a
2514
+ longer value that shows up later never spills into its neighbour. Want a
2515
+ column to wrap (prose, notes)? Declare a `width` on it: a declared width
2516
+ wins as always and keeps wrapping. A resize overrides as always, and an
2517
+ empty→non-empty transition (a refetch) re-measures from the new first
2518
+ batch.
2519
+ - **A fast wheel never shows blank space.** A band of skeleton rows is kept
2520
+ rendered beyond the window on both sides (cheap: empty cells at the
2521
+ measured row height), so a wheel that the compositor scrolls ahead of the
2522
+ main thread reveals skeletons, not blank spacer, and they turn into real
2523
+ rows as the window reaches them. While the scroll is fast, rows never
2524
+ mounted before stay skeletons and are swapped for real rows as soon as
2525
+ it slows; rows you have already seen stay real. A moderate scroll never
2526
+ sees a skeleton.
2527
+ - **Dynamic heights are fine.** Every mounted row is measured (wrapping
2528
+ cells, a "show more", an expand row that opens a nested table three levels
2529
+ deep), measurements are cached by row so a row seen once stays right, and a
2530
+ height change _above_ the viewport moves the scroll offset with it so what
2531
+ you are reading does not jump.
2532
+ - **Focus is never lost to an unmount.** The row (or expand row) that owns
2533
+ focus stays mounted however far it scrolls — a control inside a nested
2534
+ table included — and is released on blur. Arrow keys, Home/End and
2535
+ PageUp/PageDown work across the whole list (the target row is scrolled in,
2536
+ mounted, then focused).
2537
+ - **Nested tables are not windowed.** A nested table inside an expand row is
2538
+ one tall row to the virtualizer; give a huge nested dataset its own page.
2539
+ `virtual` is ignored on a table that inherits its widths, and under a
2540
+ `#tbody` slot.
2541
+ - **Rows keep their absolute position for AT** (`aria-rowcount` /
2542
+ `aria-rowindex`), page or no page — a virtual table is one page unless you
2543
+ say otherwise.
2544
+
2545
+ **5,000 rows in a 320px scroller**
2546
+
2547
+ ```vue
2548
+ <template>
2549
+ <!-- Bounding the table's height is what makes it the scroller of its own
2550
+ rows; `virtual` windows them and implies the stuck header. -->
2551
+ <div
2552
+ class="max-w-2xl overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
2553
+ >
2554
+ <BbTable
2555
+ caption="Ledger"
2556
+ class="max-h-80"
2557
+ :columns="columns"
2558
+ compact
2559
+ :fixed-columns="[0]"
2560
+ item-value="id"
2561
+ :items="rows"
2562
+ virtual
2563
+ />
2564
+ </div>
2565
+ </template>
2566
+ <script setup lang="ts">
2567
+ import { BbTable } from 'bitboss-ui';
2568
+ import type { BbTableColumn } from 'bitboss-ui';
2569
+
2570
+ type Entry = {
2571
+ id: number;
2572
+ number: string;
2573
+ client: string;
2574
+ issued: string;
2575
+ amount: string;
2576
+ };
2577
+
2578
+ const columns: Array<BbTableColumn<Entry>> = [
2579
+ { key: 'number', label: 'Invoice' },
2580
+ { key: 'client', label: 'Client' },
2581
+ { key: 'issued', label: 'Issued' },
2582
+ { key: 'amount', label: 'Amount', align: 'right' },
2583
+ ];
2584
+
2585
+ const clients = ['Northwind Traders', 'Globex', 'Initech', 'Umbrella', 'Hooli'];
2586
+ const rows: Entry[] = Array.from({ length: 5000 }, (_, i) => ({
2587
+ id: i,
2588
+ number: `INV-${10000 + i}`,
2589
+ client: clients[i % clients.length],
2590
+ issued: `2026-${String(1 + (i % 12)).padStart(2, '0')}-${String(1 + (i % 28)).padStart(2, '0')}`,
2591
+ amount: `€${((i * 37) % 9000) + 100}`,
2592
+ }));
2593
+ </script>
2594
+ ```
2595
+
2052
2596
  ### Narrow screens: hide columns, don't scroll everything
2053
2597
 
2054
2598
  An ops table with ten columns is unusable on a phone as a horizontal scroller —
@@ -2057,10 +2601,14 @@ leave the screen. Collapse instead of scroll:
2057
2601
 
2058
2602
  - **Rank the columns.** Identity + status + the one number that matters stay;
2059
2603
  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.
2604
+ - **Drop the secondary ones from `columns` at the breakpoint** — a `computed`
2605
+ over `useMobile()` (or your own `matchMedia`) that filters the secondary keys
2606
+ out. Removing the column removes its grid track. Do **not** hide cells with
2607
+ `thClass`/`tdClass` + `display: none`: the table is a grid, a hidden cell
2608
+ leaves its track in place and every following cell shifts one track over.
2609
+ A dropped key keeps its slot in `order` (the write-back never drops
2610
+ unknown keys), so the column returns where it was — never filter the
2611
+ order array.
2064
2612
  - **Stack what survives into the primary cell.** The identity slot renders the
2065
2613
  name plus the hidden facts under it, at `--bb-text-muted`, so nothing is
2066
2614
  actually lost.
@@ -2069,6 +2617,132 @@ Keep the sticky/pinned identity column for the case where the full grid really
2069
2617
  must stay reachable (a spreadsheet-shaped tool), not as the default for a
2070
2618
  records list.
2071
2619
 
2620
+ ### Styling hooks
2621
+
2622
+ **The table's surface is `--bg`** (default `var(--bb-panel)`), a local on
2623
+ `.bb-table`. Cells are transparent at rest so the row can tint them, but
2624
+ anything that slides over scrolled content — the stuck header band with
2625
+ `fixed-headers`, and `fixed-columns` cells — is opaque and paints `--bg` (a
2626
+ pinned cell also picks up its row's hover/highlight tint, so it never reads
2627
+ as a hole). When a table sits on a surface that is not the panel colour, set
2628
+ the one knob and everything pinned follows:
2629
+
2630
+ ```css
2631
+ .my-card .bb-table {
2632
+ --bg: var(--card-bg);
2633
+ }
2634
+ ```
2635
+
2636
+ The table is a **CSS grid of `<div>`s with ARIA roles, not a `<table>`**: the
2637
+ root `.bb-table__table[role=table]` owns the column tracks and every row group
2638
+ and row is a subgrid of it. Style it through the classes and the roles — never
2639
+ through element names, which match nothing:
2640
+
2641
+ | Region | Class | Role |
2642
+ | -------------- | -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
2643
+ | grid root | `.bb-table__table` | `table` |
2644
+ | header group | `.bb-table__head` | `rowgroup` |
2645
+ | header row | `.bb-table-header-row` | `row` |
2646
+ | header cell | `.bb-table-header` (+ `--select`, `--actions`) | `columnheader` |
2647
+ | body group | `.bb-table__body` | `rowgroup` |
2648
+ | data row | `.bb-table-data__row` (+ `--highlighted`) | `row` |
2649
+ | data cell | `.bb-table-data__cell` (+ `--select`) | `cell` |
2650
+ | actions cell | `.bb-table__cell--actions` | `cell` |
2651
+ | expand row | `.bb-table-expand__row` > `.bb-table-expand__cell` | `row` > `cell` |
2652
+ | empty state | `.bb-table-no-data__row` > `.bb-table-no-data__cell` | `row` > `cell` |
2653
+ | footer group | `.bb-table__foot` (`#tfoot` content) | `rowgroup` only when `#tfoot` is provided |
2654
+ | focused header | `.bb-table-header[data-column-key]:focus-visible` (`reorderable` only; inset ring, the header row is one roving tab stop) | `columnheader` |
2655
+ | dragged column | `.bb-table-header--dragging` on the header; `.bb-table__head--dragging` on the head group during a drag | — |
2656
+ | clipped header | `.bb-table-header--clip` (the track is a length or an equal split, so the label ellipsizes instead of flooring the column) | `columnheader` |
2657
+ | virtual table | `.bb-table--virtual` on the container (`overflow: auto`, the scroller once its height is bounded) | — |
2658
+ | spacer row | `.bb-table-spacer__row` (a `virtual` body's stand-in for unmounted rows, `aria-hidden`; never style its height) | — (`aria-hidden`) |
2659
+ | skeleton row | `.bb-table-placeholder__row` > `.bb-table-placeholder__cell` (a `virtual` body's skeleton band and fast-scroll stand-in) | `row` > `cell` (`aria-hidden`) |
2660
+ | drag chip | `.bb-table-column-ghost` (a `popover="manual"` in the top layer; paints `--bb-panel`, not the table's `--bg`) | — (`aria-hidden`) |
2661
+ | caption | `.bb-table-caption` (before the root, `aria-labelledby`) | — |
2662
+
2663
+ `[role='row']`, `[role='cell']` and `[role='columnheader']` are supported
2664
+ hooks, not incidental markup — use them when you mean "every row kind" or
2665
+ "every cell"; use the classes when you mean one kind. Cells are direct
2666
+ children of their row, so `:first-child` / `:last-child` / `:nth-child(n)`
2667
+ address columns.
2668
+
2669
+ The full-bleed idiom every workspace page reaches for — the table bleeds past
2670
+ the content padding and hands the inset back to its outermost cells — is
2671
+ written with a **direct-child chain** so a nested table inheriting the
2672
+ parent's tracks (it lives inside `.bb-table-expand__cell`) is never touched:
2673
+
2674
+ ```css
2675
+ .table-flush {
2676
+ --page-inset: 1rem;
2677
+ margin-inline: calc(var(--page-inset) * -1);
2678
+ width: calc(100% + var(--page-inset) * 2);
2679
+ }
2680
+
2681
+ .table-flush
2682
+ :deep(> .bb-table__table > .bb-table__head > [role='row'] > :first-child),
2683
+ .table-flush
2684
+ :deep(
2685
+ > .bb-table__table
2686
+ > .bb-table__body
2687
+ > [role='row']
2688
+ > :first-child:not(.bb-table-expand__cell)
2689
+ ) {
2690
+ padding-inline-start: var(--page-inset);
2691
+ }
2692
+
2693
+ .table-flush
2694
+ :deep(> .bb-table__table > .bb-table__head > [role='row'] > :last-child),
2695
+ .table-flush
2696
+ :deep(
2697
+ > .bb-table__table
2698
+ > .bb-table__body
2699
+ > [role='row']
2700
+ > :last-child:not(.bb-table-expand__cell)
2701
+ ) {
2702
+ padding-inline-end: var(--page-inset);
2703
+ }
2704
+
2705
+ /* Expand rows stay edge-to-edge so the nested table can snap onto the parent
2706
+ tracks without an extra inset shifting it sideways. */
2707
+ .table-flush
2708
+ :deep(
2709
+ > .bb-table__table > .bb-table__body > [role='row'] > .bb-table-expand__cell
2710
+ ) {
2711
+ padding-inline: 0;
2712
+ }
2713
+ ```
2714
+
2715
+ Leave the geometry alone: `display: grid` / `gap: 0` on the root, row groups
2716
+ and rows, `grid-column: 1 / -1` **and `contain: inline-size`** on the
2717
+ full-width cells (without the containment, expand-row content resizes the
2718
+ parent's columns), `white-space: nowrap` on the actions cell (it sizes the
2719
+ actions track), and `position: sticky` on `.bb-table__head` under
2720
+ `fixed-headers`.
2721
+
2722
+ When you replace a region with `#thead` / `#tbody` / `#tfoot`, ship the same
2723
+ shape the table does — role-complete rows, spanning with `grid-column` rather
2724
+ than `colspan`:
2725
+
2726
+ ```vue
2727
+ <template #tfoot="{ columnCount }">
2728
+ <div role="row">
2729
+ <div
2730
+ role="cell"
2731
+ :aria-colspan="columnCount - 1"
2732
+ :style="{ gridColumn: `span ${columnCount - 1}` }"
2733
+ >
2734
+ Total
2735
+ </div>
2736
+ <div role="cell" class="text-right">{{ total }}</div>
2737
+ </div>
2738
+ </template>
2739
+ ```
2740
+
2741
+ A `<tr>` inside the grid is not merely unsemantic — the browser wraps it in an
2742
+ implicit `<table>` and the cells never land on the tracks. Nothing lints the
2743
+ roles: a `<div>` row without them renders fine and is invisible to a screen
2744
+ reader.
2745
+
2072
2746
  ### Config and platform
2073
2747
 
2074
2748
  - **Locale.** The plugin's `locale` (`it`/`en`) drives the built-in strings:
@@ -2080,10 +2754,13 @@ records list.
2080
2754
  plugin is still on the default locale, which is `it` — set the plugin's
2081
2755
  `locale` option, or override per table with `actions-text` (it is visually
2082
2756
  hidden in the header but read by screen readers).
2083
- - **SSR / Nuxt.** A provider `items` function participates in
2084
- `onServerPrefetch`, so the first page can render server-side. The table
2085
- context (and thus `table-id` pagination wiring) is client-side only —
2086
- hydration-time onward.
2757
+ - **SSR / Nuxt.** Under `bitboss-ui/nuxt` a provider `items` function
2758
+ participates in `onServerPrefetch` and its rows travel in the Nuxt payload, so
2759
+ the first page renders server-side and hydrates without re-fetching. On other
2760
+ SSR hosts the provider runs on the client only and the server renders the
2761
+ empty table — see [SSR prefill](./guides/ssr-prefill.md). The table context
2762
+ (and thus `table-id` pagination wiring) is client-side only — hydration-time
2763
+ onward.
2087
2764
  - **Forms.** With `name`, the selection submits as hidden inputs — no JS
2088
2765
  needed between the table and a plain `<form>` POST (Inertia included).
2089
2766
 
@@ -2092,6 +2769,8 @@ records list.
2092
2769
  - `BbPagination` — shared page ref, or zero-wiring via `id` + `table-id`.
2093
2770
  - `useBbTableContext` (`./composables/useBbTableContext.md`) — bulk-action
2094
2771
  toolbars, detail panels, "reset sort" buttons living outside the table.
2772
+ (Column order is not mirrored there: a column panel needs `columns` too,
2773
+ so it lives where `v-model:order` is bound.)
2095
2774
  - `BbButton` — per-row links (`href`/`to`) in cells or `#actions`;
2096
2775
  `BbDropdown` for an overflow menu of row actions.
2097
2776
  - `BbBadge` / `BbAvatar` — status and identity cells via slots.
@@ -2114,12 +2793,18 @@ records list.
2114
2793
  localized `common.loadingText`, so an unset value still announces.
2115
2794
  - Row selection labels default to the localized "select" word plus every
2116
2795
  cell's content. `accessible-label` overrides that per row — it is called
2117
- `(columns, item)` with the mapped cells and the raw record, and whatever it
2118
- returns becomes the checkbox's label.
2796
+ `(columns, item)` with the mapped cells (in **render** order — look them up
2797
+ by `key`, never by position) and the raw record, and whatever it returns
2798
+ becomes the checkbox's label.
2799
+ - `fixed-columns` pins **slots**, not columns: under `order` the
2800
+ column that lands in a pinned slot is the pinned one (see Column order).
2801
+ - A column panel that holds a partial `order` receives the completed
2802
+ array on the user's first move — its persisted list grows to every key.
2119
2803
  - Enabling select-all does not re-emit an empty `modelValue` (see the
2120
2804
  select-all section) — in all-mode, read `select-all` +
2121
2805
  `unselected-items`, not `modelValue`.
2122
- - Row event payloads are the internal row wrapper — your record is `row.item`.
2806
+ - Row event payloads are `(event, item)` with `item` your raw record — there is
2807
+ no wrapper to unpack and no `selected` argument.
2123
2808
  - A mistyped context/pagination `id` does not error; it silently syncs with
2124
2809
  nothing. If a toolbar "works" but the table never reacts, check the id.
2125
2810
  - In multiple mode `modelValue` must be an array, or it throws.
@@ -2154,6 +2839,15 @@ records list.
2154
2839
  identity rather than object reference.
2155
2840
  - Sortable headers set `aria-sort` and render real buttons; custom header
2156
2841
  slots should keep a button calling `toggleSort`.
2842
+ - With `reorderable` the header row becomes one roving tab stop
2843
+ (the AG Grid grammar): ←/→ move focus between headers, Home/End to the
2844
+ ends, **Shift+←/→** move the focused column one slot (Shift+Home/End to
2845
+ the ends), Enter/Space sorts a sortable header — every move committed
2846
+ immediately and announced through a polite status region ("X moved to
2847
+ position 2 of 5"); a one-line sr-only instruction describes each header.
2848
+ The sort button leaves the tab order (the header is the target; it still
2849
+ works for the mouse). NVDA/JAWS browse mode intercepts arrows on a focused
2850
+ element — the same focus-mode step as row keyboard navigation.
2157
2851
  - When you paginate, pass `page` / `per-page` / `total-items` so the table
2158
2852
  announces the full result count (`aria-rowcount`) and each row's absolute
2159
2853
  index (`aria-rowindex`, expansion-aware), not just the current page.
@@ -2173,6 +2867,7 @@ records list.
2173
2867
  - `v-model:expandedItems` — type: `any[] \| undefined`
2174
2868
  - `v-model:highlighted` — type: `any`
2175
2869
  - `v-model` — type: `any`
2870
+ - `v-model:order` — type: `string[] \| undefined`
2176
2871
  - `v-model:selectAll` — type: `boolean \| undefined`
2177
2872
  - `v-model:sort` — type: `BbTableSortEntry[] \| undefined`
2178
2873
  - `v-model:unselectedItems` — type: `any[] \| undefined`
@@ -2181,64 +2876,70 @@ records list.
2181
2876
 
2182
2877
  | Prop | Type | Default | Required | Description |
2183
2878
  | --- | --- | --- | --- | --- |
2184
- | `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. |
2185
- | `actions` | `boolean \| undefined` | | | Displays the actions column. |
2879
+ | `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`… |
2880
+ | `actions` | `boolean \| undefined` | `false` | | Displays the actions column. |
2186
2881
  | `actionsText` | `string \| undefined` | | | Label used in the header of the actions column. |
2187
2882
  | `align` | `"left" \| "center" \| "right" \| undefined` | `"left"` | | Text alignment of the columns. |
2188
2883
  | `caption` | `string \| undefined` | | | Caption that describes the content of the table. Used for accessibility purposes. |
2189
2884
  | `columns` | `InternalColumn<T>[] \| undefined` | `[]` | | Array of definitions of how the columns should be rendered. |
2190
- | `compact` | `boolean \| undefined` | | | Adds a CSS class that applies a compact style to the component. |
2885
+ | `compact` | `boolean \| undefined` | `false` | | Adds a CSS class that applies a compact style to the component. |
2191
2886
  | `dependencies` | `unknown[] \| undefined` | `[]` | | Defines an array of dependencies that will trigger actions in the component upon change. |
2192
2887
  | `depsDebounceTime` | `number \| undefined` | `0` | | Timeout used to debounce response to changes to dependencies. |
2193
- | `disabled` | `boolean \| undefined` | | | Disables the component |
2194
- | `disableSelectAll` | `boolean \| undefined` | | | Hides the "Select all" checkbox that selectable multi-row tables display by default. |
2195
- | `displayCaption` | `boolean \| undefined` | | | Boolean that defines whether to display or hide the caption. By default is true and the caption is hidden. |
2196
- | `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… |
2888
+ | `disabled` | `boolean \| undefined` | `false` | | Disables the component |
2889
+ | `disableSelectAll` | `boolean \| undefined` | `false` | | Hides the "Select all" checkbox that selectable multi-row tables display by default. |
2890
+ | `displayCaption` | `boolean \| undefined` | `false` | | Boolean that defines whether to display or hide the caption. By default is true and the caption is hidden. |
2891
+ | `enforceCoherence` | `boolean \| undefined` | `false` | | After every load (initial, `dependencies` refetch or a change of the `items` array) prunes the row-keyed models of values whose row is no longer in the result set: `modelValue`, `unselected-items`, `highlighted` and `expanded-items`. Values… |
2197
2892
  | `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. |
2199
- | `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
- | `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>`. |
2202
- | `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… |
2893
+ | `fixed` | `boolean \| undefined` | `false` | | Splits the width equally among the columns that declare no `width` (each undeclared column becomes a `minmax(0, 1fr)` track) making each column take up the same amount of space. |
2894
+ | `fixedColumns` | `(number \| { index: number; position: "left" \| "right"; })[] \| undefined` | `[]` | | Definition for which column should be fixed. It can be an array of index of the column to fix on the left side of the table or an array of objects indicating the index and the position `left` or `right` where to affix the columns. Indices a… |
2895
+ | `fixedHeaders` | `boolean \| undefined` | `false` | | Boolean that sets the headers as sticky to the top of the table. |
2896
+ | `headerRowClass` | `Classes \| undefined` | | | Defines the classes to be passed to the header row. |
2897
+ | `highlighted` | `any` | `undefined` | | Used by `v-model:highlighted`. A parallel, UI-intent state to selection — typically "the row whose details are open". A single item value (or `null`) — highlight is single by design. **Opt-in, and `undefined` is the opt-out.** Leave it unbo… |
2203
2898
  | `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… |
2205
- | `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 … |
2899
+ | `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… |
2900
+ | `interactiveWhileLoading` | `boolean \| undefined` | `false` | | 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
2901
  | `items` | `Item[] \| ((prefill: boolean, modelValue?: any) => Item[] \| Promise<Item[]>)` | `[]` | yes | Rows to render, or a fetcher that returns them. |
2207
2902
  | `itemValue` | `ItemAccessor<Item> \| undefined` | | | |
2208
- | `keyboardNavigation` | `boolean \| undefined` | | | Opt-in keyboard navigation with a roving tabindex: the body is one tab stop, ArrowUp/ArrowDown move the focused row (Home/End jump), and Enter/Space activate it through the same path as a mouse click (highlight toggle + `click:row`). Focus … |
2903
+ | `keyboardNavigation` | `boolean \| undefined` | `false` | | Opt-in keyboard navigation with a roving tabindex: the body is one tab stop, ArrowUp/ArrowDown move the focused row (Home/End jump), and Enter/Space activate it through the same path as a mouse click (highlight toggle + `click:row`). Focus … |
2209
2904
  | `legend` | `string \| undefined` | | | Text content of the `<legend>` a `selectable` table renders (it becomes a `<fieldset>`, and the legend is that fieldset's accessible name). Omit it and the table falls back to `caption`, then to a localized default — the fieldset always end… |
2210
- | `loading` | `boolean \| undefined` | | | Sets the component in a loading state, usually triggering some visual styles. With no rows yet this renders skeletons; over existing rows the rows stay on screen (dimmed, with an indeterminate bar) rather than being replaced — a refetch sho… |
2211
- | `loadingText` | `string \| undefined` | | | Screen-reader announcement for the loading row's `aria-live` status. Visually hidden — the skeleton (or the `#loading` slot) is the visible signal. |
2905
+ | `loading` | `boolean \| undefined` | `false` | | Sets the component in a loading state, usually triggering some visual styles. With no rows yet this renders skeletons; over existing rows the rows stay on screen (dimmed, with an indeterminate bar) rather than being replaced — a refetch sho… |
2906
+ | `loadingText` | `string \| undefined` | `the localized `common.loadingText` ("Loading...")` | | Screen-reader announcement for the loading row's `aria-live` status. Visually hidden — the skeleton (or the `#loading` slot) is the visible signal. |
2212
2907
  | `max` | `number \| undefined` | `Infinity` | | Maximum number of items that can be selected. |
2213
2908
  | `modelValue` | `any` | `[]` | | Used by v-model. Can be any serializable type. |
2214
2909
  | `multiple` | `boolean \| undefined` | `true` | | Allows the selection of multiple items. |
2215
2910
  | `name` | `string \| undefined` | | | Defines the name of the input. |
2216
- | `noDataText` | `string \| undefined` | | | String displayed when there are no items to display. |
2911
+ | `noDataText` | `string \| undefined` | `the localized `common.noDataText` ("No data to display")` | | String displayed when there are no items to display. |
2912
+ | `order` | `string[] \| undefined` | `[]` | | Used by `v-model:order`. Ordered array of column keys — the render order of the data columns. Keys listed here render first, in this order; declared columns it omits follow, in declaration order (so a column added to `columns` later renders… |
2217
2913
  | `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
2914
  | `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
- | `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. |
2915
+ | `readonly` | `boolean \| undefined` | `false` | | Sets the input in a readonly state. |
2916
+ | `reorderable` | `boolean \| undefined` | `false` | | Lets the user reorder the data columns: drag a header cell (AG Grid style — the column moves live under the pointer and a chip with its label follows) or, from the keyboard, use each header's reorder handle (ArrowLeft / ArrowRight move the … |
2917
+ | `resizable` | `boolean \| undefined` | `false` | | Lets the user resize the data columns of a root table: drag the handle on a header's trailing edge (the column follows live, the width commits on release), or press Alt + ArrowLeft / ArrowRight on the focused header; double-click the handle… |
2918
+ | `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
2919
  | `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
2920
  | `selectAll` | `boolean \| undefined` | `false` | | Defines whether the "Select all" checkbox is checked. |
2223
2921
  | `selectAllLabel` | `string \| undefined` | | | Text of the label used by the "Select all" boolean. |
2224
2922
  | `selectText` | `string \| undefined` | | | Text of the header used for the radio inputs when the table is not `multiple`. |
2225
2923
  | `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>`. |
2924
+ | `tdClass` | `ColumnClasses<Item> \| undefined` | | | Defines the classes to be passed to every data cell. Can also be a function for dynamic values. |
2925
+ | `thClass` | `Classes \| undefined` | | | Defines the classes to be passed to every header cell. |
2228
2926
  | `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
2927
  | `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
2928
  | `unselectedItems` | `any[] \| undefined` | `[]` | | Items that are not selected. |
2929
+ | `virtual` | `boolean \| undefined` | `false` | | Windows the body: only the rows in (and just around) the vertical scrollport are mounted, so a client-side dataset of any size scrolls at a constant cost. The table itself is the scroller as soon as you bound its height (`max-height` / `hei… |
2231
2930
 
2232
2931
  ## Events
2233
2932
 
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`
2933
+ - `click:row` — `(e: "click:row", event: MouseEvent, item: any): void`
2934
+ - `contextmenu:row` — `(e: "contextmenu:row", event: MouseEvent, item: any): void`
2935
+ - `dblclick:row` — `(e: "dblclick:row", event: MouseEvent, item: any): void`
2237
2936
  - `item:selected` — `(e: "item:selected", value: any): void`
2238
2937
  - `item:unselected` — `(e: "item:unselected", value: any): void`
2938
+ - `resize:column` — `(e: "resize:column", key: string, width: number \| null): void` — A column was resized by the user (`resizable`): the new width in px, or `null` when the handle was double-clicked to reset the column to its declared width.
2239
2939
  - `update:expandedItems` — `(e: "update:expandedItems", value: any[]): void`
2240
2940
  - `update:highlighted` — `(e: "update:highlighted", value: any): void`
2241
2941
  - `update:modelValue` — `(e: "update:modelValue", value: any): void`
2942
+ - `update:order` — `(e: "update:order", value: string[]): void`
2242
2943
  - `update:selectAll` — `(e: "update:selectAll", value: boolean): void`
2243
2944
  - `update:sort` — `(e: "update:sort", value: BbTableSortEntry[]): void`
2244
2945
  - `update:unselectedItems` — `(e: "update:unselectedItems", value: any[]): void`
@@ -2248,13 +2949,15 @@ records list.
2248
2949
  - `actions` — scope: `{ expanded?: boolean \| undefined; expandProps?: ExpandControllerProps \| undefined; toggleExpanded?: (() => void) \| undefined; highlighted?: boolean \| undefi…` — Content rendered in the actions cell for each row.
2249
2950
  - `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
2951
  - `header:actions` — scope: `{ text: string; }` — Replaces the default actions header cell content.
2952
+ - `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…
2953
+ - `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
2954
  - `header:select` — scope: `{ multiple: boolean; selectAll: boolean; text: string; }` — Replaces the default selection header cell content (checkbox or radio label).
2252
2955
  - `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…
2956
+ - `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
2957
  - `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.
2958
+ - `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-…
2959
+ - `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>`.
2960
+ - `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
2961
 
2259
2962
  ## See Also
2260
2963