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

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 (350) 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 +920 -109
  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 +128 -2
  84. package/dist/ai/components.json +917 -100
  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 +18 -18
  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 +11 -5
  93. package/dist/ai/guides/installation-and-plugin-setup.md +85 -23
  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 +560 -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/approvals-inbox.md +0 -1
  107. package/dist/ai/recipes/inertia/inline-edit-workspace.md +0 -1
  108. package/dist/ai/recipes/inertia/layout-scaffold.md +5 -1
  109. package/dist/ai/recipes/inertia/ownership-atlas.md +49 -29
  110. package/dist/ai/recipes/inertia/record-form.md +0 -1
  111. package/dist/ai/recipes/inertia/records-workspace.md +26 -8
  112. package/dist/ai/recipes/inertia/upload-center.md +0 -1
  113. package/dist/ai/recipes/nuxt/approvals-inbox.md +0 -1
  114. package/dist/ai/recipes/nuxt/inline-edit-workspace.md +0 -1
  115. package/dist/ai/recipes/nuxt/layout-scaffold.md +5 -1
  116. package/dist/ai/recipes/nuxt/record-form.md +0 -1
  117. package/dist/ai/recipes/nuxt/records-workspace.md +26 -8
  118. package/dist/ai/recipes/nuxt/upload-center.md +0 -1
  119. package/dist/ai/recipes/vue/approvals-inbox.md +0 -1
  120. package/dist/ai/recipes/vue/inline-edit-workspace.md +0 -1
  121. package/dist/ai/recipes/vue/layout-scaffold.md +5 -1
  122. package/dist/ai/recipes/vue/records-workspace.md +26 -8
  123. package/dist/ai/recipes/vue/upload-center.md +0 -1
  124. package/dist/ai/source/BbAlert.md +60 -2
  125. package/dist/ai/source/BbAvatar.md +3 -3
  126. package/dist/ai/source/BbBadge.md +60 -15
  127. package/dist/ai/source/BbBadgeButton.md +8 -2
  128. package/dist/ai/source/BbBaseButton.md +3 -1
  129. package/dist/ai/source/BbBaseCheckboxGroup.md +6 -2
  130. package/dist/ai/source/BbBaseDatePicker.md +3 -1
  131. package/dist/ai/source/BbBaseDatePickerInput.md +5 -1
  132. package/dist/ai/source/BbBaseInputContainer.md +52 -1
  133. package/dist/ai/source/BbBaseRadioGroup.md +4 -0
  134. package/dist/ai/source/BbBaseRating.md +3 -1
  135. package/dist/ai/source/BbBaseSelect.md +71 -16
  136. package/dist/ai/source/BbBaseSwitchGroup.md +4 -0
  137. package/dist/ai/source/BbBaseTimePickerInput.md +5 -1
  138. package/dist/ai/source/BbBreadcrumbs.md +31 -0
  139. package/dist/ai/source/BbButton.md +51 -24
  140. package/dist/ai/source/BbCheckbox.md +15 -1
  141. package/dist/ai/source/BbCheckboxGroup.md +2 -1
  142. package/dist/ai/source/BbCollapsible.md +7 -1
  143. package/dist/ai/source/BbColorInput.md +3 -4
  144. package/dist/ai/source/BbColorPalette.md +3 -4
  145. package/dist/ai/source/BbDatePicker.md +5 -1
  146. package/dist/ai/source/BbDatePickerInput.md +5 -1
  147. package/dist/ai/source/BbDialog.md +175 -19
  148. package/dist/ai/source/BbDropdown.md +376 -69
  149. package/dist/ai/source/BbDropdownGroup.md +330 -59
  150. package/dist/ai/source/BbForm.md +93 -5
  151. package/dist/ai/source/BbIcon.md +46 -36
  152. package/dist/ai/source/BbNumberInput.md +3 -4
  153. package/dist/ai/source/BbOffCanvas.md +474 -194
  154. package/dist/ai/source/BbPopover.md +24 -17
  155. package/dist/ai/source/BbProgress.md +2 -2
  156. package/dist/ai/source/BbRadio.md +15 -1
  157. package/dist/ai/source/BbRadioGroup.md +3 -1
  158. package/dist/ai/source/BbSelect.md +50 -2
  159. package/dist/ai/source/BbSelectPopover.md +171 -23
  160. package/dist/ai/source/BbSlider.md +3 -4
  161. package/dist/ai/source/BbSwitch.md +15 -1
  162. package/dist/ai/source/BbSwitchGroup.md +8 -5
  163. package/dist/ai/source/BbTable.md +5092 -1446
  164. package/dist/ai/source/BbTabs.md +6 -7
  165. package/dist/ai/source/BbTabsList.md +6 -7
  166. package/dist/ai/source/BbTabsPanes.md +6 -7
  167. package/dist/ai/source/BbTabsRoot.md +6 -7
  168. package/dist/ai/source/BbTag.md +3 -4
  169. package/dist/ai/source/BbTextInput.md +3 -4
  170. package/dist/ai/source/BbTextarea.md +3 -4
  171. package/dist/ai/source/BbTimePicker.md +4 -1
  172. package/dist/ai/source/BbTimePickerInput.md +5 -1
  173. package/dist/ai/source/BbToast.md +3 -1
  174. package/dist/ai/source/BbTooltip.md +5 -1
  175. package/dist/ai/source/CommonInputWrapper.md +4 -0
  176. package/dist/ai/source/CommonPopover.md +6 -0
  177. package/dist/ai/source/FlatListBox.md +11 -5
  178. package/dist/ai/source/GroupedListBox.md +7 -5
  179. package/dist/components/BbAlert/BbAlert.vue_vue_type_script_setup_true_lang.js +4 -4
  180. package/dist/components/BbAlert/types.d.ts +10 -1
  181. package/dist/components/BbAvatar/BbAvatar.vue.d.ts +1 -1
  182. package/dist/components/BbAvatar/BbAvatar.vue_vue_type_script_setup_true_lang.js +33 -33
  183. package/dist/components/BbBadge/BadgeBodyContent.vue_vue_type_script_setup_true_lang.js +22 -16
  184. package/dist/components/BbBadge/BbBadge.vue_vue_type_script_setup_true_lang.js +36 -15
  185. package/dist/components/BbBadge/badgeButtonContext.d.ts +21 -8
  186. package/dist/components/BbBadge/types.d.ts +8 -2
  187. package/dist/components/BbBaseButton/BbBaseButton.vue_vue_type_script_setup_true_lang.js +47 -47
  188. package/dist/components/BbBaseButton/RouterComponent.vue_vue_type_script_setup_true_lang.js +14 -14
  189. package/dist/components/BbBaseCheckboxGroup/BbBaseCheckboxGroup.vue_vue_type_script_setup_true_lang.js +30 -28
  190. package/dist/components/BbBaseCheckboxGroup/types.d.ts +2 -2
  191. package/dist/components/BbBaseDatePicker/BbBaseDatePicker.vue_vue_type_script_setup_true_lang.js +167 -167
  192. package/dist/components/BbBaseDatePicker/BbBaseDatePickerInputDaySelector.vue.d.ts +9 -9
  193. package/dist/components/BbBaseDatePicker/BbBaseDatePickerInputDaySelector.vue_vue_type_script_setup_true_lang.js +78 -78
  194. package/dist/components/BbBaseDatePicker/BbBaseDatePickerMonthSelector.vue_vue_type_script_setup_true_lang.js +64 -64
  195. package/dist/components/BbBaseDatePickerInput/BbBaseDatePickerInput.vue.d.ts +1 -1
  196. package/dist/components/BbBaseDatePickerInput/BbBaseDatePickerInput.vue_vue_type_script_setup_true_lang.js +138 -138
  197. package/dist/components/BbBaseInputContainer/BbBaseInputContainer.vue_vue_type_script_setup_true_lang.js +2 -2
  198. package/dist/components/BbBaseRadioGroup/BbBaseRadioGroup.vue_vue_type_script_setup_true_lang.js +24 -22
  199. package/dist/components/BbBaseRating/BbBaseRating.vue_vue_type_script_setup_true_lang.js +40 -40
  200. package/dist/components/BbBaseSelect/BbBaseSelect.vue_vue_type_script_setup_true_lang.js +74 -65
  201. package/dist/components/BbBaseSelect/types.d.ts +22 -0
  202. package/dist/components/BbBaseSlider/BbBaseSlider.vue_vue_type_script_setup_true_lang.js +5 -5
  203. package/dist/components/BbBaseSwitchGroup/BbBaseSwitchGroup.vue_vue_type_script_setup_true_lang.js +30 -28
  204. package/dist/components/BbBaseTimePickerInput/BbBaseTimePickerInput.vue.d.ts +1 -1
  205. package/dist/components/BbBaseTimePickerInput/BbBaseTimePickerInput.vue_vue_type_script_setup_true_lang.js +104 -104
  206. package/dist/components/BbBreadcrumbs/BbBreadcrumbs.vue_vue_type_script_setup_true_lang.js +35 -35
  207. package/dist/components/BbButton/BbButton.vue_vue_type_script_setup_true_lang.js +85 -81
  208. package/dist/components/BbCheckboxGroup/types.d.ts +2 -1
  209. package/dist/components/BbCollapsible/types.d.ts +7 -1
  210. package/dist/components/BbColorInput/BbColorInput.vue.d.ts +1 -1
  211. package/dist/components/BbColorInput/BbColorInput.vue_vue_type_script_setup_true_lang.js +18 -18
  212. package/dist/components/BbColorPalette/BbColorPalette.vue.d.ts +2 -2
  213. package/dist/components/BbColorPalette/BbColorPalette.vue_vue_type_script_setup_true_lang.js +50 -50
  214. package/dist/components/BbConfirm/BbConfirm.vue_vue_type_script_setup_true_lang.js +17 -17
  215. package/dist/components/BbDatePicker/BbDatePicker.vue.d.ts +1 -1
  216. package/dist/components/BbDatePicker/BbDatePicker.vue_vue_type_script_setup_true_lang.js +100 -100
  217. package/dist/components/BbDatePickerInput/BbDatePickerInput.vue_vue_type_script_setup_true_lang.js +21 -21
  218. package/dist/components/BbDialog/BbDialog.vue.d.ts +1 -1
  219. package/dist/components/BbDialog/BbDialog.vue_vue_type_script_setup_true_lang.js +133 -107
  220. package/dist/components/BbDialog/types.d.ts +29 -2
  221. package/dist/components/BbDropdown/AdaptiveDropdown.vue.d.ts +2 -0
  222. package/dist/components/BbDropdown/AdaptiveDropdown.vue_vue_type_script_setup_true_lang.js +99 -100
  223. package/dist/components/BbDropdown/BbDropdown.vue.d.ts +2 -2
  224. package/dist/components/BbDropdown/BbDropdown.vue_vue_type_script_setup_true_lang.js +208 -201
  225. package/dist/components/BbDropdown/BbDropdownList.vue_vue_type_script_setup_true_lang.js +225 -208
  226. package/dist/components/BbDropdown/DropdownPipelineResolver.vue_vue_type_script_setup_true_lang.js +21 -21
  227. package/dist/components/BbDropdown/types.d.ts +20 -5
  228. package/dist/components/BbDropdownButton/BbDropdownButton.vue_vue_type_script_setup_true_lang.js +5 -5
  229. package/dist/components/BbIcon/BbIcon.vue_vue_type_script_setup_true_lang.js +46 -46
  230. package/dist/components/BbIcon/iconCache.d.ts +33 -0
  231. package/dist/components/BbIcon/iconCache.js +17 -0
  232. package/dist/components/BbNumberInput/BbNumberInput.vue.d.ts +1 -1
  233. package/dist/components/BbNumberInput/BbNumberInput.vue_vue_type_script_setup_true_lang.js +15 -15
  234. package/dist/components/BbOffCanvas/BbOffCanvas.vue.d.ts +1 -1
  235. package/dist/components/BbOffCanvas/BbOffCanvas.vue_vue_type_script_setup_true_lang.js +254 -208
  236. package/dist/components/BbOffCanvas/types.d.ts +30 -5
  237. package/dist/components/BbPagination/BbPagination.vue.d.ts +1 -1
  238. package/dist/components/BbPopover/BbPopover.vue.d.ts +2 -2
  239. package/dist/components/BbPopover/BbPopover.vue_vue_type_script_setup_true_lang.js +149 -146
  240. package/dist/components/BbPopover/types.d.ts +3 -2
  241. package/dist/components/BbRadioGroup/types.d.ts +3 -1
  242. package/dist/components/BbSelect/BbSelect.vue_vue_type_script_setup_true_lang.js +27 -23
  243. package/dist/components/BbSelect/types.d.ts +32 -0
  244. package/dist/components/BbSelectPopover/BbSelectPopover.vue_vue_type_script_setup_true_lang.js +221 -213
  245. package/dist/components/BbSlider/BbSlider.vue.d.ts +1 -1
  246. package/dist/components/BbSlider/BbSlider.vue_vue_type_script_setup_true_lang.js +20 -20
  247. package/dist/components/BbTable/BbTable.vue.d.ts +10 -4
  248. package/dist/components/BbTable/BbTable.vue_vue_type_script_setup_true_lang.js +1433 -600
  249. package/dist/components/BbTable/BbTableDataRow.d.ts +162 -0
  250. package/dist/components/BbTable/BbTableDataRow.js +136 -0
  251. package/dist/components/BbTable/BbTableExpandRow.d.ts +73 -0
  252. package/dist/components/BbTable/BbTableExpandRow.js +52 -0
  253. package/dist/components/BbTable/types.d.ts +324 -111
  254. package/dist/components/BbTable/utils.d.ts +92 -4
  255. package/dist/components/BbTable/utils.js +71 -36
  256. package/dist/components/BbTabs/types.d.ts +0 -8
  257. package/dist/components/BbTag/BbTag.vue.d.ts +1 -1
  258. package/dist/components/BbTag/BbTag.vue_vue_type_script_setup_true_lang.js +18 -18
  259. package/dist/components/BbTextInput/BbTextInput.vue.d.ts +1 -1
  260. package/dist/components/BbTextInput/BbTextInput.vue_vue_type_script_setup_true_lang.js +18 -18
  261. package/dist/components/BbTextarea/BbTextarea.vue.d.ts +1 -1
  262. package/dist/components/BbTextarea/BbTextarea.vue_vue_type_script_setup_true_lang.js +54 -54
  263. package/dist/components/BbTimePicker/BbTimePicker.vue.d.ts +1 -1
  264. package/dist/components/BbTimePicker/BbTimePicker.vue_vue_type_script_setup_true_lang.js +80 -80
  265. package/dist/components/BbTimePickerInput/BbTimePickerInput.vue_vue_type_script_setup_true_lang.js +61 -61
  266. package/dist/components/BbToast/BbToast.vue_vue_type_script_setup_true_lang.js +17 -17
  267. package/dist/components/BbToast/BbToastMessage.vue_vue_type_script_setup_true_lang.js +13 -13
  268. package/dist/components/BbTooltip/types.d.ts +5 -1
  269. package/dist/components/ChipsBox/ChipsBox.vue_vue_type_script_setup_true_lang.js +17 -17
  270. package/dist/components/ClearableButton/ClearableButton.vue_vue_type_script_setup_true_lang.js +5 -5
  271. package/dist/components/CommonPopover/CommonPopover.vue_vue_type_script_setup_true_lang.js +2 -1
  272. package/dist/components/FlatListBox/FlatListBox.vue_vue_type_script_setup_true_lang.js +83 -83
  273. package/dist/components/GroupedListBox/GroupedListBox.vue_vue_type_script_setup_true_lang.js +56 -56
  274. package/dist/composables/useBaseOptions.js +30 -20
  275. package/dist/composables/useBbConfig.d.ts +27 -6
  276. package/dist/composables/useBbConfig.js +8 -2
  277. package/dist/composables/useConfig.d.ts +59 -0
  278. package/dist/composables/useConfig.js +9 -0
  279. package/dist/composables/useItemsGetter.d.ts +11 -0
  280. package/dist/composables/useItemsGetter.js +44 -18
  281. package/dist/composables/useLocale.js +10 -7
  282. package/dist/composables/useMobile.js +5 -5
  283. package/dist/composables/usePrefill.d.ts +21 -2
  284. package/dist/composables/usePrefill.js +25 -19
  285. package/dist/composables/useSsrState.d.ts +20 -0
  286. package/dist/composables/useSsrState.js +13 -0
  287. package/dist/composables/useTableWidthContext.d.ts +24 -4
  288. package/dist/composables/useTableWidthContext.js +31 -28
  289. package/dist/composables/useToast.js +5 -5
  290. package/dist/deprecation/ai-deprecations.json.d.ts +121 -11
  291. package/dist/deprecation/ai-deprecations.json.js +1 -1
  292. package/dist/directives/bbDropdown.d.ts +15 -4
  293. package/dist/directives/bbDropdown.js +31 -9
  294. package/dist/directives/createPopoverDirective.d.ts +62 -0
  295. package/dist/directives/createPopoverDirective.js +63 -23
  296. package/dist/i18n/locales/cs.json +4 -1
  297. package/dist/i18n/locales/da.json +4 -1
  298. package/dist/i18n/locales/de.json +4 -1
  299. package/dist/i18n/locales/el.json +4 -1
  300. package/dist/i18n/locales/en.json +4 -1
  301. package/dist/i18n/locales/es.json +4 -1
  302. package/dist/i18n/locales/fi.json +4 -1
  303. package/dist/i18n/locales/fr.json +4 -1
  304. package/dist/i18n/locales/hu.json +4 -1
  305. package/dist/i18n/locales/it.json +4 -1
  306. package/dist/i18n/locales/ja.json +4 -1
  307. package/dist/i18n/locales/ko.json +4 -1
  308. package/dist/i18n/locales/nb.json +4 -1
  309. package/dist/i18n/locales/nl.json +4 -1
  310. package/dist/i18n/locales/pl.json +4 -1
  311. package/dist/i18n/locales/pt.json +4 -1
  312. package/dist/i18n/locales/ro.json +4 -1
  313. package/dist/i18n/locales/ru.json +4 -1
  314. package/dist/i18n/locales/sv.json +4 -1
  315. package/dist/i18n/locales/tr.json +4 -1
  316. package/dist/i18n/locales/uk.json +4 -1
  317. package/dist/i18n/locales/zh-cn.json +4 -1
  318. package/dist/index.d.ts +7 -2
  319. package/dist/index.js +69 -67
  320. package/dist/llms-full.txt +3997 -1458
  321. package/dist/llms-medium.txt +140 -73
  322. package/dist/llms.txt +7 -6
  323. package/dist/locale-blueprint.json +4 -1
  324. package/dist/nuxt-auto-imports.d.ts +100 -0
  325. package/dist/nuxt-auto-imports.js +55 -0
  326. package/dist/nuxt-module.d.ts +22 -1
  327. package/dist/nuxt.js +22 -8
  328. package/dist/plugin.d.ts +10 -1
  329. package/dist/plugin.js +20 -19
  330. package/dist/runtime/nuxt-plugin.js +28 -21
  331. package/dist/styles.css +1 -1
  332. package/dist/utilities/misc/listboxMetrics.d.ts +3 -3
  333. package/dist/utilities/misc/listboxMetrics.js +2 -2
  334. package/dist/utilities/misc/ssrStateInjectionKey.d.ts +42 -0
  335. package/dist/utilities/misc/ssrStateInjectionKey.js +4 -0
  336. package/dist/validated/BbForm.vue_vue_type_script_setup_true_lang.js +45 -27
  337. package/dist/validated/BbSelect.vue.d.ts +2 -0
  338. package/dist/validated/BbSelect.vue_vue_type_script_setup_true_lang.js +1 -0
  339. package/dist/validated/useValidatedField.js +25 -23
  340. package/dist/vite.js +7 -3
  341. package/llms.txt +8 -7
  342. package/package.json +2 -2
  343. package/scripts/lib/eslint-plugin.d.ts +19 -1
  344. package/scripts/lib/eslint-plugin.mjs +155 -14
  345. package/scripts/lib/html-attributes.mjs +91 -0
  346. package/scripts/lib/validate-bb-markup.mjs +59 -11
  347. package/dist/assets/svgs/sort.svg_raw.js +0 -4
  348. package/dist/components/BbBadge/BadgeAppendIcon.vue.d.ts +0 -3
  349. package/dist/components/BbBadge/BadgeAppendIcon.vue.js +0 -5
  350. 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,17 +57,19 @@ 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.
68
- - **`snap`** maps a nested table's column onto its parent's tracks — see
72
+ - **`snap`** names the parent column a nested table's column sits under — see
69
73
  Nested tables.
70
74
 
71
75
  **Columns, formatters and a per-row class**
@@ -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'
@@ -194,11 +198,11 @@ pre-mapping your items:
194
198
  ```
195
199
 
196
200
  **`actions` and `select` are not columns.** Both are rendered by the table
197
- itself, from the `actions` and `selectable` props. Putting
198
- `{ key: 'actions' }` in `columns` adds an ordinary data column that reads
199
- `item.actions`, so you get a stray empty cell _and_ the `#actions` slot never
200
- renders which looks like a broken slot and is really a bad column list. Dev
201
- builds warn.
201
+ itself: the selection column from the `selectable` prop, the actions column
202
+ from the `#actions` slot providing the slot is what creates the column,
203
+ there is no prop. Putting `{ key: 'actions' }` in `columns` adds an ordinary
204
+ data column that reads `item.actions`, so you get a stray empty cell next to
205
+ the real one. Dev builds warn.
202
206
 
203
207
  ### Custom cells and headers
204
208
 
@@ -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`
@@ -217,7 +225,24 @@ Body cell scope: `content` (the formatted value), `item` (raw row), `value`
217
225
  one-way state; the `toggle*` callbacks are the write path.
218
226
 
219
227
  Header scope: `label`, `items` (the full list — handy for counts), `sortable`,
220
- `sortOrder`, `toggleSort`, `classes`.
228
+ `sortOrder`, `toggleSort`, `hideColumn` (hides the column, see "Column
229
+ visibility"), `classes`.
230
+
231
+ **Add beside a header instead of replacing it** with
232
+ `#header:<key>:prepend` / `#header:<key>:append` — rendered before / after the
233
+ column's existing header content (the label + sort control, or your own
234
+ `#header:<key>` replacement), so a resize grip or reorder handle no longer
235
+ costs you the built-in sort button. The table-wide pair `#header:prepend` /
236
+ `#header:append` applies one template to every data column's header (never the
237
+ structural select/actions cells); both forms receive the header scope above
238
+ plus `columnKey` (the raw column key) so a table-wide template can branch per
239
+ column. When both a per-column and the table-wide slot are provided for the
240
+ same side, the per-column one wins for that column — they never stack. Affixes
241
+ render even beside a full `#header:<key>` replacement (the same rule as
242
+ BbDropdown's `#<key>:prepend/append`). One caveat: a column key that
243
+ normalizes to `prepend` or `append` collides with the table-wide slot names;
244
+ the table-wide interpretation wins, per-column header slots are unavailable
245
+ for that column, and dev builds warn.
221
246
 
222
247
  Below, the status badges carry brand glyphs shipped in the app's `iconDir`,
223
248
  referenced as `local:*` names on the badge's `prepend:icon`. Per the icons
@@ -365,7 +390,10 @@ Beyond per-column slots, the structural slots cover every other region:
365
390
  (replace the per-row checkbox/radio; scope: `item`, `value`, `checked`,
366
391
  `disabled`, `readonly`, `inputName`, `toggleSelected`), `#loading`, `#no-data`
367
392
  (both below), and `#thead` / `#tbody` / `#tfoot` to replace an entire region
368
- when you need full control (you then own its markup and semantics).
393
+ when you need full control (you then own its markup and semantics: the table
394
+ is a CSS grid, so pass `<div role="row">` rows holding `<div role="cell">` /
395
+ `<div role="columnheader">` cells — never `<tr>`/`<td>`; a spanning cell uses
396
+ `grid-column: span N` with `#tfoot`'s `columnCount`, see **Styling hooks**).
369
397
 
370
398
  ### Loading and empty states
371
399
 
@@ -396,8 +424,9 @@ it renders only when there is nothing to keep. (Scope: the current, possibly
396
424
  stale `items` — handy for matching the previous row count.) When `items` resolves
397
425
  empty the table shows its no-data row — localized default text, overridable via
398
426
  `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.
427
+ full-width empty-state cell (already centred, already spanning every column),
428
+ so pass the content — a message, a "clear filters" button — not a row of your
429
+ own.
401
430
 
402
431
  `loading-text` is the loading row's `aria-live` announcement. Like
403
432
  `no-data-text` it has a **localized default** (`common.loadingText` —
@@ -685,14 +714,21 @@ scriptable from anywhere via `useBbTableContext`
685
714
  ### Sorting
686
715
 
687
716
  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
717
+ `BbTableSortEntry[]` (`[columnKey, 'asc' | 'desc']`). A click anywhere on a
718
+ sortable header (its whitespace included AG Grid parity; the built-in
719
+ button is the keyboard/AT control) cycles the column unsorted → asc → desc →
720
+ removed; a direction flip updates the entry in
690
721
  place, so the column keeps its priority in a multi-column sort and other
691
722
  entries are never touched. The array order **is** the priority: first entry
692
723
  sorts first. The table only reports the model; **apply it to your data**
693
724
  (client-side, as below) or forward it to the server as `orderBy` and refetch
694
725
  via `dependencies`.
695
726
 
727
+ The affordance is one arrow: nothing at rest, a faint arrow previewed while
728
+ the header is hovered or focused, solid once the column is sorted (up for
729
+ ascending, down for descending — the AG Grid convention), gone again on the
730
+ third click.
731
+
696
732
  **Sortable headers driving a client-side sort**
697
733
 
698
734
  ```vue
@@ -773,11 +809,279 @@ const sortedInvoices = computed(() => {
773
809
 
774
810
  A custom `#header:<key>` slot replaces the built-in sort button, but its scope
775
811
  hands you `sortOrder` and `toggleSort` so your header can keep sorting alive
776
- (render your own button and call `toggleSort`). The column-level `sorted`
812
+ (render your own button and call `toggleSort`). To merely add something next
813
+ to the control — a resize grip, a filter icon — use
814
+ `#header:<key>:prepend/append` instead and keep the built-in button (see
815
+ "Custom cells and headers"). The column-level `sorted`
777
816
  field only seeds the header's `aria-sort` when no live model entry exists — it
778
817
  does not move the visual indicator; to start pre-sorted, seed `v-model:sort`
779
818
  instead (as the example does).
780
819
 
820
+ ### Column order
821
+
822
+ `columns` declares what a column **is**; `v-model:order` is the order
823
+ it **renders** in — an array of column keys, the same split as `sortable` +
824
+ `v-model:sort`. Two things write it:
825
+
826
+ - **Your own control** — a column panel, a settings drawer, a persisted
827
+ preference. Bind the model and write the array; the table follows. No flag
828
+ needed for this.
829
+ - **The user, on the table** — with `reorderable`, every data header
830
+ is a grab surface: drag it and the column moves live under the pointer (AG
831
+ Grid style, a chip with the label follows), release to commit. The same
832
+ header row is also one keyboard tab stop (the AG Grid grammar): ←/→ move
833
+ focus between headers, **Shift+←/→** move the column one slot
834
+ (Shift+Home/End to the ends), Enter sorts a sortable header.
835
+ Each committed move writes the model once and is announced.
836
+
837
+ The model's contract is what makes a column panel trivial:
838
+
839
+ - Keys listed first render first; declared columns the array omits follow, in
840
+ declaration order (a column added to `columns` later renders last).
841
+ - A `hidden` column (next section) keeps its slot without rendering, and keys
842
+ that match no declared column render nothing but are **preserved in
843
+ place** — a column that exists only for some users comes back exactly where
844
+ it was.
845
+ - A user move relocates **exactly one key**; everything else, hidden keys
846
+ included, keeps its relative order. The first move on a partial (or empty)
847
+ model emits the completed array — every declared key, hidden ones included
848
+ — so persist that and the model is complete from then on.
849
+ - A drop that changes nothing emits nothing; unbound, the table keeps the
850
+ order as per-mount state like `sort`.
851
+
852
+ **A column panel and the header drag sharing one order**
853
+
854
+ ```vue
855
+ <template>
856
+ <div class="flex max-w-2xl flex-col gap-3">
857
+ <!-- ONE order, two writers: the panel's buttons and the table's own
858
+ header drag / keyboard handles both write `v-model:order`.
859
+ Visibility is a flag ON the definition (`hidden`), so a hidden
860
+ column stays declared, keeps its slot in the order, and comes back
861
+ where it was. -->
862
+ <ul
863
+ aria-label="Columns"
864
+ class="flex flex-wrap gap-2 text-xs text-[color:var(--bb-text-muted)]"
865
+ >
866
+ <li
867
+ v-for="(column, index) in panelColumns"
868
+ :key="column.key"
869
+ class="flex items-center gap-1 rounded-[var(--bb-radius-sm)] border border-[color:var(--bb-border)] py-0.5 pl-2 pr-1"
870
+ >
871
+ <BbCheckbox
872
+ :label="column.label"
873
+ :model-value="!hidden.has(column.key)"
874
+ @update:model-value="setHidden(column.key, !$event)"
875
+ />
876
+ <BbButton
877
+ :aria-label="`Move ${column.label} left`"
878
+ :disabled="index === 0"
879
+ icon="lucide:chevron-left"
880
+ size="xs"
881
+ variant="ghost"
882
+ @click="move(column.key, -1)"
883
+ />
884
+ <BbButton
885
+ :aria-label="`Move ${column.label} right`"
886
+ :disabled="index === panelColumns.length - 1"
887
+ icon="lucide:chevron-right"
888
+ size="xs"
889
+ variant="ghost"
890
+ @click="move(column.key, 1)"
891
+ />
892
+ </li>
893
+ </ul>
894
+ <div
895
+ class="overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
896
+ >
897
+ <BbTable
898
+ v-model:order="order"
899
+ caption="Invoices"
900
+ :columns="columns"
901
+ compact
902
+ item-value="id"
903
+ :items="invoices"
904
+ reorderable
905
+ @hide:column="(key) => setHidden(key, true)"
906
+ >
907
+ <!-- A "hide" control beside every header: the slot's `hideColumn()` is
908
+ one-way (a hidden column has no header), so the panel above
909
+ is what brings a column back. -->
910
+ <template #header:append="{ hideColumn, label }">
911
+ <BbButton
912
+ :aria-label="`Hide ${label}`"
913
+ icon="lucide:eye-off"
914
+ size="xs"
915
+ variant="ghost"
916
+ @click="hideColumn"
917
+ />
918
+ </template>
919
+ </BbTable>
920
+ </div>
921
+ <p class="text-xs text-[color:var(--bb-text-muted)]">
922
+ Order model: <code>{{ JSON.stringify(order) }}</code> · hidden:
923
+ <code>{{ JSON.stringify([...hidden]) }}</code>
924
+ </p>
925
+ </div>
926
+ </template>
927
+ <script setup lang="ts">
928
+ import { computed, ref } from 'vue';
929
+ import { BbButton, BbCheckbox, BbTable } from 'bitboss-ui';
930
+ import type { BbTableColumn } from 'bitboss-ui';
931
+
932
+ type Invoice = {
933
+ id: string;
934
+ number: string;
935
+ client: string;
936
+ issued: string;
937
+ amount: number;
938
+ };
939
+
940
+ const euro = new Intl.NumberFormat('en-IE', {
941
+ style: 'currency',
942
+ currency: 'EUR',
943
+ });
944
+
945
+ // The declaration: what a column IS. Never reordered, never filtered.
946
+ const definitions: Array<BbTableColumn<Invoice>> = [
947
+ { key: 'number', label: 'Invoice', width: 110 },
948
+ { key: 'client', label: 'Client' },
949
+ { key: 'issued', label: 'Issued', width: 110 },
950
+ {
951
+ key: 'amount',
952
+ label: 'Amount',
953
+ align: 'right',
954
+ width: 110,
955
+ formatter: euro.format,
956
+ },
957
+ ];
958
+
959
+ const invoices: Invoice[] = [
960
+ {
961
+ id: '1',
962
+ number: 'INV-1041',
963
+ client: 'Northwind Traders',
964
+ issued: '2026-08-02',
965
+ amount: 4200,
966
+ },
967
+ {
968
+ id: '2',
969
+ number: 'INV-1042',
970
+ client: 'Globex',
971
+ issued: '2026-08-05',
972
+ amount: 980,
973
+ },
974
+ {
975
+ id: '3',
976
+ number: 'INV-1043',
977
+ client: 'Initech',
978
+ issued: '2026-08-11',
979
+ amount: 15600,
980
+ },
981
+ {
982
+ id: '4',
983
+ number: 'INV-1044',
984
+ client: 'Umbrella',
985
+ issued: '2026-08-19',
986
+ amount: 2750,
987
+ },
988
+ ];
989
+
990
+ // The state, both persistable as-is: render order (keys) and the hidden set.
991
+ // A partial or empty order is fine — the first move writes back the
992
+ // complete array, hidden keys in their slots, because every column stays
993
+ // declared below.
994
+ const order = ref<string[]>([]);
995
+ const hidden = ref(new Set<string>());
996
+
997
+ // The definitions, with the persisted visibility mapped in. Same shape you
998
+ // would use for persisted widths (`width`) — the definition is the truth.
999
+ const columns = computed(() =>
1000
+ definitions.map((column) => ({
1001
+ ...column,
1002
+ hidden: hidden.value.has(column.key),
1003
+ }))
1004
+ );
1005
+
1006
+ // The panel lists every declared column in the order the model has them
1007
+ // (omitted keys follow in declaration order, exactly as the table renders).
1008
+ const panelColumns = computed(() => {
1009
+ const rank = (key: string) => {
1010
+ const index = order.value.indexOf(key);
1011
+ return index < 0 ? order.value.length : index;
1012
+ };
1013
+ return [...definitions].sort((a, b) => rank(a.key) - rank(b.key));
1014
+ });
1015
+
1016
+ const setHidden = (key: string, value: boolean) => {
1017
+ const next = new Set(hidden.value);
1018
+ if (value) next.add(key);
1019
+ else next.delete(key);
1020
+ hidden.value = next;
1021
+ };
1022
+
1023
+ const move = (key: string, delta: 1 | -1) => {
1024
+ const next = panelColumns.value.map((column) => column.key);
1025
+ const from = next.indexOf(key);
1026
+ const to = from + delta;
1027
+ if (from < 0 || to < 0 || to >= next.length) return;
1028
+ next.splice(from, 1);
1029
+ next.splice(to, 0, key);
1030
+ order.value = next;
1031
+ };
1032
+ </script>
1033
+ ```
1034
+
1035
+ ### Column visibility
1036
+
1037
+ `hidden: true` on a column definition hides it: no header, no cells, no grid
1038
+ track. The column stays **declared**, and that is the whole point — the order
1039
+ model is completed against every declared key, so a hidden column keeps its
1040
+ slot through any number of header drags, and it returns to that slot the
1041
+ moment the flag clears. Two rules follow:
1042
+
1043
+ - **Hidden means "exists, not shown".** A column panel, a persisted
1044
+ preference, a narrow-screen breakpoint all write `hidden` into the
1045
+ definition (a `computed` over your persisted set — the example above does
1046
+ exactly that). Filter `columns` only for a column that does **not exist**
1047
+ for this user (a custom field they don't have); its key, if a saved order
1048
+ still lists it, is preserved in place like any unknown key.
1049
+ - **There is no visibility model, on purpose** — the `width` shape, not the
1050
+ `sort` one. The definition is the single declared truth. Header slots
1051
+ (`#header:<key>`, `#header:<key>:append`, the table-wide `#header:append`)
1052
+ receive a `hideColumn()` callback for a "Hide column" control of your own; calling
1053
+ it hides the column for the mount and fires `@hide:column="(key) => …"`
1054
+ once. Persist by writing `hidden: true` back into the definition — the
1055
+ override matches it and stays; write `hidden: false` (the panel showing the
1056
+ column again) and the definition wins. Unbound, the hide lives for the
1057
+ mount, like an unbound resize. A hidden column has no header, so `hideColumn()`
1058
+ is one-way by nature: if you offer it, own the persistence and give the
1059
+ user a panel that shows columns again.
1060
+
1061
+ A nested table that shares its parent's keys shares its `hidden` flags too,
1062
+ the same way it shares `order` — a hidden parent column has no track for a
1063
+ child cell to land on.
1064
+
1065
+ **Pins travel with the column.** `fixed` is a field on the column
1066
+ definition (see "Pinned columns"), so a reorder moves the pin with the cell:
1067
+ drag the identity column to slot 3 and it is still sticky, now at slot 3.
1068
+ The offset is still positional — the pinned columns before it on the same
1069
+ side — so a pinned column dragged away from the edge sticks at `left: 0` and
1070
+ slides over the columns before it. Lock the pinned columns in place by
1071
+ normalizing the model in your `@update:order` handler when that matters.
1072
+
1073
+ **Nested tables.** A parent that uses `order` or `reorderable` is inherited
1074
+ by **position**: its columns can move, so the child's `snap` targets and
1075
+ same-key pairing are ignored there (dev builds warn) and the child aligns to
1076
+ the parent's rails slot by slot. Give both tables the same `v-model:order`
1077
+ when they share keys and they stay paired. Never reorder (or share the order
1078
+ with) a snapped child itself — dev builds warn.
1079
+
1080
+ Apply the model synchronously in your handler and persist afterwards: an
1081
+ `await` before the write moves the cell in a later flush, after the table
1082
+ has re-focused the handle, and keyboard focus is usually lost (whether it
1083
+ survives depends on which cell Vue's keyed patch detaches).
1084
+
781
1085
  ### Selection and bulk actions
782
1086
 
783
1087
  Add `selectable` for multi-select (`multiple` is the default) bound with
@@ -1143,7 +1447,7 @@ fields.
1143
1447
  Track open rows with `v-model:expanded-items` (an array of row values — set
1144
1448
  `item-value` so expansion is stable across refetches) and render their detail
1145
1449
  through the `#expand` slot, a full-width row below the main one. Trigger it
1146
- from the `#actions` slot (enable the column with `actions`, label it with
1450
+ from the `#actions` slot (the slot creates the actions column; label it with
1147
1451
  `actions-text`): the scope hands you `toggleExpanded` — a **callback** — plus
1148
1452
  the read-only `expanded` flag. Drive open/close through the callback; the
1149
1453
  `expanded` prop is one-way state, not something to mutate.
@@ -1160,7 +1464,6 @@ the read-only `expanded` flag. Drive open/close through the callback; the
1160
1464
  >
1161
1465
  <BbTable
1162
1466
  v-model:expanded-items="expanded"
1163
- actions
1164
1467
  actions-text="Details"
1165
1468
  caption="Invoices"
1166
1469
  :columns="columns"
@@ -1277,39 +1580,161 @@ fixed first track).
1277
1580
  The actions-column toggle remains valid — prefer it when expansion reveals
1278
1581
  _detail about the row_ rather than _children of it_.
1279
1582
 
1583
+ ### Column resize
1584
+
1585
+ `resizable` puts a handle on the trailing edge of every data header:
1586
+ drag it and the column follows live, release to commit; double-click it to go
1587
+ back to the declared width. From the keyboard, **Alt+←/→** resizes the
1588
+ focused header in 10px steps (the header row is a tab stop whenever columns
1589
+ are resizable or reorderable).
1590
+
1591
+ There is no width model on purpose. `width` on the column definition stays
1592
+ the single declared truth, and a user resize is an **override** the table
1593
+ keeps until that column's `width` changes:
1594
+
1595
+ - Each commit fires `@resize:column="(key, width) => …"` **once** with the new width
1596
+ in px — or `null` after a double-click reset.
1597
+ - To persist, write the reported width back into your column definition (a
1598
+ `computed` over your persisted widths). The definition wins whenever it
1599
+ changes, so a programmatic `width` change always takes effect.
1600
+ - Unbound, the override lives for the mount, like an unbound `sort`.
1601
+ - A resized width is always px; `width` still accepts `'20%'` / `'12rem'`
1602
+ for what you declare yourself.
1603
+
1604
+ Headers never wrap. A column you have not sized is never narrower than its
1605
+ own heading — the label is the table's size hint, so an empty or slim column
1606
+ with a long heading still reads on a narrow screen (the table overflows
1607
+ instead). Once a track is a length (a declared `width`, a user resize) the
1608
+ label cannot widen it: squeezed, it ellipsizes and the sort arrow stays; at
1609
+ the narrowest only the arrow (and the handle) survive, and the column is
1610
+ still resizable. The data cells of a resized column clip with an ellipsis
1611
+ instead of spilling into the neighbour (a squeezed track cannot wrap a single
1612
+ word); every other cell keeps wrapping. While a resize runs the whole page
1613
+ shows the resize cursor.
1614
+
1615
+ A resized column becomes an **exact** track; the other columns keep doing
1616
+ what they did — unfrozen ones share the surplus, and a table whose columns
1617
+ are all declared with a `width` keeps sharing it among the un-resized ones —
1618
+ so a filling table stays filled and nothing collapses. **Shift+drag** moves
1619
+ the border instead: the right neighbour absorbs the delta, the total stays
1620
+ put, and both columns are reported. A table that inherits its widths from a
1621
+ parent (`inherit-column-widths`) renders no handles — its tracks are the
1622
+ parent's.
1623
+
1624
+ **Resizable columns reporting widths back into the definition**
1625
+
1626
+ ```vue
1627
+ <template>
1628
+ <div class="flex max-w-2xl flex-col gap-1.5">
1629
+ <div
1630
+ class="overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
1631
+ >
1632
+ <BbTable
1633
+ caption="Invoices"
1634
+ :columns="columns"
1635
+ compact
1636
+ item-value="id"
1637
+ :items="invoices"
1638
+ resizable
1639
+ @resize:column="onResize"
1640
+ />
1641
+ </div>
1642
+ <p class="text-xs text-[color:var(--bb-text-muted)]">
1643
+ Widths: <code>{{ JSON.stringify(widths) }}</code>
1644
+ </p>
1645
+ </div>
1646
+ </template>
1647
+ <script setup lang="ts">
1648
+ import { computed, ref } from 'vue';
1649
+ import { BbTable } from 'bitboss-ui';
1650
+ import type { BbTableColumn } from 'bitboss-ui';
1651
+
1652
+ type Invoice = { id: string; number: string; client: string; amount: number };
1653
+
1654
+ const base: Array<BbTableColumn<Invoice>> = [
1655
+ { key: 'number', label: 'Invoice' },
1656
+ { key: 'client', label: 'Client' },
1657
+ { key: 'amount', label: 'Amount', align: 'right' },
1658
+ ];
1659
+ // Persist THIS (localStorage, a user preference…): key → px.
1660
+ const widths = ref<Record<string, number>>({});
1661
+ // The definition carries the persisted widths; the table treats them as the
1662
+ // declared truth, so nothing jumps after a reload.
1663
+ const columns = computed(() =>
1664
+ base.map((column) => ({ ...column, width: widths.value[column.key] }))
1665
+ );
1666
+ const onResize = (key: string, width: number | null) => {
1667
+ const next = { ...widths.value };
1668
+ if (width === null) delete next[key];
1669
+ else next[key] = width;
1670
+ widths.value = next;
1671
+ };
1672
+ const invoices: Invoice[] = [
1673
+ { id: '1', number: 'INV-1041', client: 'Northwind Traders', amount: 4200 },
1674
+ { id: '2', number: 'INV-1042', client: 'Globex', amount: 980 },
1675
+ { id: '3', number: 'INV-1043', client: 'Initech', amount: 15600 },
1676
+ ];
1677
+ </script>
1678
+ ```
1679
+
1280
1680
  ### Nested tables that align with the parent
1281
1681
 
1282
1682
  A `BbTable` rendered inside another's `#expand` slot is independent by
1283
1683
  default. Add `inherit-column-widths` and it snaps its columns to the parent's
1284
1684
  tracks, so a line-items breakdown reads as a continuation of the invoice row
1285
- above it (this also forces fixed layout). The mechanics:
1685
+ above it. The mechanics:
1286
1686
 
1287
1687
  - `inherit-column-widths` — `true` inherits from the nearest ancestor table;
1288
1688
  a string targets a specific ancestor's `id` (useful when an intermediate
1289
1689
  table has a different column count).
1290
- - By default child columns map one-to-one onto the parent's data columns. A
1291
- column's `snap` remaps it: a number is a start track (`2` occupies the
1292
- parent's third data region), `[start, end]` spans a range (fractions
1293
- allowed; `end: -1` runs to the end of the data region). Structural columns
1294
- (`select`, actions) are accounted for automatically.
1690
+ - By default a child column pairs with the parent column of the **same key**,
1691
+ and a column the parent lacks flows into the next parent track no other
1692
+ column claimed. A column's `snap` names the parent column instead:
1693
+ `snap: 'client'` takes that track, `['client', 'amount']` spans from the
1694
+ start of one through the end of the other, and the parent's structural
1695
+ columns are targets by name (`'select'`, `'actions'` — `['name',
1696
+ 'actions']` runs a column across the parent's actions cell too). A
1697
+ trailing `.digits` on a key in a pair is a fraction of that track
1698
+ (`['a', 'b.5']` = all of `a` plus half of `b`). Snaps and keys resolve
1699
+ against the parent's **rendered** columns: a target the parent hides (or
1700
+ never had) makes the column flow like an unsnapped one; a snap that lands
1701
+ before the previous column's cannot tile and flows too (dev builds warn);
1702
+ under a parent that reorders, snaps are ignored (see Column order).
1295
1703
  - 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 regionright-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).
1704
+ overflows into the parent's scroll (see below); with **fewer**, the leading
1705
+ columns stay 1:1, the actions column inherits the parent's actions width,
1706
+ and the last content column runs across every remaining parent track so
1707
+ both tables end flush (a right-aligned total lands under the parent's last
1708
+ column). Give that last column a `width` or an explicit `[start, end]`
1709
+ snap only if you want it to stop short the leftover then joins the
1710
+ actions region instead. A column with nothing free left to flow into is an
1711
+ overflow column too.
1712
+ - **An inheriting child does not scroll on its own — the parent's scrollport
1713
+ is the only one.** That is what keeps the two grids locked together: a child
1714
+ with its own scroll position would fall out of alignment the moment either
1715
+ one moved, which is the whole thing `inherit-column-widths` exists to
1716
+ prevent. One scrollbar moves both.
1717
+ - **The parent's `selectable`/`actions` columns are handled for you** — the
1718
+ child's content columns pair with the parent's content columns by key (no
1719
+ `snap` needed for that), and the parent's select/actions _regions_ are reserved on
1720
+ each side: a child with its own select or actions column widens that cell
1721
+ to the parent's (two equal cells, controls at the row's end, so the ⌃ sits
1722
+ under the parent's); a child without one folds the region into its first/
1723
+ last cell as _padding_, so its content stays flush with the parent's data
1724
+ grid on both edges (the inset reads as hierarchy). Only a child cell
1725
+ _wider_ than the parent's — a select the parent lacks, more buttons — moves
1726
+ anything, and only the adjacent content column: it shrinks by the surplus
1727
+ so every other rail holds. What a child publishes to its own children is
1728
+ the same contract (content rails + regions), so a three-level nest aligns
1729
+ at every depth.
1308
1730
  - **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.
1731
+ bare `:deep([role='cell']:first-child)` also matches the nested table's
1732
+ cells and clobbers the offset padding it computes to stay aligned — use the
1733
+ direct-child chain through the grid's containers
1734
+ (`:deep(> .bb-table__table > .bb-table__body > [role='row'] > :first-child)`)
1735
+ so the child, which lives inside `.bb-table-expand__cell`, keeps its
1736
+ structural padding. The canonical `.table-flush` idiom is under **Styling
1737
+ hooks**.
1313
1738
 
1314
1739
  **Invoice lines aligned under the invoice columns**
1315
1740
 
@@ -1323,7 +1748,6 @@ above it (this also forces fixed layout). The mechanics:
1323
1748
  >
1324
1749
  <BbTable
1325
1750
  v-model:expanded-items="expanded"
1326
- actions
1327
1751
  actions-text="Line items"
1328
1752
  caption="Invoices"
1329
1753
  :columns="columns"
@@ -1419,6 +1843,50 @@ const expanded = ref<string[]>([]);
1419
1843
  </script>
1420
1844
  ```
1421
1845
 
1846
+ #### Affixed actions on both tables
1847
+
1848
+ A nested table can pin its own actions column with `fixed-actions` — child
1849
+ rows usually need their own row actions — and both action columns pin to the
1850
+ same visible edge:
1851
+
1852
+ ```vue
1853
+ <BbTable
1854
+ id="programmes"
1855
+ :columns="columns"
1856
+ fixed-actions
1857
+ :items="rows"
1858
+ item-value="id"
1859
+ >
1860
+ <template #actions="{ item }">…</template>
1861
+ <template #expand="{ item }">
1862
+ <BbTable
1863
+ :columns="childColumns"
1864
+ fixed-actions
1865
+ inherit-column-widths="programmes"
1866
+ :items="item.children"
1867
+ item-value="id"
1868
+ >
1869
+ <template #actions="{ item }">…</template>
1870
+ </BbTable>
1871
+ </template>
1872
+ </BbTable>
1873
+ ```
1874
+
1875
+ This works because the child shares the parent's scrollport. `position: sticky`
1876
+ resolves against the nearest scrolling ancestor, and the child's own box is as
1877
+ wide as the parent's **content** — so if it opened a scrollport of its own, the
1878
+ child's actions would affix to an edge sitting out in the overflow, hundreds of
1879
+ pixels past what anyone can see, while the parent's pinned correctly. The
1880
+ child's actions column inherits the parent's actions region by itself (the
1881
+ structural rule above), so the two columns line up rather than merely both
1882
+ being pinned.
1883
+
1884
+ A pinned cell is opaque — it paints the table's `--bg` surface because it
1885
+ slides over its siblings — so a nested table sitting on a tinted expand row
1886
+ (`--bb-muted` is the usual choice) shows white pinned actions unless you tell
1887
+ it which surface it is on: `.nest .bb-table { --bg: var(--bb-muted); }`. Same
1888
+ rule as any table on a non-panel surface (see **Styling hooks**).
1889
+
1422
1890
  #### Flush tables + inherited widths: mind the edge padding
1423
1891
 
1424
1892
  `table-flush` removes the outer cell padding so the table reads edge-to-edge in
@@ -1432,9 +1900,10 @@ Two rules keep them honest:
1432
1900
  1. **Snap against the same base you paint.** Measure the parent's columns with
1433
1901
  the same flush/padded state the child renders in — never measure padded and
1434
1902
  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.
1903
+ 2. **Never add nest padding on the body only.** Padding the child's body
1904
+ (`.bb-table__body`) but not its header (or vice versa) reintroduces exactly
1905
+ the offset the snap just removed. Put the inset on the nest container, where
1906
+ both row groups inherit it.
1438
1907
 
1439
1908
  ### Row highlight (master–detail)
1440
1909
 
@@ -1447,11 +1916,42 @@ clicks are ignored on purpose: those inside interactive elements (links,
1447
1916
  buttons, inputs, labels keep their own semantics) and those ending a text
1448
1917
  selection (dragging to copy a cell must not mutate state).
1449
1918
 
1919
+ **Highlight is opt-in, and the model's value is the switch.** Bind nothing and
1920
+ the mechanic is entirely off — a row click emits `click:row` and does nothing
1921
+ else: no class, no `aria-current`, no state. That matters because the table has
1922
+ nowhere to put a highlight it invents; a table that highlighted itself would
1923
+ drive a cursor the app can neither read nor clear.
1924
+
1925
+ The switch reads the value, not the binding: **`undefined` means "never
1926
+ initialised" and keeps highlight off; `null` means "initialised and currently
1927
+ empty" and turns it on.** So initialise the model:
1928
+
1929
+ ```ts
1930
+ const highlighted = ref(null); // ✅ connected
1931
+ const highlighted = ref(); // ❌ undefined — highlight stays off (warns in dev)
1932
+ ```
1933
+
1934
+ A value seeded through the shared context counts exactly the same, and binds
1935
+ two ways just like the prop:
1936
+
1937
+ ```ts
1938
+ const highlighted = ref(null);
1939
+ useBbTableContext('invoices-table', { highlighted });
1940
+ ```
1941
+
1942
+ `click:row` is never gated — it fires on every row click whether or not
1943
+ highlight is connected, so listening for clicks costs you nothing. Calling
1944
+ `toggleHighlighted` from a cell slot on an unconnected table does nothing and
1945
+ warns once in dev.
1946
+
1450
1947
  Highlight ships with a **default cursor**: the row gets `aria-current="true"`
1451
1948
  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;
1949
+ paints that class with a quiet neutral fill plus a left accent bar in your
1950
+ brand primary — distinct from hover and from a plain focus ring, and readable
1951
+ in both light and dark. The fill is `--bb-muted` (the shared "selected/active"
1952
+ stop on the neutral surface ladder, one step above the `--bb-surface-hover`
1953
+ used for row hover) and steps to `--bb-pressed` while hovered, so it never
1954
+ 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
1955
  sighted users now get a visible cursor out of the box too, including once
1456
1956
  `keyboard-navigation` is on and the roving tabindex needs to be seen. Override
1457
1957
  it with `row-class` (or equivalent visible styling) whenever the default
@@ -1636,9 +2136,11 @@ const rowClass = (event: AuditEvent) =>
1636
2136
  ```
1637
2137
 
1638
2138
  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
2139
+ `(event, item)` the native event and **your record exactly as you passed it
2140
+ in `items`**, nothing else. No wrapper to unpack, no third argument; selection
2141
+ state is already yours through the `v-model`, and the row's value resolves
2142
+ from the record through `item-value` exactly as the models do. Reserve these
2143
+ events for side effects that are not navigation —
1642
2144
  logging, opening a drawer (though `v-model:highlighted` usually covers that
1643
2145
  without any handler).
1644
2146
 
@@ -1665,7 +2167,6 @@ open-in-new-tab for free.
1665
2167
  class="max-w-2xl overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
1666
2168
  >
1667
2169
  <BbTable
1668
- actions
1669
2170
  actions-text="Actions"
1670
2171
  caption="Invoices"
1671
2172
  :columns="columns"
@@ -2001,12 +2502,16 @@ canonical palette lives in the
2001
2502
 
2002
2503
  `compact` tightens row padding. `fixed-headers` makes the header stick to the
2003
2504
  top of the nearest scroll container. `fixed` switches to an equal-width fixed
2004
- layout. `fixed-columns` pins columns while the rest scroll horizontally: an
2005
- array of column indices (left-pinned) or `{ index, position: 'left'|'right' }`
2006
- objects; indices count rendered columns (a selection column is index 0), and
2007
- 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.
2505
+ layout. Pinned columns (next section) stick while the rest scroll
2506
+ horizontally. When content can't
2507
+ fit, the table keeps every column at its measured minimum its header label
2508
+ included and overflows its container instead of clipping; this is by
2509
+ design, not a bug to work around. Note that truncating cells yourself
2510
+ (`overflow: hidden` + `text-overflow: ellipsis` via `tdClass`) removes that
2511
+ column's content floor — only the label floors it then, and the grid pads
2512
+ every column equally with whatever is left — so declare a `width` on the
2513
+ columns you truncate, or truncate with `white-space: nowrap` alone and let
2514
+ the table scroll.
2010
2515
 
2011
2516
  **The consumer overflow contract has two parts.** Wrap the table in an
2012
2517
  `overflow-x: auto` element (below), **and** make sure that wrapper is actually
@@ -2049,6 +2554,144 @@ your wrapper actually sits on before sprinkling `min-width: 0` everywhere.)
2049
2554
  </div>
2050
2555
  ```
2051
2556
 
2557
+ ### Pinned columns
2558
+
2559
+ `fixed: 'left' | 'right'` on a column definition makes its cells
2560
+ `position: sticky` on that side — in place, at the column's own render
2561
+ position, like a spreadsheet's freeze panes. Pin a contiguous run from the
2562
+ edge and the offsets accumulate (the second left-pinned column sticks right
2563
+ after the first). The two structural columns have no definition to carry the
2564
+ field, so they pin through the table: `fixed-select` for the selection
2565
+ column and `fixed-actions` for the actions column (each has one sensible
2566
+ side, so both are booleans).
2567
+
2568
+ ```vue
2569
+ <BbTable :columns="columns" fixed-actions fixed-select :items="rows" selectable>
2570
+ <template #actions="{ item }">…</template>
2571
+ </BbTable>
2572
+ ```
2573
+
2574
+ ```ts
2575
+ const columns = [
2576
+ { key: 'name', label: 'Name', fixed: 'left' },
2577
+ { key: 'region', label: 'Region' },
2578
+ { key: 'amount', label: 'Amount', align: 'right' },
2579
+ ];
2580
+ ```
2581
+
2582
+ Three rules:
2583
+
2584
+ - **The pin is the column's.** Under `order` / `reorderable` it travels with
2585
+ the cell, and a `hidden` pinned column pins nothing. The offset is still
2586
+ positional, so a lone left-pinned column dragged to the middle sticks at
2587
+ `left: 0` and slides over the columns before it — by design; lock the
2588
+ pinned columns in place by normalizing `@update:order` if you need to.
2589
+ - **Pinned cells are opaque** — they paint the table's `--bg` (see Styling
2590
+ hooks), so a table on a tinted surface needs `--bg` set once.
2591
+ - **Nested tables pin against the ancestor scrollport.** A child that
2592
+ inherits the parent's tracks has no scrollport of its own, so both tables'
2593
+ pins land on the same visible edge (see Nested tables).
2594
+
2595
+ ### Virtual rows
2596
+
2597
+ `virtual` windows the body: only the rows in the vertical scrollport (plus a
2598
+ few on each side) are mounted, and two spacer rows hold the scrollport at the
2599
+ full list height, so 5,000 or 50,000 rows scroll at the cost of a few dozen.
2600
+ Everything else is the same table — the rows stay in flow in the grid, so the
2601
+ stuck header, pinned columns, nested tables, selection, highlight and the
2602
+ keyboard grammar work untouched. One prop, no knobs.
2603
+
2604
+ - **Give it a scroller.** Bound the table's height (`max-height` / `height`
2605
+ on the table, or a flex item with `min-height: 0`) and the table itself
2606
+ scrolls its rows; left unbounded it grows with the page and the page (or
2607
+ the nearest scrolling ancestor) is the scroller. `virtual` implies a stuck
2608
+ header — a windowed body without column names is a scroller, not a table.
2609
+ - **Columns are sized once.** A windowed body only ever holds a few rows, so
2610
+ `auto` tracks would move as rows come and go. The first rendered batch is
2611
+ measured and its widths become floors (`minmax(px, 1fr)`): they still share
2612
+ surplus, later rows never widen them — and the cells of those columns are
2613
+ single-line and **clip with an ellipsis** (like a resized column) so a
2614
+ longer value that shows up later never spills into its neighbour. Want a
2615
+ column to wrap (prose, notes)? Declare a `width` on it: a declared width
2616
+ wins as always and keeps wrapping. A resize overrides as always, and an
2617
+ empty→non-empty transition (a refetch) re-measures from the new first
2618
+ batch.
2619
+ - **A fast wheel never shows blank space.** A band of skeleton rows is kept
2620
+ rendered beyond the window on both sides (cheap: empty cells at the
2621
+ measured row height), so a wheel that the compositor scrolls ahead of the
2622
+ main thread reveals skeletons, not blank spacer, and they turn into real
2623
+ rows as the window reaches them. While the scroll is fast, rows never
2624
+ mounted before stay skeletons and are swapped for real rows as soon as
2625
+ it slows; rows you have already seen stay real. A moderate scroll never
2626
+ sees a skeleton.
2627
+ - **Dynamic heights are fine.** Every mounted row is measured (wrapping
2628
+ cells, a "show more", an expand row that opens a nested table three levels
2629
+ deep), measurements are cached by row so a row seen once stays right, and a
2630
+ height change _above_ the viewport moves the scroll offset with it so what
2631
+ you are reading does not jump.
2632
+ - **Focus is never lost to an unmount.** The row (or expand row) that owns
2633
+ focus stays mounted however far it scrolls — a control inside a nested
2634
+ table included — and is released on blur. Arrow keys, Home/End and
2635
+ PageUp/PageDown work across the whole list (the target row is scrolled in,
2636
+ mounted, then focused).
2637
+ - **Nested tables are not windowed.** A nested table inside an expand row is
2638
+ one tall row to the virtualizer; give a huge nested dataset its own page.
2639
+ `virtual` is ignored on a table that inherits its widths, and under a
2640
+ `#tbody` slot.
2641
+ - **Rows keep their absolute position for AT** (`aria-rowcount` /
2642
+ `aria-rowindex`), page or no page — a virtual table is one page unless you
2643
+ say otherwise.
2644
+
2645
+ **5,000 rows in a 320px scroller**
2646
+
2647
+ ```vue
2648
+ <template>
2649
+ <!-- Bounding the table's height is what makes it the scroller of its own
2650
+ rows; `virtual` windows them and implies the stuck header. -->
2651
+ <div
2652
+ class="max-w-2xl overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
2653
+ >
2654
+ <BbTable
2655
+ caption="Ledger"
2656
+ class="max-h-80"
2657
+ :columns="columns"
2658
+ compact
2659
+ item-value="id"
2660
+ :items="rows"
2661
+ virtual
2662
+ />
2663
+ </div>
2664
+ </template>
2665
+ <script setup lang="ts">
2666
+ import { BbTable } from 'bitboss-ui';
2667
+ import type { BbTableColumn } from 'bitboss-ui';
2668
+
2669
+ type Entry = {
2670
+ id: number;
2671
+ number: string;
2672
+ client: string;
2673
+ issued: string;
2674
+ amount: string;
2675
+ };
2676
+
2677
+ const columns: Array<BbTableColumn<Entry>> = [
2678
+ { key: 'number', label: 'Invoice', fixed: 'left' },
2679
+ { key: 'client', label: 'Client' },
2680
+ { key: 'issued', label: 'Issued' },
2681
+ { key: 'amount', label: 'Amount', align: 'right' },
2682
+ ];
2683
+
2684
+ const clients = ['Northwind Traders', 'Globex', 'Initech', 'Umbrella', 'Hooli'];
2685
+ const rows: Entry[] = Array.from({ length: 5000 }, (_, i) => ({
2686
+ id: i,
2687
+ number: `INV-${10000 + i}`,
2688
+ client: clients[i % clients.length],
2689
+ issued: `2026-${String(1 + (i % 12)).padStart(2, '0')}-${String(1 + (i % 28)).padStart(2, '0')}`,
2690
+ amount: `€${((i * 37) % 9000) + 100}`,
2691
+ }));
2692
+ </script>
2693
+ ```
2694
+
2052
2695
  ### Narrow screens: hide columns, don't scroll everything
2053
2696
 
2054
2697
  An ops table with ten columns is unusable on a phone as a horizontal scroller —
@@ -2057,10 +2700,14 @@ leave the screen. Collapse instead of scroll:
2057
2700
 
2058
2701
  - **Rank the columns.** Identity + status + the one number that matters stay;
2059
2702
  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.
2703
+ - **Flag the secondary ones `hidden` at the breakpoint** a `computed`
2704
+ over `useMobile()` (or your own `matchMedia`) that maps the definitions
2705
+ with `hidden: isMobile && secondary.has(key)`. A hidden column has no grid
2706
+ track. Do **not** hide cells with `thClass`/`tdClass` + `display: none`:
2707
+ the table is a grid, a hidden cell leaves its track in place and every
2708
+ following cell shifts one track over. The hidden column stays declared, so
2709
+ it keeps its slot in `order` and returns where it was when the screen
2710
+ widens — never filter the order array.
2064
2711
  - **Stack what survives into the primary cell.** The identity slot renders the
2065
2712
  name plus the hidden facts under it, at `--bb-text-muted`, so nothing is
2066
2713
  actually lost.
@@ -2069,6 +2716,132 @@ Keep the sticky/pinned identity column for the case where the full grid really
2069
2716
  must stay reachable (a spreadsheet-shaped tool), not as the default for a
2070
2717
  records list.
2071
2718
 
2719
+ ### Styling hooks
2720
+
2721
+ **The table's surface is `--bg`** (default `var(--bb-panel)`), a local on
2722
+ `.bb-table`. Cells are transparent at rest so the row can tint them, but
2723
+ anything that slides over scrolled content — the stuck header band with
2724
+ `fixed-headers`, and pinned (`fixed`) cells — is opaque and paints `--bg` (a
2725
+ pinned cell also picks up its row's hover/highlight tint, so it never reads
2726
+ as a hole). When a table sits on a surface that is not the panel colour, set
2727
+ the one knob and everything pinned follows:
2728
+
2729
+ ```css
2730
+ .my-card .bb-table {
2731
+ --bg: var(--card-bg);
2732
+ }
2733
+ ```
2734
+
2735
+ The table is a **CSS grid of `<div>`s with ARIA roles, not a `<table>`**: the
2736
+ root `.bb-table__table[role=table]` owns the column tracks and every row group
2737
+ and row is a subgrid of it. Style it through the classes and the roles — never
2738
+ through element names, which match nothing:
2739
+
2740
+ | Region | Class | Role |
2741
+ | -------------- | -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
2742
+ | grid root | `.bb-table__table` | `table` |
2743
+ | header group | `.bb-table__head` | `rowgroup` |
2744
+ | header row | `.bb-table-header-row` | `row` |
2745
+ | header cell | `.bb-table-header` (+ `--select`, `--actions`) | `columnheader` |
2746
+ | body group | `.bb-table__body` | `rowgroup` |
2747
+ | data row | `.bb-table-data__row` (+ `--highlighted`) | `row` |
2748
+ | data cell | `.bb-table-data__cell` (+ `--select`) | `cell` |
2749
+ | actions cell | `.bb-table__cell--actions` | `cell` |
2750
+ | expand row | `.bb-table-expand__row` > `.bb-table-expand__cell` | `row` > `cell` |
2751
+ | empty state | `.bb-table-no-data__row` > `.bb-table-no-data__cell` | `row` > `cell` |
2752
+ | footer group | `.bb-table__foot` (`#tfoot` content) | `rowgroup` only when `#tfoot` is provided |
2753
+ | focused header | `.bb-table-header[data-column-key]:focus-visible` (`reorderable` only; inset ring, the header row is one roving tab stop) | `columnheader` |
2754
+ | dragged column | `.bb-table-header--dragging` on the header; `.bb-table__head--dragging` on the head group during a drag | — |
2755
+ | 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` |
2756
+ | virtual table | `.bb-table--virtual` on the container (`overflow: auto`, the scroller once its height is bounded) | — |
2757
+ | spacer row | `.bb-table-spacer__row` (a `virtual` body's stand-in for unmounted rows, `aria-hidden`; never style its height) | — (`aria-hidden`) |
2758
+ | 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`) |
2759
+ | drag chip | `.bb-table-column-ghost` (a `popover="manual"` in the top layer; paints `--bb-panel`, not the table's `--bg`) | — (`aria-hidden`) |
2760
+ | caption | `.bb-table-caption` (before the root, `aria-labelledby`) | — |
2761
+
2762
+ `[role='row']`, `[role='cell']` and `[role='columnheader']` are supported
2763
+ hooks, not incidental markup — use them when you mean "every row kind" or
2764
+ "every cell"; use the classes when you mean one kind. Cells are direct
2765
+ children of their row, so `:first-child` / `:last-child` / `:nth-child(n)`
2766
+ address columns.
2767
+
2768
+ The full-bleed idiom every workspace page reaches for — the table bleeds past
2769
+ the content padding and hands the inset back to its outermost cells — is
2770
+ written with a **direct-child chain** so a nested table inheriting the
2771
+ parent's tracks (it lives inside `.bb-table-expand__cell`) is never touched:
2772
+
2773
+ ```css
2774
+ .table-flush {
2775
+ --page-inset: 1rem;
2776
+ margin-inline: calc(var(--page-inset) * -1);
2777
+ width: calc(100% + var(--page-inset) * 2);
2778
+ }
2779
+
2780
+ .table-flush
2781
+ :deep(> .bb-table__table > .bb-table__head > [role='row'] > :first-child),
2782
+ .table-flush
2783
+ :deep(
2784
+ > .bb-table__table
2785
+ > .bb-table__body
2786
+ > [role='row']
2787
+ > :first-child:not(.bb-table-expand__cell)
2788
+ ) {
2789
+ padding-inline-start: var(--page-inset);
2790
+ }
2791
+
2792
+ .table-flush
2793
+ :deep(> .bb-table__table > .bb-table__head > [role='row'] > :last-child),
2794
+ .table-flush
2795
+ :deep(
2796
+ > .bb-table__table
2797
+ > .bb-table__body
2798
+ > [role='row']
2799
+ > :last-child:not(.bb-table-expand__cell)
2800
+ ) {
2801
+ padding-inline-end: var(--page-inset);
2802
+ }
2803
+
2804
+ /* Expand rows stay edge-to-edge so the nested table can snap onto the parent
2805
+ tracks without an extra inset shifting it sideways. */
2806
+ .table-flush
2807
+ :deep(
2808
+ > .bb-table__table > .bb-table__body > [role='row'] > .bb-table-expand__cell
2809
+ ) {
2810
+ padding-inline: 0;
2811
+ }
2812
+ ```
2813
+
2814
+ Leave the geometry alone: `display: grid` / `gap: 0` on the root, row groups
2815
+ and rows, `grid-column: 1 / -1` **and `contain: inline-size`** on the
2816
+ full-width cells (without the containment, expand-row content resizes the
2817
+ parent's columns), `white-space: nowrap` on the actions cell (it sizes the
2818
+ actions track), and `position: sticky` on `.bb-table__head` under
2819
+ `fixed-headers`.
2820
+
2821
+ When you replace a region with `#thead` / `#tbody` / `#tfoot`, ship the same
2822
+ shape the table does — role-complete rows, spanning with `grid-column` rather
2823
+ than `colspan`:
2824
+
2825
+ ```vue
2826
+ <template #tfoot="{ columnCount }">
2827
+ <div role="row">
2828
+ <div
2829
+ role="cell"
2830
+ :aria-colspan="columnCount - 1"
2831
+ :style="{ gridColumn: `span ${columnCount - 1}` }"
2832
+ >
2833
+ Total
2834
+ </div>
2835
+ <div role="cell" class="text-right">{{ total }}</div>
2836
+ </div>
2837
+ </template>
2838
+ ```
2839
+
2840
+ A `<tr>` inside the grid is not merely unsemantic — the browser wraps it in an
2841
+ implicit `<table>` and the cells never land on the tracks. Nothing lints the
2842
+ roles: a `<div>` row without them renders fine and is invisible to a screen
2843
+ reader.
2844
+
2072
2845
  ### Config and platform
2073
2846
 
2074
2847
  - **Locale.** The plugin's `locale` (`it`/`en`) drives the built-in strings:
@@ -2080,10 +2853,13 @@ records list.
2080
2853
  plugin is still on the default locale, which is `it` — set the plugin's
2081
2854
  `locale` option, or override per table with `actions-text` (it is visually
2082
2855
  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.
2856
+ - **SSR / Nuxt.** Under `bitboss-ui/nuxt` a provider `items` function
2857
+ participates in `onServerPrefetch` and its rows travel in the Nuxt payload, so
2858
+ the first page renders server-side and hydrates without re-fetching. On other
2859
+ SSR hosts the provider runs on the client only and the server renders the
2860
+ empty table — see [SSR prefill](./guides/ssr-prefill.md). The table context
2861
+ (and thus `table-id` pagination wiring) is client-side only — hydration-time
2862
+ onward.
2087
2863
  - **Forms.** With `name`, the selection submits as hidden inputs — no JS
2088
2864
  needed between the table and a plain `<form>` POST (Inertia included).
2089
2865
 
@@ -2092,6 +2868,9 @@ records list.
2092
2868
  - `BbPagination` — shared page ref, or zero-wiring via `id` + `table-id`.
2093
2869
  - `useBbTableContext` (`./composables/useBbTableContext.md`) — bulk-action
2094
2870
  toolbars, detail panels, "reset sort" buttons living outside the table.
2871
+ (Column order and visibility are not mirrored there: a column panel writes
2872
+ `v-model:order` and the `hidden` flags, so it lives where `columns` is
2873
+ built.)
2095
2874
  - `BbButton` — per-row links (`href`/`to`) in cells or `#actions`;
2096
2875
  `BbDropdown` for an overflow menu of row actions.
2097
2876
  - `BbBadge` / `BbAvatar` — status and identity cells via slots.
@@ -2114,12 +2893,25 @@ records list.
2114
2893
  localized `common.loadingText`, so an unset value still announces.
2115
2894
  - Row selection labels default to the localized "select" word plus every
2116
2895
  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.
2896
+ `(columns, item)` with the mapped cells (in **render** order look them up
2897
+ by `key`, never by position) and the raw record, and whatever it returns
2898
+ becomes the checkbox's label.
2899
+ - A pin (`column.fixed`) travels with its column under `order`, but its
2900
+ offset is positional: dragged away from the edge, a pinned column sticks
2901
+ at `left: 0` and slides over the columns before it (see Pinned columns).
2902
+ - The actions column has no prop: the `#actions` slot creates it. A
2903
+ conditional slot (`<template v-if="…" #actions>`) adds and removes the
2904
+ column live.
2905
+ - A column panel that holds a partial `order` receives the completed
2906
+ array on the user's first move — its persisted list grows to every declared
2907
+ key, `hidden` ones included. Hide with the flag, not by filtering
2908
+ `columns`: a filtered column is undeclared, so a partial model completes
2909
+ without it and the key is gone from the persisted order.
2119
2910
  - Enabling select-all does not re-emit an empty `modelValue` (see the
2120
2911
  select-all section) — in all-mode, read `select-all` +
2121
2912
  `unselected-items`, not `modelValue`.
2122
- - Row event payloads are the internal row wrapper your record is `row.item`.
2913
+ - Row event payloads are `(event, item)` with `item` your raw record — there is
2914
+ no wrapper to unpack and no `selected` argument.
2123
2915
  - A mistyped context/pagination `id` does not error; it silently syncs with
2124
2916
  nothing. If a toolbar "works" but the table never reacts, check the id.
2125
2917
  - In multiple mode `modelValue` must be an array, or it throws.
@@ -2154,6 +2946,15 @@ records list.
2154
2946
  identity rather than object reference.
2155
2947
  - Sortable headers set `aria-sort` and render real buttons; custom header
2156
2948
  slots should keep a button calling `toggleSort`.
2949
+ - With `reorderable` the header row becomes one roving tab stop
2950
+ (the AG Grid grammar): ←/→ move focus between headers, Home/End to the
2951
+ ends, **Shift+←/→** move the focused column one slot (Shift+Home/End to
2952
+ the ends), Enter/Space sorts a sortable header — every move committed
2953
+ immediately and announced through a polite status region ("X moved to
2954
+ position 2 of 5"); a one-line sr-only instruction describes each header.
2955
+ The sort button leaves the tab order (the header is the target; it still
2956
+ works for the mouse). NVDA/JAWS browse mode intercepts arrows on a focused
2957
+ element — the same focus-mode step as row keyboard navigation.
2157
2958
  - When you paginate, pass `page` / `per-page` / `total-items` so the table
2158
2959
  announces the full result count (`aria-rowcount`) and each row's absolute
2159
2960
  index (`aria-rowindex`, expansion-aware), not just the current page.
@@ -2173,6 +2974,7 @@ records list.
2173
2974
  - `v-model:expandedItems` — type: `any[] \| undefined`
2174
2975
  - `v-model:highlighted` — type: `any`
2175
2976
  - `v-model` — type: `any`
2977
+ - `v-model:order` — type: `string[] \| undefined`
2176
2978
  - `v-model:selectAll` — type: `boolean \| undefined`
2177
2979
  - `v-model:sort` — type: `BbTableSortEntry[] \| undefined`
2178
2980
  - `v-model:unselectedItems` — type: `any[] \| undefined`
@@ -2181,80 +2983,89 @@ records list.
2181
2983
 
2182
2984
  | Prop | Type | Default | Required | Description |
2183
2985
  | --- | --- | --- | --- | --- |
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. |
2986
+ | `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`… |
2186
2987
  | `actionsText` | `string \| undefined` | | | Label used in the header of the actions column. |
2187
2988
  | `align` | `"left" \| "center" \| "right" \| undefined` | `"left"` | | Text alignment of the columns. |
2188
2989
  | `caption` | `string \| undefined` | | | Caption that describes the content of the table. Used for accessibility purposes. |
2189
2990
  | `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. |
2991
+ | `compact` | `boolean \| undefined` | `false` | | Adds a CSS class that applies a compact style to the component. |
2191
2992
  | `dependencies` | `unknown[] \| undefined` | `[]` | | Defines an array of dependencies that will trigger actions in the component upon change. |
2192
2993
  | `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… |
2994
+ | `disabled` | `boolean \| undefined` | `false` | | Disables the component |
2995
+ | `disableSelectAll` | `boolean \| undefined` | `false` | | Hides the "Select all" checkbox that selectable multi-row tables display by default. |
2996
+ | `displayCaption` | `boolean \| undefined` | `false` | | Boolean that defines whether to display or hide the caption. By default is true and the caption is hidden. |
2997
+ | `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
2998
  | `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… |
2999
+ | `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. |
3000
+ | `fixedActions` | `boolean \| undefined` | `false` | | Pins the actions column (the one the `#actions` slot creates) to the right edge, `position: sticky`, in place. Data columns pin through `column.fixed`; this is the same feature for the structural column that has no definition to carry it. N… |
3001
+ | `fixedHeaders` | `boolean \| undefined` | `false` | | Boolean that sets the headers as sticky to the top of the table. |
3002
+ | `fixedSelect` | `boolean \| undefined` | `false` | | Pins the selection column to the left edge, `position: sticky`, in place. Data columns pin through `column.fixed`; this is the same feature for the structural column that has no definition to carry it. No effect unless the table is `selecta… |
3003
+ | `headerRowClass` | `Classes \| undefined` | | | Defines the classes to be passed to the header row. |
3004
+ | `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
3005
  | `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 … |
3006
+ | `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… |
3007
+ | `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
3008
  | `items` | `Item[] \| ((prefill: boolean, modelValue?: any) => Item[] \| Promise<Item[]>)` | `[]` | yes | Rows to render, or a fetcher that returns them. |
2207
3009
  | `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 … |
3010
+ | `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
3011
  | `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. |
3012
+ | `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… |
3013
+ | `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
3014
  | `max` | `number \| undefined` | `Infinity` | | Maximum number of items that can be selected. |
2213
3015
  | `modelValue` | `any` | `[]` | | Used by v-model. Can be any serializable type. |
2214
3016
  | `multiple` | `boolean \| undefined` | `true` | | Allows the selection of multiple items. |
2215
3017
  | `name` | `string \| undefined` | | | Defines the name of the input. |
2216
- | `noDataText` | `string \| undefined` | | | String displayed when there are no items to display. |
3018
+ | `noDataText` | `string \| undefined` | `the localized `common.noDataText` ("No data to display")` | | String displayed when there are no items to display. |
3019
+ | `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
3020
  | `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
3021
  | `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. |
3022
+ | `readonly` | `boolean \| undefined` | `false` | | Sets the input in a readonly state. |
3023
+ | `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 … |
3024
+ | `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… |
3025
+ | `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
3026
  | `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
3027
  | `selectAll` | `boolean \| undefined` | `false` | | Defines whether the "Select all" checkbox is checked. |
2223
3028
  | `selectAllLabel` | `string \| undefined` | | | Text of the label used by the "Select all" boolean. |
2224
3029
  | `selectText` | `string \| undefined` | | | Text of the header used for the radio inputs when the table is not `multiple`. |
2225
3030
  | `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>`. |
3031
+ | `tdClass` | `ColumnClasses<Item> \| undefined` | | | Defines the classes to be passed to every data cell. Can also be a function for dynamic values. |
3032
+ | `thClass` | `Classes \| undefined` | | | Defines the classes to be passed to every header cell. |
2228
3033
  | `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
3034
  | `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
3035
  | `unselectedItems` | `any[] \| undefined` | `[]` | | Items that are not selected. |
3036
+ | `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
3037
 
2232
3038
  ## Events
2233
3039
 
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`
3040
+ - `click:row` — `(e: "click:row", event: MouseEvent, item: any): void`
3041
+ - `contextmenu:row` — `(e: "contextmenu:row", event: MouseEvent, item: any): void`
3042
+ - `dblclick:row` — `(e: "dblclick:row", event: MouseEvent, item: any): void`
3043
+ - `hide:column` — `(e: "hide:column", key: string): void` — A column was hidden from its header (a `#header:<key>` / `#header:<key>:append` control calling the slot's `hideColumn()`). Fired once; the table keeps the column hidden for the mount, or until the column's `hidden` flag changes — write `hi…
2237
3044
  - `item:selected` — `(e: "item:selected", value: any): void`
2238
3045
  - `item:unselected` — `(e: "item:unselected", value: any): void`
3046
+ - `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
3047
  - `update:expandedItems` — `(e: "update:expandedItems", value: any[]): void`
2240
3048
  - `update:highlighted` — `(e: "update:highlighted", value: any): void`
2241
3049
  - `update:modelValue` — `(e: "update:modelValue", value: any): void`
3050
+ - `update:order` — `(e: "update:order", value: string[]): void`
2242
3051
  - `update:selectAll` — `(e: "update:selectAll", value: boolean): void`
2243
3052
  - `update:sort` — `(e: "update:sort", value: BbTableSortEntry[]): void`
2244
3053
  - `update:unselectedItems` — `(e: "update:unselectedItems", value: any[]): void`
2245
3054
 
2246
3055
  ## Slots
2247
3056
 
2248
- - `actions` — scope: `{ expanded?: boolean \| undefined; expandProps?: ExpandControllerProps \| undefined; toggleExpanded?: (() => void) \| undefined; highlighted?: boolean \| undefi…` — Content rendered in the actions cell for each row.
3057
+ - `actions` — scope: `{ expanded?: boolean \| undefined; expandProps?: ExpandControllerProps \| undefined; toggleExpanded?: (() => void) \| undefined; highlighted?: boolean \| undefi…` — Content rendered in the actions cell for each row. Providing this slot is what CREATES the actions column (there is no prop): the header cell (labelled by `actions-text`, replaceable through `#header:actions`) and one cell per row appear wi…
2249
3058
  - `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
3059
  - `header:actions` — scope: `{ text: string; }` — Replaces the default actions header cell content.
3060
+ - `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…
3061
+ - `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
3062
  - `header:select` — scope: `{ multiple: boolean; selectAll: boolean; text: string; }` — Replaces the default selection header cell content (checkbox or radio label).
2252
3063
  - `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
3064
+ - `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
3065
  - `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.
3066
+ - `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-…
3067
+ - `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>`.
3068
+ - `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
3069
 
2259
3070
  ## See Also
2260
3071