bitboss-ui 2.1.134 → 3.0.0-alpha.1

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 (1358) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +244 -21
  3. package/bin/bitboss-ui-mcp.mjs +557 -0
  4. package/bin/bitboss-ui.mjs +626 -0
  5. package/dist/ai/BbAccordion.md +522 -113
  6. package/dist/ai/BbAlert.md +377 -197
  7. package/dist/ai/BbAsterisk.md +46 -0
  8. package/dist/ai/BbAvatar.md +454 -178
  9. package/dist/ai/BbBadge.md +695 -225
  10. package/dist/ai/BbBadgeButton.md +54 -0
  11. package/dist/ai/BbBaseButton.md +489 -0
  12. package/dist/ai/BbBaseCheckbox.md +49 -0
  13. package/dist/ai/BbBaseCheckboxGroup.md +64 -0
  14. package/dist/ai/BbBaseCheckboxIcon.md +174 -0
  15. package/dist/ai/BbBaseColorInput.md +64 -0
  16. package/dist/ai/BbBaseColorPalette.md +39 -0
  17. package/dist/ai/BbBaseDatePicker.md +42 -0
  18. package/dist/ai/BbBaseDatePickerInput.md +73 -0
  19. package/dist/ai/BbBaseInputContainer.md +48 -0
  20. package/dist/ai/BbBaseNumberInput.md +71 -0
  21. package/dist/ai/BbBaseRadio.md +46 -0
  22. package/dist/ai/BbBaseRadioGroup.md +63 -0
  23. package/dist/ai/BbBaseRadioIcon.md +187 -0
  24. package/dist/ai/BbBaseRating.md +51 -0
  25. package/dist/ai/BbBaseSelect.md +88 -0
  26. package/dist/ai/BbBaseSlider.md +56 -0
  27. package/dist/ai/BbBaseSwitch.md +49 -0
  28. package/dist/ai/BbBaseSwitchGroup.md +64 -0
  29. package/dist/ai/BbBaseSwitchIcon.md +184 -0
  30. package/dist/ai/BbBaseTag.md +68 -0
  31. package/dist/ai/BbBaseTextInput.md +75 -0
  32. package/dist/ai/BbBaseTextarea.md +62 -0
  33. package/dist/ai/BbBaseTimePickerInput.md +62 -0
  34. package/dist/ai/BbBreadcrumbs.md +542 -503
  35. package/dist/ai/BbButton.md +767 -636
  36. package/dist/ai/BbCheckbox.md +572 -254
  37. package/dist/ai/BbCheckboxGroup.md +561 -361
  38. package/dist/ai/BbCollapsible.md +419 -103
  39. package/dist/ai/BbColorInput.md +529 -323
  40. package/dist/ai/BbColorPalette.md +565 -330
  41. package/dist/ai/BbConfirm.md +612 -136
  42. package/dist/ai/BbConfirmPortal.md +28 -0
  43. package/dist/ai/BbDatePicker.md +244 -0
  44. package/dist/ai/BbDatePickerInput.md +706 -388
  45. package/dist/ai/BbDialog.md +770 -99
  46. package/dist/ai/BbDropdown.md +1054 -763
  47. package/dist/ai/BbDropdownButton.md +511 -615
  48. package/dist/ai/BbDropdownGroup.md +34 -0
  49. package/dist/ai/BbDropzone.md +1148 -436
  50. package/dist/ai/BbForm.md +239 -0
  51. package/dist/ai/BbIcon.md +379 -212
  52. package/dist/ai/BbIndicator.md +283 -0
  53. package/dist/ai/BbNumberInput.md +713 -355
  54. package/dist/ai/BbOffCanvas.md +875 -498
  55. package/dist/ai/BbPagination.md +483 -535
  56. package/dist/ai/BbPopover.md +583 -546
  57. package/dist/ai/BbProgress.md +456 -69
  58. package/dist/ai/BbRadio.md +343 -229
  59. package/dist/ai/BbRadioGroup.md +426 -353
  60. package/dist/ai/BbRating.md +361 -226
  61. package/dist/ai/BbSelect.md +1380 -536
  62. package/dist/ai/BbSelectPopover.md +1240 -1579
  63. package/dist/ai/BbSlider.md +478 -257
  64. package/dist/ai/BbSmoothHeight.md +472 -139
  65. package/dist/ai/BbSpinner.md +718 -131
  66. package/dist/ai/BbSwitch.md +549 -119
  67. package/dist/ai/BbSwitchGroup.md +503 -214
  68. package/dist/ai/BbTable.md +1956 -2358
  69. package/dist/ai/BbTabs.md +1113 -0
  70. package/dist/ai/BbTabsList.md +29 -0
  71. package/dist/ai/BbTabsPanes.md +27 -0
  72. package/dist/ai/BbTabsRoot.md +57 -0
  73. package/dist/ai/BbTag.md +393 -292
  74. package/dist/ai/BbTextInput.md +852 -333
  75. package/dist/ai/BbTextarea.md +836 -255
  76. package/dist/ai/BbTimePicker.md +231 -0
  77. package/dist/ai/BbTimePickerInput.md +196 -0
  78. package/dist/ai/BbToast.md +624 -196
  79. package/dist/ai/BbToastPortal.md +28 -0
  80. package/dist/ai/BbTooltip.md +443 -346
  81. package/dist/ai/BbTree.md +984 -235
  82. package/dist/ai/ChipsBox.md +27 -199
  83. package/dist/ai/ClearableButton.md +21 -55
  84. package/dist/ai/CommaBox.md +25 -200
  85. package/dist/ai/CommonInputWrapper.md +42 -0
  86. package/dist/ai/CommonPopover.md +30 -510
  87. package/dist/ai/ErrorIcon.md +19 -56
  88. package/dist/ai/FlatListBox.md +35 -379
  89. package/dist/ai/GroupedListBox.md +37 -535
  90. package/dist/ai/GuidesComponent.md +21 -0
  91. package/dist/ai/ListBox.md +38 -231
  92. package/dist/ai/OptionsContainer.md +34 -253
  93. package/dist/ai/WarningIcon.md +21 -0
  94. package/dist/ai/components.json +15883 -0
  95. package/dist/ai/composables/useBbConfig.md +80 -0
  96. package/dist/ai/composables/useBbFormContext.md +110 -0
  97. package/dist/ai/composables/useBbTableContext.md +164 -0
  98. package/dist/ai/composables/useBbTabsContext.md +118 -0
  99. package/dist/ai/composables/useBroadcastChannelInstance.md +97 -0
  100. package/dist/ai/composables/useConfirm.md +306 -0
  101. package/dist/ai/composables/useCountdown.md +84 -0
  102. package/dist/ai/composables/useId.md +60 -0
  103. package/dist/ai/composables/useMobile.md +85 -0
  104. package/dist/ai/composables/useToast.md +230 -0
  105. package/dist/ai/guides/agent-contract.md +108 -0
  106. package/dist/ai/guides/ai-router.md +111 -0
  107. package/dist/ai/guides/app-layout.md +295 -0
  108. package/dist/ai/guides/coherence-playbook.md +136 -0
  109. package/dist/ai/guides/component-picker.md +195 -0
  110. package/dist/ai/guides/design-language.md +461 -0
  111. package/dist/ai/guides/design-tokens.md +424 -0
  112. package/dist/ai/guides/fetch-items-playbook.md +161 -0
  113. package/dist/ai/guides/floating-bottom-panel.md +191 -0
  114. package/dist/ai/guides/icons-policy.md +257 -0
  115. package/dist/ai/guides/inertia-helpers.md +1394 -0
  116. package/dist/ai/guides/inline-edit-playbook.md +258 -0
  117. package/dist/ai/guides/installation-and-plugin-setup.md +377 -0
  118. package/dist/ai/guides/migration/components/bb-alert.md +37 -0
  119. package/dist/ai/guides/migration/components/bb-avatar.md +24 -0
  120. package/dist/ai/guides/migration/components/bb-badge.md +110 -0
  121. package/dist/ai/guides/migration/components/bb-breadcrumbs.md +26 -0
  122. package/dist/ai/guides/migration/components/bb-button.md +55 -0
  123. package/dist/ai/guides/migration/components/bb-checkbox-group.md +54 -0
  124. package/dist/ai/guides/migration/components/bb-checkbox.md +45 -0
  125. package/dist/ai/guides/migration/components/bb-chip.md +35 -0
  126. package/dist/ai/guides/migration/components/bb-collapsible.md +26 -0
  127. package/dist/ai/guides/migration/components/bb-color-input.md +23 -0
  128. package/dist/ai/guides/migration/components/bb-color-palette.md +31 -0
  129. package/dist/ai/guides/migration/components/bb-date-picker-input.md +84 -0
  130. package/dist/ai/guides/migration/components/bb-dialog.md +46 -0
  131. package/dist/ai/guides/migration/components/bb-dropdown-button.md +31 -0
  132. package/dist/ai/guides/migration/components/bb-dropdown.md +36 -0
  133. package/dist/ai/guides/migration/components/bb-dropzone.md +27 -0
  134. package/dist/ai/guides/migration/components/bb-icon.md +27 -0
  135. package/dist/ai/guides/migration/components/bb-off-canvas.md +25 -0
  136. package/dist/ai/guides/migration/components/bb-offcanvas.md +54 -0
  137. package/dist/ai/guides/migration/components/bb-pagination.md +24 -0
  138. package/dist/ai/guides/migration/components/bb-popover.md +52 -0
  139. package/dist/ai/guides/migration/components/bb-rating.md +24 -0
  140. package/dist/ai/guides/migration/components/bb-select-popover.md +58 -0
  141. package/dist/ai/guides/migration/components/bb-select.md +68 -0
  142. package/dist/ai/guides/migration/components/bb-slider.md +25 -0
  143. package/dist/ai/guides/migration/components/bb-smooth-height.md +45 -0
  144. package/dist/ai/guides/migration/components/bb-spinner.md +23 -0
  145. package/dist/ai/guides/migration/components/bb-table.md +66 -0
  146. package/dist/ai/guides/migration/components/bb-tabs.md +47 -0
  147. package/dist/ai/guides/migration/components/bb-tag.md +29 -0
  148. package/dist/ai/guides/migration/components/bb-text-input.md +37 -0
  149. package/dist/ai/guides/migration/components/bb-toast.md +47 -0
  150. package/dist/ai/guides/migration/components/bb-tooltip.md +34 -0
  151. package/dist/ai/guides/migration/components/bb-tree.md +77 -0
  152. package/dist/ai/guides/migration/components/use-confirm.md +46 -0
  153. package/dist/ai/guides/migration/v2-to-v3.md +649 -0
  154. package/dist/ai/guides/options-items-playbook.md +222 -0
  155. package/dist/ai/guides/page-shell.md +314 -0
  156. package/dist/ai/guides/validated-forms.md +177 -0
  157. package/dist/ai/index.md +156 -32
  158. package/dist/ai/recipes/inertia/approvals-inbox.md +1598 -0
  159. package/dist/ai/recipes/inertia/inline-edit-workspace.md +1733 -0
  160. package/dist/ai/recipes/inertia/onboarding.md +1765 -0
  161. package/dist/ai/recipes/inertia/ownership-atlas.md +1281 -0
  162. package/dist/ai/recipes/inertia/record-form.md +1442 -0
  163. package/dist/ai/recipes/inertia/records-workspace.md +1328 -0
  164. package/dist/ai/recipes/inertia/upload-center.md +1926 -0
  165. package/dist/ai/recipes/inertia/wizard-form.md +1477 -0
  166. package/dist/ai/recipes/nuxt/approvals-inbox.md +1561 -0
  167. package/dist/ai/recipes/nuxt/inline-edit-workspace.md +1690 -0
  168. package/dist/ai/recipes/nuxt/onboarding.md +1698 -0
  169. package/dist/ai/recipes/nuxt/record-form.md +1456 -0
  170. package/dist/ai/recipes/nuxt/records-workspace.md +1314 -0
  171. package/dist/ai/recipes/nuxt/upload-center.md +1847 -0
  172. package/dist/ai/recipes/nuxt/wizard-form.md +1486 -0
  173. package/dist/ai/recipes/vue/approvals-inbox.md +1479 -0
  174. package/dist/ai/recipes/vue/inline-edit-workspace.md +1581 -0
  175. package/dist/ai/recipes/vue/onboarding.md +1653 -0
  176. package/dist/ai/recipes/vue/record-form.md +1540 -0
  177. package/dist/ai/recipes/vue/records-workspace.md +1230 -0
  178. package/dist/ai/recipes/vue/upload-center.md +1806 -0
  179. package/dist/ai/recipes/vue/wizard-form.md +1418 -0
  180. package/dist/ai/source/BbAccordion.md +122 -0
  181. package/dist/ai/source/BbAlert.md +396 -0
  182. package/dist/ai/source/BbAsterisk.md +32 -0
  183. package/dist/ai/source/BbAvatar.md +256 -0
  184. package/dist/ai/source/BbBadge.md +560 -0
  185. package/dist/ai/source/BbBadgeButton.md +490 -0
  186. package/dist/ai/source/BbBaseButton.md +682 -0
  187. package/dist/ai/source/BbBaseCheckbox.md +459 -0
  188. package/dist/ai/source/BbBaseCheckboxGroup.md +554 -0
  189. package/dist/ai/source/BbBaseCheckboxIcon.md +201 -0
  190. package/dist/ai/source/BbBaseColorInput.md +413 -0
  191. package/dist/ai/source/BbBaseColorPalette.md +934 -0
  192. package/dist/ai/source/BbBaseDatePicker.md +1001 -0
  193. package/dist/ai/source/BbBaseDatePickerInput.md +1781 -0
  194. package/dist/ai/source/BbBaseInputContainer.md +1148 -0
  195. package/dist/ai/source/BbBaseNumberInput.md +615 -0
  196. package/dist/ai/source/BbBaseRadio.md +377 -0
  197. package/dist/ai/source/BbBaseRadioGroup.md +499 -0
  198. package/dist/ai/source/BbBaseRadioIcon.md +153 -0
  199. package/dist/ai/source/BbBaseRating.md +573 -0
  200. package/dist/ai/source/BbBaseSelect.md +895 -0
  201. package/dist/ai/source/BbBaseSlider.md +1085 -0
  202. package/dist/ai/source/BbBaseSwitch.md +246 -0
  203. package/dist/ai/source/BbBaseSwitchGroup.md +302 -0
  204. package/dist/ai/source/BbBaseSwitchIcon.md +206 -0
  205. package/dist/ai/source/BbBaseTag.md +688 -0
  206. package/dist/ai/source/BbBaseTextInput.md +511 -0
  207. package/dist/ai/source/BbBaseTextarea.md +480 -0
  208. package/dist/ai/source/BbBaseTimePickerInput.md +1013 -0
  209. package/dist/ai/source/BbBreadcrumbs.md +811 -0
  210. package/dist/ai/source/BbButton.md +990 -0
  211. package/dist/ai/source/BbCheckbox.md +319 -0
  212. package/dist/ai/source/BbCheckboxGroup.md +412 -0
  213. package/dist/ai/source/BbCollapsible.md +133 -0
  214. package/dist/ai/source/BbColorInput.md +315 -0
  215. package/dist/ai/source/BbColorPalette.md +361 -0
  216. package/dist/ai/source/BbConfirm.md +173 -0
  217. package/dist/ai/source/BbConfirmPortal.md +74 -0
  218. package/dist/ai/source/BbDatePicker.md +725 -0
  219. package/dist/ai/source/BbDatePickerInput.md +518 -0
  220. package/dist/ai/source/BbDialog.md +812 -0
  221. package/dist/ai/source/BbDropdown.md +2369 -0
  222. package/dist/ai/source/BbDropdownButton.md +313 -0
  223. package/dist/ai/source/BbDropdownGroup.md +1249 -0
  224. package/dist/ai/source/BbDropzone.md +749 -0
  225. package/dist/ai/source/BbForm.md +100 -0
  226. package/dist/ai/source/BbIcon.md +212 -0
  227. package/dist/ai/source/BbIndicator.md +298 -0
  228. package/dist/ai/source/BbNumberInput.md +365 -0
  229. package/dist/ai/source/BbOffCanvas.md +1260 -0
  230. package/dist/ai/source/BbPagination.md +829 -0
  231. package/dist/ai/source/BbPopover.md +853 -0
  232. package/dist/ai/source/BbProgress.md +140 -0
  233. package/dist/ai/source/BbRadio.md +294 -0
  234. package/dist/ai/source/BbRadioGroup.md +401 -0
  235. package/dist/ai/source/BbRating.md +362 -0
  236. package/dist/ai/source/BbSelect.md +681 -0
  237. package/dist/ai/source/BbSelectPopover.md +2209 -0
  238. package/dist/ai/source/BbSlider.md +371 -0
  239. package/dist/ai/source/BbSmoothHeight.md +261 -0
  240. package/dist/ai/source/BbSpinner.md +127 -0
  241. package/dist/ai/source/BbSwitch.md +175 -0
  242. package/dist/ai/source/BbSwitchGroup.md +240 -0
  243. package/dist/ai/source/BbTable.md +3348 -0
  244. package/dist/ai/source/BbTabs.md +403 -0
  245. package/dist/ai/source/BbTabsList.md +578 -0
  246. package/dist/ai/source/BbTabsPanes.md +368 -0
  247. package/dist/ai/source/BbTabsRoot.md +372 -0
  248. package/dist/ai/source/BbTag.md +383 -0
  249. package/dist/ai/source/BbTextInput.md +345 -0
  250. package/dist/ai/source/BbTextarea.md +330 -0
  251. package/dist/ai/source/BbTimePicker.md +591 -0
  252. package/dist/ai/source/BbTimePickerInput.md +367 -0
  253. package/dist/ai/source/BbToast.md +524 -0
  254. package/dist/ai/source/BbToastPortal.md +482 -0
  255. package/dist/ai/source/BbTooltip.md +508 -0
  256. package/dist/ai/source/BbTree.md +319 -0
  257. package/dist/ai/source/ChipsBox.md +201 -0
  258. package/dist/ai/source/ClearableButton.md +70 -0
  259. package/dist/ai/source/CommaBox.md +202 -0
  260. package/dist/ai/source/CommonInputWrapper.md +554 -0
  261. package/dist/ai/source/CommonPopover.md +606 -0
  262. package/dist/ai/source/ErrorIcon.md +43 -0
  263. package/dist/ai/source/FlatListBox.md +396 -0
  264. package/dist/ai/source/GroupedListBox.md +540 -0
  265. package/dist/ai/source/GuidesComponent.md +383 -0
  266. package/dist/ai/source/ListBox.md +238 -0
  267. package/dist/ai/source/OptionsContainer.md +191 -0
  268. package/dist/ai/source/WarningIcon.md +47 -0
  269. package/dist/alert-variants.d.ts +12 -0
  270. package/dist/assets/css/reset.css_inline.js +4 -0
  271. package/dist/assets/svgs/alert.svg_raw.js +4 -0
  272. package/dist/assets/svgs/calendar.svg_raw.js +4 -0
  273. package/dist/assets/svgs/check_stroke.svg_raw.js +4 -0
  274. package/dist/assets/svgs/chevron_down.svg_raw.js +4 -0
  275. package/dist/assets/svgs/chevron_right.svg_raw.js +4 -0
  276. package/dist/assets/svgs/clear.svg_raw.js +4 -0
  277. package/dist/assets/svgs/clock.svg_raw.js +4 -0
  278. package/dist/assets/svgs/error.svg_raw.js +4 -0
  279. package/dist/assets/svgs/search.svg_raw.js +4 -0
  280. package/dist/assets/svgs/sort.svg_raw.js +4 -0
  281. package/dist/assets/svgs/spinner.svg_raw.js +4 -0
  282. package/dist/assets/svgs/user.svg_raw.js +4 -0
  283. package/dist/badge-variants.d.ts +18 -0
  284. package/dist/button-variants.d.ts +15 -0
  285. package/dist/components/BbAccordion/BbAccordion.vue.d.ts +9 -3
  286. package/dist/components/BbAccordion/BbAccordion.vue.js +6 -0
  287. package/dist/components/BbAccordion/BbAccordion.vue_vue_type_script_setup_true_lang.js +66 -0
  288. package/dist/components/BbAccordion/types.d.ts +10 -7
  289. package/dist/components/BbAlert/BbAlert.vue.d.ts +12 -3
  290. package/dist/components/BbAlert/BbAlert.vue.js +6 -0
  291. package/dist/components/BbAlert/BbAlert.vue_vue_type_script_setup_true_lang.js +65 -0
  292. package/dist/components/BbAlert/types.d.ts +51 -12
  293. package/dist/components/BbAsterisk/BbAsterisk.vue.d.ts +3 -0
  294. package/dist/components/BbAsterisk/BbAsterisk.vue.js +6 -0
  295. package/dist/components/BbAsterisk/BbAsterisk.vue_vue_type_script_setup_true_lang.js +13 -0
  296. package/dist/components/BbAvatar/BbAvatar.vue.d.ts +5 -0
  297. package/dist/components/BbAvatar/BbAvatar.vue.js +6 -0
  298. package/dist/components/BbAvatar/BbAvatar.vue_vue_type_script_setup_true_lang.js +78 -0
  299. package/dist/components/BbAvatar/types.d.ts +4 -5
  300. package/dist/components/BbBadge/BadgeAppendIcon.vue.d.ts +3 -0
  301. package/dist/components/BbBadge/BadgeAppendIcon.vue.js +5 -0
  302. package/dist/components/BbBadge/BadgeAppendIcon.vue_vue_type_script_setup_true_lang.js +22 -0
  303. package/dist/components/BbBadge/BadgeBodyContent.vue.d.ts +15 -0
  304. package/dist/components/BbBadge/BadgeBodyContent.vue.js +5 -0
  305. package/dist/components/BbBadge/BadgeBodyContent.vue_vue_type_script_setup_true_lang.js +34 -0
  306. package/dist/components/BbBadge/BbBadge.vue.d.ts +8 -1
  307. package/dist/components/BbBadge/BbBadge.vue.js +6 -0
  308. package/dist/components/BbBadge/BbBadge.vue_vue_type_script_setup_true_lang.js +83 -0
  309. package/dist/components/BbBadge/BbBadgeButton.vue.d.ts +12 -0
  310. package/dist/components/BbBadge/BbBadgeButton.vue.js +5 -0
  311. package/dist/components/BbBadge/BbBadgeButton.vue_vue_type_script_setup_true_lang.js +54 -0
  312. package/dist/components/BbBadge/badgeButtonContext.d.ts +27 -0
  313. package/dist/components/BbBadge/badgeButtonContext.js +5 -0
  314. package/dist/components/BbBadge/types.d.ts +61 -16
  315. package/dist/components/BbBaseButton/BbBaseButton.vue.d.ts +13 -0
  316. package/dist/components/BbBaseButton/BbBaseButton.vue.js +6 -0
  317. package/dist/components/BbBaseButton/BbBaseButton.vue_vue_type_script_setup_true_lang.js +171 -0
  318. package/dist/components/BbBaseButton/RouterComponent.vue.js +5 -0
  319. package/dist/components/BbBaseButton/RouterComponent.vue_vue_type_script_setup_true_lang.js +28 -0
  320. package/dist/components/BbBaseButton/types.d.ts +191 -0
  321. package/dist/components/BbBaseButton/types.js +33 -0
  322. package/dist/components/BbBaseCheckbox/BbBaseCheckbox.vue.d.ts +40 -0
  323. package/dist/components/BbBaseCheckbox/BbBaseCheckbox.vue.js +6 -0
  324. package/dist/components/BbBaseCheckbox/BbBaseCheckbox.vue_vue_type_script_setup_true_lang.js +145 -0
  325. package/dist/components/BbBaseCheckbox/types.d.ts +222 -0
  326. package/dist/components/BbBaseCheckboxGroup/BbBaseCheckboxGroup.vue.d.ts +19 -0
  327. package/dist/components/BbBaseCheckboxGroup/BbBaseCheckboxGroup.vue.js +6 -0
  328. package/dist/components/BbBaseCheckboxGroup/BbBaseCheckboxGroup.vue_vue_type_script_setup_true_lang.js +190 -0
  329. package/dist/components/BbBaseCheckboxGroup/types.d.ts +257 -0
  330. package/dist/components/BbBaseCheckboxIcon/BbBaseCheckboxIcon.vue.d.ts +4 -0
  331. package/dist/components/BbBaseCheckboxIcon/BbBaseCheckboxIcon.vue.js +6 -0
  332. package/dist/components/BbBaseCheckboxIcon/BbBaseCheckboxIcon.vue_vue_type_script_setup_true_lang.js +43 -0
  333. package/dist/components/BbBaseCheckboxIcon/types.d.ts +47 -0
  334. package/dist/components/BbBaseColorInput/BbBaseColorInput.vue.d.ts +42 -0
  335. package/dist/components/BbBaseColorInput/BbBaseColorInput.vue.js +6 -0
  336. package/dist/components/BbBaseColorInput/BbBaseColorInput.vue_vue_type_script_setup_true_lang.js +178 -0
  337. package/dist/components/BbBaseColorInput/types.d.ts +207 -0
  338. package/dist/components/BbBaseColorPalette/BbBaseColorPalette.vue.d.ts +13 -0
  339. package/dist/components/BbBaseColorPalette/BbBaseColorPalette.vue.js +6 -0
  340. package/dist/components/BbBaseColorPalette/BbBaseColorPalette.vue_vue_type_script_setup_true_lang.js +395 -0
  341. package/dist/components/BbBaseColorPalette/types.d.ts +40 -0
  342. package/dist/components/BbBaseDatePicker/BbBaseDatePicker.vue.js +6 -0
  343. package/dist/components/BbBaseDatePicker/BbBaseDatePicker.vue_vue_type_script_setup_true_lang.js +300 -0
  344. package/dist/components/BbBaseDatePicker/BbBaseDatePickerInputDaySelector.vue.d.ts +44 -0
  345. package/dist/components/BbBaseDatePicker/BbBaseDatePickerInputDaySelector.vue.js +5 -0
  346. package/dist/components/BbBaseDatePicker/BbBaseDatePickerInputDaySelector.vue_vue_type_script_setup_true_lang.js +310 -0
  347. package/dist/components/BbBaseDatePicker/BbBaseDatePickerMonthSelector.vue.d.ts +5 -0
  348. package/dist/components/BbBaseDatePicker/BbBaseDatePickerMonthSelector.vue.js +5 -0
  349. package/dist/components/BbBaseDatePicker/BbBaseDatePickerMonthSelector.vue_vue_type_script_setup_true_lang.js +63 -0
  350. package/dist/components/BbBaseDatePicker/BbBaseDatePickerYearSelector.vue.d.ts +5 -0
  351. package/dist/components/BbBaseDatePicker/BbBaseDatePickerYearSelector.vue.js +5 -0
  352. package/dist/components/BbBaseDatePicker/BbBaseDatePickerYearSelector.vue_vue_type_script_setup_true_lang.js +75 -0
  353. package/dist/components/BbBaseDatePicker/typeahead.d.ts +112 -0
  354. package/dist/components/BbBaseDatePicker/typeahead.js +37 -0
  355. package/dist/components/BbBaseDatePicker/types.d.ts +97 -0
  356. package/dist/components/BbBaseDatePicker/useDatePickerContext.d.ts +58 -0
  357. package/dist/components/BbBaseDatePicker/useDatePickerContext.js +11 -0
  358. package/dist/components/BbBaseDatePickerInput/BbBaseDatePickerInput.vue.d.ts +30 -0
  359. package/dist/components/BbBaseDatePickerInput/BbBaseDatePickerInput.vue.js +6 -0
  360. package/dist/components/BbBaseDatePickerInput/BbBaseDatePickerInput.vue_vue_type_script_setup_true_lang.js +713 -0
  361. package/dist/components/BbBaseDatePickerInput/BbBaseDatePickerTimeSelector.vue.d.ts +30 -0
  362. package/dist/components/BbBaseDatePickerInput/BbBaseDatePickerTimeSelector.vue.js +6 -0
  363. package/dist/components/BbBaseDatePickerInput/BbBaseDatePickerTimeSelector.vue_vue_type_script_setup_true_lang.js +204 -0
  364. package/dist/components/BbBaseDatePickerInput/types.d.ts +309 -0
  365. package/dist/components/BbBaseInputContainer/BbBaseInputContainer.vue.d.ts +17 -0
  366. package/dist/components/BbBaseInputContainer/BbBaseInputContainer.vue.js +6 -0
  367. package/dist/components/BbBaseInputContainer/BbBaseInputContainer.vue_vue_type_script_setup_true_lang.js +184 -0
  368. package/dist/components/BbBaseInputContainer/types.d.ts +185 -0
  369. package/dist/components/BbBaseNumberInput/BbBaseNumberInput.vue.d.ts +52 -0
  370. package/dist/components/BbBaseNumberInput/BbBaseNumberInput.vue.js +6 -0
  371. package/dist/components/BbBaseNumberInput/BbBaseNumberInput.vue_vue_type_script_setup_true_lang.js +211 -0
  372. package/dist/components/BbBaseNumberInput/types.d.ts +211 -0
  373. package/dist/components/BbBaseRadio/BbBaseRadio.vue.d.ts +33 -0
  374. package/dist/components/BbBaseRadio/BbBaseRadio.vue.js +6 -0
  375. package/dist/components/BbBaseRadio/BbBaseRadio.vue_vue_type_script_setup_true_lang.js +114 -0
  376. package/dist/components/BbBaseRadio/types.d.ts +179 -0
  377. package/dist/components/BbBaseRadioGroup/BbBaseRadioGroup.vue.d.ts +19 -0
  378. package/dist/components/BbBaseRadioGroup/BbBaseRadioGroup.vue.js +6 -0
  379. package/dist/components/BbBaseRadioGroup/BbBaseRadioGroup.vue_vue_type_script_setup_true_lang.js +172 -0
  380. package/dist/components/BbBaseRadioGroup/types.d.ts +215 -0
  381. package/dist/components/BbBaseRadioIcon/BbBaseRadioIcon.vue.d.ts +4 -0
  382. package/dist/components/BbBaseRadioIcon/BbBaseRadioIcon.vue.js +6 -0
  383. package/dist/components/BbBaseRadioIcon/BbBaseRadioIcon.vue_vue_type_script_setup_true_lang.js +28 -0
  384. package/dist/components/BbBaseRadioIcon/types.d.ts +41 -0
  385. package/dist/components/BbBaseRating/BbBaseRating.vue.d.ts +38 -0
  386. package/dist/components/BbBaseRating/BbBaseRating.vue.js +6 -0
  387. package/dist/components/BbBaseRating/BbBaseRating.vue_vue_type_script_setup_true_lang.js +184 -0
  388. package/dist/components/BbBaseRating/types.d.ts +168 -0
  389. package/dist/components/BbBaseSelect/BbBaseSelect.vue.d.ts +19 -0
  390. package/dist/components/BbBaseSelect/BbBaseSelect.vue.js +6 -0
  391. package/dist/components/BbBaseSelect/BbBaseSelect.vue_vue_type_script_setup_true_lang.js +307 -0
  392. package/dist/components/BbBaseSelect/types.d.ts +294 -0
  393. package/dist/components/BbBaseSlider/BbBaseSlider.vue.d.ts +35 -0
  394. package/dist/components/BbBaseSlider/BbBaseSlider.vue.js +6 -0
  395. package/dist/components/BbBaseSlider/BbBaseSlider.vue_vue_type_script_setup_true_lang.js +324 -0
  396. package/dist/components/BbBaseSlider/types.d.ts +200 -0
  397. package/dist/components/BbBaseSwitch/BbBaseSwitch.vue.d.ts +35 -0
  398. package/dist/components/BbBaseSwitch/BbBaseSwitch.vue.js +6 -0
  399. package/dist/components/BbBaseSwitch/BbBaseSwitch.vue_vue_type_script_setup_true_lang.js +146 -0
  400. package/dist/components/BbBaseSwitch/types.d.ts +10 -0
  401. package/dist/components/BbBaseSwitchGroup/BbBaseSwitchGroup.vue.d.ts +19 -0
  402. package/dist/components/BbBaseSwitchGroup/BbBaseSwitchGroup.vue.js +6 -0
  403. package/dist/components/BbBaseSwitchGroup/BbBaseSwitchGroup.vue_vue_type_script_setup_true_lang.js +190 -0
  404. package/dist/components/BbBaseSwitchGroup/types.d.ts +8 -0
  405. package/dist/components/BbBaseSwitchIcon/BbBaseSwitchIcon.vue.d.ts +4 -0
  406. package/dist/components/BbBaseSwitchIcon/BbBaseSwitchIcon.vue.js +6 -0
  407. package/dist/components/BbBaseSwitchIcon/BbBaseSwitchIcon.vue_vue_type_script_setup_true_lang.js +30 -0
  408. package/dist/components/BbBaseSwitchIcon/types.d.ts +48 -0
  409. package/dist/components/BbBaseTag/BbBaseTag.vue.d.ts +50 -0
  410. package/dist/components/BbBaseTag/BbBaseTag.vue.js +6 -0
  411. package/dist/components/BbBaseTag/BbBaseTag.vue_vue_type_script_setup_true_lang.js +266 -0
  412. package/dist/components/BbBaseTag/types.d.ts +160 -0
  413. package/dist/components/BbBaseTextInput/BbBaseTextInput.vue.d.ts +45 -0
  414. package/dist/components/BbBaseTextInput/BbBaseTextInput.vue.js +6 -0
  415. package/dist/components/BbBaseTextInput/BbBaseTextInput.vue_vue_type_script_setup_true_lang.js +200 -0
  416. package/dist/components/BbBaseTextInput/types.d.ts +209 -0
  417. package/dist/components/BbBaseTextarea/BbBaseTextarea.vue.d.ts +39 -0
  418. package/dist/components/BbBaseTextarea/BbBaseTextarea.vue.js +6 -0
  419. package/dist/components/BbBaseTextarea/BbBaseTextarea.vue_vue_type_script_setup_true_lang.js +175 -0
  420. package/dist/components/BbBaseTextarea/types.d.ts +170 -0
  421. package/dist/components/BbBaseTimePickerInput/BbBaseTimePickerInput.vue.d.ts +27 -0
  422. package/dist/components/BbBaseTimePickerInput/BbBaseTimePickerInput.vue.js +6 -0
  423. package/dist/components/BbBaseTimePickerInput/BbBaseTimePickerInput.vue_vue_type_script_setup_true_lang.js +517 -0
  424. package/dist/components/BbBaseTimePickerInput/types.d.ts +151 -0
  425. package/dist/components/BbBreadcrumbs/BbBreadcrumbs.vue.d.ts +0 -1
  426. package/dist/components/BbBreadcrumbs/BbBreadcrumbs.vue.js +6 -0
  427. package/dist/components/BbBreadcrumbs/BbBreadcrumbs.vue_vue_type_script_setup_true_lang.js +224 -0
  428. package/dist/components/BbBreadcrumbs/estimateBreadcrumbItemWidth.js +47 -0
  429. package/dist/components/BbBreadcrumbs/types.d.ts +33 -22
  430. package/dist/components/BbButton/BbButton.vue.d.ts +6 -5
  431. package/dist/components/BbButton/BbButton.vue.js +6 -0
  432. package/dist/components/BbButton/BbButton.vue_vue_type_script_setup_true_lang.js +177 -0
  433. package/dist/components/BbButton/types.d.ts +48 -47
  434. package/dist/components/BbCheckbox/BbCheckbox.vue.d.ts +4 -3
  435. package/dist/components/BbCheckbox/BbCheckbox.vue.js +6 -0
  436. package/dist/components/BbCheckbox/BbCheckbox.vue_vue_type_script_setup_true_lang.js +146 -0
  437. package/dist/components/BbCheckbox/types.d.ts +24 -10
  438. package/dist/components/BbCheckboxGroup/BbCheckboxGroup.vue.d.ts +2 -13
  439. package/dist/components/BbCheckboxGroup/BbCheckboxGroup.vue.js +6 -0
  440. package/dist/components/BbCheckboxGroup/BbCheckboxGroup.vue_vue_type_script_setup_true_lang.js +190 -0
  441. package/dist/components/BbCheckboxGroup/types.d.ts +44 -23
  442. package/dist/components/BbCollapsible/BbCollapsible.vue.d.ts +2 -1
  443. package/dist/components/BbCollapsible/BbCollapsible.vue.js +6 -0
  444. package/dist/components/BbCollapsible/BbCollapsible.vue_vue_type_script_setup_true_lang.js +31 -0
  445. package/dist/components/BbCollapsible/types.d.ts +10 -9
  446. package/dist/components/BbColorInput/BbColorInput.vue.d.ts +10 -1
  447. package/dist/components/BbColorInput/BbColorInput.vue.js +6 -0
  448. package/dist/components/BbColorInput/BbColorInput.vue_vue_type_script_setup_true_lang.js +177 -0
  449. package/dist/components/BbColorInput/types.d.ts +37 -11
  450. package/dist/components/BbColorPalette/BbColorPalette.vue.d.ts +5 -8
  451. package/dist/components/BbColorPalette/BbColorPalette.vue.js +5 -0
  452. package/dist/components/BbColorPalette/BbColorPalette.vue_vue_type_script_setup_true_lang.js +125 -0
  453. package/dist/components/BbColorPalette/types.d.ts +66 -53
  454. package/dist/components/BbConfirm/BbConfirm.vue.js +6 -0
  455. package/dist/components/BbConfirm/BbConfirm.vue_vue_type_script_setup_true_lang.js +130 -0
  456. package/dist/components/BbConfirm/BbConfirmPortal.vue.d.ts +5 -6
  457. package/dist/components/BbConfirm/BbConfirmPortal.vue.js +5 -0
  458. package/dist/components/BbConfirm/BbConfirmPortal.vue_vue_type_script_setup_true_lang.js +19 -0
  459. package/dist/components/BbDatePicker/BbDatePicker.vue.d.ts +37 -0
  460. package/dist/components/BbDatePicker/BbDatePicker.vue.js +6 -0
  461. package/dist/components/BbDatePicker/BbDatePicker.vue_vue_type_script_setup_true_lang.js +319 -0
  462. package/dist/components/BbDatePicker/types.d.ts +155 -0
  463. package/dist/components/BbDatePickerInput/BbDatePickerInput.vue.d.ts +3 -1
  464. package/dist/components/BbDatePickerInput/BbDatePickerInput.vue.js +6 -0
  465. package/dist/components/BbDatePickerInput/BbDatePickerInput.vue_vue_type_script_setup_true_lang.js +179 -0
  466. package/dist/components/BbDatePickerInput/types.d.ts +127 -16
  467. package/dist/components/BbDialog/BbDialog.vue.d.ts +8 -6
  468. package/dist/components/BbDialog/BbDialog.vue.js +6 -0
  469. package/dist/components/BbDialog/BbDialog.vue_vue_type_script_setup_true_lang.js +214 -0
  470. package/dist/components/BbDialog/types.d.ts +146 -1
  471. package/dist/components/BbDialog/useDialogStack.d.ts +5 -0
  472. package/dist/components/BbDialog/useDialogStack.js +20 -0
  473. package/dist/components/BbDropdown/AdaptiveDropdown.vue.d.ts +18 -0
  474. package/dist/components/BbDropdown/AdaptiveDropdown.vue.js +6 -0
  475. package/dist/components/BbDropdown/AdaptiveDropdown.vue_vue_type_script_setup_true_lang.js +116 -0
  476. package/dist/components/BbDropdown/BbDropdown.vue.d.ts +14 -7
  477. package/dist/components/BbDropdown/BbDropdown.vue.js +6 -0
  478. package/dist/components/BbDropdown/BbDropdown.vue_vue_type_script_setup_true_lang.js +560 -0
  479. package/dist/components/BbDropdown/BbDropdownGroup.vue.d.ts +44 -0
  480. package/dist/components/BbDropdown/BbDropdownGroup.vue.js +5 -0
  481. package/dist/components/BbDropdown/BbDropdownGroup.vue_vue_type_script_setup_true_lang.js +36 -0
  482. package/dist/components/BbDropdown/BbDropdownList.vue.d.ts +27 -0
  483. package/dist/components/BbDropdown/BbDropdownList.vue.js +6 -0
  484. package/dist/components/BbDropdown/BbDropdownList.vue_vue_type_script_setup_true_lang.js +464 -0
  485. package/dist/components/BbDropdown/DropdownPipelineResolver.vue.d.ts +16 -0
  486. package/dist/components/BbDropdown/DropdownPipelineResolver.vue.js +5 -0
  487. package/dist/components/BbDropdown/DropdownPipelineResolver.vue_vue_type_script_setup_true_lang.js +168 -0
  488. package/dist/components/BbDropdown/normalizeGroups.d.ts +18 -0
  489. package/dist/components/BbDropdown/normalizeGroups.js +46 -0
  490. package/dist/components/BbDropdown/types.d.ts +457 -45
  491. package/dist/components/BbDropdown/types.js +4 -0
  492. package/dist/components/BbDropdown/useDropdownContext.d.ts +89 -0
  493. package/dist/components/BbDropdown/useDropdownContext.js +9 -0
  494. package/dist/components/BbDropdown/useDropdownGroupRegistry.d.ts +108 -0
  495. package/dist/components/BbDropdown/useDropdownGroupRegistry.js +5 -0
  496. package/dist/components/BbDropdownButton/BbDropdownButton.vue.d.ts +3 -3
  497. package/dist/components/BbDropdownButton/BbDropdownButton.vue.js +6 -0
  498. package/dist/components/BbDropdownButton/BbDropdownButton.vue_vue_type_script_setup_true_lang.js +119 -0
  499. package/dist/components/BbDropdownButton/types.d.ts +45 -52
  500. package/dist/components/BbDropzone/BbDropzone.vue.d.ts +5 -4
  501. package/dist/components/BbDropzone/BbDropzone.vue.js +6 -0
  502. package/dist/components/BbDropzone/BbDropzone.vue_vue_type_script_setup_true_lang.js +284 -0
  503. package/dist/components/BbDropzone/types.d.ts +55 -7
  504. package/dist/components/BbIcon/BbIcon.vue.d.ts +3 -2
  505. package/dist/components/BbIcon/BbIcon.vue.js +6 -0
  506. package/dist/components/BbIcon/BbIcon.vue_vue_type_script_setup_true_lang.js +65 -0
  507. package/dist/components/BbIcon/types.d.ts +6 -19
  508. package/dist/components/BbIndicator/BbIndicator.vue.d.ts +14 -0
  509. package/dist/components/BbIndicator/BbIndicator.vue.js +6 -0
  510. package/dist/components/BbIndicator/BbIndicator.vue_vue_type_script_setup_true_lang.js +56 -0
  511. package/dist/components/BbIndicator/types.d.ts +49 -0
  512. package/dist/components/BbNumberInput/BbNumberInput.vue.d.ts +12 -6
  513. package/dist/components/BbNumberInput/BbNumberInput.vue.js +6 -0
  514. package/dist/components/BbNumberInput/BbNumberInput.vue_vue_type_script_setup_true_lang.js +194 -0
  515. package/dist/components/BbNumberInput/types.d.ts +31 -4
  516. package/dist/components/BbOffCanvas/BbOffCanvas.vue.d.ts +10 -10
  517. package/dist/components/BbOffCanvas/BbOffCanvas.vue.js +6 -0
  518. package/dist/components/BbOffCanvas/BbOffCanvas.vue_vue_type_script_setup_true_lang.js +284 -0
  519. package/dist/components/BbOffCanvas/types.d.ts +71 -39
  520. package/dist/components/BbOffCanvas/useOffCanvasStack.d.ts +25 -0
  521. package/dist/components/BbOffCanvas/useOffCanvasStack.js +68 -0
  522. package/dist/components/BbPagination/BbPagination.vue.d.ts +3 -4
  523. package/dist/components/BbPagination/BbPagination.vue.js +6 -0
  524. package/dist/components/BbPagination/BbPagination.vue_vue_type_script_setup_true_lang.js +244 -0
  525. package/dist/components/BbPagination/paginationFit.d.ts +38 -0
  526. package/dist/components/BbPagination/paginationFit.js +13 -0
  527. package/dist/components/BbPagination/types.d.ts +51 -16
  528. package/dist/components/BbPopover/BbPopover.vue.d.ts +10 -15
  529. package/dist/components/BbPopover/BbPopover.vue.js +6 -0
  530. package/dist/components/BbPopover/BbPopover.vue_vue_type_script_setup_true_lang.js +310 -0
  531. package/dist/components/BbPopover/escapeStack.d.ts +6 -0
  532. package/dist/components/BbPopover/escapeStack.js +10 -0
  533. package/dist/components/BbPopover/types.d.ts +84 -31
  534. package/dist/components/BbProgress/BbProgress.vue.d.ts +1 -0
  535. package/dist/components/BbProgress/BbProgress.vue.js +6 -0
  536. package/dist/components/BbProgress/BbProgress.vue_vue_type_script_setup_true_lang.js +36 -0
  537. package/dist/components/BbProgress/types.d.ts +15 -1
  538. package/dist/components/BbRadio/BbRadio.vue.d.ts +5 -3
  539. package/dist/components/BbRadio/BbRadio.vue.js +6 -0
  540. package/dist/components/BbRadio/BbRadio.vue_vue_type_script_setup_true_lang.js +133 -0
  541. package/dist/components/BbRadio/types.d.ts +23 -9
  542. package/dist/components/BbRadioGroup/BbRadioGroup.vue.d.ts +4 -14
  543. package/dist/components/BbRadioGroup/BbRadioGroup.vue.js +6 -0
  544. package/dist/components/BbRadioGroup/BbRadioGroup.vue_vue_type_script_setup_true_lang.js +184 -0
  545. package/dist/components/BbRadioGroup/types.d.ts +41 -22
  546. package/dist/components/BbRating/BbRating.vue.d.ts +4 -1
  547. package/dist/components/BbRating/BbRating.vue.js +6 -0
  548. package/dist/components/BbRating/BbRating.vue_vue_type_script_setup_true_lang.js +150 -0
  549. package/dist/components/BbRating/types.d.ts +37 -10
  550. package/dist/components/BbSelect/BbSelect.vue.d.ts +2 -11
  551. package/dist/components/BbSelect/BbSelect.vue.js +6 -0
  552. package/dist/components/BbSelect/BbSelect.vue_vue_type_script_setup_true_lang.js +241 -0
  553. package/dist/components/BbSelect/types.d.ts +105 -59
  554. package/dist/components/BbSelectPopover/BbSelectPopover.vue.d.ts +1 -10
  555. package/dist/components/BbSelectPopover/BbSelectPopover.vue.js +6 -0
  556. package/dist/components/BbSelectPopover/BbSelectPopover.vue_vue_type_script_setup_true_lang.js +637 -0
  557. package/dist/components/BbSelectPopover/types.d.ts +189 -102
  558. package/dist/components/BbSlider/BbSlider.vue.d.ts +3 -6
  559. package/dist/components/BbSlider/BbSlider.vue.js +6 -0
  560. package/dist/components/BbSlider/BbSlider.vue_vue_type_script_setup_true_lang.js +166 -0
  561. package/dist/components/BbSlider/types.d.ts +37 -13
  562. package/dist/components/BbSmoothHeight/BbSmoothHeight.vue.d.ts +3 -1
  563. package/dist/components/BbSmoothHeight/BbSmoothHeight.vue.js +6 -0
  564. package/dist/components/BbSmoothHeight/BbSmoothHeight.vue_vue_type_script_setup_true_lang.js +79 -0
  565. package/dist/components/BbSmoothHeight/types.d.ts +13 -2
  566. package/dist/components/BbSpinner/BbSpinner.vue.d.ts +1 -1
  567. package/dist/components/BbSpinner/BbSpinner.vue.js +6 -0
  568. package/dist/components/BbSpinner/BbSpinner.vue_vue_type_script_setup_true_lang.js +30 -0
  569. package/dist/components/BbSpinner/types.d.ts +10 -3
  570. package/dist/components/BbSwitch/BbSwitch.vue.d.ts +5 -3
  571. package/dist/components/BbSwitch/BbSwitch.vue.js +6 -0
  572. package/dist/components/BbSwitch/BbSwitch.vue_vue_type_script_setup_true_lang.js +142 -0
  573. package/dist/components/BbSwitch/types.d.ts +3 -1
  574. package/dist/components/BbSwitchGroup/BbSwitchGroup.vue.d.ts +4 -14
  575. package/dist/components/BbSwitchGroup/BbSwitchGroup.vue.js +6 -0
  576. package/dist/components/BbSwitchGroup/BbSwitchGroup.vue_vue_type_script_setup_true_lang.js +190 -0
  577. package/dist/components/BbSwitchGroup/types.d.ts +3 -22
  578. package/dist/components/BbTable/BbTable.vue.d.ts +53 -31
  579. package/dist/components/BbTable/BbTable.vue.js +6 -0
  580. package/dist/components/BbTable/BbTable.vue_vue_type_script_setup_true_lang.js +889 -0
  581. package/dist/components/BbTable/types.d.ts +176 -37
  582. package/dist/components/BbTable/utils.d.ts +9 -1
  583. package/dist/components/BbTable/utils.js +53 -0
  584. package/dist/components/BbTabs/BbTabs.vue.d.ts +20 -0
  585. package/dist/components/BbTabs/BbTabs.vue.js +6 -0
  586. package/dist/components/BbTabs/BbTabs.vue_vue_type_script_setup_true_lang.js +64 -0
  587. package/dist/components/BbTabs/BbTabsList.vue.d.ts +19 -0
  588. package/dist/components/BbTabs/BbTabsList.vue.js +6 -0
  589. package/dist/components/BbTabs/BbTabsList.vue_vue_type_script_setup_true_lang.js +123 -0
  590. package/dist/components/BbTabs/BbTabsPanes.vue.d.ts +16 -0
  591. package/dist/components/BbTabs/BbTabsPanes.vue.js +6 -0
  592. package/dist/components/BbTabs/BbTabsPanes.vue_vue_type_script_setup_true_lang.js +35 -0
  593. package/dist/components/BbTabs/BbTabsRoot.vue.d.ts +20 -0
  594. package/dist/components/BbTabs/BbTabsRoot.vue.js +5 -0
  595. package/dist/components/BbTabs/BbTabsRoot.vue_vue_type_script_setup_true_lang.js +55 -0
  596. package/dist/components/BbTabs/types.d.ts +264 -0
  597. package/dist/components/BbTag/BbTag.vue.d.ts +19 -6
  598. package/dist/components/BbTag/BbTag.vue.js +6 -0
  599. package/dist/components/BbTag/BbTag.vue_vue_type_script_setup_true_lang.js +187 -0
  600. package/dist/components/BbTag/types.d.ts +48 -17
  601. package/dist/components/BbTextInput/BbTextInput.vue.d.ts +10 -1
  602. package/dist/components/BbTextInput/BbTextInput.vue.js +6 -0
  603. package/dist/components/BbTextInput/BbTextInput.vue_vue_type_script_setup_true_lang.js +195 -0
  604. package/dist/components/BbTextInput/types.d.ts +24 -4
  605. package/dist/components/BbTextarea/BbTextarea.vue.d.ts +10 -1
  606. package/dist/components/BbTextarea/BbTextarea.vue.js +6 -0
  607. package/dist/components/BbTextarea/BbTextarea.vue_vue_type_script_setup_true_lang.js +178 -0
  608. package/dist/components/BbTextarea/types.d.ts +26 -2
  609. package/dist/components/BbTimePicker/BbTimePicker.vue.d.ts +33 -0
  610. package/dist/components/BbTimePicker/BbTimePicker.vue.js +6 -0
  611. package/dist/components/BbTimePicker/BbTimePicker.vue_vue_type_script_setup_true_lang.js +244 -0
  612. package/dist/components/BbTimePicker/types.d.ts +129 -0
  613. package/dist/components/BbTimePickerInput/BbTimePickerInput.vue.d.ts +25 -0
  614. package/dist/components/BbTimePickerInput/BbTimePickerInput.vue.js +6 -0
  615. package/dist/components/BbTimePickerInput/BbTimePickerInput.vue_vue_type_script_setup_true_lang.js +157 -0
  616. package/dist/components/BbTimePickerInput/types.d.ts +165 -0
  617. package/dist/components/BbToast/BbToast.vue.d.ts +2 -3
  618. package/dist/components/BbToast/BbToast.vue.js +6 -0
  619. package/dist/components/BbToast/BbToast.vue_vue_type_script_setup_true_lang.js +35 -0
  620. package/dist/components/BbToast/BbToastMessage.vue.d.ts +17 -8
  621. package/dist/components/BbToast/BbToastMessage.vue.js +5 -0
  622. package/dist/components/BbToast/BbToastMessage.vue_vue_type_script_setup_true_lang.js +170 -0
  623. package/dist/components/BbToast/BbToastPortal.vue.d.ts +30 -0
  624. package/dist/components/BbToast/BbToastPortal.vue.js +5 -0
  625. package/dist/components/BbToast/BbToastPortal.vue_vue_type_script_setup_true_lang.js +19 -0
  626. package/dist/components/BbToast/BbToastRegion.vue.d.ts +8 -0
  627. package/dist/components/BbToast/BbToastRegion.vue.js +5 -0
  628. package/dist/components/BbToast/BbToastRegion.vue_vue_type_script_setup_true_lang.js +100 -0
  629. package/dist/components/BbToast/types.d.ts +69 -5
  630. package/dist/components/BbTooltip/BbTooltip.vue.d.ts +4 -3
  631. package/dist/components/BbTooltip/BbTooltip.vue.js +6 -0
  632. package/dist/components/BbTooltip/BbTooltip.vue_vue_type_script_setup_true_lang.js +160 -0
  633. package/dist/components/BbTooltip/types.d.ts +61 -11
  634. package/dist/components/BbTree/BbTree.vue.d.ts +26 -13
  635. package/dist/components/BbTree/BbTree.vue.js +6 -0
  636. package/dist/components/BbTree/BbTree.vue_vue_type_script_setup_true_lang.js +55 -0
  637. package/dist/components/BbTree/BbTreeLevel.vue.d.ts +19 -0
  638. package/dist/components/BbTree/BbTreeLevel.vue.js +5 -0
  639. package/dist/components/BbTree/BbTreeLevel.vue_vue_type_script_setup_true_lang.js +94 -0
  640. package/dist/components/BbTree/context.d.ts +17 -0
  641. package/dist/components/BbTree/context.js +4 -0
  642. package/dist/components/BbTree/types.d.ts +93 -42
  643. package/dist/components/ChipsBox/ChipsBox.vue.d.ts +2 -1
  644. package/dist/components/ChipsBox/ChipsBox.vue.js +5 -0
  645. package/dist/components/ChipsBox/ChipsBox.vue_vue_type_script_setup_true_lang.js +98 -0
  646. package/dist/components/ChipsBox/types.d.ts +12 -5
  647. package/dist/components/ClearableButton/ClearableButton.vue.d.ts +2 -1
  648. package/dist/components/ClearableButton/ClearableButton.vue.js +6 -0
  649. package/dist/components/ClearableButton/ClearableButton.vue_vue_type_script_setup_true_lang.js +19 -0
  650. package/dist/components/ClearableButton/types.d.ts +7 -1
  651. package/dist/components/CloseIcon/CloseButton.vue.d.ts +5 -0
  652. package/dist/components/CloseIcon/CloseButton.vue.js +7 -0
  653. package/dist/components/CloseIcon/CloseButton.vue_vue_type_script_setup_true_lang.js +23 -0
  654. package/dist/components/CloseIcon/types.d.ts +2 -0
  655. package/dist/components/CommaBox/CommaBox.vue.d.ts +2 -1
  656. package/dist/components/CommaBox/CommaBox.vue.js +6 -0
  657. package/dist/components/CommaBox/CommaBox.vue_vue_type_script_setup_true_lang.js +81 -0
  658. package/dist/components/CommaBox/types.d.ts +6 -7
  659. package/dist/components/CommonInputWrapper/CommonInputWrapper.vue.d.ts +19 -0
  660. package/dist/components/CommonInputWrapper/CommonInputWrapper.vue.js +6 -0
  661. package/dist/components/CommonInputWrapper/CommonInputWrapper.vue_vue_type_script_setup_true_lang.js +107 -0
  662. package/dist/components/CommonInputWrapper/types.d.ts +96 -0
  663. package/dist/components/CommonPopover/CommonPopover.vue.d.ts +3 -3
  664. package/dist/components/CommonPopover/CommonPopover.vue.js +6 -0
  665. package/dist/components/CommonPopover/CommonPopover.vue_vue_type_script_setup_true_lang.js +160 -0
  666. package/dist/components/CommonPopover/types.d.ts +61 -16
  667. package/dist/components/ErrorIcon/ErrorIcon.vue.d.ts +2 -0
  668. package/dist/components/ErrorIcon/ErrorIcon.vue.js +6 -0
  669. package/dist/components/ErrorIcon/ErrorIcon.vue_vue_type_script_setup_true_lang.js +16 -0
  670. package/dist/components/FlatListBox/FlatListBox.vue.d.ts +2 -5
  671. package/dist/components/FlatListBox/FlatListBox.vue.js +5 -0
  672. package/dist/components/FlatListBox/FlatListBox.vue_vue_type_script_setup_true_lang.js +167 -0
  673. package/dist/components/FlatListBox/types.d.ts +1 -1
  674. package/dist/components/GroupedListBox/GroupedListBox.vue.d.ts +2 -5
  675. package/dist/components/GroupedListBox/GroupedListBox.vue.js +5 -0
  676. package/dist/components/GroupedListBox/GroupedListBox.vue_vue_type_script_setup_true_lang.js +218 -0
  677. package/dist/components/ListBox/ListBox.vue.d.ts +3 -6
  678. package/dist/components/ListBox/ListBox.vue.js +5 -0
  679. package/dist/components/ListBox/ListBox.vue_vue_type_script_setup_true_lang.js +112 -0
  680. package/dist/components/ListBox/types.d.ts +1 -1
  681. package/dist/components/OptionsContainer/OptionsContainer.vue.d.ts +1 -0
  682. package/dist/components/OptionsContainer/OptionsContainer.vue.js +5 -0
  683. package/dist/components/OptionsContainer/OptionsContainer.vue_vue_type_script_setup_true_lang.js +52 -0
  684. package/dist/components/OptionsContainer/types.d.ts +14 -24
  685. package/dist/components/Transitions/ScaleFade.vue.js +5 -0
  686. package/dist/components/Transitions/ScaleFade.vue_vue_type_script_setup_true_lang.js +47 -0
  687. package/dist/components/Transitions/Slide.vue.js +7 -0
  688. package/dist/components/Transitions/Slide.vue_vue_type_script_setup_true_lang.js +28 -0
  689. package/dist/components/WarningIcon/WarningIcon.vue.d.ts +5 -0
  690. package/dist/components/WarningIcon/WarningIcon.vue.js +6 -0
  691. package/dist/components/WarningIcon/WarningIcon.vue_vue_type_script_setup_true_lang.js +16 -0
  692. package/dist/components/WarningIcon/types.d.ts +3 -0
  693. package/dist/composables/__testResets.d.ts +1 -0
  694. package/dist/composables/useActiveInactiveState.d.ts +4 -0
  695. package/dist/composables/useActiveInactiveState.js +21 -0
  696. package/dist/composables/useBaseOptions.d.ts +3 -3
  697. package/dist/composables/useBaseOptions.js +31 -0
  698. package/dist/composables/useBbConfig.d.ts +8 -37
  699. package/dist/composables/useBbConfig.js +5 -0
  700. package/dist/composables/useBbDropdownContext.d.ts +89 -0
  701. package/dist/composables/useBbDropdownContext.js +46 -0
  702. package/dist/composables/useBbFormContext.d.ts +90 -0
  703. package/dist/composables/useBbFormContext.js +69 -0
  704. package/dist/composables/useBbTableContext.d.ts +195 -0
  705. package/dist/composables/useBbTableContext.js +208 -0
  706. package/dist/composables/useBbTabsContext.d.ts +111 -0
  707. package/dist/composables/useBbTabsContext.js +219 -0
  708. package/dist/composables/useBreakpoint.d.ts +13 -0
  709. package/dist/composables/useBreakpoint.js +39 -0
  710. package/dist/composables/useBroadcastChannelInstance.d.ts +1 -3
  711. package/dist/composables/useBroadcastChannelInstance.js +39 -0
  712. package/dist/composables/useCancelableFunction.js +37 -0
  713. package/dist/composables/useCoherence.js +16 -0
  714. package/dist/composables/useConfirm.d.ts +257 -67
  715. package/dist/composables/useConfirm.js +122 -0
  716. package/dist/composables/useCountdown.js +42 -0
  717. package/dist/composables/useHashedWatcher.d.ts +17 -0
  718. package/dist/composables/useHashedWatcher.js +14 -0
  719. package/dist/composables/useId.d.ts +3 -2
  720. package/dist/composables/useIndexById.js +16 -0
  721. package/dist/composables/useInertiaPage.d.ts +10 -0
  722. package/dist/composables/useInertiaPage.js +8 -0
  723. package/dist/composables/useItemText.js +17 -0
  724. package/dist/composables/useItemValue.js +9 -0
  725. package/dist/composables/useItemsGetter.d.ts +1 -1
  726. package/dist/composables/useItemsGetter.js +30 -0
  727. package/dist/composables/useListboxFocus.js +66 -0
  728. package/dist/composables/useLocale.d.ts +1 -1
  729. package/dist/composables/useLocale.js +11 -0
  730. package/dist/composables/useLogger.d.ts +1 -1
  731. package/dist/composables/useLogger.js +10 -0
  732. package/dist/composables/useMinimalQueue.d.ts +1 -1
  733. package/dist/composables/useMobile.js +7 -0
  734. package/dist/composables/useOnFalsy.d.ts +2 -0
  735. package/dist/composables/useOnFalsy.js +9 -0
  736. package/dist/composables/useOnTruthy.d.ts +7 -0
  737. package/dist/composables/useOnTruthy.js +9 -0
  738. package/dist/composables/usePrefill.d.ts +2 -1
  739. package/dist/composables/usePrefill.js +24 -0
  740. package/dist/composables/useQuery.d.ts +15 -3
  741. package/dist/composables/useQuery.js +42 -0
  742. package/dist/composables/useRoute.js +11 -0
  743. package/dist/composables/useRouter.js +8 -0
  744. package/dist/composables/useSafeArea.d.ts +51 -0
  745. package/dist/composables/useSafeArea.js +16 -0
  746. package/dist/composables/useSegmentedFields.d.ts +103 -0
  747. package/dist/composables/useSegmentedFields.js +152 -0
  748. package/dist/composables/useTableWidthContext.d.ts +5 -0
  749. package/dist/composables/useTableWidthContext.js +42 -0
  750. package/dist/composables/useToast.d.ts +17 -73
  751. package/dist/composables/useToast.js +49 -0
  752. package/dist/composables/useTopLayer.d.ts +9 -0
  753. package/dist/composables/useTopLayer.js +7 -0
  754. package/dist/composables/useUntil.js +13 -0
  755. package/dist/composables/useViewportWidth.d.ts +17 -0
  756. package/dist/composables/useViewportWidth.js +6 -0
  757. package/dist/confirm-variants.d.ts +10 -0
  758. package/dist/deprecation/ai-deprecations.json.d.ts +784 -0
  759. package/dist/deprecation/ai-deprecations.json.js +4 -0
  760. package/dist/deprecation/removedProps.d.ts +3 -0
  761. package/dist/deprecation/removedProps.js +13 -0
  762. package/dist/directives/bbColor.d.ts +65 -0
  763. package/dist/directives/bbColor.js +24 -0
  764. package/dist/directives/bbDate.d.ts +66 -0
  765. package/dist/directives/bbDate.js +24 -0
  766. package/dist/directives/bbDropdown.d.ts +15 -11
  767. package/dist/directives/bbDropdown.js +23 -0
  768. package/dist/directives/bbTime.d.ts +64 -0
  769. package/dist/directives/bbTime.js +24 -0
  770. package/dist/directives/bbTooltip.d.ts +16 -11
  771. package/dist/directives/bbTooltip.js +24 -0
  772. package/dist/dropdown-item-variants.d.ts +10 -0
  773. package/dist/i18n/index.d.ts +51 -3
  774. package/dist/i18n/index.js +27 -0
  775. package/dist/i18n/locales/cs.json +115 -0
  776. package/dist/i18n/locales/da.json +115 -0
  777. package/dist/i18n/locales/de.json +115 -0
  778. package/dist/i18n/locales/el.json +115 -0
  779. package/dist/i18n/locales/en.json +115 -0
  780. package/dist/i18n/locales/es.json +115 -0
  781. package/dist/i18n/locales/fi.json +115 -0
  782. package/dist/i18n/locales/fr.json +115 -0
  783. package/dist/i18n/locales/hu.json +115 -0
  784. package/dist/i18n/locales/it.json +115 -0
  785. package/dist/i18n/locales/ja.json +115 -0
  786. package/dist/i18n/locales/ko.json +115 -0
  787. package/dist/i18n/locales/nb.json +115 -0
  788. package/dist/i18n/locales/nl.json +115 -0
  789. package/dist/i18n/locales/pl.json +115 -0
  790. package/dist/i18n/locales/pt.json +115 -0
  791. package/dist/i18n/locales/ro.json +115 -0
  792. package/dist/i18n/locales/ru.json +115 -0
  793. package/dist/i18n/locales/sv.json +115 -0
  794. package/dist/i18n/locales/tr.json +115 -0
  795. package/dist/i18n/locales/uk.json +115 -0
  796. package/dist/i18n/locales/zh-cn.json +115 -0
  797. package/dist/icons-whitelist.d.ts +8 -0
  798. package/dist/index.d.ts +94 -79
  799. package/dist/index.js +76 -82
  800. package/dist/indicator-variants.d.ts +13 -0
  801. package/dist/llms-full.txt +77073 -0
  802. package/dist/llms-medium.txt +1334 -0
  803. package/dist/llms.txt +103 -0
  804. package/dist/locale-blueprint.json +141 -0
  805. package/dist/nuxt-module.d.ts +3 -0
  806. package/dist/nuxt.js +19 -0
  807. package/dist/plugin.d.ts +4 -0
  808. package/dist/plugin.js +22 -0
  809. package/dist/reset.css +1 -0
  810. package/dist/reset.d.ts +0 -0
  811. package/dist/runtime/nuxt-plugin.d.ts +2 -0
  812. package/dist/runtime/nuxt-plugin.js +24 -0
  813. package/dist/styles.css +2 -0
  814. package/dist/styles.d.ts +0 -0
  815. package/dist/toast-variants.d.ts +13 -0
  816. package/dist/tooltip-variants.d.ts +10 -0
  817. package/dist/types/ActionButton.d.ts +32 -0
  818. package/dist/types/AlertVariant.d.ts +16 -0
  819. package/dist/types/BadgeVariant.d.ts +16 -0
  820. package/dist/types/ButtonVariant.d.ts +17 -0
  821. package/dist/types/Classes.d.ts +1 -1
  822. package/dist/types/ColumnName.d.ts +1 -1
  823. package/dist/types/CommonProps.d.ts +66 -279
  824. package/dist/types/Config.d.ts +84 -0
  825. package/dist/types/ConfirmVariant.d.ts +15 -0
  826. package/dist/types/DropdownItemVariant.d.ts +18 -0
  827. package/dist/types/Icon.d.ts +5 -7
  828. package/dist/types/IndicatorVariant.d.ts +16 -0
  829. package/dist/types/InputDirection.d.ts +15 -0
  830. package/dist/types/ItemAccessor.d.ts +21 -0
  831. package/dist/types/Placement.d.ts +6 -0
  832. package/dist/types/ResponsiveSize.d.ts +20 -0
  833. package/dist/types/SizeScale.d.ts +8 -0
  834. package/dist/types/ToastVariant.d.ts +17 -0
  835. package/dist/types/TooltipVariant.d.ts +18 -0
  836. package/dist/utilities/functions/bytesToUnit.d.ts +2 -0
  837. package/dist/utilities/functions/bytesToUnit.js +18 -0
  838. package/dist/utilities/functions/clampPrecision.js +10 -0
  839. package/dist/utilities/functions/convertDateValue.d.ts +23 -10
  840. package/dist/utilities/functions/convertDateValue.js +12 -0
  841. package/dist/utilities/functions/deburr.js +5 -0
  842. package/dist/utilities/functions/empty.js +5 -0
  843. package/dist/utilities/functions/flattenTree.d.ts +1 -1
  844. package/dist/utilities/functions/hash.js +5 -0
  845. package/dist/utilities/functions/isDateDisabled.d.ts +1 -0
  846. package/dist/utilities/functions/isDateDisabled.js +20 -0
  847. package/dist/utilities/functions/isEqual.js +5 -0
  848. package/dist/utilities/functions/isNotEmpty.js +5 -0
  849. package/dist/utilities/functions/matchAnyKey.d.ts +1 -1
  850. package/dist/utilities/functions/matchAnyKey.js +15 -0
  851. package/dist/utilities/functions/parseSize.d.ts +1 -1
  852. package/dist/utilities/functions/parseSize.js +15 -0
  853. package/dist/utilities/functions/percentage.js +11 -0
  854. package/dist/utilities/functions/pipe.d.ts +1 -1
  855. package/dist/utilities/functions/reject.d.ts +25 -0
  856. package/dist/utilities/functions/reject.js +28 -0
  857. package/dist/utilities/functions/resolveEnumProp.d.ts +19 -0
  858. package/dist/utilities/functions/resolveEnumProp.js +5 -0
  859. package/dist/utilities/functions/slotKey.d.ts +13 -0
  860. package/dist/utilities/functions/slotKey.js +4 -0
  861. package/dist/utilities/functions/sort.d.ts +2 -2
  862. package/dist/utilities/functions/sort.js +19 -0
  863. package/dist/utilities/functions/throttle.js +5 -0
  864. package/dist/utilities/functions/waitFor.d.ts +9 -2
  865. package/dist/utilities/functions/waitFor.js +14 -0
  866. package/dist/utilities/functions/warnRemovedAttrs.d.ts +11 -0
  867. package/dist/utilities/functions/warnRemovedAttrs.js +20 -0
  868. package/dist/utilities/functions/when.js +5 -0
  869. package/dist/utilities/misc/iconsInjectionKey.d.ts +15 -0
  870. package/dist/utilities/misc/iconsInjectionKey.js +4 -0
  871. package/dist/utilities/misc/transitions.d.ts +5 -0
  872. package/dist/utilities/misc/transitions.js +8 -0
  873. package/dist/utils/injectLibraryStyles.d.ts +6 -0
  874. package/dist/utils/injectLibraryStyles.js +38 -0
  875. package/dist/validated/BbCheckbox.vue.d.ts +29 -0
  876. package/dist/validated/BbCheckbox.vue.js +5 -0
  877. package/dist/validated/BbCheckbox.vue_vue_type_script_setup_true_lang.js +75 -0
  878. package/dist/validated/BbCheckboxGroup.vue.d.ts +34 -0
  879. package/dist/validated/BbCheckboxGroup.vue.js +5 -0
  880. package/dist/validated/BbCheckboxGroup.vue_vue_type_script_setup_true_lang.js +76 -0
  881. package/dist/validated/BbColorInput.vue.d.ts +36 -0
  882. package/dist/validated/BbColorInput.vue.js +5 -0
  883. package/dist/validated/BbColorInput.vue_vue_type_script_setup_true_lang.js +71 -0
  884. package/dist/validated/BbDatePickerInput.vue.d.ts +40 -0
  885. package/dist/validated/BbDatePickerInput.vue.js +5 -0
  886. package/dist/validated/BbDatePickerInput.vue_vue_type_script_setup_true_lang.js +87 -0
  887. package/dist/validated/BbDropzone.vue.d.ts +24 -0
  888. package/dist/validated/BbDropzone.vue.js +5 -0
  889. package/dist/validated/BbDropzone.vue_vue_type_script_setup_true_lang.js +45 -0
  890. package/dist/validated/BbForm.vue.d.ts +28 -0
  891. package/dist/validated/BbForm.vue.js +5 -0
  892. package/dist/validated/BbForm.vue_vue_type_script_setup_true_lang.js +42 -0
  893. package/dist/validated/BbNumberInput.vue.d.ts +36 -0
  894. package/dist/validated/BbNumberInput.vue.js +5 -0
  895. package/dist/validated/BbNumberInput.vue_vue_type_script_setup_true_lang.js +72 -0
  896. package/dist/validated/BbRadioGroup.vue.d.ts +34 -0
  897. package/dist/validated/BbRadioGroup.vue.js +5 -0
  898. package/dist/validated/BbRadioGroup.vue_vue_type_script_setup_true_lang.js +75 -0
  899. package/dist/validated/BbRating.vue.d.ts +30 -0
  900. package/dist/validated/BbRating.vue.js +5 -0
  901. package/dist/validated/BbRating.vue_vue_type_script_setup_true_lang.js +63 -0
  902. package/dist/validated/BbSelect.vue.d.ts +46 -0
  903. package/dist/validated/BbSelect.vue.js +5 -0
  904. package/dist/validated/BbSelect.vue_vue_type_script_setup_true_lang.js +100 -0
  905. package/dist/validated/BbSlider.vue.d.ts +36 -0
  906. package/dist/validated/BbSlider.vue.js +5 -0
  907. package/dist/validated/BbSlider.vue_vue_type_script_setup_true_lang.js +69 -0
  908. package/dist/validated/BbSwitch.vue.d.ts +29 -0
  909. package/dist/validated/BbSwitch.vue.js +5 -0
  910. package/dist/validated/BbSwitch.vue_vue_type_script_setup_true_lang.js +75 -0
  911. package/dist/validated/BbSwitchGroup.vue.d.ts +34 -0
  912. package/dist/validated/BbSwitchGroup.vue.js +5 -0
  913. package/dist/validated/BbSwitchGroup.vue_vue_type_script_setup_true_lang.js +76 -0
  914. package/dist/validated/BbTag.vue.d.ts +36 -0
  915. package/dist/validated/BbTag.vue.js +5 -0
  916. package/dist/validated/BbTag.vue_vue_type_script_setup_true_lang.js +72 -0
  917. package/dist/validated/BbTextInput.vue.d.ts +36 -0
  918. package/dist/validated/BbTextInput.vue.js +5 -0
  919. package/dist/validated/BbTextInput.vue_vue_type_script_setup_true_lang.js +72 -0
  920. package/dist/validated/BbTextarea.vue.d.ts +36 -0
  921. package/dist/validated/BbTextarea.vue.js +5 -0
  922. package/dist/validated/BbTextarea.vue_vue_type_script_setup_true_lang.js +70 -0
  923. package/dist/validated/BbTimePickerInput.vue.d.ts +36 -0
  924. package/dist/validated/BbTimePickerInput.vue.js +5 -0
  925. package/dist/validated/BbTimePickerInput.vue_vue_type_script_setup_true_lang.js +78 -0
  926. package/dist/validated/index.d.ts +29 -0
  927. package/dist/validated/useValidatedField.d.ts +52 -0
  928. package/dist/validated/useValidatedField.js +51 -0
  929. package/dist/validated.js +21 -0
  930. package/dist/vite-plugin.d.ts +217 -0
  931. package/dist/vite.js +530 -0
  932. package/llms.txt +105 -0
  933. package/package.json +161 -59
  934. package/scripts/lib/__fixtures__/validated-manifest.json +65 -0
  935. package/scripts/lib/ensure-manifest.mjs +39 -0
  936. package/scripts/lib/eslint-plugin-autofix.node.test.mjs +109 -0
  937. package/scripts/lib/eslint-plugin.mjs +285 -0
  938. package/scripts/lib/eslint-plugin.node.test.mjs +161 -0
  939. package/scripts/lib/html-attributes.mjs +191 -0
  940. package/scripts/lib/mcp-config.mjs +126 -0
  941. package/scripts/lib/migrate-fix.mjs +115 -0
  942. package/scripts/lib/validate-bb-markup.mjs +421 -0
  943. package/dist/ai/BaseButton.md +0 -449
  944. package/dist/ai/BaseCheckbox.md +0 -512
  945. package/dist/ai/BaseCheckboxGroup.md +0 -597
  946. package/dist/ai/BaseColorInput.md +0 -461
  947. package/dist/ai/BaseDatePicker.md +0 -739
  948. package/dist/ai/BaseDatePickerInput.md +0 -1517
  949. package/dist/ai/BaseDialog.md +0 -610
  950. package/dist/ai/BaseInputContainer.md +0 -570
  951. package/dist/ai/BaseNumberInput.md +0 -509
  952. package/dist/ai/BaseRadio.md +0 -405
  953. package/dist/ai/BaseRadioGroup.md +0 -535
  954. package/dist/ai/BaseRating.md +0 -489
  955. package/dist/ai/BaseSelect.md +0 -1742
  956. package/dist/ai/BaseSlider.md +0 -871
  957. package/dist/ai/BaseSwitch.md +0 -322
  958. package/dist/ai/BaseSwitchGroup.md +0 -298
  959. package/dist/ai/BaseTag.md +0 -623
  960. package/dist/ai/BaseTextInput.md +0 -392
  961. package/dist/ai/BaseTextarea.md +0 -398
  962. package/dist/ai/BbChip.md +0 -147
  963. package/dist/ai/BbIntersection.md +0 -120
  964. package/dist/ai/BbRatio.md +0 -61
  965. package/dist/ai/BbRows.md +0 -307
  966. package/dist/ai/BbTab.md +0 -950
  967. package/dist/ai/CommonInputInnerContainer.md +0 -414
  968. package/dist/ai/CommonInputOuterContainer.md +0 -55
  969. package/dist/components/BaseButton/BaseButton.vue.d.ts +0 -14
  970. package/dist/components/BaseButton/types.d.ts +0 -159
  971. package/dist/components/BaseCheckbox/BaseCheckbox.vue.d.ts +0 -39
  972. package/dist/components/BaseCheckbox/types.d.ts +0 -213
  973. package/dist/components/BaseCheckboxGroup/BaseCheckboxGroup.vue.d.ts +0 -30
  974. package/dist/components/BaseCheckboxGroup/types.d.ts +0 -282
  975. package/dist/components/BaseColorInput/BaseColorInput.vue.d.ts +0 -35
  976. package/dist/components/BaseColorInput/ColorPalette.vue.d.ts +0 -13
  977. package/dist/components/BaseColorInput/types.d.ts +0 -233
  978. package/dist/components/BaseDatePicker/BaseDatePickerHeader.vue.d.ts +0 -15
  979. package/dist/components/BaseDatePicker/BaseDatePickerInputDaySelector.vue.d.ts +0 -58
  980. package/dist/components/BaseDatePicker/BaseDatePickerMonthSelector.vue.d.ts +0 -12
  981. package/dist/components/BaseDatePicker/BaseDatePickerYearSelector.vue.d.ts +0 -12
  982. package/dist/components/BaseDatePicker/types.d.ts +0 -100
  983. package/dist/components/BaseDatePickerInput/BaseDatePickerInput.vue.d.ts +0 -26
  984. package/dist/components/BaseDatePickerInput/types.d.ts +0 -206
  985. package/dist/components/BaseDialog/BaseDialog.vue.d.ts +0 -37
  986. package/dist/components/BaseDialog/types.d.ts +0 -180
  987. package/dist/components/BaseInputContainer/BaseInputContainer.vue.d.ts +0 -18
  988. package/dist/components/BaseInputContainer/types.d.ts +0 -126
  989. package/dist/components/BaseNumberInput/BaseNumberInput.vue.d.ts +0 -46
  990. package/dist/components/BaseNumberInput/types.d.ts +0 -191
  991. package/dist/components/BaseRadio/BaseRadio.vue.d.ts +0 -31
  992. package/dist/components/BaseRadio/types.d.ts +0 -173
  993. package/dist/components/BaseRadioGroup/BaseRadioGroup.vue.d.ts +0 -30
  994. package/dist/components/BaseRadioGroup/types.d.ts +0 -240
  995. package/dist/components/BaseRating/BaseRating.vue.d.ts +0 -35
  996. package/dist/components/BaseRating/types.d.ts +0 -144
  997. package/dist/components/BaseSelect/BaseSelect.vue.d.ts +0 -28
  998. package/dist/components/BaseSelect/types.d.ts +0 -470
  999. package/dist/components/BaseSlider/BaseSlider.vue.d.ts +0 -38
  1000. package/dist/components/BaseSlider/types.d.ts +0 -201
  1001. package/dist/components/BaseSwitch/BaseSwitch.vue.d.ts +0 -34
  1002. package/dist/components/BaseSwitch/types.d.ts +0 -25
  1003. package/dist/components/BaseSwitchGroup/BaseSwitchGroup.vue.d.ts +0 -30
  1004. package/dist/components/BaseSwitchGroup/types.d.ts +0 -8
  1005. package/dist/components/BaseTag/BaseTag.vue.d.ts +0 -37
  1006. package/dist/components/BaseTag/types.d.ts +0 -136
  1007. package/dist/components/BaseTextInput/BaseTextInput.vue.d.ts +0 -33
  1008. package/dist/components/BaseTextInput/types.d.ts +0 -166
  1009. package/dist/components/BaseTextarea/BaseTextarea.vue.d.ts +0 -31
  1010. package/dist/components/BaseTextarea/types.d.ts +0 -151
  1011. package/dist/components/BbChip/BbChip.vue.d.ts +0 -17
  1012. package/dist/components/BbChip/types.d.ts +0 -23
  1013. package/dist/components/BbIntersection/BbIntersection.vue.d.ts +0 -23
  1014. package/dist/components/BbIntersection/types.d.ts +0 -36
  1015. package/dist/components/BbRatio/BbRatio.vue.d.ts +0 -14
  1016. package/dist/components/BbRatio/types.d.ts +0 -15
  1017. package/dist/components/BbRows/BbRows.vue.d.ts +0 -62
  1018. package/dist/components/BbRows/types.d.ts +0 -21
  1019. package/dist/components/BbTab/BbTab.vue.d.ts +0 -27
  1020. package/dist/components/BbTab/types.d.ts +0 -186
  1021. package/dist/components/CommonInputInnerContainer/CommonInputInnerContainer.vue.d.ts +0 -15
  1022. package/dist/components/CommonInputInnerContainer/types.d.ts +0 -47
  1023. package/dist/components/CommonInputOuterContainer/CommonInputOuterContainer.vue.d.ts +0 -11
  1024. package/dist/components/CommonInputOuterContainer/types.d.ts +0 -16
  1025. package/dist/composables/useModalsState.d.ts +0 -5
  1026. package/dist/composables/useQueue.d.ts +0 -58
  1027. package/dist/composables/useWizard.d.ts +0 -85
  1028. package/dist/i18n/en.d.ts +0 -67
  1029. package/dist/i18n/it.d.ts +0 -67
  1030. package/dist/index.css +0 -2
  1031. package/dist/index100.js +0 -6
  1032. package/dist/index101.js +0 -300
  1033. package/dist/index102.js +0 -16
  1034. package/dist/index103.js +0 -16
  1035. package/dist/index104.js +0 -27
  1036. package/dist/index109.js +0 -19
  1037. package/dist/index11.js +0 -42
  1038. package/dist/index110.js +0 -12
  1039. package/dist/index111.js +0 -19
  1040. package/dist/index112.js +0 -25
  1041. package/dist/index114.js +0 -7
  1042. package/dist/index115.js +0 -241
  1043. package/dist/index116.js +0 -5
  1044. package/dist/index117.js +0 -42
  1045. package/dist/index118.js +0 -5
  1046. package/dist/index119.js +0 -53
  1047. package/dist/index120.js +0 -5
  1048. package/dist/index121.js +0 -32
  1049. package/dist/index122.js +0 -5
  1050. package/dist/index124.js +0 -52
  1051. package/dist/index125.js +0 -5
  1052. package/dist/index126.js +0 -223
  1053. package/dist/index128.js +0 -6
  1054. package/dist/index129.js +0 -162
  1055. package/dist/index13.js +0 -92
  1056. package/dist/index130.js +0 -25
  1057. package/dist/index131.js +0 -51
  1058. package/dist/index132.js +0 -5
  1059. package/dist/index133.js +0 -607
  1060. package/dist/index135.js +0 -6
  1061. package/dist/index136.js +0 -14
  1062. package/dist/index137.js +0 -153
  1063. package/dist/index139.js +0 -6
  1064. package/dist/index14.js +0 -306
  1065. package/dist/index140.js +0 -224
  1066. package/dist/index141.js +0 -5
  1067. package/dist/index143.js +0 -128
  1068. package/dist/index145.js +0 -6
  1069. package/dist/index146.js +0 -8
  1070. package/dist/index147.js +0 -182
  1071. package/dist/index149.js +0 -6
  1072. package/dist/index15.js +0 -10
  1073. package/dist/index151.js +0 -4
  1074. package/dist/index152.js +0 -95
  1075. package/dist/index154.js +0 -6
  1076. package/dist/index155.js +0 -168
  1077. package/dist/index157.js +0 -6
  1078. package/dist/index158.js +0 -145
  1079. package/dist/index16.js +0 -59
  1080. package/dist/index160.js +0 -6
  1081. package/dist/index161.js +0 -53
  1082. package/dist/index163.js +0 -6
  1083. package/dist/index164.js +0 -16
  1084. package/dist/index166.js +0 -35
  1085. package/dist/index168.js +0 -6
  1086. package/dist/index169.js +0 -89
  1087. package/dist/index171.js +0 -6
  1088. package/dist/index172.js +0 -82
  1089. package/dist/index174.js +0 -6
  1090. package/dist/index175.js +0 -38
  1091. package/dist/index176.js +0 -461
  1092. package/dist/index177.js +0 -28
  1093. package/dist/index178.js +0 -66
  1094. package/dist/index18.js +0 -35
  1095. package/dist/index180.js +0 -162
  1096. package/dist/index181.js +0 -5
  1097. package/dist/index182.js +0 -217
  1098. package/dist/index183.js +0 -5
  1099. package/dist/index184.js +0 -112
  1100. package/dist/index185.js +0 -5
  1101. package/dist/index186.js +0 -459
  1102. package/dist/index188.js +0 -6
  1103. package/dist/index19.js +0 -4
  1104. package/dist/index192.js +0 -268
  1105. package/dist/index194.js +0 -6
  1106. package/dist/index195.js +0 -124
  1107. package/dist/index197.js +0 -6
  1108. package/dist/index198.js +0 -174
  1109. package/dist/index2.js +0 -20
  1110. package/dist/index20.js +0 -9
  1111. package/dist/index200.js +0 -6
  1112. package/dist/index201.js +0 -224
  1113. package/dist/index203.js +0 -6
  1114. package/dist/index204.js +0 -135
  1115. package/dist/index206.js +0 -6
  1116. package/dist/index207.js +0 -5
  1117. package/dist/index208.js +0 -170
  1118. package/dist/index21.js +0 -35
  1119. package/dist/index210.js +0 -6
  1120. package/dist/index211.js +0 -37
  1121. package/dist/index213.js +0 -6
  1122. package/dist/index214.js +0 -63
  1123. package/dist/index216.js +0 -6
  1124. package/dist/index217.js +0 -74
  1125. package/dist/index219.js +0 -6
  1126. package/dist/index22.js +0 -6
  1127. package/dist/index220.js +0 -65
  1128. package/dist/index222.js +0 -6
  1129. package/dist/index223.js +0 -67
  1130. package/dist/index225.js +0 -6
  1131. package/dist/index226.js +0 -41
  1132. package/dist/index228.js +0 -6
  1133. package/dist/index229.js +0 -10
  1134. package/dist/index23.js +0 -7
  1135. package/dist/index230.js +0 -22
  1136. package/dist/index232.js +0 -8
  1137. package/dist/index233.js +0 -297
  1138. package/dist/index235.js +0 -6
  1139. package/dist/index236.js +0 -47
  1140. package/dist/index237.js +0 -165
  1141. package/dist/index239.js +0 -6
  1142. package/dist/index24.js +0 -9
  1143. package/dist/index241.js +0 -159
  1144. package/dist/index243.js +0 -6
  1145. package/dist/index244.js +0 -134
  1146. package/dist/index246.js +0 -6
  1147. package/dist/index247.js +0 -167
  1148. package/dist/index249.js +0 -6
  1149. package/dist/index25.js +0 -24
  1150. package/dist/index250.js +0 -147
  1151. package/dist/index252.js +0 -6
  1152. package/dist/index253.js +0 -35
  1153. package/dist/index255.js +0 -6
  1154. package/dist/index256.js +0 -65
  1155. package/dist/index258.js +0 -6
  1156. package/dist/index259.js +0 -19
  1157. package/dist/index260.js +0 -5
  1158. package/dist/index261.js +0 -160
  1159. package/dist/index263.js +0 -6
  1160. package/dist/index264.js +0 -60
  1161. package/dist/index266.js +0 -6
  1162. package/dist/index267.js +0 -134
  1163. package/dist/index269.js +0 -6
  1164. package/dist/index27.js +0 -4
  1165. package/dist/index271.js +0 -206
  1166. package/dist/index273.js +0 -6
  1167. package/dist/index274.js +0 -85
  1168. package/dist/index275.js +0 -5
  1169. package/dist/index276.js +0 -55
  1170. package/dist/index277.js +0 -5
  1171. package/dist/index278.js +0 -164
  1172. package/dist/index28.js +0 -8
  1173. package/dist/index280.js +0 -6
  1174. package/dist/index281.js +0 -145
  1175. package/dist/index283.js +0 -6
  1176. package/dist/index284.js +0 -11
  1177. package/dist/index285.js +0 -200
  1178. package/dist/index287.js +0 -6
  1179. package/dist/index288.js +0 -241
  1180. package/dist/index29.js +0 -7
  1181. package/dist/index290.js +0 -6
  1182. package/dist/index291.js +0 -21
  1183. package/dist/index293.js +0 -6
  1184. package/dist/index294.js +0 -125
  1185. package/dist/index296.js +0 -6
  1186. package/dist/index297.js +0 -161
  1187. package/dist/index299.js +0 -6
  1188. package/dist/index30.js +0 -118
  1189. package/dist/index300.js +0 -127
  1190. package/dist/index302.js +0 -6
  1191. package/dist/index303.js +0 -20
  1192. package/dist/index305.js +0 -6
  1193. package/dist/index306.js +0 -116
  1194. package/dist/index307.js +0 -5
  1195. package/dist/index308.js +0 -507
  1196. package/dist/index31.js +0 -10
  1197. package/dist/index310.js +0 -6
  1198. package/dist/index311.js +0 -196
  1199. package/dist/index313.js +0 -6
  1200. package/dist/index314.js +0 -142
  1201. package/dist/index316.js +0 -6
  1202. package/dist/index317.js +0 -134
  1203. package/dist/index319.js +0 -6
  1204. package/dist/index32.js +0 -28
  1205. package/dist/index320.js +0 -162
  1206. package/dist/index322.js +0 -6
  1207. package/dist/index323.js +0 -319
  1208. package/dist/index325.js +0 -6
  1209. package/dist/index326.js +0 -40
  1210. package/dist/index327.js +0 -43
  1211. package/dist/index328.js +0 -665
  1212. package/dist/index33.js +0 -5
  1213. package/dist/index330.js +0 -6
  1214. package/dist/index331.js +0 -155
  1215. package/dist/index333.js +0 -6
  1216. package/dist/index334.js +0 -142
  1217. package/dist/index336.js +0 -6
  1218. package/dist/index337.js +0 -155
  1219. package/dist/index339.js +0 -6
  1220. package/dist/index34.js +0 -112
  1221. package/dist/index340.js +0 -58
  1222. package/dist/index341.js +0 -5
  1223. package/dist/index342.js +0 -20
  1224. package/dist/index344.js +0 -6
  1225. package/dist/index345.js +0 -142
  1226. package/dist/index347.js +0 -6
  1227. package/dist/index349.js +0 -109
  1228. package/dist/index351.js +0 -6
  1229. package/dist/index353.js +0 -24
  1230. package/dist/index354.js +0 -23
  1231. package/dist/index355.js +0 -839
  1232. package/dist/index356.js +0 -474
  1233. package/dist/index357.js +0 -52
  1234. package/dist/index358.js +0 -7
  1235. package/dist/index36.js +0 -6
  1236. package/dist/index38.js +0 -63
  1237. package/dist/index39.js +0 -63
  1238. package/dist/index40.js +0 -9
  1239. package/dist/index42.js +0 -8
  1240. package/dist/index43.js +0 -179
  1241. package/dist/index44.js +0 -21
  1242. package/dist/index46.js +0 -385
  1243. package/dist/index48.js +0 -6
  1244. package/dist/index49.js +0 -33
  1245. package/dist/index5.js +0 -34
  1246. package/dist/index50.js +0 -12
  1247. package/dist/index51.js +0 -8
  1248. package/dist/index52.js +0 -106
  1249. package/dist/index53.js +0 -330
  1250. package/dist/index54.js +0 -97
  1251. package/dist/index55.js +0 -328
  1252. package/dist/index56.js +0 -28
  1253. package/dist/index57.js +0 -102
  1254. package/dist/index58.js +0 -157
  1255. package/dist/index6.js +0 -38
  1256. package/dist/index60.js +0 -6
  1257. package/dist/index61.js +0 -131
  1258. package/dist/index62.js +0 -5
  1259. package/dist/index65.js +0 -35
  1260. package/dist/index66.js +0 -26
  1261. package/dist/index68.js +0 -6
  1262. package/dist/index69.js +0 -54
  1263. package/dist/index7.js +0 -58
  1264. package/dist/index71.js +0 -6
  1265. package/dist/index72.js +0 -14
  1266. package/dist/index74.js +0 -6
  1267. package/dist/index75.js +0 -159
  1268. package/dist/index77.js +0 -6
  1269. package/dist/index78.js +0 -4
  1270. package/dist/index79.js +0 -4
  1271. package/dist/index80.js +0 -54
  1272. package/dist/index81.js +0 -5
  1273. package/dist/index82.js +0 -5
  1274. package/dist/index83.js +0 -141
  1275. package/dist/index85.js +0 -6
  1276. package/dist/index86.js +0 -65
  1277. package/dist/index88.js +0 -6
  1278. package/dist/index89.js +0 -30
  1279. package/dist/index9.js +0 -11
  1280. package/dist/index90.js +0 -19
  1281. package/dist/index91.js +0 -17
  1282. package/dist/index92.js +0 -9
  1283. package/dist/index93.js +0 -34
  1284. package/dist/index95.js +0 -16
  1285. package/dist/index96.js +0 -10
  1286. package/dist/index97.js +0 -18
  1287. package/dist/index98.js +0 -189
  1288. package/dist/types/VoidHtmlElements.d.ts +0 -1
  1289. package/dist/utilities/functions/add.d.ts +0 -1
  1290. package/dist/utilities/functions/booleanishToBoolean.d.ts +0 -6
  1291. package/dist/utilities/functions/capitalize.d.ts +0 -4
  1292. package/dist/utilities/functions/cloneDeep.d.ts +0 -1
  1293. package/dist/utilities/functions/debounce.d.ts +0 -1
  1294. package/dist/utilities/functions/drop.d.ts +0 -1
  1295. package/dist/utilities/functions/exceedsPrecision.d.ts +0 -1
  1296. package/dist/utilities/functions/findRight.d.ts +0 -4
  1297. package/dist/utilities/functions/findRightIndex.d.ts +0 -4
  1298. package/dist/utilities/functions/flow.d.ts +0 -1
  1299. package/dist/utilities/functions/getHueFromRGB.d.ts +0 -9
  1300. package/dist/utilities/functions/groupBy.d.ts +0 -6
  1301. package/dist/utilities/functions/gt.d.ts +0 -7
  1302. package/dist/utilities/functions/hexToRGB.d.ts +0 -7
  1303. package/dist/utilities/functions/hslToHex.d.ts +0 -1
  1304. package/dist/utilities/functions/includes.d.ts +0 -1
  1305. package/dist/utilities/functions/initial.d.ts +0 -1
  1306. package/dist/utilities/functions/isCssColor.d.ts +0 -5
  1307. package/dist/utilities/functions/lt.d.ts +0 -7
  1308. package/dist/utilities/functions/makeUrl.d.ts +0 -1
  1309. package/dist/utilities/functions/mapKeys.d.ts +0 -5
  1310. package/dist/utilities/functions/mapValues.d.ts +0 -1
  1311. package/dist/utilities/functions/matchesPropertyInsensitive.d.ts +0 -1
  1312. package/dist/utilities/functions/max.d.ts +0 -2
  1313. package/dist/utilities/functions/mean.d.ts +0 -4
  1314. package/dist/utilities/functions/merge.d.ts +0 -11
  1315. package/dist/utilities/functions/millisecondsTo.d.ts +0 -1
  1316. package/dist/utilities/functions/negate.d.ts +0 -4
  1317. package/dist/utilities/functions/normalizeItemGroups.d.ts +0 -13
  1318. package/dist/utilities/functions/omit.d.ts +0 -2
  1319. package/dist/utilities/functions/overEvery.d.ts +0 -4
  1320. package/dist/utilities/functions/pick.d.ts +0 -1
  1321. package/dist/utilities/functions/replace.d.ts +0 -1
  1322. package/dist/utilities/functions/replaceInTree.d.ts +0 -4
  1323. package/dist/utilities/functions/retry.d.ts +0 -5
  1324. package/dist/utilities/functions/roundToTheNearest.d.ts +0 -8
  1325. package/dist/utilities/functions/set.d.ts +0 -15
  1326. package/dist/utilities/functions/stub.d.ts +0 -6
  1327. package/dist/utilities/functions/transposePercent.d.ts +0 -9
  1328. package/dist/utilities/functions/tryTo.d.ts +0 -2
  1329. package/dist/utilities/functions/uniqBy.d.ts +0 -1
  1330. /package/dist/{index64.js → _virtual/_plugin-vue_export-helper.js} +0 -0
  1331. /package/dist/components/{BaseButton → BbBaseButton}/RouterComponent.vue.d.ts +0 -0
  1332. /package/dist/components/{BaseDatePicker/BaseDatePicker.vue.d.ts → BbBaseDatePicker/BbBaseDatePicker.vue.d.ts} +0 -0
  1333. /package/dist/{index165.js → composables/useArray.js} +0 -0
  1334. /package/dist/{index17.js → composables/useFrameworkDetection.js} +0 -0
  1335. /package/dist/{index12.js → composables/useId.js} +0 -0
  1336. /package/dist/{index352.js → directives/createPopoverDirective.js} +0 -0
  1337. /package/dist/{index107.js → utilities/functions/chunk.js} +0 -0
  1338. /package/dist/{index8.js → utilities/functions/clamp.js} +0 -0
  1339. /package/dist/{index150.js → utilities/functions/curry.js} +0 -0
  1340. /package/dist/{index45.js → utilities/functions/extractDomContainer.js} +0 -0
  1341. /package/dist/{index270.js → utilities/functions/findInTree.js} +0 -0
  1342. /package/dist/{index348.js → utilities/functions/flattenTree.js} +0 -0
  1343. /package/dist/{index41.js → utilities/functions/get.js} +0 -0
  1344. /package/dist/{index189.js → utilities/functions/head.js} +0 -0
  1345. /package/dist/{index108.js → utilities/functions/identity.js} +0 -0
  1346. /package/dist/{index179.js → utilities/functions/indexBy.js} +0 -0
  1347. /package/dist/{index37.js → utilities/functions/isHex.js} +0 -0
  1348. /package/dist/{index94.js → utilities/functions/isNil.js} +0 -0
  1349. /package/dist/{index26.js → utilities/functions/isNotNil.js} +0 -0
  1350. /package/dist/{index105.js → utilities/functions/last.js} +0 -0
  1351. /package/dist/{index142.js → utilities/functions/noop.js} +0 -0
  1352. /package/dist/{index231.js → utilities/functions/parseWidthString.js} +0 -0
  1353. /package/dist/{index240.js → utilities/functions/pickBy.js} +0 -0
  1354. /package/dist/{index123.js → utilities/functions/range.js} +0 -0
  1355. /package/dist/{index190.js → utilities/functions/snapToStep.js} +0 -0
  1356. /package/dist/{index191.js → utilities/functions/transpose.js} +0 -0
  1357. /package/dist/{index106.js → utilities/functions/validateDateFormat.js} +0 -0
  1358. /package/dist/{index10.js → utilities/functions/wait.js} +0 -0
@@ -1,2515 +1,2113 @@
1
1
  # BbTable
2
2
 
3
- ## Template & Script
3
+ ## Agent Contract
4
+
5
+ - Safety level: **Consumer API**
6
+ - Group: **Data Display**
7
+ - Description: Displays tabular data, configurable columns and data mapper.
8
+ - Usage rule: Prefer this component for consumer-facing implementations.
9
+ - Implementation source (only when the contract below is not enough): [source/BbTable.md](./source/BbTable.md)
10
+
11
+ ## Usage & Guidelines
12
+
13
+ ### When to use
14
+
15
+ Reach for `BbTable` to present **rows of records with columns** — an invoices
16
+ listing, a members admin, an audit log. It owns extraction, formatting, loading
17
+ skeletons, empty states, selection, sorting indicators, row expansion and
18
+ keyboard navigation, so your markup stays a column definition plus a few slots.
19
+
20
+ - For a **single-select or multi-select list of options** in a form, use
21
+ `BbSelect` / `BbCheckboxGroup` / `BbRadioGroup` — not a selectable table.
22
+ - For **hierarchical, expandable nodes** (a file tree, nested categories), use
23
+ `BbTree`. Flat rows with one level of detail stay in `BbTable` (`#expand`).
24
+ - `BbTable` is items-driven: read the Options/Items and Fetch playbooks. Pass
25
+ domain objects straight to `items` and set `item-value` for stable row
26
+ identity — never pre-map rows into `{ label, value }` shapes.
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
30
+ that state to your data source (client-side or via `dependencies`-driven
31
+ refetch). This keeps it equally at home in a client-only screen and a
32
+ server-paginated one.
33
+
34
+ ### Columns: the full grammar
35
+
36
+ A column is `{ key, label }` plus optional presentation — everything a cell
37
+ needs is declared here, not in markup:
38
+
39
+ - **`key`** extracts the cell from the item; dot paths reach nested fields
40
+ (`'customer.address.city'`). Keys autocomplete from the item type (up to 3
41
+ levels deep), but any string is accepted — that is how virtual columns work.
42
+ - **`formatter(content, key, item)`** transforms the extracted value. It runs
43
+ even when the value is nullish, so a virtual column whose `key` matches no
44
+ field can still compute from `item`; set `formatOnNull: false` when a
45
+ formatter must only see real values.
46
+ - **`placeholder`** fills the cell when the pipeline still ends nullish.
47
+ - **`align`** (`'left' | 'center' | 'right'`) per column; the table-level
48
+ `align` prop sets the default for the whole table (`left`).
49
+ - **`width`** freezes the column: numbers and bare numeric strings are px
50
+ (`130`, `'130'`); anything carrying a unit passes through (`'12rem'`,
51
+ `'20%'`). Declaring any `width` switches the table to `table-layout: fixed`
52
+ (widths are only authoritative there); unfrozen columns then share the
53
+ leftover space proportionally to their content.
54
+ - **`skeleton`** picks the loading placeholder shape (see Loading below).
55
+ - **`sortable` / `sorted`** — see Sorting.
56
+ - **`thClass` / `tdClass` / `rowClass`** style the header cell, the data cells,
57
+ and the whole `<tr>`. `tdClass` and `rowClass` may be functions
58
+ `(content, key, item) => Classes` for state-dependent styling. Table-level
59
+ `thClass` / `tdClass` / `rowClass` / `headerRowClass` apply everywhere; a
60
+ row's classes accumulate the table `rowClass` plus every column's.
61
+ - **`snap`** maps a nested table's column onto its parent's tracks — see
62
+ Nested tables.
63
+
64
+ **Columns, formatters and a per-row class**
4
65
 
5
66
  ```vue
6
67
  <template>
7
- <component
8
- :is="containerTag"
9
- :id="id"
10
- ref="containerRef"
11
- :aria-busy="loading"
12
- :aria-rowcount="hasProvidedAccessibilityData ? props.totalItems : undefined"
13
- :class="containerClass"
14
- :data-bb-table-id="tableId"
15
- :inert="disabled"
16
- :style="containerStyle"
68
+ <div
69
+ class="max-w-2xl overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
17
70
  >
18
- <legend v-if="selectable" class="sr-only">{{ legend }}</legend>
19
- <table ref="tableRef">
20
- <caption
21
- v-if="caption"
22
- :class="{
23
- 'bb-table-caption': true,
24
- 'sr-only': !displayCaption,
25
- }"
26
- >
27
- {{
28
- caption
29
- }}
30
- </caption>
31
- <colgroup>
32
- <col
33
- v-for="track in columnTracks"
34
- :key="track.key"
35
- :style="{ width: track.width }"
36
- />
37
- </colgroup>
38
- <thead>
39
- <!-- @vue-ignore -->
40
- <slot name="thead">
41
- <tr
42
- :aria-rowindex="hasProvidedAccessibilityData ? 1 : undefined"
43
- class="bb-table-header-row"
44
- >
45
- <th
46
- v-if="selectable"
47
- class="bb-table-header bb-table-header--select"
48
- :class="thClass"
49
- scope="col"
50
- >
51
- <!-- @vue-ignore -->
52
- <slot
53
- :multiple="multiple"
54
- :name="'header:select'"
55
- :select-all="!!selectAll"
56
- :text="selectText || t('table.selectText').value"
57
- >
58
- <span class="bb-table-header__content">
59
- <label
60
- v-if="allowSelectAll && multiple"
61
- class="bb-table-check__label"
62
- :class="{
63
- 'bb-table-check__label--disabled': disabled,
64
- }"
65
- >
66
- <BaseCheckbox
67
- :key="indeterminate.toString()"
68
- v-model="selectAll"
69
- class="bb-table-check"
70
- :disabled="disabled"
71
- :indeterminate="indeterminate"
72
- :name="`select_all_${name || ''}`"
73
- :readonly="readonly"
74
- />
75
- <span class="bb-table-check__label-text sr-only"
76
- >{{ selectAllLabel || t('table.selectAllLabel').value }}
77
- </span>
78
- </label>
79
- <span
80
- v-else-if="!multiple"
81
- class="bb-table-radio__label-text sr-only"
82
- >{{ selectText || t('table.selectText').value }}</span
83
- >
84
- </span>
85
- </slot>
86
- </th>
87
- <th
88
- v-for="(header, index) in mappedHeaders"
89
- :key="header.key"
90
- :aria-sort="header.sortDirection"
91
- class="bb-table-header"
92
- :class="[
93
- header.classes,
94
- {
95
- 'bb-table-header--edge-start': index === 0,
96
- 'bb-table-header--edge-end':
97
- index === mappedHeaders.length - 1,
98
- },
99
- ]"
100
- scope="col"
101
- >
102
- <!-- @vue-ignore -->
103
- <slot
104
- :classes="header.classes"
105
- :items="internalItems"
106
- :label="header.label"
107
- :name="header.slotName"
108
- ><span class="bb-table-header__content">{{
109
- header.label
110
- }}</span></slot
111
- >
112
- </th>
113
- <th
114
- v-if="actions"
115
- class="bb-table-header bb-table-header--actions"
116
- :class="thClass"
117
- scope="col"
118
- >
119
- <!-- @vue-ignore -->
120
- <slot name="header:actions" :text="actionsText">
121
- <span class="bb-table-header__content sr-only">
122
- {{ actionsText }}
123
- </span>
124
- </slot>
125
- </th>
126
- </tr>
127
- </slot>
128
- </thead>
129
- <tbody>
130
- <slot v-if="loading" :items="internalItems" name="loading">
131
- <template v-for="(_, rowIndex) in skeletonLength" :key="rowIndex">
132
- <tr
133
- :aria-rowindex="
134
- hasProvidedAccessibilityData ? rowIndex + 2 : undefined
135
- "
136
- class="bb-table-skeleton__row"
137
- >
138
- <td
139
- v-if="selectable"
140
- class="bb-table-skeleton__cell bb-table-skeleton__cell--select"
141
- >
142
- <div class="bb-table-skeleton__placeholder" />
143
- </td>
144
- <td
145
- v-for="(col, colIndex) in props.columns"
146
- :key="col.key"
147
- :class="[
148
- 'bb-table-skeleton__cell',
149
- `bb-table-skeleton__cell--${col.skeleton ?? 'text'}`,
150
- ]"
151
- >
152
- <!-- text: 2 lines with dynamic widths -->
153
- <template v-if="!col.skeleton || col.skeleton === 'text'">
154
- <div class="bb-table-skeleton__cell-content">
155
- <div
156
- class="bb-table-skeleton__line"
157
- :style="{
158
- width: `${skeletonLineWidths[(rowIndex + colIndex) % 4]}%`,
159
- }"
160
- />
161
- <div
162
- class="bb-table-skeleton__line"
163
- :style="{
164
- width: `${skeletonLineWidths[(rowIndex + colIndex + 2) % 4]}%`,
165
- }"
166
- />
167
- </div>
168
- </template>
169
- <!-- avatar: circle only -->
170
- <div
171
- v-else-if="col.skeleton === 'avatar'"
172
- class="bb-table-skeleton__avatar"
173
- />
174
- <!-- avatar-text: avatar + 2 lines -->
175
- <div
176
- v-else-if="col.skeleton === 'avatar-text'"
177
- class="bb-table-skeleton__avatar-text"
178
- >
179
- <div class="bb-table-skeleton__avatar" />
180
- <div class="bb-table-skeleton__cell-content">
181
- <div
182
- class="bb-table-skeleton__line"
183
- :style="{
184
- width: `${skeletonLineWidths[(rowIndex + colIndex) % 4]}%`,
185
- }"
186
- />
187
- <div
188
- class="bb-table-skeleton__line"
189
- :style="{
190
- width: `${skeletonLineWidths[(rowIndex + colIndex + 2) % 4]}%`,
191
- }"
192
- />
193
- </div>
194
- </div>
195
- <!-- image: rounded rect -->
196
- <div
197
- v-else-if="col.skeleton === 'image'"
198
- class="bb-table-skeleton__image"
199
- />
200
- <!-- badge: small pill -->
201
- <div
202
- v-else-if="col.skeleton === 'badge'"
203
- class="bb-table-skeleton__badge"
204
- />
205
- </td>
206
- <td
207
- v-if="actions"
208
- class="bb-table-skeleton__cell bb-table-skeleton__cell--actions"
209
- >
210
- <div class="bb-table-skeleton__placeholder" />
211
- </td>
212
- </tr>
213
- </template>
214
- <tr
215
- :aria-rowindex="
216
- hasProvidedAccessibilityData ? skeletonLength + 2 : undefined
217
- "
218
- class="bb-table-loading__row bb-table-loading__row--sr-only"
219
- >
220
- <td
221
- :colspan="replacementContentSpan"
222
- class="bb-table-loading__cell"
223
- aria-live="polite"
224
- role="status"
225
- >
226
- <span class="bb-table-loading__text sr-only">{{
227
- loadingText
228
- }}</span>
229
- </td>
230
- </tr>
231
- </slot>
232
- <slot v-else-if="!options.length" :name="'no-data'">
233
- <tr
234
- :aria-rowindex="hasProvidedAccessibilityData ? 2 : undefined"
235
- class="bb-table-no-data__row"
236
- >
237
- <td
238
- class="bb-table-no-data__cell"
239
- :colspan="replacementContentSpan"
240
- >
241
- <span class="bb-table-no-data__text">{{
242
- noDataText || t('table.noDataText').value
243
- }}</span>
244
- </td>
245
- </tr>
246
- </slot>
247
- <slot v-else name="tbody">
248
- <template v-for="item in mappedItems" :key="item.valueHash">
249
- <tr
250
- :aria-rowindex="
251
- hasProvidedAccessibilityData && item.rowIndex
252
- ? item.rowIndex
253
- : undefined
254
- "
255
- class="bb-table-data__row"
256
- @click="item.onRowClick"
257
- @contextmenu="item.onRowContextMenu"
258
- @dblclick="item.onRowDblClick"
259
- >
260
- <td
261
- v-if="selectable"
262
- class="bb-table-data__cell bb-table-data__cell--select"
263
- :class="tdClass"
264
- >
265
- <!-- @vue-ignore -->
266
- <slot
267
- :checked="isSelected(item)"
268
- :disabled="!!item.disabled"
269
- :input-name="randomName"
270
- :item="item.item"
271
- :name="'select'"
272
- :readonly="!!readonly"
273
- :value="item.value"
274
- >
275
- <!-- We use temp names for these inputs because we don't submit them -->
276
- <label
277
- v-if="multiple"
278
- class="bb-table-check__label"
279
- :class="{
280
- 'bb-table-check__label--disabled': item.disabled,
281
- }"
282
- >
283
- <BaseCheckbox
284
- :checked="isSelected(item)"
285
- class="bb-table-check"
286
- :disabled="item.disabled"
287
- :readonly="readonly"
288
- @change="() => onInputChange(item)"
289
- />
290
- <span class="bb-table-check__label-text sr-only">{{
291
- item.accessibleLabel
292
- }}</span>
293
- </label>
294
-
295
- <label
296
- v-else
297
- class="bb-table-radio__label"
298
- :class="{
299
- 'bb-table-radio__label--disabled': item.disabled,
300
- }"
301
- >
302
- <BaseRadio
303
- :checked="item.selected"
304
- class="bb-table-radio"
305
- :disabled="item.disabled"
306
- :name="randomName"
307
- :readonly="readonly"
308
- :value="item.value"
309
- @change="() => onInputChange(item)"
310
- />
311
- <span class="bb-table-radio__label-text sr-only">{{
312
- item.accessibleLabel
313
- }}</span>
314
- </label>
315
- </slot>
316
- </td>
317
- <td
318
- v-for="(col, colIndex) in item.cols"
319
- :key="col.key"
320
- class="bb-table-data__cell"
321
- :class="[
322
- col.classes,
323
- {
324
- 'bb-table-data__cell--edge-start': colIndex === 0,
325
- 'bb-table-data__cell--edge-end':
326
- colIndex === item.cols.length - 1,
327
- },
328
- ]"
329
- >
330
- <!-- @vue-ignore -->
331
- <slot
332
- :classes="col.classes"
333
- :content="col.content"
334
- :expanded="item.expanded"
335
- :item="item.item"
336
- :name="col.slotName"
337
- :expand-props="getExpandControllerProps(item)"
338
- :toggle-expanded="() => toggleExpandedFor(item.valueHash)"
339
- :value="item.value"
340
- >{{ col.content }}</slot
341
- >
342
- </td>
343
- <td
344
- v-if="actions"
345
- class="bb-table__cell bb-table__cell--actions"
346
- :class="tdClass"
347
- >
348
- <!-- @vue-ignore -->
349
- <slot
350
- :expanded="item.expanded"
351
- :item="item.item"
352
- :name="'actions'"
353
- :expand-props="getExpandControllerProps(item)"
354
- :toggle-expanded="() => toggleExpandedFor(item.valueHash)"
355
- :value="item.value"
356
- ></slot>
357
- </td>
358
- </tr>
359
- <tr
360
- v-if="item.expanded"
361
- :aria-rowindex="
362
- hasProvidedAccessibilityData && item.rowIndex
363
- ? item.rowIndex + 1
364
- : undefined
365
- "
366
- :id="`expanded_${item.valueHash}`"
367
- class="bb-table-expand__row"
368
- >
369
- <td
370
- :colspan="replacementContentSpan"
371
- class="bb-table-expand__cell"
372
- >
373
- <!-- @vue-ignore -->
374
- <slot
375
- :expanded="item.expanded"
376
- :item="item.item"
377
- :name="'expand'"
378
- :expand-props="getExpandControllerProps(item)"
379
- :toggle-expanded="() => toggleExpandedFor(item.valueHash)"
380
- :value="item.value"
381
- ></slot>
382
- </td>
383
- </tr>
384
- </template>
385
- </slot>
386
- </tbody>
387
- <tfoot>
388
- <!-- @vue-ignore -->
389
- <slot name="tfoot"></slot>
390
- </tfoot>
391
- </table>
392
- <input v-for="input in hiddenInputs" :key="input.value" v-bind="input" />
393
- </component>
71
+ <BbTable
72
+ caption="Recent invoices"
73
+ :columns="columns"
74
+ compact
75
+ item-value="id"
76
+ :items="invoices"
77
+ :row-class="rowClass"
78
+ />
79
+ </div>
394
80
  </template>
395
-
396
- <script
397
- setup
398
- lang="ts"
399
- generic="Item = any, T extends ItemKey<Item> = string & {}"
400
- >
401
- import {
402
- computed,
403
- nextTick,
404
- onBeforeUnmount,
405
- onMounted,
406
- reactive,
407
- ref,
408
- toRef,
409
- watch,
410
- } from 'vue';
411
- import { clamp } from '@/utilities/functions/clamp';
412
- import { hash } from '@/utilities/functions/hash';
413
- import { isNil } from '@/utilities/functions/isNil';
414
- import { when } from '@/utilities/functions/when';
415
- import { useId } from '@/composables/useId';
416
- import { useItemValue } from '@/composables/useItemValue';
417
- import { useItemsGetter } from '@/composables/useItemsGetter';
418
- import { usePrefill } from '@/composables/usePrefill';
419
- import { useIndexById } from '@/composables/useIndexById';
420
- import { useBaseOptions } from '@/composables/useBaseOptions';
421
- import { useHashedWatcher } from '@/composables/useHashedWatcher';
422
- import { useTableWidthContext } from '@/composables/useTableWidthContext';
423
- import { useLocale } from '@/composables/useLocale';
424
- import BaseCheckbox from '../BaseCheckbox/BaseCheckbox.vue';
425
- import BaseRadio from '../BaseRadio/BaseRadio.vue';
426
- import type { Option as BaseOption } from '@/types/Option';
427
- import type { Classes } from '@/types/Classes';
428
- import type { ColumnName } from '@/types/ColumnName';
429
- import type { PrefixWith } from '@/types/PrefixWith';
430
- import type {
431
- BbTableColumn,
432
- BbTableEvents,
433
- BbTableProps,
434
- BbTableSlots,
435
- ExpandControllerProps,
436
- ItemKey,
437
- MappedCell,
438
- MappedHeader,
439
- MappedItem,
440
- } from './types';
441
- import {
442
- applyFixedTableColumns,
443
- columnKeyToSlotName,
444
- mergeCellClasses,
445
- mergeHeaderClasses,
446
- } from './utils';
447
- import { useLogger } from '@/composables/useLogger';
448
- import { isEmpty } from '@/utilities/functions/empty';
449
-
450
- const { getItemValue } = useItemValue();
451
-
452
- /** Temporary name for selection inputs; hidden inputs handle real submission. */
453
- const randomName = useId().id.value;
454
-
455
- type InternalColumn<K extends string> = Omit<BbTableColumn<Item>, 'key'> & {
456
- key: K;
457
- };
458
-
459
- type InternalProps<K extends string> = Omit<BbTableProps<Item>, 'columns'> & {
460
- columns: InternalColumn<K>[];
81
+ <script setup lang="ts">
82
+ import { BbTable } from 'bitboss-ui';
83
+ import type { BbTableColumn } from 'bitboss-ui';
84
+
85
+ type Invoice = {
86
+ id: string;
87
+ number: string;
88
+ client: string;
89
+ issuedAt: string;
90
+ note: string | null;
91
+ amount: number;
92
+ status: 'paid' | 'pending' | 'overdue';
461
93
  };
462
94
 
463
- const props = withDefaults(defineProps<InternalProps<T>>(), {
464
- align: 'left',
465
- actionsText: 'Azioni',
466
- allowSelectAll: true,
467
- columns: () => [],
468
- dependencies: () => [],
469
- depsDebounceTime: 0,
470
- fixedColumns: () => [],
471
- items: () => [],
472
- max: Infinity,
473
- multiple: true,
474
- modelValue: () => [],
475
- unselectedItems: () => [],
95
+ const euro = new Intl.NumberFormat('en-IE', {
96
+ style: 'currency',
97
+ currency: 'EUR',
476
98
  });
477
99
 
478
- const emit = defineEmits<BbTableEvents>();
479
-
480
- type InferredBodySlots = ColumnName<T>;
481
- type InferredHeaderSlots = PrefixWith<ColumnName<T>, 'header:'>;
482
-
483
- defineSlots<
484
- BbTableSlots<Item> & {
485
- [K in InferredBodySlots]?: (props: {
486
- classes?: Classes;
487
- content?: any;
488
- expanded?: boolean;
489
- expandProps?: ExpandControllerProps;
490
- toggleExpanded?: () => void;
491
- item: Item;
492
- value?: any;
493
- }) => any;
494
- } & {
495
- [K in InferredHeaderSlots]?: (props: {
496
- classes?: Classes;
497
- content?: any;
498
- items?: Item[];
499
- label?: string;
500
- }) => any;
501
- }
502
- >();
503
-
504
- if (props.multiple && !Array.isArray(props.modelValue)) {
505
- const logger = useLogger();
506
- logger.throw(
507
- 'BbTable is set for multiple selection but modelValue is not an array.'
508
- );
509
- }
100
+ // Columns declare extraction (`key`), formatting (`formatter`), alignment and a
101
+ // fixed `width`. `placeholder` fills in when a cell resolves to nullish.
102
+ const columns: Array<BbTableColumn<Invoice>> = [
103
+ { key: 'number', label: 'Invoice', width: 110 },
104
+ { key: 'client', label: 'Client' },
105
+ {
106
+ key: 'issuedAt',
107
+ label: 'Issued',
108
+ width: 100,
109
+ formatter: (value: string) => new Date(value).toLocaleDateString('en-GB'),
110
+ },
111
+ { key: 'note', label: 'Note', placeholder: '—' },
112
+ {
113
+ key: 'amount',
114
+ label: 'Amount',
115
+ align: 'right',
116
+ width: 110,
117
+ formatter: euro.format,
118
+ },
119
+ ];
510
120
 
511
- const { t } = useLocale();
512
-
513
- const tableRef = ref<HTMLTableElement>();
514
- const containerRef = ref<HTMLElement>();
515
-
516
- const {
517
- id: tableId,
518
- parentId,
519
- parentNode,
520
- cssVars,
521
- parentVar,
522
- setTracks,
523
- } = useTableWidthContext({
524
- id: () => props.id,
525
- container: () => containerRef.value,
526
- inheritFrom: () =>
527
- typeof props.inheritColumnWidths === 'string'
528
- ? props.inheritColumnWidths
529
- : undefined,
530
- });
121
+ const invoices: Invoice[] = [
122
+ {
123
+ id: 'inv-1041',
124
+ number: 'INV-1041',
125
+ client: 'Northwind Traders',
126
+ issuedAt: '2026-06-02',
127
+ note: 'Retainer',
128
+ amount: 4200,
129
+ status: 'paid',
130
+ },
131
+ {
132
+ id: 'inv-1042',
133
+ number: 'INV-1042',
134
+ client: 'Globex',
135
+ issuedAt: '2026-06-11',
136
+ note: null,
137
+ amount: 980.5,
138
+ status: 'pending',
139
+ },
140
+ {
141
+ id: 'inv-1043',
142
+ number: 'INV-1043',
143
+ client: 'Initech',
144
+ issuedAt: '2026-05-19',
145
+ note: 'Overage',
146
+ amount: 15600,
147
+ status: 'overdue',
148
+ },
149
+ ];
150
+
151
+ // `row-class` is applied to each `<tr>`; use it to flag rows by state. Soft amber
152
+ // wash matches the design-language attention accent — public API, no internals.
153
+ const rowClass = (invoice: Invoice) =>
154
+ invoice.status === 'overdue'
155
+ ? 'bg-[color:color-mix(in_oklab,#d97706_8%,var(--bb-panel))]'
156
+ : '';
157
+ </script>
158
+ ```
531
159
 
532
- /** Any column pinned to a parent track range a per-column inherit signal. */
533
- const hasSnap = computed(() => props.columns.some((c) => c.snap != null));
160
+ A virtual column has a `key` that matches nothing and derives its content in
161
+ the formatter use it for computed cells (totals, ratios) instead of
162
+ pre-mapping your items:
534
163
 
535
- // Opt-in: inherit when explicitly enabled (`true`, or an ancestor id string),
536
- // or implicitly when any column carries a `snap` — a snap only resolves against
537
- // a parent's tracks, so asking for one is asking to inherit. An absent prop with
538
- // no snaps does not inherit, so a plain nested table stays independent. An
539
- // explicit `false` is authoritative and overrides the implicit snap signal.
540
- const inheritColumnWidths = computed(
541
- () => !!props.inheritColumnWidths || hasSnap.value
542
- );
164
+ **A virtual total column computed from the row**
543
165
 
544
- /**
545
- * Assigned column widths (frozen, snapped, or inherited) are only authoritative
546
- * under `table-layout: fixed`; in auto layout they behave like a minimum. So a
547
- * table that drives widths — explicitly `fixed`, inheriting from a parent, or
548
- * with a column carrying a `width` or `snap` — uses fixed layout even when
549
- * `fixed` was not set. (`snap` is a width definition: the parent track range.)
550
- */
551
- const fixedLayout = computed(
552
- () =>
553
- props.fixed ||
554
- hasSnap.value ||
555
- props.columns.some((column) => column.width != null) ||
556
- (inheritColumnWidths.value && !!parentId.value)
557
- );
166
+ ```vue
167
+ <BbTable
168
+ caption="Order lines"
169
+ :columns="[
170
+ { key: 'sku', label: 'SKU' },
171
+ { key: 'qty', label: 'Qty', align: 'right' },
172
+ { key: 'unitPrice', label: 'Unit €', align: 'right' },
173
+ {
174
+ key: 'total',
175
+ label: 'Total €',
176
+ align: 'right',
177
+ formatter: (_, __, item) =>
178
+ (item.qty * item.unitPrice).toLocaleString('en-IE'),
179
+ },
180
+ ]"
181
+ :items="[
182
+ { sku: 'CHAIR-OAK', qty: 4, unitPrice: 189 },
183
+ { sku: 'DESK-140', qty: 2, unitPrice: 420 },
184
+ { sku: 'LAMP-LED', qty: 6, unitPrice: 39 },
185
+ ]"
186
+ />
187
+ ```
558
188
 
559
- /**
560
- * A nested table inheriting its parent's widths must size itself to the sum of
561
- * those columns rather than stretching to fill its container — otherwise fixed
562
- * layout would distribute the slack and the columns would no longer match the
563
- * parent. Tables with the same column count already sum to the full width, so
564
- * this is a no-op for them and only narrows tables with fewer columns.
565
- */
566
- /** True for a nested table that inherits its column widths from a parent. */
567
- const isInheriting = computed(
568
- () => inheritColumnWidths.value && !!parentId.value
569
- );
189
+ ### Custom cells and headers
570
190
 
571
- const sizeToColumns = isInheriting;
191
+ Override any column's body with a `#<key>` slot and its header with a
192
+ `#header:<key>` slot. The slot name is the key with every run of non-word
193
+ characters turned into `_` (dashes are kept as-is), **lowercased**:
194
+ `address.city` → `#address_city`, `first_name` → `#first_name`, `issued-at` →
195
+ `#issued-at`, but also `issuedAt` → `#issuedat` — prefer snake_case keys so
196
+ slot names read naturally. Keep the column in `columns`
197
+ even when you fully slot it: label, alignment and width still come from there.
572
198
 
573
- /** Numbers and bare numeric strings (e.g. `400`, `'400'`) default to px; values
574
- * that already carry a unit/keyword (`'50%'`, `'10rem'`, `'auto'`) pass through. */
575
- const toLength = (width: number | string) => {
576
- if (typeof width === 'number') return `${width}px`;
577
- const trimmed = width.trim();
578
- return /^-?\d*\.?\d+$/.test(trimmed) ? `${trimmed}px` : trimmed;
579
- };
199
+ Body cell scope: `content` (the formatted value), `item` (raw row), `value`
200
+ (resolved via `item-value`), `classes`, plus the row's interaction surface
201
+ `selected` / `toggleSelected`, `highlighted` / `toggleHighlighted`,
202
+ `expanded` / `toggleExpanded` / `expandProps`, and `sortOrder`. The flags are
203
+ one-way state; the `toggle*` callbacks are the write path.
580
204
 
581
- /** Number of data columns the parent renders (numeric track keys). */
582
- const parentDataColumns = computed(() => {
583
- const tracks = parentNode.value?.tracks;
584
- return tracks
585
- ? Object.keys(tracks).filter((key) => /^\d+$/.test(key)).length
586
- : 0;
587
- });
205
+ Header scope: `label`, `items` (the full list handy for counts), `sortable`,
206
+ `sortOrder`, `toggleSort`, `classes`.
588
207
 
589
- /**
590
- * The parent's rendered columns in order — `select` (if any), data columns
591
- * `0…N-1`, then `actions` (if any) form a gridline coordinate, one cell per
592
- * column. Cell `c` is the range `[c, c + 1]`. So a no-select parent starts its
593
- * data at cell 0, while a selectable parent starts it at cell 1.
594
- */
595
- const parentColumnKeys = computed(() => {
596
- const tracks = parentNode.value?.tracks ?? {};
597
- const keys: string[] = [];
598
- if ('select' in tracks) keys.push('select');
599
- for (let k = 0; k < parentDataColumns.value; k++) keys.push(String(k));
600
- if ('actions' in tracks) keys.push('actions');
601
- return keys;
602
- });
208
+ Below, the status badges carry brand glyphs shipped in the app's `iconDir`,
209
+ referenced as `local:*` names on the badge's `prepend:icon`. Per the icons
210
+ policy the dynamic status maps to **full-name literals** (a lookup object)
211
+ never a template-composed `` `local:${status}` ``. Colours use the registered
212
+ `soft-*` badge variants (design-language palette); the companion carries their
213
+ CSS in a non-scoped style block.
603
214
 
604
- /** Gridline cells where the parent's data region begins/ends and the table ends. */
605
- const firstDataCell = computed(() =>
606
- 'select' in (parentNode.value?.tracks ?? {}) ? 1 : 0
607
- );
608
- const dataEndCell = computed(
609
- () => firstDataCell.value + parentDataColumns.value
610
- );
611
- const tableEndCell = computed(() => parentColumnKeys.value.length);
215
+ **Status badges and an annotated header**
612
216
 
613
- /** Parent track custom property for the integer cell `[cell, cell + 1]`. */
614
- const cellTrackVar = (cell: number) => {
615
- const key = parentColumnKeys.value[cell];
616
- return key ? parentVar(key, '0px') : '0px';
217
+ ```vue
218
+ <template>
219
+ <div
220
+ class="max-w-2xl overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
221
+ >
222
+ <BbTable
223
+ caption="Invoice status"
224
+ :columns="columns"
225
+ compact
226
+ item-value="id"
227
+ :items="invoices"
228
+ >
229
+ <!-- Cell slot: name = the column key (dots/non-word chars become `_`,
230
+ dashes are kept).
231
+ The glyphs are brand assets from the app's `iconDir`, referenced as
232
+ `local:*` names — mapped to full literals, never template-composed. -->
233
+ <template #status="{ value }">
234
+ <BbBadge
235
+ :prepend:icon="glyphs[value as Status]"
236
+ size="sm"
237
+ :variant="variants[value as Status]"
238
+ >{{ labels[value as Status] }}</BbBadge
239
+ >
240
+ </template>
241
+
242
+ <!-- Header slot: `header:` + the column key. `label` comes from the scope. -->
243
+ <template #header:amount="{ label }">
244
+ <span class="inline-flex items-center gap-1">
245
+ {{ label }}
246
+ <span class="text-xs font-normal text-gray-500">EUR</span>
247
+ </span>
248
+ </template>
249
+ </BbTable>
250
+ </div>
251
+ </template>
252
+ <script setup lang="ts">
253
+ import { BbBadge, BbTable } from 'bitboss-ui';
254
+ import type { BadgeVariantType, BbTableColumn, IconType } from 'bitboss-ui';
255
+
256
+ type Status = 'paid' | 'pending' | 'overdue';
257
+ type Invoice = {
258
+ id: string;
259
+ number: string;
260
+ client: string;
261
+ status: Status;
262
+ amount: string;
617
263
  };
618
264
 
619
- /** CSS length summing the parent tracks across the gridline range `[a, b]`. */
620
- const gridRangeWidth = (a: number, b: number): string | null => {
621
- if (!parentId.value || b - a <= 1e-6) return null;
622
- const parts: string[] = [];
623
- for (let cell = Math.floor(a); cell < Math.ceil(b - 1e-6); cell++) {
624
- const fraction = Math.min(b, cell + 1) - Math.max(a, cell);
625
- if (fraction <= 1e-6) continue;
626
- const track = cellTrackVar(cell);
627
- parts.push(
628
- fraction >= 1 - 1e-6 ? track : `${Number(fraction.toFixed(4))} * ${track}`
629
- );
630
- }
631
- if (!parts.length) return null;
632
- // A bare `var()` can stand alone, but a single fractional term still needs a
633
- // `calc()` wrapper for the multiplication to be valid CSS.
634
- if (parts.length === 1 && !parts[0].includes('*')) return parts[0];
635
- return `calc(${parts.join(' + ')})`;
265
+ const columns: Array<BbTableColumn<Invoice>> = [
266
+ { key: 'number', label: 'Invoice', width: 110 },
267
+ { key: 'client', label: 'Client' },
268
+ { key: 'status', label: 'Status', width: 120 },
269
+ { key: 'amount', label: 'Amount', align: 'right', width: 110 },
270
+ ];
271
+
272
+ const labels: Record<Status, string> = {
273
+ paid: 'Paid',
274
+ pending: 'Pending',
275
+ overdue: 'Overdue',
636
276
  };
637
277
 
638
- /**
639
- * Resolved parent snap interval for each data column.
640
- * - A number snaps to that start point; the column then runs up to the next
641
- * column's snap point (or one whole track if the next is unset). So a single
642
- * snap that skips ahead widens this column to cover the gap — `snap: 4` on the
643
- * second column makes the first effectively `[start, 4]`.
644
- * - A `[start, end]` pair is taken verbatim (`end` of `-1` runs to the end of
645
- * the data region).
646
- * - Omitted, the column chains from the previous column's end.
647
- */
648
- const columnIntervals = computed(() => {
649
- const cols = props.columns;
650
- // Explicit start points; a later column's snap sets the previous column's end.
651
- const starts = cols.map((column) =>
652
- typeof column.snap === 'number'
653
- ? column.snap
654
- : Array.isArray(column.snap)
655
- ? column.snap[0]
656
- : null
657
- );
658
- let cursor = firstDataCell.value;
659
- return cols.map((column, i) => {
660
- const start = starts[i] ?? cursor;
661
- let end: number;
662
- if (Array.isArray(column.snap)) {
663
- end = column.snap[1] === -1 ? dataEndCell.value : column.snap[1];
664
- } else {
665
- const next = starts[i + 1];
666
- end = next != null && next > start ? next : Math.floor(start) + 1;
667
- }
668
- cursor = end;
669
- return { start, end };
670
- });
671
- });
672
-
673
- /**
674
- * A column whose interval begins at or beyond the parent's last data cell has no
675
- * matching parent column to inherit from — the table has more columns than its
676
- * parent. Such columns are sized to their own measured content and scroll within
677
- * the expand row instead of inheriting a track.
678
- */
679
- const isOverflowColumn = (interval: { start: number }) =>
680
- isInheriting.value && interval.start >= dataEndCell.value;
681
-
682
- /** True when any column overflows the parent (more columns than the parent). */
683
- const overflowsParent = computed(() =>
684
- columnIntervals.value.some(isOverflowColumn)
685
- );
686
-
687
- /**
688
- * A "flex" column shares a root table's leftover space (min-content floor)
689
- * proportionally via the probe. Only root tables flex this way; an inheriting
690
- * table instead snaps its in-range columns to the parent, content-sizes its
691
- * overflow columns, and lets a single fill column take up any slack.
692
- */
693
- const isFlexColumn = (index: number) =>
694
- props.columns[index]?.width == null && !isInheriting.value;
695
-
696
- /**
697
- * The last column of an inheriting table whose width is left undefined — neither
698
- * a `width` nor a `snap` (a `snap` *is* a width definition: the track range it
699
- * spans). That column absorbs the leftover space so the table fills its parent
700
- * when a frozen/snapped column narrower than the parent's would otherwise leave a
701
- * gap. `-1` for a root table, or when every column defines its own width.
702
- */
703
- const fillColumnIndex = computed(() => {
704
- if (!isInheriting.value) return -1;
705
- for (let i = props.columns.length - 1; i >= 0; i--) {
706
- const column = props.columns[i];
707
- if (column.width == null && column.snap == null) return i;
708
- }
709
- return -1;
710
- });
711
-
712
- // Parent columns left of the first column (e.g. a select this table doesn't
713
- // render, or columns skipped by the first snap) and those right of the last are
714
- // folded into the edge columns as the structural offset, keeping the table
715
- // aligned to the parent. `offsetStartValue` becomes the first column's padding.
716
- const offsetStartValue = computed(() => {
717
- const first = columnIntervals.value[0];
718
- // No parent to align to (a root table) means no offset at all — without this
719
- // the structural reclaim below would still fold a negative `-ownSelect` in.
720
- if (!inheritColumnWidths.value || !parentId.value || !first) return null;
721
- // The fill runs from the parent's left edge to this table's first data
722
- // column — the parent's select cell, if any, plus any skipped data columns.
723
- const head = gridRangeWidth(0, first.start);
724
- // This table's own select column sits flush at the left edge and takes its
725
- // measured width, so reclaim it from the fill (mirror of the actions side):
726
- // the first data column then lines the *next* column up flush with the
727
- // parent. When the parent has a select cell it is part of `head`, so only the
728
- // surplus over it is reclaimed; when the parent has none the whole select
729
- // width comes out of the first column, which shrinks to make room for it.
730
- const ownSelect = props.selectable ? structuralFit.select : null;
731
- if (!ownSelect) return head;
732
- return `calc(${head ?? '0px'} - ${ownSelect})`;
733
- });
734
- const offsetEndValue = computed(() => {
735
- const intervals = columnIntervals.value;
736
- const last = intervals[intervals.length - 1];
737
- // No parent to align to (a root table) means no offset at all — without this
738
- // the structural reclaim below would still fold a negative `-ownActions` in.
739
- if (!inheritColumnWidths.value || !parentId.value || !last) return null;
740
- // With more columns than the parent, the table runs past the parent's right
741
- // edge: there is no trailing parent region to fold, and the last column is a
742
- // content-sized overflow column, so there is no end fill.
743
- if (overflowsParent.value) return null;
744
- // When the last column is the (auto) fill column it already absorbs the whole
745
- // trailing region via fixed layout, so folding the offset in too would double
746
- // it — and skew its right-aligned content inward.
747
- if (fillColumnIndex.value === props.columns.length - 1) return null;
748
- // The fill runs from the last data column's end all the way to the parent's
749
- // right edge — any unmapped data columns plus the parent's actions cell, if
750
- // it has one.
751
- const tail = gridRangeWidth(last.end, tableEndCell.value);
752
- // This table's own actions column sits flush against that right edge and
753
- // takes its measured content width, so reclaim it from the fill: the table
754
- // then ends exactly at the parent's edge instead of overflowing by the
755
- // actions width. When the parent has an actions cell it is part of `tail`, so
756
- // only the surplus over it is effectively reclaimed; when the parent has none
757
- // the whole actions width comes out of the data fill. The result may be
758
- // negative — the last data column then shrinks to make room for the actions.
759
- const ownActions = props.actions ? structuralFit.actions : null;
760
- if (!ownActions) return tail;
761
- return `calc(${tail ?? '0px'} - ${ownActions})`;
762
- });
763
- const offsetStartActive = computed(() => offsetStartValue.value !== null);
764
- const offsetEndActive = computed(() => offsetEndValue.value !== null);
765
-
766
- /**
767
- * Measured intrinsic content widths of the structural columns. In a fixed table
768
- * these pin `select`/`actions` to their content so the remaining space is shared
769
- * among the data columns; `null` falls back to the regular auto behavior.
770
- */
771
- const structuralFit = reactive<{
772
- select: string | null;
773
- actions: string | null;
774
- }>({ select: null, actions: null });
278
+ // Status → full-name literals (icons policy: no `local:${status}` templates).
279
+ const glyphs: Record<Status, IconType> = {
280
+ paid: 'local:check_circle',
281
+ pending: 'local:clock',
282
+ overdue: 'local:alert_circle',
283
+ };
775
284
 
776
- /**
777
- * Rendered columns in DOM order, each with the track key it publishes/inherits
778
- * and the width to apply via `<colgroup>`. A frozen column width wins; otherwise
779
- * an unfrozen column syncs to the parent's matching track (or `auto` at root).
780
- * The first/last column additionally absorbs the structural edge offset.
781
- */
782
- const columnTracks = computed(() => {
783
- const sync = (key: string) =>
784
- inheritColumnWidths.value ? parentVar(key) : 'auto';
785
- // In fixed layout `auto` columns share width equally, so pin the structural
786
- // columns to their measured content width and let the data columns share.
787
- const structural = (key: 'select' | 'actions') =>
788
- fixedLayout.value && structuralFit[key]
789
- ? (structuralFit[key] as string)
790
- : sync(key);
791
- const tracks: { key: string; width: string }[] = [];
792
-
793
- if (props.selectable) {
794
- tracks.push({ key: 'select', width: structural('select') });
795
- }
285
+ const variants: Record<Status, BadgeVariantType> = {
286
+ paid: 'soft-green',
287
+ pending: 'soft-amber',
288
+ overdue: 'soft-red',
289
+ };
796
290
 
797
- const lastIndex = props.columns.length - 1;
798
- props.columns.forEach((column, index) => {
799
- const key = String(index);
800
- const frozen = column.width != null ? toLength(column.width) : null;
801
- const interval = columnIntervals.value[index];
802
-
803
- // The fill column is left `auto` so fixed layout (the inheriting table has
804
- // a definite `width: 100%`) hands it the leftover space — its own inherited
805
- // track plus the folded edge region plus any deficit. Skipped when the
806
- // table overflows: there is no leftover then, and it must content-size.
807
- if (index === fillColumnIndex.value && !overflowsParent.value) {
808
- tracks.push({ key, width: 'auto' });
809
- return;
810
- }
291
+ const invoices: Invoice[] = [
292
+ {
293
+ id: 'inv-1041',
294
+ number: 'INV-1041',
295
+ client: 'Northwind Traders',
296
+ status: 'paid',
297
+ amount: '€4,200.00',
298
+ },
299
+ {
300
+ id: 'inv-1042',
301
+ number: 'INV-1042',
302
+ client: 'Globex',
303
+ status: 'pending',
304
+ amount: '€980.50',
305
+ },
306
+ {
307
+ id: 'inv-1043',
308
+ number: 'INV-1043',
309
+ client: 'Initech',
310
+ status: 'overdue',
311
+ amount: '€15,600.00',
312
+ },
313
+ {
314
+ id: 'inv-1044',
315
+ number: 'INV-1044',
316
+ client: 'Umbrella',
317
+ status: 'pending',
318
+ amount: '€2,750.00',
319
+ },
320
+ ];
321
+ </script>
811
322
 
812
- let span: string | null;
813
- if (frozen != null) {
814
- span = frozen;
815
- } else if (isFlexColumn(index)) {
816
- // Root table auto column: a precise share of the leftover space from the
817
- // proportional probe rather than fixed layout's equal split. `auto` is
818
- // the fallback before the probe runs.
819
- span = (fixedLayout.value && probeColWidths.value[index]) || null;
820
- } else if (isOverflowColumn(interval)) {
821
- // Inheriting table, past the parent's last column: no track to snap to,
822
- // so size to its own content and let the table overflow (and scroll).
823
- span = columnMinWidths.value[index] ?? null;
824
- } else {
825
- // In range: snap to the parent's matching column.
826
- span = gridRangeWidth(interval.start, interval.end);
827
- }
323
+ <!-- Soft status palette — canonical source: ai/guides/design-language.md § Using color.
324
+ Non-scoped so variant classes on the child badge receive the accents. -->
325
+ <style>
326
+ .bb-badge.bb-badge--soft-green {
327
+ --accent: #16a34a;
328
+ }
329
+ .bb-badge.bb-badge--soft-amber {
330
+ --accent: #d97706;
331
+ }
332
+ .bb-badge.bb-badge--soft-red {
333
+ --accent: #dc2626;
334
+ }
335
+ .bb-badge.bb-badge--soft-green,
336
+ .bb-badge.bb-badge--soft-amber,
337
+ .bb-badge.bb-badge--soft-red {
338
+ --bg: color-mix(in oklab, var(--accent) 15%, var(--bb-panel));
339
+ --color: color-mix(in oklab, var(--accent) 80%, var(--bb-text));
340
+ --border-width: 1px;
341
+ --border-color: color-mix(in oklab, var(--accent) 25%, var(--bb-panel));
342
+ --ring: color-mix(in oklab, var(--accent) 45%, transparent);
343
+ }
344
+ </style>
345
+ ```
828
346
 
829
- const offsets: string[] = [];
830
- if (index === 0 && offsetStartActive.value) {
831
- offsets.push('var(--bb-table-offset-internal-start)');
832
- }
833
- if (index === lastIndex && offsetEndActive.value) {
834
- offsets.push('var(--bb-table-offset-internal-end)');
835
- }
347
+ Beyond per-column slots, the structural slots cover every other region:
348
+ `#header:select` (the select-all cell content), `#header:actions`, `#select`
349
+ (replace the per-row checkbox/radio; scope: `item`, `value`, `checked`,
350
+ `disabled`, `readonly`, `inputName`, `toggleSelected`), `#loading`, `#no-data`
351
+ (both below), and `#thead` / `#tbody` / `#tfoot` to replace an entire region
352
+ when you need full control (you then own its markup and semantics).
353
+
354
+ ### Loading and empty states
355
+
356
+ `loading` means two different things and the table tells them apart for you:
357
+
358
+ - **Nothing to show yet** (first load, or a filter that emptied the table) →
359
+ **skeleton rows**, whose shape you tune per column with `skeleton` (`text`,
360
+ `avatar`, `avatar-text`, `image`, `badge`). Between 4 and 10 rows, matching the
361
+ current row count when it has one.
362
+ - **Already showing rows** (a refetch: a `dependencies` change, a poll, a
363
+ save-then-reload) → **the rows stay put**, dimmed, with an indeterminate bar
364
+ along the header. Blanking a populated table to skeleton on every refetch loses
365
+ the user's place and flashes — so it doesn't.
366
+
367
+ A function `items` source triggers this automatically while it resolves; so does
368
+ the `loading` prop. `aria-busy` is set on the table in both cases, and the
369
+ `loading-text` announcement fires in both.
370
+
371
+ **While loading, the header and the rows are `inert`** — a refetch is about to
372
+ replace the rows, so editing, selecting or sorting them races the incoming data.
373
+ Same shape as `BbButton`: blocked by default, with `interactive-while-loading` as the
374
+ escape hatch for tables that must stay usable during a background refresh (a poll
375
+ that must not interrupt an open editor, say). The loading announcement is never
376
+ inert, so the busy state is always conveyed.
377
+
378
+ The `#loading` slot replaces the **skeleton**, so it follows the skeleton's rule:
379
+ it renders only when there is nothing to keep. (Scope: the current, possibly
380
+ stale `items` — handy for matching the previous row count.) When `items` resolves
381
+ empty the table shows its no-data row — localized default text, overridable via
382
+ `no-data-text` or fully via the `#no-data` slot. That slot fills the table's own
383
+ full-width empty-state cell (already centred, colspan already counted), so pass
384
+ the content — a message, a "clear filters" button — not a `<tr>` of your own.
385
+
386
+ Always set `loading-text`: it is the loading row's `aria-live` announcement
387
+ and — unlike `no-data-text` — it has **no built-in default**; omitting it
388
+ announces an empty string.
389
+
390
+ **Skeletons and a custom empty state**
836
391
 
837
- // When folding an offset, use a calc-safe length base (no `auto`) so the
838
- // column width = offset(s) + the column's own span.
839
- const width = offsets.length
840
- ? `calc(${[...offsets, span ?? '0px'].join(' + ')})`
841
- : (span ?? 'auto');
392
+ ```vue
393
+ <template>
394
+ <div class="flex max-w-2xl flex-col gap-2">
395
+ <div class="flex gap-1.5">
396
+ <BbButton size="sm" variant="outline" @click="loading = !loading">
397
+ Toggle loading
398
+ </BbButton>
399
+ <BbButton size="sm" variant="ghost" @click="empty = !empty">
400
+ Toggle empty
401
+ </BbButton>
402
+ </div>
403
+
404
+ <div
405
+ class="overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
406
+ >
407
+ <BbTable
408
+ caption="Team members"
409
+ :columns="columns"
410
+ compact
411
+ item-value="id"
412
+ :items="empty ? [] : members"
413
+ :loading="loading"
414
+ loading-text="Loading members…"
415
+ >
416
+ <template #no-data>
417
+ <p class="p-3 text-center text-sm text-gray-500">
418
+ No members match your filters yet.
419
+ </p>
420
+ </template>
421
+ </BbTable>
422
+ </div>
423
+ </div>
424
+ </template>
425
+ <script setup lang="ts">
426
+ import { ref } from 'vue';
427
+ import { BbButton, BbTable } from 'bitboss-ui';
428
+ import type { BbTableColumn } from 'bitboss-ui';
429
+
430
+ type Member = { id: string; name: string; email: string; role: string };
431
+
432
+ // `skeleton` shapes each column's placeholder while `loading` is true so the
433
+ // skeleton mirrors the real content (avatar + text, a badge, plain text).
434
+ const columns: Array<BbTableColumn<Member>> = [
435
+ { key: 'name', label: 'Name', skeleton: 'avatar-text' },
436
+ { key: 'email', label: 'Email' },
437
+ { key: 'role', label: 'Role', skeleton: 'badge', width: 90 },
438
+ ];
439
+
440
+ const members: Member[] = [
441
+ { id: 'u1', name: 'Ada Lovelace', email: 'ada@acme.io', role: 'Admin' },
442
+ { id: 'u2', name: 'Grace Hopper', email: 'grace@acme.io', role: 'Editor' },
443
+ { id: 'u3', name: 'Ken Thompson', email: 'ken@acme.io', role: 'Viewer' },
444
+ ];
445
+
446
+ const loading = ref(false);
447
+ const empty = ref(false);
448
+ </script>
449
+ ```
842
450
 
843
- tracks.push({ key, width });
844
- });
451
+ ### Async items and filters
845
452
 
846
- if (props.actions) {
847
- tracks.push({ key: 'actions', width: structural('actions') });
848
- }
453
+ Set `items` to a provider function and the table runs it, tracking loading
454
+ itself:
849
455
 
850
- return tracks;
851
- });
456
+ ```ts
457
+ type ItemsProvider<T> = (
458
+ prefill: boolean,
459
+ selected?: any // the current selection (v-model)
460
+ ) => T[] | Promise<T[]>;
461
+ ```
852
462
 
853
- /**
854
- * Standalone edge-offset custom properties consumed by the first/last cell
855
- * padding and the first/last column width. Set only when an offset is active;
856
- * otherwise the CSS default of `0px` makes both calcs collapse to their base.
857
- */
858
- const structuralOffset = computed<Record<string, string>>(() => {
859
- const style: Record<string, string> = {};
860
- if (offsetStartValue.value) {
861
- style['--bb-table-offset-start'] = offsetStartValue.value;
862
- }
863
- if (offsetEndValue.value) {
864
- style['--bb-table-offset-end'] = offsetEndValue.value;
865
- }
866
- return style;
867
- });
463
+ - On mount it is called `(true, selection)`; when `dependencies` (or the
464
+ `items` prop itself) change it is called `(false, selection)`, debounced by
465
+ `deps-debounce-time`.
466
+ - `dependencies` are compared **by value** (hashed), so `[filters]` refetches
467
+ only when content actually changes. Never wire a manual
468
+ `watch(filter, refetch)` that is exactly what `dependencies` replaces.
469
+ - Out-of-order responses are discarded (only the latest request writes rows);
470
+ a rejected provider is logged via `console.error` and the previous rows are
471
+ kept surface failures yourself (e.g. `useToast`) if the user must know.
472
+ - A plain array bypasses the provider machinery entirely.
868
473
 
869
- const containerStyle = computed(() => {
870
- const vars: Record<string, string> = {
871
- ...cssVars.value,
872
- ...structuralOffset.value,
873
- };
474
+ **Debounced search through the items function**
874
475
 
875
- if (naturalTotalWidth.value) {
876
- vars['--bb-table-natural-width'] = naturalTotalWidth.value;
877
- }
476
+ ```vue
477
+ <template>
478
+ <div class="flex max-w-2xl flex-col gap-2">
479
+ <BbTextInput
480
+ v-model="search"
481
+ clearable
482
+ compact
483
+ label="Search invoices"
484
+ label-mode="outside"
485
+ placeholder="Client or number…"
486
+ />
487
+
488
+ <!-- `items` is an async function; the table re-runs it (debounced) whenever
489
+ an entry in `dependencies` changes, showing skeletons while it awaits. -->
490
+ <div
491
+ class="overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
492
+ >
493
+ <BbTable
494
+ caption="Invoices"
495
+ :columns="columns"
496
+ compact
497
+ :dependencies="[search]"
498
+ :deps-debounce-time="300"
499
+ item-value="id"
500
+ :items="fetchInvoices"
501
+ loading-text="Searching…"
502
+ />
503
+ </div>
504
+ </div>
505
+ </template>
506
+ <script setup lang="ts">
507
+ import { ref } from 'vue';
508
+ import { BbTable, BbTextInput } from 'bitboss-ui';
509
+ import type { BbTableColumn } from 'bitboss-ui';
878
510
 
879
- if (fillValue.value > 0) {
880
- vars['--bb-table-fill'] = `${fillValue.value}px`;
881
- }
511
+ type Invoice = { id: string; number: string; client: string; amount: string };
882
512
 
883
- return vars;
884
- });
513
+ const columns: Array<BbTableColumn<Invoice>> = [
514
+ { key: 'number', label: 'Invoice', width: 110 },
515
+ { key: 'client', label: 'Client' },
516
+ { key: 'amount', label: 'Amount', align: 'right', width: 100 },
517
+ ];
885
518
 
886
- /** Reads the header cells in render order: [select?, ...data, actions?]. */
887
- const headerCells = () =>
888
- Array.from(
889
- tableRef.value?.querySelectorAll<HTMLElement>(
890
- ':scope > thead > tr.bb-table-header-row > th'
891
- ) ?? []
519
+ const catalogue: Invoice[] = [
520
+ {
521
+ id: '1',
522
+ number: 'INV-1041',
523
+ client: 'Northwind Traders',
524
+ amount: '€4,200',
525
+ },
526
+ { id: '2', number: 'INV-1042', client: 'Globex', amount: '€980' },
527
+ { id: '3', number: 'INV-1043', client: 'Initech', amount: '€15,600' },
528
+ { id: '4', number: 'INV-1044', client: 'Umbrella', amount: '€2,750' },
529
+ ];
530
+
531
+ const search = ref('');
532
+
533
+ // Real apps forward the query to the backend; here we filter a local catalogue
534
+ // after a short simulated round-trip.
535
+ const fetchInvoices = async (): Promise<Invoice[]> => {
536
+ await new Promise((resolve) => setTimeout(resolve, 250));
537
+ const term = search.value.trim().toLowerCase();
538
+ if (!term) return catalogue;
539
+ return catalogue.filter(
540
+ (invoice) =>
541
+ invoice.client.toLowerCase().includes(term) ||
542
+ invoice.number.toLowerCase().includes(term)
892
543
  );
893
-
894
- /** Measures the rendered columns and publishes them as this table's tracks. */
895
- const measureTracks = () => {
896
- const keys = columnTracks.value.map((track) => track.key);
897
- const tracks: Record<string, string> = {};
898
- headerCells().forEach((cell, index) => {
899
- const key = keys[index];
900
- if (key != null) tracks[key] = `${cell.getBoundingClientRect().width}px`;
901
- });
902
- setTracks(tracks);
903
- };
904
-
905
- /**
906
- * Measures the intrinsic content width of the structural columns across header
907
- * and body. Fixed layout sizes columns from the header row only, so the body is
908
- * required here: the actions header is `sr-only`, while the real control lives
909
- * in the body. The measured child width is independent of the column width, so
910
- * pinning the column to it converges instead of oscillating.
911
- */
912
- const measureStructuralFit = () => {
913
- const table = tableRef.value;
914
- if (!table) return;
915
-
916
- const fit = (selector: string) => {
917
- let max = 0;
918
- table.querySelectorAll<HTMLElement>(selector).forEach((cell) => {
919
- // Sum each child's own intrinsic width (plus horizontal margins) — an
920
- // actions cell holds several side-by-side controls and the column must
921
- // fit them all. Crucially this is *not* their shared extent: the extent
922
- // depends on how the cell spreads them across its width, which feeds the
923
- // column width back into its own measurement and runs away into the
924
- // thousands. A child's own width does not depend on the cell, so this
925
- // converges. Out-of-flow children (e.g. an `sr-only` label parked far
926
- // off-screen) are skipped so they cannot inflate the result.
927
- let content = 0;
928
- cell.querySelectorAll<HTMLElement>(':scope > *').forEach((child) => {
929
- const childStyle = getComputedStyle(child);
930
- if (
931
- childStyle.position === 'absolute' ||
932
- childStyle.position === 'fixed'
933
- )
934
- return;
935
- const margin =
936
- parseFloat(childStyle.marginLeft) +
937
- parseFloat(childStyle.marginRight);
938
- content += child.getBoundingClientRect().width + (margin || 0);
939
- });
940
- const style = getComputedStyle(cell);
941
- const padding =
942
- parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
943
- max = Math.max(max, content + padding);
944
- });
945
- return max > 0 ? `${Math.ceil(max)}px` : null;
946
- };
947
-
948
- // Scoped to this table's own header/body rows: an unscoped descendant query
949
- // would also match the structural cells of a nested table living in an expand
950
- // row, letting a child's actions/select width hijack this table's column.
951
- structuralFit.select = props.selectable
952
- ? fit(
953
- ':scope > thead > tr > th.bb-table-header--select, :scope > tbody > tr > td.bb-table-data__cell--select'
954
- )
955
- : null;
956
- structuralFit.actions = props.actions
957
- ? fit(
958
- ':scope > thead > tr > th.bb-table-header--actions, :scope > tbody > tr > td.bb-table__cell--actions'
959
- )
960
- : null;
961
- };
962
-
963
- const measure = () => {
964
- measureStructuralFit();
965
- measureTracks();
966
544
  };
545
+ </script>
546
+ ```
967
547
 
968
- /**
969
- * Proportional `<col>` widths derived from an auto-layout probe, one entry per
970
- * data column (`null` for frozen columns — they keep their declared width).
971
- *
972
- * For each auto column the formula is:
973
- * width = max(minimum, available × (minimum / minimumTotal))
974
- *
975
- * where `minimum` is the column's `min-content` width (what it shrinks to in a
976
- * very narrow container, i.e. the floor below which content would be clipped),
977
- * `available` = containerWidth − frozenColumnWidths − structuralColumnWidths
978
- * and `minimumTotal` = sum of the auto columns' minimums.
979
- *
980
- * So when the container has room (`available ≥ minimumTotal`) the slack is shared
981
- * out proportionally to each column's minimum, scaling them up together; when it
982
- * does not, every column drops to its minimum and the table-level `min-width`
983
- * lets it overflow (the container scrolls) rather than squashing content further.
984
- *
985
- * Re-measured after any DOM mutation (debounced 200 ms) and after any container
986
- * resize (via a dedicated ResizeObserver on the container element).
987
- */
988
- const probeColWidths = ref<(string | null)[]>([]);
989
-
990
- /**
991
- * Each unfrozen column's measured `min-content` width (`null` for frozen). Used
992
- * to content-size the overflow columns of an inheriting table — those past the
993
- * parent's last column, with no track to snap to.
994
- */
995
- const columnMinWidths = ref<(string | null)[]>([]);
996
-
997
- /**
998
- * Leftover px the inheriting fill column adds so the table reaches its parent's
999
- * width. Published as `--bb-table-fill`; `0` when the table already fills or
1000
- * overflows. Derived self-correctingly from the live total (see the probe).
1001
- */
1002
- const fillValue = ref(0);
1003
-
1004
- /**
1005
- * Total minimum width of the table (all columns at their `min-content`, plus the
1006
- * structural columns). Published as `--bb-table-natural-width` and used as
1007
- * `min-width` on the `<table>` so the table scrolls horizontally only once the
1008
- * container is narrower than this true minimum.
1009
- */
1010
- const naturalTotalWidth = ref<string | null>(null);
1011
-
1012
- const runNaturalWidthProbe = () => {
1013
- const container = containerRef.value;
1014
- if (!container || !fixedLayout.value) return;
1015
- // Runs for a frozen-column table (to share space), or any inheriting table
1016
- // (to content-size overflow columns and drive the fill column's slack).
1017
- if (
1018
- !props.columns.some((col) => col.width != null) &&
1019
- !overflowsParent.value &&
1020
- fillColumnIndex.value < 0
1021
- )
1022
- return;
1023
-
1024
- const containerWidth = container.offsetWidth;
1025
- const dataOffset = props.selectable ? 1 : 0;
1026
-
1027
- // ── Real-table measurements ────────────────────────────────────────────
1028
- // Structural column widths come from measureStructuralFit (already run).
1029
- const selectWidth = structuralFit.select
1030
- ? parseFloat(structuralFit.select)
1031
- : 0;
1032
- const actionsWidth = structuralFit.actions
1033
- ? parseFloat(structuralFit.actions)
1034
- : 0;
1035
-
1036
- // Live rendered widths from the fixed-layout table. `frozenTotal` (declared
1037
- // widths) feeds the stable min-width floor; `nonFlexTotal` (frozen + inherited)
1038
- // is what the flex columns share the remainder of. The inherited part is only
1039
- // used for that `available` figure — never fed back into min-width, since those
1040
- // widths stretch with the table and would otherwise lock min-width above
1041
- // max-content and force a spurious uniform stretch on every column.
1042
- const realThs = headerCells();
1043
- let frozenTotal = 0;
1044
- let nonFlexTotal = 0;
1045
- props.columns.forEach((col, i) => {
1046
- const width = realThs[dataOffset + i]?.getBoundingClientRect().width ?? 0;
1047
- if (col.width != null) frozenTotal += width;
1048
- if (!isFlexColumn(i)) nonFlexTotal += width;
1049
- });
548
+ On a server-paginated screen the provider forwards filters, sort and page to
549
+ the backend and returns the current slice:
1050
550
 
1051
- // ── Offscreen probe for the auto columns' minimum (min-content) widths ──
1052
- // The clone's inner table is forced to `width: min-content` and the auto <col>
1053
- // hints are stripped so each <th> reports the narrowest width it can take
1054
- // before content would be clipped, independent of the container size or
1055
- // previous probe cycles.
1056
- const probe = document.createElement('div');
1057
- probe.style.cssText = [
1058
- 'position:absolute',
1059
- 'top:-9999px',
1060
- 'left:-9999px',
1061
- 'visibility:hidden',
1062
- 'pointer-events:none',
1063
- `width:${containerWidth}px`,
1064
- `height:${container.offsetHeight}px`,
1065
- 'overflow:visible',
1066
- ].join(';');
1067
-
1068
- const clone = container.cloneNode(true) as HTMLElement;
1069
- clone.classList.remove('bb-table--fixed');
1070
-
1071
- // Expand rows are full-width (colspan) and hold nested tables; under the
1072
- // auto-layout probe their content would back-pressure this table's own
1073
- // columns, so drop them and measure this table's columns in isolation.
1074
- clone
1075
- .querySelectorAll('.bb-table-expand__row')
1076
- .forEach((row) => row.remove());
1077
-
1078
- // Auto <col> hints are cleared so a previous cycle's computed width cannot
1079
- // act as a minimum and inflate the measurement; frozen hints are left intact
1080
- // since their declared width is a genuine floor for that column.
1081
- const probeColEls = Array.from(clone.querySelectorAll<HTMLElement>('col'));
1082
- props.columns.forEach((col, i) => {
1083
- if (col.width == null) {
1084
- const probeCol = probeColEls[dataOffset + i];
1085
- if (probeCol) probeCol.style.width = '';
1086
- }
551
+ ```ts
552
+ const fetchInvoices = async (): Promise<Invoice[]> => {
553
+ const res = await api.invoices.list({
554
+ search: search.value,
555
+ status: status.value,
556
+ orderBy: sort.value, // the v-model:sort array, forwarded as-is
557
+ page: page.value,
558
+ perPage,
1087
559
  });
560
+ totalItems.value = res.total;
561
+ return res.data;
562
+ };
563
+ // dependencies: [search, status, sort]
564
+ // page is NOT listed: a provider table already refetches when the shared
565
+ // (table-id) page moves — list only your own filters and sort here.
566
+ ```
1088
567
 
1089
- const innerTable = clone.querySelector<HTMLElement>('table');
1090
- if (innerTable) {
1091
- innerTable.style.width = 'min-content';
1092
- innerTable.style.minWidth = '0';
1093
- }
568
+ This is the shape of every server-driven listing — the full assembly (filters,
569
+ pagination, confirm-delete, toasts) is the records workspace recipe
570
+ (`./recipes/vue/records-workspace.md`), which works unchanged under Inertia or
571
+ Nuxt since the table itself never touches the router.
1094
572
 
1095
- probe.appendChild(clone);
1096
- document.body.appendChild(probe);
573
+ ### Pagination
1097
574
 
1098
- const headerRow = probe.querySelector<HTMLElement>(
1099
- 'table thead tr.bb-table-header-row'
1100
- );
1101
- const ths = headerRow
1102
- ? Array.from(headerRow.querySelectorAll<HTMLElement>(':scope > th'))
1103
- : [];
1104
-
1105
- // Min-content of every unfrozen column (0 for frozen). Independent of the live
1106
- // table's width, so it is a stable basis for both the flex share and the
1107
- // min-width floor — unlike the rendered inherited widths.
1108
- const minContents = props.columns.map((col, i) => {
1109
- if (col.width != null) return 0;
1110
- const th = ths[dataOffset + i];
1111
- return th ? th.getBoundingClientRect().width : 0;
1112
- });
575
+ `BbTable` never slices its own data — you do, or the server does. Feed it the
576
+ current slice as `items` and pass `page` / `per-page` / `total-items` so it
577
+ can announce `aria-rowcount` and per-row absolute indices (when `items` is an
578
+ array, `per-page` and `total-items` default to its length — so a one-page
579
+ table is accessible with just `page`). Pair it with `BbPagination` bound to
580
+ the same page ref.
1113
581
 
1114
- document.body.removeChild(probe);
582
+ **BbPagination alongside the table**
1115
583
 
1116
- // ── Proportional <col> widths ──────────────────────────────────────────
1117
- // Only the flex columns share `available`; the inherited columns are already
1118
- // laid out and excluded via `nonFlexTotal`.
1119
- const flexMinTotal = props.columns.reduce(
1120
- (s, _, i) => s + (isFlexColumn(i) ? minContents[i] : 0),
1121
- 0
1122
- );
1123
- const available = containerWidth - nonFlexTotal - selectWidth - actionsWidth;
1124
-
1125
- // Share the available space proportionally to each flex column's minimum, but
1126
- // never drop below that minimum — when space runs out the table overflows
1127
- // (and the container scrolls) instead of clipping content.
1128
- //
1129
- // Widths are kept sub-pixel (not rounded up): the raw shares sum to exactly
1130
- // `available`, whereas ceiling each column would over-allocate by up to a
1131
- // pixel apiece and, under `table-layout: fixed`, push the table a few px past
1132
- // the container and trip the horizontal scrollbar. `toFixed` only trims the
1133
- // float tail, well below the 1px a scrollbar needs.
1134
- const px = (n: number) => `${Number(n.toFixed(3))}px`;
1135
- probeColWidths.value = props.columns.map((_, i) => {
1136
- if (!isFlexColumn(i)) return null;
1137
- const minimum = minContents[i];
1138
- if (flexMinTotal <= 0) return px(minimum);
1139
- const share = available > 0 ? available * (minimum / flexMinTotal) : 0;
1140
- return px(Math.max(minimum, share));
1141
- });
584
+ ```vue
585
+ <template>
586
+ <div class="flex max-w-2xl flex-col gap-2">
587
+ <!-- `page` / `per-page` / `total-items` power the table's accessibility
588
+ announcements (aria-rowcount / aria-rowindex); the actual slicing is
589
+ yours (or the server's). -->
590
+ <div
591
+ class="overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
592
+ >
593
+ <BbTable
594
+ caption="Invoices"
595
+ :columns="columns"
596
+ compact
597
+ item-value="id"
598
+ :items="pageRows"
599
+ :page="page"
600
+ :per-page="perPage"
601
+ :total-items="invoices.length"
602
+ />
603
+ </div>
604
+ <div class="flex items-center justify-between gap-2">
605
+ <p class="text-xs text-gray-500">
606
+ {{ rangeStart }}–{{ rangeEnd }} of {{ invoices.length }}
607
+ </p>
608
+ <BbPagination v-model="page" :total-pages="totalPages" />
609
+ </div>
610
+ </div>
611
+ </template>
612
+ <script setup lang="ts">
613
+ import { computed, ref } from 'vue';
614
+ import { BbPagination, BbTable } from 'bitboss-ui';
615
+ import type { BbTableColumn } from 'bitboss-ui';
616
+
617
+ type Invoice = { id: string; number: string; client: string; amount: string };
618
+
619
+ const columns: Array<BbTableColumn<Invoice>> = [
620
+ { key: 'number', label: 'Invoice', width: 110 },
621
+ { key: 'client', label: 'Client' },
622
+ { key: 'amount', label: 'Amount', align: 'right', width: 100 },
623
+ ];
624
+
625
+ const invoices: Invoice[] = Array.from({ length: 13 }, (_, index) => ({
626
+ id: String(index + 1),
627
+ number: `INV-${1041 + index}`,
628
+ client: ['Northwind Traders', 'Globex', 'Initech', 'Umbrella'][index % 4],
629
+ amount: `€${((index + 1) * 640).toLocaleString('en-IE')}`,
630
+ }));
1142
631
 
1143
- // Min-content per unfrozen column, for content-sizing an inheriting table's
1144
- // overflow columns (no parent track to snap to).
1145
- columnMinWidths.value = minContents.map((w) => (w > 0 ? px(w) : null));
1146
-
1147
- // Leftover the inheriting fill column must absorb to reach the parent's width.
1148
- // `parentTotal` is the parent table's full width (the expand cell this table
1149
- // sits in); the container itself shrink-wraps to the table, so it can't be the
1150
- // reference. Self-correcting: the live total already includes last cycle's
1151
- // fill, so adding it back recovers the intrinsic total and the result is the
1152
- // same once the table fills (0 when it already fills or overflows).
1153
- if (fillColumnIndex.value >= 0) {
1154
- const parentTotal = Object.values(parentNode.value?.tracks ?? {}).reduce(
1155
- (s, w) => s + (parseFloat(w) || 0),
1156
- 0
1157
- );
1158
- const liveTotal = realThs.reduce(
1159
- (s, th) => s + th.getBoundingClientRect().width,
1160
- 0
1161
- );
1162
- fillValue.value = Math.max(0, parentTotal - liveTotal + fillValue.value);
1163
- } else {
1164
- fillValue.value = 0;
1165
- }
632
+ const perPage = 5;
633
+ const page = ref(1);
634
+ const totalPages = computed(() => Math.ceil(invoices.length / perPage));
635
+ const rangeStart = computed(() => (page.value - 1) * perPage + 1);
636
+ const rangeEnd = computed(() =>
637
+ Math.min(page.value * perPage, invoices.length)
638
+ );
1166
639
 
1167
- // Table min-width = frozen (declared) widths + every unfrozen column's
1168
- // min-content + structural. Built only from values that do not depend on the
1169
- // table's own width, so it can never exceed max-content and force a stretch.
1170
- const minContentTotal = minContents.reduce((s, w) => s + w, 0);
1171
- const naturalTotal =
1172
- frozenTotal + minContentTotal + selectWidth + actionsWidth;
1173
- naturalTotalWidth.value =
1174
- naturalTotal > 0 ? `${Math.ceil(naturalTotal)}px` : null;
1175
- };
640
+ const pageRows = computed(() =>
641
+ invoices.slice((page.value - 1) * perPage, page.value * perPage)
642
+ );
643
+ </script>
644
+ ```
1176
645
 
1177
- let naturalWidthDebounceTimer: ReturnType<typeof setTimeout> | null = null;
1178
- let mutationObserver: MutationObserver | undefined;
1179
-
1180
- /** Invalidates the cached natural width and schedules a fresh probe. */
1181
- const scheduleNaturalWidthProbe = () => {
1182
- if (naturalWidthDebounceTimer !== null)
1183
- clearTimeout(naturalWidthDebounceTimer);
1184
- naturalWidthDebounceTimer = setTimeout(() => {
1185
- naturalWidthDebounceTimer = null;
1186
- runNaturalWidthProbe();
1187
- }, 200);
1188
- };
646
+ For no-prop-drilling wiring, give the table an `id` and the pagination a
647
+ matching `table-id`: both join the same table context and stay in sync without
648
+ a shared ref. The pagination props are symmetric across the pair — `page` /
649
+ `per-page` / `total-items` / `total-pages` are accepted by both components and
650
+ publish into the same context, so the server's fields go to whichever one is
651
+ convenient. `total-pages` is a seed for when no row count is known (an
652
+ externally paged array table, or a server that only reports a page count): a
653
+ count derived from a real `total-items` always wins over it. Add `navigation`
654
+ to `BbPagination` to mirror the page in the URL query. The same context is
655
+ scriptable from anywhere via `useBbTableContext`
656
+ (`./composables/useBbTableContext.md`).
657
+
658
+ ### Sorting
659
+
660
+ Mark columns `sortable` and bind `v-model:sort` — an ordered
661
+ `BbTableSortEntry[]` (`[columnKey, 'asc' | 'desc']`). Header buttons cycle a
662
+ column unsorted → asc → desc → removed; a direction flip updates the entry in
663
+ place, so the column keeps its priority in a multi-column sort and other
664
+ entries are never touched. The array order **is** the priority: first entry
665
+ sorts first. The table only reports the model; **apply it to your data**
666
+ (client-side, as below) or forward it to the server as `orderBy` and refetch
667
+ via `dependencies`.
668
+
669
+ **Sortable headers driving a client-side sort**
1189
670
 
1190
- let resizeObserver: ResizeObserver | undefined;
1191
- /** Observes the container element specifically for size changes so the
1192
- * proportional <col> widths are recomputed whenever the container grows or
1193
- * shrinks separately from the header-cell observer that drives measureTracks. */
1194
- let containerResizeObserver: ResizeObserver | undefined;
1195
-
1196
- const observeColumns = () => {
1197
- if (!resizeObserver) return;
1198
- resizeObserver.disconnect();
1199
- headerCells().forEach((cell) => resizeObserver?.observe(cell));
1200
- measure();
1201
- };
671
+ ```vue
672
+ <template>
673
+ <div class="flex max-w-2xl flex-col gap-1.5">
674
+ <!-- Mark columns `sortable` and bind `v-model:sort`. The header buttons
675
+ cycle asc desc → off; the table only REPORTS the model — you sort
676
+ the data. -->
677
+ <div
678
+ class="overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
679
+ >
680
+ <BbTable
681
+ v-model:sort="sort"
682
+ caption="Invoices"
683
+ :columns="columns"
684
+ compact
685
+ item-value="id"
686
+ :items="sortedInvoices"
687
+ />
688
+ </div>
689
+ <p class="text-xs text-gray-500">
690
+ Sort model: <code>{{ JSON.stringify(sort) }}</code>
691
+ </p>
692
+ </div>
693
+ </template>
694
+ <script setup lang="ts">
695
+ import { computed, ref } from 'vue';
696
+ import { BbTable } from 'bitboss-ui';
697
+ import type { BbTableColumn, BbTableSortEntry } from 'bitboss-ui';
1202
698
 
1203
- onMounted(() => {
1204
- resizeObserver = new ResizeObserver(() => measure());
1205
- observeColumns();
1206
-
1207
- // Initial probe runs after the first render so all cells have content.
1208
- nextTick(runNaturalWidthProbe);
1209
-
1210
- // Invalidate the cache whenever rows or cell content change.
1211
- if (tableRef.value) {
1212
- mutationObserver = new MutationObserver(scheduleNaturalWidthProbe);
1213
- mutationObserver.observe(tableRef.value, {
1214
- childList: true,
1215
- subtree: true,
1216
- characterData: true,
1217
- });
1218
- }
699
+ type Invoice = { id: string; number: string; client: string; amount: number };
1219
700
 
1220
- // Re-probe whenever the container itself is resized so that proportional
1221
- // <col> widths stay accurate as the available space changes.
1222
- if (containerRef.value) {
1223
- containerResizeObserver = new ResizeObserver(scheduleNaturalWidthProbe);
1224
- containerResizeObserver.observe(containerRef.value);
1225
- }
701
+ const euro = new Intl.NumberFormat('en-IE', {
702
+ style: 'currency',
703
+ currency: 'EUR',
1226
704
  });
1227
705
 
1228
- onBeforeUnmount(() => {
1229
- resizeObserver?.disconnect();
1230
- containerResizeObserver?.disconnect();
1231
- mutationObserver?.disconnect();
1232
- if (naturalWidthDebounceTimer !== null)
1233
- clearTimeout(naturalWidthDebounceTimer);
706
+ const columns: Array<BbTableColumn<Invoice>> = [
707
+ { key: 'number', label: 'Invoice', sortable: true, width: 110 },
708
+ { key: 'client', label: 'Client', sortable: true },
709
+ {
710
+ key: 'amount',
711
+ label: 'Amount',
712
+ align: 'right',
713
+ sortable: true,
714
+ width: 110,
715
+ formatter: euro.format,
716
+ },
717
+ ];
718
+
719
+ const invoices: Invoice[] = [
720
+ { id: '1', number: 'INV-1041', client: 'Northwind Traders', amount: 4200 },
721
+ { id: '2', number: 'INV-1042', client: 'Globex', amount: 980 },
722
+ { id: '3', number: 'INV-1043', client: 'Initech', amount: 15600 },
723
+ { id: '4', number: 'INV-1044', client: 'Umbrella', amount: 2750 },
724
+ ];
725
+
726
+ const sort = ref<BbTableSortEntry[]>([['amount', 'desc']]);
727
+
728
+ // Apply the ordered sort model to the data — first entry wins ties first.
729
+ const sortedInvoices = computed(() => {
730
+ if (!sort.value.length) return invoices;
731
+ return [...invoices].sort((a, b) => {
732
+ for (const [key, direction] of sort.value) {
733
+ const left = a[key as keyof Invoice];
734
+ const right = b[key as keyof Invoice];
735
+ const compared =
736
+ typeof left === 'number' && typeof right === 'number'
737
+ ? left - right
738
+ : String(left).localeCompare(String(right));
739
+ if (compared !== 0) return direction === 'asc' ? compared : -compared;
740
+ }
741
+ return 0;
742
+ });
1234
743
  });
744
+ </script>
745
+ ```
1235
746
 
1236
- watch(columnTracks, () => nextTick(observeColumns), { flush: 'post' });
1237
-
1238
- /** Builds accessible label for selection controls from row content. */
1239
- const getAccessibleLabel = (columns: MappedCell[]) =>
1240
- [t('table.select').value, ...columns.map((col) => col.content)].join(' ');
1241
-
1242
- const containerTag = computed(() => (props.selectable ? 'fieldset' : 'div'));
1243
-
1244
- const loading = computed(() => props.loading || innerLoading.value);
747
+ A custom `#header:<key>` slot replaces the built-in sort button, but its scope
748
+ hands you `sortOrder` and `toggleSort` so your header can keep sorting alive
749
+ (render your own button and call `toggleSort`). The column-level `sorted`
750
+ field only seeds the header's `aria-sort` when no live model entry exists — it
751
+ does not move the visual indicator; to start pre-sorted, seed `v-model:sort`
752
+ instead (as the example does).
1245
753
 
1246
- const hasProvidedAccessibilityData = computed(
1247
- () => !isNil(props.page) && !isNil(props.perPage) && !isNil(props.totalItems)
1248
- );
754
+ ### Selection and bulk actions
1249
755
 
1250
- const containerClass = computed(() => ({
1251
- 'bb-table': true,
1252
- [`bb-table--align-${props.align}`]: true,
1253
- 'bb-table--compact': props.compact,
1254
- 'bb-table--fixed': fixedLayout.value,
1255
- 'bb-table--inherit-widths': sizeToColumns.value,
1256
- 'bb-table--fixed-header': props.fixedHeaders,
1257
- 'bb-table--loading': loading.value,
1258
- 'bb-table--empty': !options.value.length,
1259
- 'bb-table--selectable': !!props.selectable,
1260
- 'scrollbar-border': true,
1261
- }));
756
+ Add `selectable` for multi-select (`multiple` is the default) bound with
757
+ `v-model` — the model **must** be an array in multiple mode (the table throws
758
+ otherwise). With `item-value` set, the model holds ids rather than whole rows.
759
+ Pass `selectable` a predicate (`(item) => boolean`) to gate specific rows: the
760
+ excluded rows' controls disable and `toggleSelected` refuses them too.
1262
761
 
1263
- const replacementContentSpan = computed(
1264
- () =>
1265
- props.columns.length + Number(!!props.selectable) + Number(!!props.actions)
1266
- );
762
+ To drive a toolbar that lives **outside** the table subtree, give the table an
763
+ `id` and read `useBbTableContext(id).selected` — the handle mirrors the
764
+ table's selection both ways, so a bulk-action bar needs no prop drilling. As
765
+ everywhere in the library, the destructive bulk action is gated by
766
+ `useConfirm` before it touches the data.
1267
767
 
1268
- const slotNamesByKey = computed(() =>
1269
- Object.fromEntries(
1270
- props.columns.map((col) => [col.key, columnKeyToSlotName(col.key)])
1271
- )
1272
- );
1273
- const {
1274
- getter,
1275
- items: internalItems,
1276
- loading: innerLoading,
1277
- } = useItemsGetter({
1278
- items: toRef(props, 'items'),
1279
- });
768
+ **Multi-select with a context-driven bulk bar**
1280
769
 
1281
- const { hasPrefilled } = usePrefill({
1282
- trigger: true,
1283
- fn: async (isPrefill) => {
1284
- await getter(isPrefill, props.modelValue);
770
+ ```vue
771
+ <template>
772
+ <div class="flex max-w-2xl flex-col gap-2">
773
+ <!-- Bulk-action bar lives OUTSIDE the table and reads the selection through
774
+ the shared context by id — no prop drilling. The destructive action is
775
+ gated by `useConfirm`. -->
776
+ <div
777
+ v-if="selectedCount"
778
+ class="flex items-center gap-2 rounded-[var(--bb-radius)] border border-[color:var(--bb-border)] bg-[color:var(--bb-muted)] px-2.5 py-1.5"
779
+ >
780
+ <span class="text-sm">{{ selectedCount }} selected</span>
781
+ <BbButton
782
+ prepend:icon="lucide:trash-2"
783
+ size="sm"
784
+ variant="destructive"
785
+ @click="removeSelected"
786
+ >
787
+ Delete
788
+ </BbButton>
789
+ <BbButton size="sm" variant="ghost" @click="clear">Clear</BbButton>
790
+ </div>
791
+
792
+ <div
793
+ class="overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
794
+ >
795
+ <BbTable
796
+ id="member-selection"
797
+ v-model="selected"
798
+ caption="Team members"
799
+ :columns="columns"
800
+ compact
801
+ item-value="id"
802
+ :items="members"
803
+ legend="Select team members"
804
+ name="members"
805
+ selectable
806
+ />
807
+ </div>
808
+
809
+ <!-- Mount the host once per app (e.g. in the root layout). -->
810
+ <BbConfirm />
811
+ </div>
812
+ </template>
813
+ <script setup lang="ts">
814
+ import { computed, ref } from 'vue';
815
+ import {
816
+ BbButton,
817
+ BbConfirm,
818
+ BbTable,
819
+ useBbTableContext,
820
+ useConfirm,
821
+ } from 'bitboss-ui';
822
+ import type { BbTableColumn } from 'bitboss-ui';
823
+
824
+ type Member = { id: string; name: string; email: string; role: string };
825
+
826
+ const columns: Array<BbTableColumn<Member>> = [
827
+ { key: 'name', label: 'Name' },
828
+ { key: 'email', label: 'Email' },
829
+ { key: 'role', label: 'Role', width: 90 },
830
+ ];
831
+
832
+ const members = ref<Member[]>([
833
+ { id: 'u1', name: 'Ada Lovelace', email: 'ada@acme.io', role: 'Admin' },
834
+ { id: 'u2', name: 'Grace Hopper', email: 'grace@acme.io', role: 'Editor' },
835
+ {
836
+ id: 'u3',
837
+ name: 'Margaret Hamilton',
838
+ email: 'maggie@acme.io',
839
+ role: 'Viewer',
1285
840
  },
1286
- });
1287
-
1288
- const { data: modelValueIndexedByHash } = useIndexById({
1289
- items: toRef(props, 'modelValue'),
1290
- });
1291
-
1292
- const { options } = useBaseOptions({
1293
- disabled: toRef(props, 'disabled'),
1294
- items: internalItems,
1295
- itemText: undefined,
1296
- itemValue: props.itemValue,
1297
- max: props.max,
1298
- selectable: true,
1299
- selectedIndexedByHash: modelValueIndexedByHash,
1300
- });
1301
-
1302
- const onOptionSelected = async (option: BaseOption) => {
1303
- if (props.multiple) {
1304
- emit('update:modelValue', props.modelValue.concat(option.value));
1305
- } else {
1306
- emit('update:modelValue', option.value);
1307
- }
841
+ { id: 'u4', name: 'Ken Thompson', email: 'ken@acme.io', role: 'Editor' },
842
+ ]);
843
+
844
+ // `item-value="id"` gives rows a stable identity, so `selected` holds ids.
845
+ const selected = ref<string[]>([]);
846
+
847
+ // Same id as the table — the handle mirrors the table's selection both ways.
848
+ const ctx = useBbTableContext('member-selection', { selected: [] });
849
+ const selectedCount = computed(() => ctx.selected.value?.length ?? 0);
850
+ const clear = () => (ctx.selected.value = []);
851
+
852
+ const { confirm } = useConfirm();
853
+
854
+ // Destructive bulk action: `await` the decision, act only on a yes.
855
+ const removeSelected = async () => {
856
+ const ids: string[] = ctx.selected.value ?? [];
857
+ const ok = await confirm({
858
+ title: `Delete ${ids.length} ${ids.length === 1 ? 'member' : 'members'}?`,
859
+ text: 'They will lose access to this workspace immediately.',
860
+ variant: 'destructive',
861
+ yes: { text: 'Delete', 'prepend:icon': 'lucide:trash-2' },
862
+ no: 'Cancel',
863
+ });
864
+ if (!ok) return;
865
+ members.value = members.value.filter((member) => !ids.includes(member.id));
866
+ clear();
1308
867
  };
868
+ </script>
869
+ ```
1309
870
 
1310
- const onOptionUnselected = async (option: BaseOption) => {
1311
- if (props.multiple) {
1312
- const copy = { ...modelValueIndexedByHash.value };
1313
- delete copy[option.valueHash];
1314
- emit('update:modelValue', Object.values(copy));
1315
- } else {
1316
- emit('update:modelValue', null);
1317
- }
1318
- };
871
+ More selection surface, all verified behavior:
872
+
873
+ - Every body slot exposes `toggleSelected`, so a bespoke control (a whole-row
874
+ button, a status chip) drives selection through the same path as the
875
+ checkbox — it even works without `selectable` (that prop only governs the
876
+ built-in selection column); explicit blocks (`disabled`, `readonly`, a
877
+ predicate exclusion) still apply.
878
+ - `item:selected` / `item:unselected` fire with the toggled row's value on
879
+ every user toggle — use them for per-row side effects; keep `v-model` as
880
+ the state.
881
+ - A selectable table renders as a `<fieldset>`; give it a `legend`. With
882
+ `name` set, each selected value renders a hidden input (objects are
883
+ JSON-stringified), so the table submits inside a plain HTML form.
884
+ - `disabled` makes the whole table inert; `readonly` leaves controls focusable
885
+ but ignores changes.
886
+
887
+ #### Selection across refetches
888
+
889
+ By default a `dependencies`-driven refetch replaces the rows but leaves
890
+ `v-model` untouched, so ids that just left the result set silently stay
891
+ selected. Set **`enforce-coherence`** — the same prop, same meaning as on the
892
+ selects and option groups — and after every load the table prunes its
893
+ row-keyed models of values whose row is gone: `v-model`,
894
+ `v-model:unselected-items`, `v-model:highlighted` and
895
+ `v-model:expanded-items`. Values whose rows survive the refetch stay put;
896
+ single-value models (`:multiple="false"`, single highlight) reset to `null`.
897
+ Set `item-value` so the match runs on stable identity.
898
+
899
+ One hard exclusion: **never combine it with pagination fed through
900
+ `dependencies`**. Every page turn is a refetch, so state held on rows of other
901
+ pages — cross-page selections, the whole `select-all` + `unselected-items`
902
+ pattern — would be pruned away. For paginated tables (or any custom rule),
903
+ reconcile in the provider instead: the table calls it `(prefill, selected)`
904
+ with the current selection, which is exactly where the new rows are known.
905
+
906
+ **Provider-side reconciliation, for when enforce-coherence doesn't fit**
1319
907
 
1320
- useHashedWatcher(
1321
- () => [props.dependencies, props.items],
1322
- async () => {
1323
- if (!hasPrefilled.value) {
1324
- return;
1325
- }
1326
- await getter(false, props.modelValue);
1327
- },
908
+ ```vue
909
+ <template>
910
+ <div class="flex max-w-2xl flex-col gap-2">
911
+ <div class="flex items-center gap-1.5">
912
+ <!-- Mirrored `false-value`: one role filter is always active. -->
913
+ <BbButton
914
+ v-for="filter in roleFilters"
915
+ :key="filter.value"
916
+ v-model="role"
917
+ :false-value="filter.value"
918
+ group
919
+ size="xs"
920
+ :true-value="filter.value"
921
+ variant="outline"
922
+ >
923
+ {{ filter.label }}
924
+ </BbButton>
925
+ <span class="ml-auto text-xs text-gray-500">
926
+ {{ selected.length }} selected
927
+ </span>
928
+ </div>
929
+
930
+ <!-- The role change refetches via `dependencies`. `enforce-coherence`
931
+ would prune `v-model` for free here — this example keeps it off to
932
+ show the provider-side reconcile, the pattern paginated tables
933
+ (where the prop must stay off) still need. -->
934
+ <div
935
+ class="overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
936
+ >
937
+ <BbTable
938
+ v-model="selected"
939
+ caption="Workspace members"
940
+ :columns="columns"
941
+ compact
942
+ :dependencies="[role]"
943
+ :deps-debounce-time="100"
944
+ item-value="id"
945
+ :items="fetchMembers"
946
+ legend="Select members"
947
+ loading-text="Loading members…"
948
+ selectable
949
+ />
950
+ </div>
951
+ </div>
952
+ </template>
953
+ <script setup lang="ts">
954
+ import { ref } from 'vue';
955
+ import { BbButton, BbTable } from 'bitboss-ui';
956
+ import type { BbTableColumn } from 'bitboss-ui';
957
+
958
+ type Role = 'Admin' | 'Editor' | 'Viewer';
959
+ type RoleFilter = 'all' | Role;
960
+ type Member = { id: string; name: string; email: string; role: Role };
961
+
962
+ const roleFilters: Array<{ label: string; value: RoleFilter }> = [
963
+ { label: 'All', value: 'all' },
964
+ { label: 'Admins', value: 'Admin' },
965
+ { label: 'Editors', value: 'Editor' },
966
+ ];
967
+
968
+ const columns: Array<BbTableColumn<Member>> = [
969
+ { key: 'name', label: 'Name' },
970
+ { key: 'email', label: 'Email' },
971
+ { key: 'role', label: 'Role', width: 90 },
972
+ ];
973
+
974
+ const directory: Member[] = [
975
+ { id: 'u1', name: 'Ada Lovelace', email: 'ada@acme.io', role: 'Admin' },
976
+ { id: 'u2', name: 'Grace Hopper', email: 'grace@acme.io', role: 'Editor' },
1328
977
  {
1329
- debounce: props.depsDebounceTime,
978
+ id: 'u3',
979
+ name: 'Margaret Hamilton',
980
+ email: 'maggie@acme.io',
981
+ role: 'Editor',
982
+ },
983
+ { id: 'u4', name: 'Ken Thompson', email: 'ken@acme.io', role: 'Viewer' },
984
+ { id: 'u5', name: 'Barbara Liskov', email: 'barbara@acme.io', role: 'Admin' },
985
+ ];
986
+
987
+ const role = ref<RoleFilter>('all');
988
+ const selected = ref<string[]>([]);
989
+
990
+ // Without `enforce-coherence` a refetch replaces the rows but leaves the
991
+ // selection untouched. The table calls the provider `(prefill, selected)`,
992
+ // so the provider is the one spot where both the current selection and the new
993
+ // rows are known — drop the ids that left the result set. Reconcile on every
994
+ // call, not just refetches: `prefill` is `true` for every fetch a component
995
+ // starts itself, so it can't tell an initial load from a `dependencies` refetch
996
+ // (only a user-typed query reads `false`, and a table has no search field).
997
+ const fetchMembers = async (
998
+ _prefill: boolean,
999
+ selectedIds?: string[]
1000
+ ): Promise<Member[]> => {
1001
+ await new Promise((resolve) => setTimeout(resolve, 200));
1002
+ const rows =
1003
+ role.value === 'all'
1004
+ ? directory
1005
+ : directory.filter((member) => member.role === role.value);
1006
+ if (selectedIds?.length) {
1007
+ selected.value = selectedIds.filter((id) =>
1008
+ rows.some((member) => member.id === id)
1009
+ );
1330
1010
  }
1331
- );
1011
+ return rows;
1012
+ };
1013
+ </script>
1014
+ ```
1332
1015
 
1333
- const mapColumnToCell = (
1334
- column: BbTableColumn<Item>,
1335
- item: BaseOption
1336
- ): MappedCell => {
1337
- const slotName = slotNamesByKey.value[column.key];
1338
- const align = column.align ?? 'left';
1339
- let content = getItemValue(item.item, column.key);
1016
+ ### Single select, select-all and limits
1340
1017
 
1341
- if (column.formatter && (!isNil(content) || column.formatOnNull !== false)) {
1342
- content = column.formatter(content, column.key, item.item);
1343
- }
1344
- if (column.placeholder && isNil(content)) {
1345
- content = column.placeholder;
1346
- }
1018
+ `:multiple="false"` renders radios and the model becomes one value (or
1019
+ `null`). Label the radio column with `select-text` (visually hidden).
1347
1020
 
1348
- return {
1349
- align,
1350
- classes: mergeCellClasses(
1351
- [`bb-table-data__cell--${align}`],
1352
- props.tdClass,
1353
- column.tdClass,
1354
- content,
1355
- column.key,
1356
- item.item
1357
- ),
1358
- content,
1359
- key: column.key,
1360
- label: column.label,
1361
- slotName,
1362
- };
1363
- };
1021
+ **Pick one plan via radio rows**
1364
1022
 
1365
- const mappedHeaders = computed(() =>
1366
- props.columns.map((column: BbTableColumn<Item>): MappedHeader => {
1367
- const align = column.align ?? 'left';
1368
- const slotName = `header:${slotNamesByKey.value[column.key]}`;
1369
- const sortDirection = column.sorted
1370
- ? column.sorted === 'asc'
1371
- ? 'ascending'
1372
- : 'descending'
1373
- : undefined;
1374
-
1375
- return {
1376
- align,
1377
- key: column.key,
1378
- label: column.label,
1379
- slotName,
1380
- classes: mergeHeaderClasses(
1381
- [`bb-table-header--${align}`],
1382
- props.thClass,
1383
- column.thClass
1384
- ),
1385
- sortDirection,
1386
- };
1387
- })
1388
- );
1023
+ ```vue
1024
+ <template>
1025
+ <div class="flex max-w-xl flex-col gap-1.5">
1026
+ <!-- `:multiple="false"` renders radios and the model is ONE value (or
1027
+ null) not an array. `select-text` labels the radio column for
1028
+ screen readers. -->
1029
+ <div
1030
+ class="overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
1031
+ >
1032
+ <BbTable
1033
+ v-model="planId"
1034
+ caption="Available plans"
1035
+ :columns="columns"
1036
+ compact
1037
+ item-value="id"
1038
+ :items="plans"
1039
+ legend="Choose a plan"
1040
+ :multiple="false"
1041
+ name="plan"
1042
+ select-text="Choose a plan"
1043
+ selectable
1044
+ />
1045
+ </div>
1046
+ <p class="text-sm text-gray-500">
1047
+ Chosen plan:
1048
+ <span class="font-medium text-[color:var(--bb-text)]">{{
1049
+ chosen ? chosen.name : 'none yet'
1050
+ }}</span>
1051
+ </p>
1052
+ </div>
1053
+ </template>
1054
+ <script setup lang="ts">
1055
+ import { computed, ref } from 'vue';
1056
+ import { BbTable } from 'bitboss-ui';
1057
+ import type { BbTableColumn } from 'bitboss-ui';
1389
1058
 
1390
- const mappedItems = computed<MappedItem[]>(() => {
1391
- let expandedRows = 0;
1392
- return options.value.map((item: BaseOption, index: number): MappedItem => {
1393
- const disabled =
1394
- !props.selectable ||
1395
- (typeof props.selectable === 'function' &&
1396
- !props.selectable(item.item)) ||
1397
- !!item.disabled;
1398
-
1399
- // How many rows have passed
1400
- const indexPageOffset = (props.page ?? 0 - 1) * (props.perPage ?? 0);
1401
- const headRowOffset = 1;
1402
- const increment = index + 1;
1403
- const rowIndex = indexPageOffset + headRowOffset + increment + expandedRows;
1404
-
1405
- // Create a projection of the columns and run all formatter logic so
1406
- // we just display content in the template
1407
- const cols: MappedCell[] = props.columns.map((column) =>
1408
- mapColumnToCell(column, item)
1409
- );
1410
- const expanded = expandedItemsIndexedByHash.value[item.valueHash] ?? false;
1411
- if (expanded) {
1412
- expandedRows++;
1413
- }
1414
- const res = {
1415
- accessibleLabel: getAccessibleLabel(cols),
1416
- cols,
1417
- expanded,
1418
- onRowClick: (event: MouseEvent) =>
1419
- emit('click:row', event, item, !!item.selected),
1420
- onRowContextMenu: (event: MouseEvent) =>
1421
- emit('contextmenu:row', event, item, !!item.selected),
1422
- onRowDblClick: (event: MouseEvent) =>
1423
- emit('dblclick:row', event, item, !!item.selected),
1424
- item: item.item,
1425
- text: item.text,
1426
- rowIndex,
1427
- valueHash: item.valueHash,
1428
- value: item.value,
1429
- selected: item.selected,
1430
- disabled,
1431
- };
1432
- return res;
1433
- });
1434
- });
1059
+ type Plan = { id: string; name: string; seats: string; price: string };
1435
1060
 
1436
- const selectAll = defineModel<boolean>('selectAll', {
1437
- required: false,
1438
- default: false,
1439
- });
1061
+ const columns: Array<BbTableColumn<Plan>> = [
1062
+ { key: 'name', label: 'Plan' },
1063
+ { key: 'seats', label: 'Seats', width: 110 },
1064
+ { key: 'price', label: 'Price / month', align: 'right', width: 120 },
1065
+ ];
1440
1066
 
1441
- const expandedItems = defineModel<any[]>('expandedItems', {
1442
- required: false,
1443
- default: () => [],
1444
- });
1067
+ const plans: Plan[] = [
1068
+ { id: 'starter', name: 'Starter', seats: 'Up to 3', price: '€0' },
1069
+ { id: 'growth', name: 'Growth', seats: 'Up to 25', price: '€49' },
1070
+ { id: 'scale', name: 'Scale', seats: 'Unlimited', price: '€199' },
1071
+ ];
1445
1072
 
1446
- const { data: expandedItemsIndexedByHash } = useIndexById({
1447
- items: expandedItems,
1448
- });
1073
+ // Single-select model: one id (via `item-value`) or null.
1074
+ const planId = ref<string | null>(null);
1449
1075
 
1450
- const onExpandControllerKeydown = (event: KeyboardEvent, valueHash: string) => {
1451
- if (!(event.target instanceof HTMLElement)) return;
1452
- if (event.target instanceof HTMLButtonElement) return;
1453
- if (event.key !== 'Enter' && event.key !== ' ') return;
1454
- event.preventDefault();
1455
- toggleExpandedFor(valueHash);
1456
- };
1076
+ const chosen = computed(() => plans.find((plan) => plan.id === planId.value));
1077
+ </script>
1078
+ ```
1457
1079
 
1458
- const getExpandControllerProps = (item: MappedItem) => ({
1459
- 'aria-controls': isNil(expandedItemsIndexedByHash.value[item.valueHash])
1460
- ? undefined
1461
- : `expanded_${item.valueHash}`,
1462
- 'aria-expanded': item.expanded ? 'true' : 'false',
1463
- onClick: (event: MouseEvent) => {
1464
- event.stopPropagation();
1465
- toggleExpandedFor(item.valueHash);
1466
- },
1467
- onKeydown: (event: KeyboardEvent) =>
1468
- onExpandControllerKeydown(event, item.valueHash),
1469
- });
1080
+ `max` caps a multi-select: once reached, every unselected row's checkbox
1081
+ disables until something is deselected.
1082
+
1083
+ Multi-select tables show a **select-all** checkbox in the header
1084
+ (`v-model:select-all`; hide it with `disable-select-all`, relabel with
1085
+ `select-all-label`). Select-all is designed for the server-side "all across
1086
+ pages" pattern: when it is on, every row reads as selected and unchecking a
1087
+ row adds its value to `v-model:unselected-items` — so your bulk endpoint
1088
+ receives `{ all: true, except: unselectedItems }` instead of thousands of ids.
1089
+ Toggling select-all clears both `modelValue` and `unselected-items`; the
1090
+ header checkbox shows an indeterminate state for partial selections.
1091
+
1092
+ Two real constraints to design around (bugs documented from the test suite):
1093
+
1094
+ - The select-all header checkbox **never renders its checked state** — it
1095
+ emits and drives row state correctly, but stays visually unchecked; the
1096
+ indeterminate dash is what users see for select-all with exclusions.
1097
+ - Turning select-all on does **not** re-emit `update:modelValue` when the
1098
+ model is already empty. Treat `select-all` + `unselected-items` as the
1099
+ source of truth in all-mode; don't infer "everything selected" from
1100
+ `modelValue`.
1101
+
1102
+ **Native `<form>` submission + select-all:** the table renders one hidden
1103
+ input per value in `modelValue`, so a plain form POST works for explicit
1104
+ selections. It does **not** serialize the select-all/exclusion pattern — in
1105
+ all-mode `modelValue` is empty, so the form posts nothing even though every
1106
+ row appears selected. For "all across pages", submit via a handler that reads
1107
+ `{ all, except: unselectedItems }` from the v-models, not via native form
1108
+ fields.
1109
+
1110
+ ### Row expansion
1111
+
1112
+ Track open rows with `v-model:expanded-items` (an array of row values — set
1113
+ `item-value` so expansion is stable across refetches) and render their detail
1114
+ through the `#expand` slot, a full-width row below the main one. Trigger it
1115
+ from the `#actions` slot (enable the column with `actions`, label it with
1116
+ `actions-text`): the scope hands you `toggleExpanded` — a **callback** — plus
1117
+ the read-only `expanded` flag. Drive open/close through the callback; the
1118
+ `expanded` prop is one-way state, not something to mutate.
1119
+
1120
+ **Expandable detail rows**
1470
1121
 
1471
- const toggleExpandedFor = (valueHash: string) => {
1472
- const linkedOption = options.value.find((o) => o.valueHash === valueHash);
1473
- if (!linkedOption) return;
1474
- if (valueHash in expandedItemsIndexedByHash.value) {
1475
- expandedItems.value = expandedItems.value.filter(
1476
- (o) => hash(o) !== valueHash
1477
- );
1478
- } else {
1479
- expandedItems.value = [...expandedItems.value, linkedOption.value];
1480
- }
1122
+ ```vue
1123
+ <template>
1124
+ <!-- Track open rows with `v-model:expanded-items`. The `#actions` scope hands
1125
+ you `toggleExpanded` (a callback) and `expanded` (read-only) — drive the
1126
+ toggle through the callback, never by mutating the slot prop. -->
1127
+ <div
1128
+ class="max-w-2xl overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
1129
+ >
1130
+ <BbTable
1131
+ v-model:expanded-items="expanded"
1132
+ actions
1133
+ actions-text="Details"
1134
+ caption="Invoices"
1135
+ :columns="columns"
1136
+ compact
1137
+ item-value="id"
1138
+ :items="invoices"
1139
+ >
1140
+ <template #actions="{ expanded: isOpen, toggleExpanded }">
1141
+ <BbButton size="xs" variant="ghost" @click="toggleExpanded">
1142
+ {{ isOpen ? 'Hide' : 'Details' }}
1143
+ </BbButton>
1144
+ </template>
1145
+
1146
+ <template #expand="{ item }">
1147
+ <div class="bg-[color:var(--bb-muted)] px-3 py-2.5 text-sm">
1148
+ <p class="font-medium">{{ item.client }} · {{ item.number }}</p>
1149
+ <p class="text-xs text-gray-500">
1150
+ Issued {{ item.issuedAt }} · net 30 · {{ item.lines }} line items
1151
+ </p>
1152
+ </div>
1153
+ </template>
1154
+ </BbTable>
1155
+ </div>
1156
+ </template>
1157
+ <script setup lang="ts">
1158
+ import { ref } from 'vue';
1159
+ import { BbButton, BbTable } from 'bitboss-ui';
1160
+ import type { BbTableColumn } from 'bitboss-ui';
1161
+
1162
+ type Invoice = {
1163
+ id: string;
1164
+ number: string;
1165
+ client: string;
1166
+ issuedAt: string;
1167
+ amount: string;
1168
+ lines: number;
1481
1169
  };
1482
1170
 
1483
- watch(selectAll, () => {
1484
- unselectedItems.clear();
1485
- emit('update:unselectedItems', []);
1486
- emit('update:modelValue', []);
1487
- });
1171
+ const columns: Array<BbTableColumn<Invoice>> = [
1172
+ { key: 'number', label: 'Invoice', width: 110 },
1173
+ { key: 'client', label: 'Client' },
1174
+ { key: 'amount', label: 'Amount', align: 'right', width: 100 },
1175
+ ];
1488
1176
 
1489
- const indeterminate = computed(() => {
1490
- if (props.multiple && props.modelValue.length > 0 && !props.selectAll) {
1491
- return true;
1492
- }
1493
- if (selectAll.value && unselectedItems.size > 0) {
1494
- return true;
1495
- }
1496
- return false;
1497
- });
1177
+ const invoices: Invoice[] = [
1178
+ {
1179
+ id: '1',
1180
+ number: 'INV-1041',
1181
+ client: 'Northwind Traders',
1182
+ issuedAt: '02/06/2026',
1183
+ amount: '€4,200',
1184
+ lines: 6,
1185
+ },
1186
+ {
1187
+ id: '2',
1188
+ number: 'INV-1042',
1189
+ client: 'Globex',
1190
+ issuedAt: '11/06/2026',
1191
+ amount: '€980',
1192
+ lines: 2,
1193
+ },
1194
+ {
1195
+ id: '3',
1196
+ number: 'INV-1043',
1197
+ client: 'Initech',
1198
+ issuedAt: '19/05/2026',
1199
+ amount: '€15,600',
1200
+ lines: 11,
1201
+ },
1202
+ ];
1498
1203
 
1499
- const unselectedItems = reactive(new Set<string>());
1500
- watch(
1501
- () => props.unselectedItems,
1502
- () => {
1503
- unselectedItems.clear();
1504
- props.unselectedItems.forEach((i) => {
1505
- unselectedItems.add(hash(i));
1506
- });
1507
- }
1508
- );
1204
+ // Holds the expanded rows' values (ids, via `item-value`).
1205
+ const expanded = ref<string[]>([]);
1206
+ </script>
1207
+ ```
1509
1208
 
1510
- const onInputChange = (item: BaseOption) => {
1511
- if (isSelected(item)) {
1512
- onOptionUnselected(item);
1513
- emit('item:unselected', item.value);
1514
- unselectedItems.add(item.valueHash);
1515
- emit('update:unselectedItems', [...props.unselectedItems, item.value]);
1516
- return;
1517
- }
1518
- if (!selectAll.value) {
1519
- onOptionSelected(item);
1520
- }
1521
- emit('item:selected', item.value);
1522
- unselectedItems.delete(item.valueHash);
1523
- emit(
1524
- 'update:unselectedItems',
1525
- props.unselectedItems.filter((i) => hash(i) !== hash(item.value))
1526
- );
1527
- };
1209
+ For a custom toggle control, `v-bind` the scope's `expandProps` onto it: you
1210
+ get `aria-expanded`/`aria-controls` kept in sync, a click handler that stops
1211
+ propagation (so the row click underneath doesn't also toggle highlight), and
1212
+ Enter/Space handling for non-button elements. The nested-table example below
1213
+ uses it.
1214
+
1215
+ ### Nested tables that align with the parent
1216
+
1217
+ A `BbTable` rendered inside another's `#expand` slot is independent by
1218
+ default. Add `inherit-column-widths` and it snaps its columns to the parent's
1219
+ tracks, so a line-items breakdown reads as a continuation of the invoice row
1220
+ above it (this also forces fixed layout). The mechanics:
1221
+
1222
+ - `inherit-column-widths` — `true` inherits from the nearest ancestor table;
1223
+ a string targets a specific ancestor's `id` (useful when an intermediate
1224
+ table has a different column count).
1225
+ - By default child columns map one-to-one onto the parent's data columns. A
1226
+ column's `snap` remaps it: a number is a start track (`2` occupies the
1227
+ parent's third data region), `[start, end]` spans a range (fractions
1228
+ allowed; `end: -1` runs to the end of the data region). Structural columns
1229
+ (`select`, actions) are accounted for automatically.
1230
+ - A child with **more** columns than the parent content-sizes the extras and
1231
+ scrolls; with fewer, the last width-free column absorbs the slack so both
1232
+ tables end flush.
1233
+ - **When the parent has `selectable`/`actions` columns, snap every child
1234
+ column** (`snap: base` … `base + n`, where `base` is `1` under a selectable
1235
+ parent, `0` otherwise). A width-free last column becomes the fill track and
1236
+ absorbs the parent's actions region — right-aligned content then sits under
1237
+ the expand/actions control instead of under the parent's last data column.
1238
+ With every column snapped, the parent's select and actions tracks fold into
1239
+ the child's first/last cell as _padding_, so the child's content stays flush
1240
+ with the parent's data grid on both edges (a child without its own select
1241
+ column starts flush with the parent's first data column — the inset itself
1242
+ reads as hierarchy).
1243
+ - **Scope any page-level cell-padding overrides to the parent's own cells.** A
1244
+ bare `:deep(tbody td:first-child)` also matches the nested table's cells and
1245
+ clobbers the offset padding it computes to stay aligned — use direct-child
1246
+ combinators (`:deep(> table > tbody > tr > td:first-child)`) so the child
1247
+ keeps its structural padding.
1248
+
1249
+ **Invoice lines aligned under the invoice columns**
1528
1250
 
1529
- const isSelected = (item: BaseOption): boolean => {
1530
- if (unselectedItems.has(item.valueHash)) return false;
1531
- return item.selected || selectAll.value;
1251
+ ```vue
1252
+ <template>
1253
+ <!-- The nested table opts into `inherit-column-widths`: its columns snap to
1254
+ the parent's tracks, so line items line up under the invoice columns.
1255
+ The toggle binds the scope's `expandProps` for wired-up aria state. -->
1256
+ <div
1257
+ class="max-w-2xl overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
1258
+ >
1259
+ <BbTable
1260
+ v-model:expanded-items="expanded"
1261
+ actions
1262
+ actions-text="Line items"
1263
+ caption="Invoices"
1264
+ :columns="columns"
1265
+ compact
1266
+ item-value="id"
1267
+ :items="invoices"
1268
+ >
1269
+ <template #actions="{ expanded: isOpen, expandProps }">
1270
+ <BbButton
1271
+ v-bind="expandProps"
1272
+ :append:icon="isOpen ? 'lucide:chevron-up' : 'lucide:chevron-down'"
1273
+ size="xs"
1274
+ variant="ghost"
1275
+ >
1276
+ Lines
1277
+ </BbButton>
1278
+ </template>
1279
+
1280
+ <template #expand="{ item }">
1281
+ <div class="bg-[color:var(--bb-muted)] px-1 py-1">
1282
+ <BbTable
1283
+ :caption="`Line items of ${item.number}`"
1284
+ :columns="lineColumns"
1285
+ compact
1286
+ inherit-column-widths
1287
+ item-value="id"
1288
+ :items="item.lines"
1289
+ />
1290
+ </div>
1291
+ </template>
1292
+ </BbTable>
1293
+ </div>
1294
+ </template>
1295
+ <script setup lang="ts">
1296
+ import { ref } from 'vue';
1297
+ import { BbButton, BbTable } from 'bitboss-ui';
1298
+ import type { BbTableColumn } from 'bitboss-ui';
1299
+
1300
+ type Line = { id: string; description: string; qty: number; amount: string };
1301
+ type Invoice = {
1302
+ id: string;
1303
+ number: string;
1304
+ client: string;
1305
+ amount: string;
1306
+ lines: Line[];
1532
1307
  };
1533
1308
 
1534
- /**
1535
- * When using this component for submission we add a bunch of hidden inputs
1536
- * so the submitted inputs is on par with v-model
1537
- */
1538
-
1539
- /**
1540
- * This is used to keep the value compatible to common html expected values.
1541
- * Convert to string everything that's not but do not double encode strings
1542
- */
1543
- const makeInputValue = when(
1544
- (item: unknown) => typeof item !== 'string',
1545
- JSON.stringify
1546
- );
1547
- const hiddenInputs = computed(() =>
1548
- [].concat(props.modelValue ?? []).map((current: unknown) => ({
1549
- disabled: props.disabled,
1550
- name: props.name,
1551
- type: 'hidden' as const,
1552
- value: makeInputValue(current),
1553
- }))
1554
- );
1309
+ const columns: Array<BbTableColumn<Invoice>> = [
1310
+ { key: 'number', label: 'Invoice', width: 110 },
1311
+ { key: 'client', label: 'Client' },
1312
+ { key: 'amount', label: 'Amount', align: 'right', width: 100 },
1313
+ ];
1555
1314
 
1556
- /** Varied line widths for skeleton (percent) to avoid a mechanical look */
1557
- const skeletonLineWidths = [70, 45, 85, 55];
1315
+ const lineColumns: Array<BbTableColumn<Line>> = [
1316
+ { key: 'description', label: 'Description' },
1317
+ { key: 'qty', label: 'Qty', width: 60 },
1318
+ { key: 'amount', label: 'Amount', align: 'right', width: 100 },
1319
+ ];
1558
1320
 
1559
- const skeletonLength = computed(() =>
1560
- clamp(options.value.length || props.columns.length || 1, 4, 10)
1561
- );
1562
-
1563
- watch(
1564
- () => [mappedItems.value, props.fixedColumns],
1565
- () => {
1566
- if (tableRef.value && props.fixedColumns?.length) {
1567
- applyFixedTableColumns(tableRef.value, props.fixedColumns);
1568
- }
1321
+ const invoices: Invoice[] = [
1322
+ {
1323
+ id: '1',
1324
+ number: 'INV-1041',
1325
+ client: 'Northwind Traders',
1326
+ amount: '€4,200',
1327
+ lines: [
1328
+ {
1329
+ id: '1a',
1330
+ description: 'Design retainer — June',
1331
+ qty: 1,
1332
+ amount: '€3,000',
1333
+ },
1334
+ {
1335
+ id: '1b',
1336
+ description: 'Icon set extension',
1337
+ qty: 24,
1338
+ amount: '€1,200',
1339
+ },
1340
+ ],
1569
1341
  },
1570
- { flush: 'post' }
1571
- );
1572
- </script>
1342
+ {
1343
+ id: '2',
1344
+ number: 'INV-1042',
1345
+ client: 'Globex',
1346
+ amount: '€980',
1347
+ lines: [
1348
+ { id: '2a', description: 'Accessibility audit', qty: 1, amount: '€980' },
1349
+ ],
1350
+ },
1351
+ ];
1573
1352
 
1574
- <style lang="postcss">
1575
- @import './index.css';
1576
- </style>
1353
+ const expanded = ref<string[]>([]);
1354
+ </script>
1577
1355
  ```
1578
1356
 
1579
- ## Types
1580
-
1581
- ```ts
1582
- import type { Classes } from '@/types/Classes';
1583
- import type { Option } from '@/types/Option';
1584
- import type { InputHTMLAttributes } from 'vue';
1585
-
1586
- /**
1587
- * Recursively builds dot-notation paths from a type's keys.
1588
- * Depth-limited to 3 levels to avoid compiler performance issues.
1589
- */
1590
- type NestedPaths<T, Depth extends unknown[] = []> = Depth['length'] extends 3
1591
- ? never
1592
- : 0 extends 1 & T
1593
- ? string
1594
- : T extends object
1595
- ? T extends any[] | Date | Function | RegExp | Map<any, any> | Set<any>
1596
- ? never
1597
- : {
1598
- [K in Extract<keyof T, string>]:
1599
- | K
1600
- | `${K}.${NestedPaths<NonNullable<T[K]>, [...Depth, unknown]>}`;
1601
- }[Extract<keyof T, string>]
1602
- : never;
1603
-
1604
- /**
1605
- * Suggested keys for a column derived from an Item type.
1606
- * Provides autocomplete for nested dot-notation paths (up to 3 levels)
1607
- * while still allowing arbitrary string keys for virtual columns.
1608
- */
1609
- export type ItemKey<Item = any> = NestedPaths<Item> | (string & {});
1357
+ ### Row highlight (master–detail)
1610
1358
 
1611
- /**
1612
- * Skeleton placeholder style for the loading state.
1613
- * Matches the expected content type for a better loading experience.
1614
- */
1615
- export type SkeletonType =
1616
- | 'text'
1617
- | 'avatar'
1618
- | 'avatar-text'
1619
- | 'image'
1620
- | 'badge';
1621
-
1622
- export type BaseColumn<Item = any> = {
1623
- /**
1624
- * When set to false the formatter will not run on nullish values.
1625
- * By default is true so you can create virtual columns with keys that do not
1626
- * reference a prop in the `item` and still extract data from the other
1627
- * arguments in the formatting function.
1628
- */
1629
- formatOnNull?: boolean;
1630
-
1631
- /**
1632
- * Formatting function used to generate enhanced content.
1633
- * @param content The content generated by the extraction of `Item[key]`
1634
- * @param key Key of the column taken from the column definition.
1635
- * @param item Item of the row.
1636
- */
1637
- formatter?: (content: any, key: string, item: Item) => any;
1638
-
1639
- /**
1640
- * Key identifying the column.
1641
- */
1642
- key: ItemKey<Item>;
1643
-
1644
- /**
1645
- * Text used in the header for this column.
1646
- */
1647
- label: string;
1648
-
1649
- /**
1650
- * Placeholder used when the pipeline returns null content.
1651
- */
1652
- placeholder?: string;
1653
- };
1359
+ `v-model:highlighted` is a parallel, UI-intent state — "the record whose
1360
+ details are open" independent of selection. Clicking anywhere on a row
1361
+ toggles it: in single mode a new click replaces the previous highlight and
1362
+ clicking the highlighted row clears it back to `null`; `highlight-multiple`
1363
+ turns the model into an array (a non-array value then throws). Two kinds of
1364
+ clicks are ignored on purpose: those inside interactive elements (links,
1365
+ buttons, inputs, labels keep their own semantics) and those ending a text
1366
+ selection (dragging to copy a cell must not mutate state).
1654
1367
 
1655
- /**
1656
- * Column definition for BbTable. Controls data extraction, formatting, alignment, and styling.
1657
- */
1658
- export type BbTableColumn<Item = any> = BaseColumn<Item> & {
1659
- /**
1660
- * Text alignment of the column.
1661
- */
1662
- align?: 'left' | 'right' | 'center';
1663
-
1664
- /**
1665
- * Skeleton placeholder style shown during loading. Defaults to `'text'`.
1666
- */
1667
- skeleton?: SkeletonType;
1668
-
1669
- /**
1670
- * Defines the sorting direction of the column.
1671
- */
1672
- sorted?: 'asc' | 'desc';
1673
-
1674
- /**
1675
- * Defines the classes to be passed to the `<td>` elements. Can also be a function for dynamic values.
1676
- */
1677
- tdClass?: ColumnClasses<Item>;
1678
-
1679
- /**
1680
- * Defines the classes to be passed to the `<th>`.
1681
- */
1682
- thClass?: Classes;
1683
-
1684
- /**
1685
- * Freezes the column to a fixed width (numbers are treated as px). When
1686
- * omitted the column syncs its width to the parent table's matching track.
1687
- */
1688
- width?: number | string;
1689
-
1690
- /**
1691
- * For a nested table, where this column snaps onto the parent's grid. The
1692
- * parent's columns form snap points where `select` occupies `[0, 1]`, data
1693
- * column `k` occupies `[k + 1, k + 2]` and `actions` occupies `[N + 1, N + 2]`.
1694
- *
1695
- * - A single number is a start snap point that takes one parent track, e.g.
1696
- * `2` → `[2, 3]`. It may skip ahead (`5` jumps straight to the sixth track),
1697
- * and the next column resumes from where it ends.
1698
- * - A `[start, end]` pair snaps between two points; the width is the sum of
1699
- * the fractional parent tracks it covers, so `[3, 4.5]` is the full fourth
1700
- * track plus half of the fifth. `end` of `-1` runs to the end of the data
1701
- * region.
1702
- *
1703
- * When omitted the column chains from the previous column's end to the next
1704
- * whole snap point (a one-to-one inheritance by index).
1705
- */
1706
- snap?: number | [start: number, end: number];
1707
- };
1368
+ Highlight ships **unstyled**: the row only gets `aria-current="true"` and the
1369
+ `bb-table-data__row--highlighted` class. Paint the active row yourself
1370
+ idiomatically through the public `row-class` API against the highlighted
1371
+ value, as here.
1708
1372
 
1709
- export type BbTableProps<Item = any> = {
1710
- /**
1711
- * Function that accepts the columns and the current item as
1712
- * arguments and returns a label to be used for accessibility purposes.
1713
- */
1714
- accessibleLabel?: (columns: MappedCell[], item: any) => string;
1715
-
1716
- /**
1717
- * Displays the actions column.
1718
- */
1719
- actions?: boolean;
1720
-
1721
- /**
1722
- * Label used in the header of the actions column.
1723
- */
1724
- actionsText?: string;
1725
-
1726
- /**
1727
- * Text alignment of the columns.
1728
- */
1729
- align?: 'left' | 'center' | 'right';
1730
-
1731
- /**
1732
- * Stable id for this table's width context. When omitted a unique id is
1733
- * generated. Nested tables use the nearest ancestor id to inherit widths.
1734
- */
1735
- id?: string;
1736
-
1737
- /**
1738
- * Opt a nested table into inheriting its column widths from an ancestor
1739
- * table's matching tracks (accounting for `select`/`actions` columns). This
1740
- * also forces `table-layout: fixed`, so it is **off by default** — a nested
1741
- * table renders independently unless you ask for alignment.
1742
- * - `false` (the default): do not inherit; size independently.
1743
- * - `true`: inherit from the nearest ancestor table.
1744
- * - a table id (string): inherit from the specific ancestor in the chain with
1745
- * that id rather than the immediate parent. Use this when an intermediate
1746
- * table has a different column count, so the extra columns would otherwise
1747
- * misalign against it.
1748
- */
1749
- inheritColumnWidths?: boolean | string;
1750
-
1751
- /**
1752
- * Boolean that defines whether to display a "Select all" checkbox.
1753
- */
1754
- allowSelectAll?: boolean;
1755
-
1756
- /**
1757
- * Caption that describes the content of the table. Used for accessibility purposes.
1758
- */
1759
- caption?: string;
1760
-
1761
- /**
1762
- * Array of definitions of how the columns should be rendered.
1763
- *
1764
- */
1765
- columns: BbTableColumn<Item>[];
1766
-
1767
- /**
1768
- * Adds a CSS class that applies a compact style to the component.
1769
- */
1770
- compact?: boolean;
1771
-
1772
- /**
1773
- * Defines an array of dependencies that will trigger actions in the component upon change.
1774
- */
1775
- dependencies?: any[];
1776
-
1777
- /**
1778
- * Timeout used to debounce response to changes to dependencies.
1779
- */
1780
- depsDebounceTime?: number;
1781
-
1782
- /**
1783
- * Disables the component
1784
- */
1785
- disabled?: boolean;
1786
-
1787
- /**
1788
- * Boolean that defines whether to display or hide the caption.
1789
- * By default is true and the caption is hidden.
1790
- */
1791
- displayCaption?: boolean;
1792
-
1793
- /**
1794
- * Defines the table layout as fixed
1795
- * making each column take up the same amount of space.
1796
- */
1797
- fixed?: boolean;
1798
-
1799
- /**
1800
- * Definition for which column should be fixed.
1801
- * It can be an array of index of the column to fix on the left side of the table or
1802
- * an array of objects indicating the index and the position `left` or `right` where to affix the columns.
1803
- */
1804
- fixedColumns?: (number | { index: number; position: 'left' | 'right' })[];
1805
-
1806
- /**
1807
- * Boolean that sets the headers as sticky to the top of the table.
1808
- */
1809
- fixedHeaders?: boolean;
1810
-
1811
- items:
1812
- | Item[]
1813
- | ((prefill: boolean, modelValue?: any[]) => Promise<Item[]>)
1814
- | ((prefill: boolean, modelValue?: any[]) => Item[]);
1815
-
1816
- itemValue?: string | ((item: Item) => any);
1817
-
1818
- /**
1819
- * Text content of the legend.
1820
- */
1821
- legend?: string;
1822
-
1823
- /**
1824
- * Sets the component in a loading state, usually triggering some visual styles.
1825
- */
1826
- loading?: boolean;
1827
-
1828
- /**
1829
- * String displayed while items are being loaded.
1830
- */
1831
- loadingText?: string;
1832
-
1833
- /**
1834
- * Maximum number of items that can be selected.
1835
- */
1836
- max?: number;
1837
-
1838
- /**
1839
- * Used by v-model. Can be any serializable type.
1840
- */
1841
- modelValue?: any;
1842
-
1843
- /**
1844
- * Allows the selection of multiple items.
1845
- */
1846
- multiple?: boolean;
1847
-
1848
- /**
1849
- * Defines the name of the input.
1850
- */
1851
- name?: InputHTMLAttributes['name'];
1852
-
1853
- /**
1854
- * String displayed when there are no items to display.
1855
- */
1856
- noDataText?: string;
1857
-
1858
- /**
1859
- * Current page number starting from 1, used for accessibility purposes.
1860
- */
1861
- page?: number;
1862
-
1863
- /**
1864
- * Number of items per page, used for accessibility purposes.
1865
- */
1866
- perPage?: number;
1867
-
1868
- /**
1869
- * Sets the input in a readonly state.
1870
- */
1871
- readonly?: boolean;
1872
-
1873
- /**
1874
- * Defines whether the "Select all" checkbox is checked.
1875
- */
1876
- selectAll?: boolean;
1877
-
1878
- /**
1879
- * Defines whether the table is selectable.
1880
- * Can be a global boolean that affects all rows or a function
1881
- * that accepts an item and returns a boolean that only affects that item.
1882
- */
1883
- selectable?: boolean | ((item: Item) => boolean);
1884
-
1885
- /**
1886
- * Text of the label used by the "Select all" boolean.
1887
- */
1888
- selectAllLabel?: string;
1889
-
1890
- /**
1891
- * Text of the header used for the radio inputs when the table is not `multiple`.
1892
- */
1893
- selectText?: string;
1894
-
1895
- /**
1896
- * Defines the classes to be passed to the `<td>` elements. Can also be a function for dynamic values.
1897
- */
1898
- tdClass?: ColumnClasses<Item>;
1899
-
1900
- /**
1901
- * Defines the classes to be passed to the `<th>`.
1902
- */
1903
- thClass?: Classes;
1904
-
1905
- /**
1906
- * Total number of items there is.
1907
- */
1908
- totalItems?: number;
1909
-
1910
- /**
1911
- * Items that are not selected.
1912
- */
1913
- unselectedItems?: any[];
1914
- };
1373
+ **Click a row to preview it**
1915
1374
 
1916
- /**
1917
- * We allow table data classes to be a function so it can depend on the item
1918
- */
1919
- export type ColumnClasses<Item = any> =
1920
- | Classes
1921
- | ((content: any, key: string, item: Item) => Classes);
1375
+ ```vue
1376
+ <template>
1377
+ <div class="flex max-w-3xl flex-col gap-3 sm:flex-row sm:gap-3">
1378
+ <!-- Clicking a row toggles `v-model:highlighted` (the "open record" state,
1379
+ separate from selection). Highlight ships unstyled, so we paint the
1380
+ active row ourselves through the public `row-class` API. -->
1381
+ <div
1382
+ class="min-w-0 flex-1 overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
1383
+ >
1384
+ <BbTable
1385
+ v-model:highlighted="highlighted"
1386
+ caption="Invoices"
1387
+ :columns="columns"
1388
+ compact
1389
+ item-value="id"
1390
+ :items="invoices"
1391
+ :row-class="rowClass"
1392
+ />
1393
+ </div>
1394
+
1395
+ <aside
1396
+ class="w-full shrink-0 rounded-[var(--bb-radius)] border border-[color:var(--bb-border)] p-3 text-sm sm:w-52"
1397
+ >
1398
+ <template v-if="active">
1399
+ <p class="font-medium">{{ active.client }}</p>
1400
+ <p class="mt-0.5 text-xs text-gray-500">
1401
+ {{ active.number }} · {{ active.amount }}
1402
+ </p>
1403
+ </template>
1404
+ <p v-else class="text-xs text-gray-500">
1405
+ Select an invoice to preview it.
1406
+ </p>
1407
+ </aside>
1408
+ </div>
1409
+ </template>
1410
+ <script setup lang="ts">
1411
+ import { computed, ref } from 'vue';
1412
+ import { BbTable } from 'bitboss-ui';
1413
+ import type { BbTableColumn } from 'bitboss-ui';
1922
1414
 
1923
- /**
1924
- * Projection that represents a row of cells with flags
1925
- * linked to the state of the item
1926
- */
1927
- export type MappedItem = Option & {
1928
- accessibleLabel: string;
1929
- cols: MappedCell[];
1930
- expanded: boolean;
1931
- onRowClick: (event: MouseEvent) => void;
1932
- onRowContextMenu: (event: MouseEvent) => void;
1933
- onRowDblClick: (event: MouseEvent) => void;
1934
- rowIndex: number | undefined;
1935
- };
1415
+ type Invoice = { id: string; number: string; client: string; amount: string };
1936
1416
 
1937
- /**
1938
- * Projection that represents a header
1939
- */
1940
- export type MappedHeader = {
1941
- align: HTMLTableCellElement['align'];
1942
- classes?: Classes;
1943
- key: string;
1944
- label: string;
1945
- slotName: string;
1946
- sortDirection?: 'ascending' | 'descending';
1947
- };
1417
+ const columns: Array<BbTableColumn<Invoice>> = [
1418
+ { key: 'number', label: 'Invoice', width: 110 },
1419
+ { key: 'client', label: 'Client' },
1420
+ { key: 'amount', label: 'Amount', align: 'right', width: 100 },
1421
+ ];
1948
1422
 
1949
- /**
1950
- * Projection that represents a cell of data for display.
1951
- */
1952
- export type MappedCell = {
1953
- align: string;
1954
- classes: Classes;
1955
- content: any;
1956
- key: string;
1957
- label: string;
1958
- slotName: string;
1959
- };
1423
+ const invoices: Invoice[] = [
1424
+ {
1425
+ id: '1',
1426
+ number: 'INV-1041',
1427
+ client: 'Northwind Traders',
1428
+ amount: '€4,200',
1429
+ },
1430
+ { id: '2', number: 'INV-1042', client: 'Globex', amount: '€980' },
1431
+ { id: '3', number: 'INV-1043', client: 'Initech', amount: '€15,600' },
1432
+ ];
1960
1433
 
1961
- export type ExpandControllerProps = {
1962
- 'aria-controls'?: string;
1963
- 'aria-expanded': 'true' | 'false';
1964
- onClick: () => void;
1965
- onKeydown: (event: KeyboardEvent) => void;
1966
- };
1434
+ // `item-value="id"` means `highlighted` is the row's id.
1435
+ const highlighted = ref<string | null>(null);
1967
1436
 
1968
- export type BbTableEvents = {
1969
- (e: 'click:row', event: MouseEvent, item: any, selected: boolean): void;
1970
- (e: 'contextmenu:row', event: MouseEvent, item: any, selected: boolean): void;
1971
- (e: 'dblclick:row', event: MouseEvent, item: any, selected: boolean): void;
1972
- (e: 'update:modelValue', value: any): void;
1973
- (e: 'update:selectAll', value: boolean): void;
1974
- (e: 'update:expandedItems', value: any[]): void;
1975
- (e: 'update:unselectedItems', value: any[]): void;
1976
- (e: 'item:selected', value: any): void;
1977
- (e: 'item:unselected', value: any): void;
1978
- };
1437
+ const active = computed(() =>
1438
+ invoices.find((invoice) => invoice.id === highlighted.value)
1439
+ );
1979
1440
 
1980
- export type BbTableSlots<Item = any> = {
1981
- /**
1982
- * Replaces the entire `<thead>` element. Use when you need full control over the header markup.
1983
- */
1984
- thead?: (props: object) => any;
1985
- /**
1986
- * Replaces the entire `<tbody>` element. Use when you need full control over the body markup.
1987
- */
1988
- tbody?: (props: object) => any;
1989
- /**
1990
- * Replaces the entire `<tfoot>` element. Use when you need full control over the footer markup.
1991
- */
1992
- tfoot?: (props: object) => any;
1993
- /**
1994
- * Replaces the default selection header cell content (checkbox or radio label).
1995
- * @param multiple - Whether the table allows multiple selection.
1996
- * @param selectAll - Whether the "select all" checkbox is currently checked.
1997
- * @param text - The accessible label text for the select-all control.
1998
- */
1999
- 'header:select'?: (props: {
2000
- multiple: boolean;
2001
- selectAll: boolean;
2002
- text: string;
2003
- }) => any;
2004
- /**
2005
- * Replaces the default actions header cell content.
2006
- * @param text - The accessible label text for the actions column header.
2007
- */
2008
- 'header:actions'?: (props: { text: string }) => any;
2009
- /**
2010
- * Content shown while table rows are loading (replaces the skeleton rows).
2011
- * @param items - The current (possibly stale) list of items, useful to match the previous row count.
2012
- */
2013
- loading?: (props: { items: Item[] }) => any;
2014
- /**
2015
- * Content shown when the items list is empty and not loading.
2016
- */
2017
- 'no-data'?: (props: object) => any;
2018
- /**
2019
- * Content rendered in the actions cell for each row.
2020
- * @param expanded - Whether this row is currently expanded.
2021
- * @param expandProps - Bind these onto an expand-toggle button to wire up aria attributes and keyboard handling.
2022
- * @param toggleExpanded - Toggles the expanded state for this row.
2023
- * @param item - The raw item for this row.
2024
- * @param value - The resolved value for this row.
2025
- */
2026
- actions?: (props: {
2027
- expanded?: boolean;
2028
- expandProps?: ExpandControllerProps;
2029
- toggleExpanded?: () => void;
2030
- item: Item;
2031
- value: any;
2032
- }) => any;
2033
- /**
2034
- * Full-width expandable content row rendered below each main row when expanded.
2035
- * @param expanded - Whether this row is currently expanded.
2036
- * @param expandProps - Bind these onto an expand-toggle button to wire up aria attributes and keyboard handling.
2037
- * @param toggleExpanded - Toggles the expanded state for this row.
2038
- * @param item - The raw item for this row.
2039
- * @param value - The resolved value for this row.
2040
- */
2041
- expand?: (props: {
2042
- expanded: boolean;
2043
- expandProps?: ExpandControllerProps;
2044
- toggleExpanded: () => void;
2045
- item: Item;
2046
- value: any;
2047
- }) => any;
2048
- /**
2049
- * Replaces the default checkbox/radio in the selection cell for each row.
2050
- * @param item - The raw item for this row.
2051
- * @param value - The resolved value for this row.
2052
- * @param checked - Whether this row is currently selected.
2053
- * @param disabled - Whether this row's selection control is disabled.
2054
- * @param inputName - The `name` attribute for the selection input.
2055
- * @param readonly - Whether the selection control is read-only.
2056
- */
2057
- select?: (props: {
2058
- item: Item;
2059
- value: any;
2060
- checked: boolean;
2061
- disabled: boolean;
2062
- inputName: string;
2063
- readonly: boolean;
2064
- }) => any;
2065
- } & {
2066
- /**
2067
- * Named slot for a specific column cell (`col-key`) or column header (`header:col-key`).
2068
- * @param classes - CSS classes computed for the cell.
2069
- * @param content - The formatted cell content.
2070
- * @param expanded - Whether this row is currently expanded (body slots only).
2071
- * @param expandProps - Bind these onto an expand-toggle button (body slots only).
2072
- * @param toggleExpanded - Toggles the expanded state for this row (body slots only).
2073
- * @param item - The raw item for this row (body slots only).
2074
- * @param value - The resolved row value (body slots only).
2075
- * @param items - The full items list (header slots only).
2076
- * @param label - The column label (header slots only).
2077
- */
2078
- [key: string]: (props: {
2079
- classes?: Classes[];
2080
- content?: any;
2081
- expanded?: boolean;
2082
- expandProps?: ExpandControllerProps;
2083
- toggleExpanded?: () => void;
2084
- item: Item;
2085
- value?: any;
2086
- items?: Item[];
2087
- label?: string;
2088
- }) => any;
2089
- };
1441
+ // Primary tint for the open row — selection chrome stays on content, not hover.
1442
+ const rowClass = (invoice: Invoice) =>
1443
+ invoice.id === highlighted.value
1444
+ ? 'cursor-pointer bg-[color:color-mix(in_oklab,var(--bb-primary)_8%,var(--bb-panel))]'
1445
+ : 'cursor-pointer';
1446
+ </script>
2090
1447
  ```
2091
1448
 
2092
- ## Styles
2093
-
2094
- ```css
2095
- .bb-table {
2096
- --padding-x: 16px;
2097
- --padding-y: 8px;
2098
- --actions-spacing: 8px;
2099
- /* Raw structural edge offset measured by the component (the parent's
2100
- select/actions track width). Default 0 keeps every calc at its base. */
2101
- --bb-table-offset-start: 0px;
2102
- --bb-table-offset-end: 0px;
2103
- /* Routing of that offset. By default it is "internal": added to the
2104
- first/last cell padding and folded into the first/last column width.
2105
- Swap internal↔external (set internal to 0 and external to the raw offset)
2106
- to pad the whole table instead of the edge cells. */
2107
- --bb-table-offset-internal-start: var(--bb-table-offset-start);
2108
- --bb-table-offset-internal-end: var(--bb-table-offset-end);
2109
- --bb-table-offset-external-start: 0px;
2110
- --bb-table-offset-external-end: 0px;
2111
- /* Slack the fill column adds to reach the parent's width. Reset per table so a
2112
- nested table never inherits an ancestor's value through the cascade. */
2113
- --bb-table-fill: 0px;
2114
- border-collapse: separate;
2115
- border-spacing: 0;
2116
- display: grid;
2117
- /* Override the UA `min-inline-size: min-content` that a <fieldset> container
2118
- (rendered for selectable tables) carries — without this the container can't
2119
- shrink below the table's content width, so it overflows its parent and
2120
- pushes the page instead of scrolling inside its own `overflow-x: auto`. */
2121
- min-width: 0;
2122
- overflow-x: auto;
2123
- padding-inline-end: var(--bb-table-offset-external-end);
2124
- padding-inline-start: var(--bb-table-offset-external-start);
2125
-
2126
- position: relative;
2127
-
2128
- &--align {
2129
- &-left {
2130
- text-align: left;
2131
- }
2132
- &-center {
2133
- text-align: center;
2134
- }
2135
- &-right {
2136
- text-align: right;
2137
- }
2138
- }
2139
-
2140
- &--compact {
2141
- --padding-x: 8px;
2142
- --padding-y: 4px;
2143
- }
1449
+ ### Keyboard navigation and row events
2144
1450
 
2145
- &--fixed {
2146
- > table {
2147
- table-layout: fixed;
2148
- width: 100%;
1451
+ Add `keyboard-navigation` to make the body a single tab stop with a roving
1452
+ tabindex: ArrowUp/ArrowDown move the focused row (Home/End jump), and
1453
+ Enter/Space activate it through the same path as a mouse click — highlight
1454
+ toggle plus `click:row`. Focus and highlight stay independent: arrowing over
1455
+ rows opens nothing until the user commits. A mouse click re-anchors arrow
1456
+ navigation, focused rows show the standard focus ring, and keys pressed inside
1457
+ interactive descendants (or a nested table) keep their own semantics.
2149
1458
 
2150
- thead {
2151
- .bb-table-header--actions {
2152
- width: auto;
2153
- }
2154
- }
2155
- }
2156
- }
2157
-
2158
- &--fixed-header {
2159
- table {
2160
- thead {
2161
- th {
2162
- position: sticky;
2163
- top: 0;
2164
- z-index: 1;
2165
- }
2166
- th[style*='sticky'] {
2167
- position: sticky;
2168
- top: 0;
2169
- z-index: 2;
2170
- }
2171
- }
2172
- }
2173
- }
1459
+ **Arrow through the audit log, Enter opens**
2174
1460
 
2175
- &--loading {
2176
- }
1461
+ ```vue
1462
+ <template>
1463
+ <div class="flex max-w-2xl flex-col gap-1.5">
1464
+ <!-- `keyboard-navigation` makes the body one tab stop: ↑/↓ move the
1465
+ focused row, Home/End jump, Enter/Space open it (same path as a
1466
+ click, so it toggles `highlighted` and emits `click:row`). -->
1467
+ <div
1468
+ class="overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
1469
+ >
1470
+ <BbTable
1471
+ v-model:highlighted="openEventId"
1472
+ caption="Audit log"
1473
+ :columns="columns"
1474
+ compact
1475
+ item-value="id"
1476
+ :items="events"
1477
+ keyboard-navigation
1478
+ :row-class="rowClass"
1479
+ />
1480
+ </div>
1481
+ <p class="text-xs text-gray-500">
1482
+ Tab into the table, move with the arrow keys, press Enter to open a row.
1483
+ Open event:
1484
+ <span class="font-medium text-[color:var(--bb-text)]">{{
1485
+ openEvent ? `${openEvent.actor} — ${openEvent.action}` : 'none'
1486
+ }}</span>
1487
+ </p>
1488
+ </div>
1489
+ </template>
1490
+ <script setup lang="ts">
1491
+ import { computed, ref } from 'vue';
1492
+ import { BbTable } from 'bitboss-ui';
1493
+ import type { BbTableColumn } from 'bitboss-ui';
2177
1494
 
2178
- &--empty {
2179
- }
1495
+ type AuditEvent = { id: string; time: string; actor: string; action: string };
2180
1496
 
2181
- &--selectable {
2182
- tr {
2183
- > .bb-table-data__cell--select {
2184
- text-align: center;
2185
- width: 0px;
2186
- }
2187
- }
2188
- }
1497
+ const columns: Array<BbTableColumn<AuditEvent>> = [
1498
+ { key: 'time', label: 'Time', width: 70 },
1499
+ { key: 'actor', label: 'Actor' },
1500
+ { key: 'action', label: 'Action' },
1501
+ ];
2189
1502
 
2190
- legend {
2191
- }
1503
+ const events: AuditEvent[] = [
1504
+ { id: 'e1', time: '09:12', actor: 'ada@acme.io', action: 'Created API key' },
1505
+ {
1506
+ id: 'e2',
1507
+ time: '09:47',
1508
+ actor: 'grace@acme.io',
1509
+ action: 'Changed billing plan',
1510
+ },
1511
+ {
1512
+ id: 'e3',
1513
+ time: '10:03',
1514
+ actor: 'ken@acme.io',
1515
+ action: 'Invited 2 members',
1516
+ },
1517
+ { id: 'e4', time: '10:31', actor: 'ada@acme.io', action: 'Rotated secrets' },
1518
+ ];
2192
1519
 
2193
- table {
2194
- min-width: 100%;
2195
- }
1520
+ const openEventId = ref<string | null>(null);
2196
1521
 
2197
- /* A nested table inheriting its parent's widths sizes to the sum of its
2198
- columns instead of filling its container, so fixed layout cannot stretch
2199
- them out of alignment. Declared after `--fixed` and the base table rule so
2200
- it overrides their width/min-width at equal specificity. */
2201
- &--inherit-widths {
2202
- /* Never grow past the parent's expand cell: a child with more columns
2203
- than the parent overflows the cell, so cap the container here and let its
2204
- own `overflow-x: auto` scroll the surplus columns inside the expand row
2205
- rather than stretching the ancestor. */
2206
- max-width: 100%;
2207
- min-width: 0;
2208
-
2209
- table {
2210
- min-width: 0;
2211
- /* A *definite* width (not `max-content`) so `table-layout: fixed` stays
2212
- content-independent: inherited columns keep their colgroup widths and
2213
- align with the parent instead of growing to fit content. The fill
2214
- column is `auto`, so fixed layout hands it the leftover; when columns
2215
- overflow, the table grows past 100% and scrolls. */
2216
- width: 100%;
2217
- }
2218
- }
1522
+ const openEvent = computed(() =>
1523
+ events.find((event) => event.id === openEventId.value)
1524
+ );
2219
1525
 
2220
- /* Declared after the base `table { min-width: 100% }` rule so it wins the
2221
- cascade at equal specificity. The probe publishes the natural content width
2222
- of all columns; the table fills its container freely but never compresses
2223
- below that floor — the container's `overflow-x: auto` then shows a
2224
- scrollbar instead of squishing content. */
2225
- &--fixed {
2226
- table {
2227
- min-width: var(--bb-table-natural-width, 0px);
2228
- }
2229
- }
1526
+ // Highlight ships unstyled paint the open row through the public row-class API.
1527
+ const rowClass = (event: AuditEvent) =>
1528
+ event.id === openEventId.value
1529
+ ? 'bg-[color:color-mix(in_oklab,var(--bb-primary)_8%,var(--bb-panel))]'
1530
+ : '';
1531
+ </script>
1532
+ ```
2230
1533
 
2231
- .bb-table-caption {
2232
- }
1534
+ Row events: `click:row`, `dblclick:row` and `contextmenu:row` fire with
1535
+ `(event, row, selected)`. Note that `row` is the table's internal row wrapper,
1536
+ not your item: read the raw record from `row.item` and its resolved value from
1537
+ `row.value`. Reserve these events for side effects that are not navigation —
1538
+ logging, opening a drawer (though `v-model:highlighted` usually covers that
1539
+ without any handler).
2233
1540
 
2234
- &-header-row {
2235
- }
1541
+ ### Opening a record
2236
1542
 
2237
- &-header {
2238
- border-bottom: 1px solid var(--bb-border);
2239
- color: color-mix(in srgb, var(--bb-text), transparent 40%);
2240
- font-size: 14px;
2241
- font-weight: 500;
2242
- padding: var(--padding-y) var(--padding-x);
1543
+ Row navigation goes through a real link component, never a row `@click`
1544
+ handler. Render a `BbButton` in a cell or the actions column with `href` (or
1545
+ `to` in a router app): the user gets a proper anchor, middle-click and
1546
+ open-in-new-tab for free.
2243
1547
 
2244
- &--center {
2245
- text-align: center;
2246
- }
1548
+ **A per-row link via BbButton**
2247
1549
 
2248
- &--right {
2249
- text-align: right;
2250
- }
1550
+ ```vue
1551
+ <template>
1552
+ <!-- Row-level navigation is offloaded to BbButton: `href` (or `to` in a
1553
+ router app) makes it a real link, not a JS click handler. -->
1554
+ <div
1555
+ class="max-w-2xl overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
1556
+ >
1557
+ <BbTable
1558
+ actions
1559
+ actions-text="Actions"
1560
+ caption="Invoices"
1561
+ :columns="columns"
1562
+ compact
1563
+ item-value="id"
1564
+ :items="invoices"
1565
+ >
1566
+ <template #actions="{ item }">
1567
+ <BbButton
1568
+ append:icon="lucide:arrow-right"
1569
+ :href="`#invoice-${item.id}`"
1570
+ size="xs"
1571
+ variant="outline"
1572
+ >
1573
+ Open
1574
+ </BbButton>
1575
+ </template>
1576
+ </BbTable>
1577
+ </div>
1578
+ </template>
1579
+ <script setup lang="ts">
1580
+ import { BbButton, BbTable } from 'bitboss-ui';
1581
+ import type { BbTableColumn } from 'bitboss-ui';
2251
1582
 
2252
- &--select {
2253
- }
1583
+ type Invoice = { id: string; number: string; client: string; amount: string };
2254
1584
 
2255
- &--actions {
2256
- width: 0;
1585
+ const columns: Array<BbTableColumn<Invoice>> = [
1586
+ { key: 'number', label: 'Invoice', width: 110 },
1587
+ { key: 'client', label: 'Client' },
1588
+ { key: 'amount', label: 'Amount', align: 'right', width: 100 },
1589
+ ];
2257
1590
 
2258
- .bb-table-header__content {
2259
- }
2260
- }
1591
+ const invoices: Invoice[] = [
1592
+ {
1593
+ id: '1041',
1594
+ number: 'INV-1041',
1595
+ client: 'Northwind Traders',
1596
+ amount: '€4,200',
1597
+ },
1598
+ { id: '1042', number: 'INV-1042', client: 'Globex', amount: '€980' },
1599
+ { id: '1043', number: 'INV-1043', client: 'Initech', amount: '€15,600' },
1600
+ ];
1601
+ </script>
1602
+ ```
2261
1603
 
2262
- &__content {
2263
- }
2264
- }
1604
+ In a router or Inertia app the same button carries the platform's navigation
1605
+ instead of an inert `href`:
2265
1606
 
2266
- tbody {
2267
- tr {
2268
- &:not(:last-child) {
2269
- border-bottom: 1px solid var(--bb-border);
2270
- }
2271
- td {
2272
- color: var(--bb-text);
2273
- font-size: 14px;
2274
- font-weight: 400;
2275
- height: var(--bb-table-cell-h);
2276
- padding: var(--padding-y) var(--padding-x);
2277
- }
2278
-
2279
- td.bb-table-expand__cell {
2280
- /* Collapse to the content's height instead of inheriting the data-row
2281
- cell height, so an expand row with no slot content takes no space. */
2282
- height: auto;
2283
- padding: 0;
2284
-
2285
- &:empty {
2286
- display: none;
2287
- }
2288
- }
2289
- }
2290
- }
1607
+ ```vue
1608
+ <!-- vue-router / Nuxt -->
1609
+ <template #actions="{ item }">
1610
+ <BbButton :to="{ name: 'invoices.show', params: { id: item.id } }" size="xs">
1611
+ Open
1612
+ </BbButton>
1613
+ </template>
1614
+ ```
2291
1615
 
2292
- /* Apply the structural edge offsets to the first/last *data* column. They
2293
- must land on the data cells, not the absolute first/last child: a select
2294
- or actions column would otherwise absorb the fill and leave the data
2295
- columns misaligned with the parent (and bloat the actions cell).
2296
-
2297
- The offset is clamped at 0 here: a positive fill widens the cell and is
2298
- reserved as extra edge padding, but a *negative* offset (a select/actions
2299
- column reclaimed from this column's width) must not eat into the normal
2300
- `--padding-x` — that only shrinks the column width, not its inner padding. */
2301
- thead tr > th.bb-table-header--edge-start,
2302
- tbody tr > td.bb-table-data__cell--edge-start {
2303
- padding-inline-start: calc(
2304
- var(--padding-x) + max(0px, var(--bb-table-offset-internal-start))
2305
- );
2306
- }
1616
+ ```vue
1617
+ <!-- Inertia -->
1618
+ <template #actions="{ item }">
1619
+ <BbButton :href="`/invoices/${item.id}`" size="xs">Open</BbButton>
1620
+ </template>
1621
+ ```
2307
1622
 
2308
- thead tr > th.bb-table-header--edge-end,
2309
- tbody tr > td.bb-table-data__cell--edge-end {
2310
- padding-inline-end: calc(
2311
- var(--padding-x) + max(0px, var(--bb-table-offset-internal-end))
2312
- );
2313
- }
1623
+ ### Inline editing
2314
1624
 
2315
- &.bb-table--loading {
2316
- .bb-table-loading__row {
2317
- .bb-table-loading__cell {
2318
- padding: 0 var(--padding-x) !important;
2319
- vertical-align: top;
2320
- }
2321
- }
1625
+ Cells can edit in place — no edit page, no modal. Two commit grammars, one
1626
+ per control kind:
2322
1627
 
2323
- .bb-table-loading__row--sr-only {
2324
- .bb-table-loading__cell {
2325
- padding: 0 !important;
2326
- border: none;
2327
- height: 0;
2328
- overflow: hidden;
2329
- }
2330
- }
2331
- }
1628
+ - **Typed cells** (text, number) bind `:model-value` down and commit on
1629
+ `@change` (blur/Enter) — never `v-model`, which fires per keystroke and
1630
+ turns an autosave into one request per character.
1631
+ - **Option-backed cells** are a badge acting as the trigger of a
1632
+ `BbSelectPopover`: the pick IS the commit. The options resolve once at page
1633
+ level as an array (never a per-row provider — see the BbSelectPopover
1634
+ guide's table section), the badge carries an internal trailing chevron, and
1635
+ while the save runs the badge's `loading` prop swaps the spinner into the
1636
+ chevron's place — same size, nothing shifts.
2332
1637
 
2333
- &-skeleton {
2334
- &__row {
2335
- }
1638
+ The deeper question — who owns the uncommitted value (a draft map with an
1639
+ explicit Save, or an optimistic autosave overlay, or the rows themselves —
1640
+ never) — is the [Inline Edit Playbook](./guides/inline-edit-playbook.md).
1641
+ This example uses the autosave grammar: per-field optimistic overlay,
1642
+ rollback on failure, and overlay retirement inside the items function.
2336
1643
 
2337
- &__cell {
2338
- height: var(--bb-table-cell-h);
2339
- overflow: hidden;
2340
- padding: var(--padding-y) var(--padding-x);
2341
- vertical-align: middle;
2342
-
2343
- &--select,
2344
- &--actions {
2345
- text-align: center;
2346
- width: 0;
2347
- }
2348
- }
1644
+ **Autosave cells: commit on blur, spinner in the chevron**
2349
1645
 
2350
- &__cell-content {
2351
- display: flex;
2352
- flex-direction: column;
2353
- gap: 6px;
2354
- justify-content: center;
2355
- max-height: calc(var(--bb-table-cell-h) - var(--padding-y) * 2);
2356
- min-width: 96px;
2357
- }
1646
+ ```vue
1647
+ <template>
1648
+ <div class="flex max-w-2xl flex-col gap-1.5">
1649
+ <div
1650
+ class="overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
1651
+ >
1652
+ <BbTable
1653
+ caption="Sprint tasks — every cell saves itself"
1654
+ :columns="columns"
1655
+ compact
1656
+ :dependencies="[rev]"
1657
+ item-value="id"
1658
+ :items="loadTasks"
1659
+ >
1660
+ <!-- Typed cell: bind down, commit on @change (blur/Enter) — never
1661
+ v-model, which fires per keystroke (one request per character). -->
1662
+ <template #title="{ item }">
1663
+ <BbTextInput
1664
+ compact
1665
+ hide-label
1666
+ label="Title"
1667
+ :model-value="valueOf(item, 'title')"
1668
+ @change="onTitleChange(item, $event)"
1669
+ />
1670
+ </template>
1671
+
1672
+ <!-- Option cell: the badge IS the trigger; the pick IS the commit.
1673
+ One page-level `statuses` array shared by every row (never a
1674
+ per-row provider). While the save runs, `loading` swaps the
1675
+ spinner into the chevron's place. -->
1676
+ <template #status="{ item }">
1677
+ <BbSelectPopover
1678
+ compact
1679
+ disable-writing
1680
+ item-text="label"
1681
+ item-value="value"
1682
+ :items="statuses"
1683
+ :model-value="valueOf(item, 'status')"
1684
+ width="170px"
1685
+ @update:model-value="persist(item, 'status', $event as Status)"
1686
+ >
1687
+ <template #activator="{ props }">
1688
+ <BbBadge
1689
+ append:icon="lucide:chevron-down"
1690
+ :loading="pending[item.id]?.status !== undefined"
1691
+ size="lg"
1692
+ :variant="VARIANT[valueOf(item, 'status')]"
1693
+ >
1694
+ <BbBadgeButton v-bind="props">{{
1695
+ labelFor(valueOf(item, 'status'))
1696
+ }}</BbBadgeButton>
1697
+ </BbBadge>
1698
+ </template>
1699
+ </BbSelectPopover>
1700
+ </template>
1701
+ </BbTable>
1702
+ </div>
1703
+
1704
+ <p v-if="lastFailure" class="text-xs text-[color:var(--bb-danger)]">
1705
+ {{ lastFailure }}
1706
+ </p>
1707
+ <p class="text-xs text-gray-500">
1708
+ No Save button anywhere — edits persist per field, optimistically.
1709
+ BB-121's save always fails and rolls back.
1710
+ </p>
1711
+ </div>
1712
+ </template>
1713
+ <script setup lang="ts">
1714
+ import { reactive, ref } from 'vue';
1715
+ import {
1716
+ BbBadge,
1717
+ BbBadgeButton,
1718
+ BbSelectPopover,
1719
+ BbTable,
1720
+ BbTextInput,
1721
+ } from 'bitboss-ui';
1722
+ import type { BadgeVariantType, BbTableColumn } from 'bitboss-ui';
1723
+
1724
+ type Status = 'open' | 'in-progress' | 'blocked' | 'done';
1725
+ type Task = { id: string; ref: string; title: string; status: Status };
1726
+ type EditableKey = 'title' | 'status';
1727
+ type Patch = Partial<Pick<Task, EditableKey>>;
1728
+
1729
+ const statuses: Array<{ value: Status; label: string }> = [
1730
+ { value: 'open', label: 'Open' },
1731
+ { value: 'in-progress', label: 'In progress' },
1732
+ { value: 'blocked', label: 'Blocked' },
1733
+ { value: 'done', label: 'Done' },
1734
+ ];
1735
+
1736
+ const VARIANT: Record<Status, BadgeVariantType> = {
1737
+ open: 'soft-gray',
1738
+ 'in-progress': 'soft-blue',
1739
+ blocked: 'soft-red',
1740
+ done: 'soft-green',
1741
+ };
2358
1742
 
2359
- &__line {
2360
- background: linear-gradient(
2361
- 90deg,
2362
- var(--bb-border) 0%,
2363
- var(--bb-border) 35%,
2364
- color-mix(in srgb, var(--bb-border) 60%, white) 50%,
2365
- var(--bb-border) 65%,
2366
- var(--bb-border) 100%
2367
- );
2368
- background-size: 200% 100%;
2369
- border-radius: 6px;
2370
- flex-shrink: 0;
2371
- height: 10px;
2372
- animation: bb-table-skeleton-shimmer 1.8s ease-in-out infinite;
2373
- }
1743
+ const columns: Array<BbTableColumn<Task>> = [
1744
+ { key: 'ref', label: 'Ref', width: 90 },
1745
+ { key: 'title', label: 'Title' },
1746
+ { key: 'status', label: 'Status', width: 140 },
1747
+ ];
2374
1748
 
2375
- @media (prefers-reduced-motion: reduce) {
2376
- &__line {
2377
- animation: none;
2378
- background: var(--bb-border);
2379
- }
2380
- }
1749
+ /* ---- Fake server (deterministic: BB-121 always rejects) ---------------- */
2381
1750
 
2382
- &__placeholder {
2383
- background-color: var(--bb-border);
2384
- border-radius: 6px;
2385
- flex-shrink: 0;
2386
- height: 22px;
2387
- margin: 0 auto;
2388
- min-width: 22px;
2389
- width: 22px;
2390
- }
1751
+ let server: Task[] = [
1752
+ {
1753
+ id: 't-1',
1754
+ ref: 'BB-104',
1755
+ title: 'Popover drops focus on Esc',
1756
+ status: 'open',
1757
+ },
1758
+ {
1759
+ id: 't-2',
1760
+ ref: 'BB-118',
1761
+ title: 'Header misaligns at 320px',
1762
+ status: 'in-progress',
1763
+ },
1764
+ {
1765
+ id: 't-3',
1766
+ ref: 'BB-121',
1767
+ title: 'Token audit for dark mode',
1768
+ status: 'blocked',
1769
+ },
1770
+ {
1771
+ id: 't-4',
1772
+ ref: 'BB-130',
1773
+ title: 'Ship the migration guide',
1774
+ status: 'done',
1775
+ },
1776
+ ];
2391
1777
 
2392
- &__avatar {
2393
- aspect-ratio: 1;
2394
- background-color: var(--bb-border);
2395
- border-radius: 50%;
2396
- flex-shrink: 0;
2397
- height: calc(var(--bb-table-cell-h) - var(--padding-y) * 2);
2398
- min-width: calc(var(--bb-table-cell-h) - var(--padding-y) * 2);
2399
- width: calc(var(--bb-table-cell-h) - var(--padding-y) * 2);
2400
- }
1778
+ const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
2401
1779
 
2402
- &__avatar-text {
2403
- align-items: center;
2404
- display: flex;
2405
- gap: 12px;
2406
- max-height: calc(var(--bb-table-cell-h) - var(--padding-y) * 2);
2407
- min-width: 140px;
2408
- }
1780
+ const api = {
1781
+ list: async (): Promise<Task[]> => {
1782
+ await wait(150);
1783
+ return server.map((task) => ({ ...task }));
1784
+ },
1785
+ patch: async (id: string, patch: Patch): Promise<void> => {
1786
+ await wait(400);
1787
+ const row = server.find((task) => task.id === id);
1788
+ if (!row) return;
1789
+ if (row.ref === 'BB-121') throw new Error('save rejected');
1790
+ server = server.map((task) =>
1791
+ task.id === id ? { ...task, ...patch } : task
1792
+ );
1793
+ },
1794
+ };
2409
1795
 
2410
- &__avatar-text .bb-table-skeleton__avatar {
2411
- width: calc(var(--bb-table-cell-h) - var(--padding-y) * 2);
2412
- }
1796
+ /* ---- Autosave state (the Inline Edit Playbook's flagship pattern) ------ */
2413
1797
 
2414
- &__avatar-text .bb-table-skeleton__cell-content {
2415
- flex: 1;
2416
- min-width: 0;
2417
- }
1798
+ const rev = ref(0);
1799
+ /** Optimistic values, per row per field — two fields can be in flight independently. */
1800
+ const pending = reactive<Record<string, Patch>>({});
1801
+ const lastFailure = ref('');
2418
1802
 
2419
- &__image {
2420
- aspect-ratio: 4 / 3;
2421
- background-color: var(--bb-border);
2422
- border-radius: 8px;
2423
- height: calc(var(--bb-table-cell-h) - var(--padding-y) * 2);
2424
- min-width: 64px;
2425
- width: auto;
2426
- }
1803
+ // `key in` rather than `??` so an overlay holding a committed empty/null
1804
+ // value still wins over the row (matters once a field is clearable).
1805
+ const valueOf = <K extends EditableKey>(item: Task, key: K): Task[K] => {
1806
+ const patch = pending[item.id];
1807
+ return patch && key in patch ? (patch[key] as Task[K]) : item[key];
1808
+ };
2427
1809
 
2428
- &__badge {
2429
- background-color: var(--bb-border);
2430
- border-radius: 999px;
2431
- flex-shrink: 0;
2432
- height: 26px;
2433
- min-width: 72px;
2434
- width: 88px;
1810
+ /**
1811
+ * The overlay is retired HERE, in the fetch, and only for fields the response
1812
+ * confirms — never when the save resolves. Between the save landing and the
1813
+ * reload arriving the table still holds the OLD row; clearing early would
1814
+ * flash the cell new → old → new for the whole reload.
1815
+ */
1816
+ const loadTasks = async (): Promise<Task[]> => {
1817
+ const rows = await api.list();
1818
+ for (const row of rows) {
1819
+ const patch = pending[row.id];
1820
+ if (!patch) continue;
1821
+ for (const key of Object.keys(patch) as EditableKey[]) {
1822
+ if (patch[key] === row[key]) delete patch[key];
2435
1823
  }
1824
+ if (!Object.keys(patch).length) delete pending[row.id];
2436
1825
  }
1826
+ return rows;
1827
+ };
2437
1828
 
2438
- @keyframes bb-table-skeleton-shimmer {
2439
- 0% {
2440
- background-position: 200% 0;
2441
- }
2442
- 100% {
2443
- background-position: -200% 0;
1829
+ const persist = async <K extends EditableKey>(
1830
+ item: Task,
1831
+ key: K,
1832
+ next: Task[K]
1833
+ ) => {
1834
+ if (next === item[key]) return; // no-op commit
1835
+ (pending[item.id] ??= {})[key] = next; // optimistic: the UI moves now
1836
+ try {
1837
+ await api.patch(item.id, { [key]: next });
1838
+ rev.value += 1; // reload; loadTasks retires the overlay once it can
1839
+ } catch {
1840
+ // Rollback: drop just this field — the cell reverts to the row's value.
1841
+ delete pending[item.id]?.[key];
1842
+ if (pending[item.id] && !Object.keys(pending[item.id]).length) {
1843
+ delete pending[item.id];
2444
1844
  }
1845
+ lastFailure.value = `${item.ref}: ${key} save failed — rolled back.`;
2445
1846
  }
1847
+ };
2446
1848
 
2447
- &-no-data {
2448
- &__row {
2449
- }
2450
- &__cell {
2451
- }
2452
- &__text {
2453
- }
2454
- }
1849
+ const onTitleChange = (item: Task, event: Event) =>
1850
+ persist(item, 'title', (event.target as HTMLInputElement).value);
2455
1851
 
2456
- &-data {
2457
- &__row {
2458
- }
2459
- &__cell {
2460
- &--center {
2461
- text-align: center;
2462
- }
1852
+ const labelFor = (status: Status) =>
1853
+ statuses.find((option) => option.value === status)?.label ?? '—';
1854
+ </script>
2463
1855
 
2464
- &--right {
2465
- text-align: right;
2466
- }
1856
+ <!-- Soft status palette — canonical source: ai/guides/design-language.md § Using color.
1857
+ Non-scoped so variant classes on the child badge receive the accents. -->
1858
+ <style>
1859
+ .bb-badge.bb-badge--soft-gray {
1860
+ --accent: #64748b;
1861
+ }
1862
+ .bb-badge.bb-badge--soft-blue {
1863
+ --accent: #2563eb;
1864
+ }
1865
+ .bb-badge.bb-badge--soft-green {
1866
+ --accent: #16a34a;
1867
+ }
1868
+ .bb-badge.bb-badge--soft-red {
1869
+ --accent: #dc2626;
1870
+ }
1871
+ .bb-badge.bb-badge--soft-gray,
1872
+ .bb-badge.bb-badge--soft-blue,
1873
+ .bb-badge.bb-badge--soft-green,
1874
+ .bb-badge.bb-badge--soft-red {
1875
+ --bg: color-mix(in oklab, var(--accent) 15%, var(--bb-panel));
1876
+ --color: color-mix(in oklab, var(--accent) 80%, var(--bb-text));
1877
+ --border-width: 1px;
1878
+ --border-color: color-mix(in oklab, var(--accent) 25%, var(--bb-panel));
1879
+ --ring: color-mix(in oklab, var(--accent) 45%, transparent);
1880
+ }
1881
+ </style>
1882
+ ```
2467
1883
 
2468
- &--select {
2469
- }
2470
- }
2471
- }
1884
+ The status colors are the registered `soft-*` badge variants: the plugin's
1885
+ `badgeVariants` option makes the names type-check, and the example carries
1886
+ their CSS in its own style block — the sanctioned consumer path. The
1887
+ canonical palette lives in the
1888
+ [design language guide](./guides/design-language.md).
2472
1889
 
2473
- &-data-row {
2474
- }
1890
+ ### Dense and wide tables
2475
1891
 
2476
- &__cell {
2477
- &--select {
2478
- }
1892
+ `compact` tightens row padding. `fixed-headers` makes the header stick to the
1893
+ top of the nearest scroll container. `fixed` switches to an equal-width fixed
1894
+ layout. `fixed-columns` pins columns while the rest scroll horizontally: an
1895
+ array of column indices (left-pinned) or `{ index, position: 'left'|'right' }`
1896
+ objects; indices count rendered columns (a selection column is index 0), and
1897
+ an out-of-range index logs a console error and is skipped. When content can't
1898
+ fit, the table keeps every column at its measured minimum and overflows its
1899
+ container instead of clipping — wrap it in an `overflow-auto` element and the
1900
+ container scrolls.
2479
1901
 
2480
- &--actions {
2481
- white-space: nowrap;
2482
- > *:not(:first-child) {
2483
- margin-left: var(--actions-spacing);
2484
- }
2485
- }
2486
- }
1902
+ **Compact rows with a sticky header**
2487
1903
 
2488
- &-check,
2489
- &-radio {
2490
- &__label {
2491
- cursor: pointer;
2492
- display: inline-block;
2493
- transform: translateY(2px);
2494
- user-select: none;
2495
-
2496
- &.bb-table {
2497
- &-check,
2498
- &-radio {
2499
- &__label--disabled {
2500
- cursor: default;
2501
- visibility: hidden;
2502
- }
2503
- }
2504
- }
2505
-
2506
- &.bb-table-radio__label--disabled {
2507
- cursor: default;
2508
- }
2509
-
2510
- &-text {
2511
- }
2512
- }
2513
- }
2514
- }
1904
+ ```vue
1905
+ <div
1906
+ class="max-h-52 max-w-2xl overflow-auto rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
1907
+ >
1908
+ <BbTable
1909
+ caption="Server fleet"
1910
+ compact
1911
+ fixed-headers
1912
+ :columns="[
1913
+ { key: 'host', label: 'Host' },
1914
+ { key: 'region', label: 'Region' },
1915
+ { key: 'cpu', label: 'CPU', align: 'right' },
1916
+ { key: 'memory', label: 'Memory', align: 'right' },
1917
+ { key: 'status', label: 'Status' },
1918
+ ]"
1919
+ :items="[
1920
+ { host: 'web-01', region: 'eu-west-1', cpu: '38%', memory: '61%', status: 'Healthy' },
1921
+ { host: 'web-02', region: 'eu-west-1', cpu: '52%', memory: '70%', status: 'Healthy' },
1922
+ { host: 'api-01', region: 'us-east-2', cpu: '74%', memory: '80%', status: 'Healthy' },
1923
+ { host: 'db-01', region: 'us-east-2', cpu: '81%', memory: '88%', status: 'Degraded' },
1924
+ { host: 'cache-01', region: 'ap-south-1', cpu: '22%', memory: '35%', status: 'Healthy' },
1925
+ ]"
1926
+ />
1927
+ </div>
2515
1928
  ```
1929
+
1930
+ ### Config and platform
1931
+
1932
+ - **Locale.** The plugin's `locale` (`it`/`en`) drives the built-in strings:
1933
+ the no-data text, the select-all label, the single-select header text, and
1934
+ the "Select" prefix of each row's accessible selection label. Override any
1935
+ of them per table via `no-data-text` / `select-all-label` / `select-text`.
1936
+ - **`actions-text` is localized** like the other built-in strings (`it` →
1937
+ `Azioni`, `en` → `Actions`). Seeing `Azioni` in an English app means the
1938
+ plugin is still on the default locale, which is `it` — set the plugin's
1939
+ `locale` option, or override per table with `actions-text` (it is visually
1940
+ hidden in the header but read by screen readers).
1941
+ - **SSR / Nuxt.** A provider `items` function participates in
1942
+ `onServerPrefetch`, so the first page can render server-side. The table
1943
+ context (and thus `table-id` pagination wiring) is client-side only —
1944
+ hydration-time onward.
1945
+ - **Forms.** With `name`, the selection submits as hidden inputs — no JS
1946
+ needed between the table and a plain `<form>` POST (Inertia included).
1947
+
1948
+ ### Works well with
1949
+
1950
+ - `BbPagination` — shared page ref, or zero-wiring via `id` + `table-id`.
1951
+ - `useBbTableContext` (`./composables/useBbTableContext.md`) — bulk-action
1952
+ toolbars, detail panels, "reset sort" buttons living outside the table.
1953
+ - `BbButton` — per-row links (`href`/`to`) in cells or `#actions`;
1954
+ `BbDropdown` for an overflow menu of row actions.
1955
+ - `BbBadge` / `BbAvatar` — status and identity cells via slots.
1956
+ - `BbTextInput` / `BbSelect` filters feeding `dependencies`.
1957
+ - `useConfirm` + `useToast` — destructive row actions with feedback; the full
1958
+ assembly is the records workspace recipe
1959
+ (`./recipes/vue/records-workspace.md`).
1960
+ - Inline editing — the ownership patterns are the
1961
+ [Inline Edit Playbook](./guides/inline-edit-playbook.md); the full autosave
1962
+ page is the inline-edit workspace recipe
1963
+ (`./recipes/vue/inline-edit-workspace.md`).
1964
+ - `BbTree` — when the data is genuinely recursive rather than rows + detail.
1965
+
1966
+ ### Gotchas and constraints
1967
+
1968
+ - Slot names are **lowercased**: a camelCase key like `issuedAt` yields
1969
+ `#issuedat`. Prefer snake_case keys.
1970
+ - `loading-text` has no default — an unset value announces nothing.
1971
+ - The `accessibleLabel` prop is currently ignored: row selection labels are
1972
+ always built from every cell's content. Rely on the default; don't expect a
1973
+ custom function to apply.
1974
+ - The select-all checkbox never renders checked, and enabling it does not
1975
+ re-emit an empty `modelValue` (see the select-all section) — in all-mode,
1976
+ read `select-all` + `unselected-items`, not `modelValue`.
1977
+ - Row event payloads are the internal row wrapper — your record is `row.item`.
1978
+ - A mistyped context/pagination `id` does not error; it silently syncs with
1979
+ nothing. If a toolbar "works" but the table never reacts, check the id.
1980
+ - In multiple mode `modelValue` must be an array and with
1981
+ `highlight-multiple`, `highlighted` must be one — both throw otherwise.
1982
+
1983
+ ### Anti-patterns
1984
+
1985
+ - Watching filters and refetching manually — list them in `dependencies`.
1986
+ - Expecting the table to sort, slice or fetch: it reports state; you apply it.
1987
+ - Hand-rolling selection pruning after a refetch — that's `enforce-coherence`
1988
+ (see Selection across refetches); reserve provider-side reconciliation for
1989
+ paginated tables, where the prop must stay off.
1990
+ - Pre-mapping items into `{ label, value }` — pass domain objects +
1991
+ `item-value`.
1992
+ - `@click:row` + `router.push` for navigation — render a real `BbButton` link.
1993
+ - Rebuilding selection controls in a cell slot — use `selectable`, the
1994
+ `#select` slot, or the scope's `toggleSelected`.
1995
+ - Mutating slot-scope flags (`expanded`, `selected`, `highlighted`) — they are
1996
+ one-way; call the paired `toggle*` callback.
1997
+ - Omitting `item-value` on tables with selection/expansion/highlight —
1998
+ object-identity matching breaks across refetches.
1999
+
2000
+ ### Accessibility
2001
+
2002
+ - Always give the table a `caption` describing its content. It is visually
2003
+ hidden by default (available to screen readers); set `display-caption` to
2004
+ show it as a visible title.
2005
+ - A selectable table is a `<fieldset>` — pass `legend`. Each selection control
2006
+ carries a visually-hidden label built from the row's cell contents.
2007
+ - Set `item-value` so selection, expansion and highlight track rows by stable
2008
+ identity rather than object reference.
2009
+ - Sortable headers set `aria-sort` and render real buttons; custom header
2010
+ slots should keep a button calling `toggleSort`.
2011
+ - When you paginate, pass `page` / `per-page` / `total-items` so the table
2012
+ announces the full result count (`aria-rowcount`) and each row's absolute
2013
+ index (`aria-rowindex`, expansion-aware), not just the current page.
2014
+ - The loading row is an `aria-live` status — always set `loading-text`.
2015
+ - The highlighted row carries `aria-current="true"`; add `keyboard-navigation`
2016
+ so row-level interaction is reachable without a mouse (roving tabindex,
2017
+ Arrow/Home/End, Enter/Space).
2018
+ - Keep row-level navigation in real `BbButton` links (`href` / `to`), never a
2019
+ bare `@click:row` — links are focusable and announced as links.
2020
+
2021
+ ## v-model
2022
+
2023
+ - `v-model:expandedItems` — type: `any[] \| undefined`
2024
+ - `v-model:highlighted` — type: `any`
2025
+ - `v-model` — type: `any`
2026
+ - `v-model:selectAll` — type: `boolean \| undefined`
2027
+ - `v-model:sort` — type: `BbTableSortEntry[] \| undefined`
2028
+ - `v-model:unselectedItems` — type: `any[] \| undefined`
2029
+
2030
+ ## Props
2031
+
2032
+ | Prop | Type | Default | Required | Description |
2033
+ | --- | --- | --- | --- | --- |
2034
+ | `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. |
2035
+ | `actions` | `boolean \| undefined` | | | Displays the actions column. |
2036
+ | `actionsText` | `string \| undefined` | | | Label used in the header of the actions column. |
2037
+ | `align` | `"left" \| "center" \| "right" \| undefined` | `"left"` | | Text alignment of the columns. |
2038
+ | `caption` | `string \| undefined` | | | Caption that describes the content of the table. Used for accessibility purposes. |
2039
+ | `columns` | `InternalColumn<T>[] \| undefined` | `[]` | | |
2040
+ | `compact` | `boolean \| undefined` | | | Adds a CSS class that applies a compact style to the component. |
2041
+ | `dependencies` | `unknown[] \| undefined` | `[]` | | Defines an array of dependencies that will trigger actions in the component upon change. |
2042
+ | `depsDebounceTime` | `number \| undefined` | `0` | | Timeout used to debounce response to changes to dependencies. |
2043
+ | `disabled` | `boolean \| undefined` | | | Disables the component |
2044
+ | `disableSelectAll` | `boolean \| undefined` | | | Hides the "Select all" checkbox that selectable multi-row tables display by default. |
2045
+ | `displayCaption` | `boolean \| undefined` | | | Boolean that defines whether to display or hide the caption. By default is true and the caption is hidden. |
2046
+ | `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… |
2047
+ | `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`/… |
2048
+ | `fixed` | `boolean \| undefined` | | | Defines the table layout as fixed making each column take up the same amount of space. |
2049
+ | `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. |
2050
+ | `fixedHeaders` | `boolean \| undefined` | | | Boolean that sets the headers as sticky to the top of the table. |
2051
+ | `headerRowClass` | `Classes \| undefined` | | | Defines the classes to be passed to the header `<tr>`. |
2052
+ | `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`) by default; an array of values with `highlightMultiple`. Purely mechanical: highlighted ro… |
2053
+ | `highlightMultiple` | `boolean \| undefined` | | | Allows highlighting multiple rows: `highlighted` becomes an array of item values instead of a single value. |
2054
+ | `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. |
2055
+ | `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… |
2056
+ | `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 … |
2057
+ | `items` | `Item[] \| ((prefill: boolean, modelValue?: any) => Item[] \| Promise<Item[]>)` | `[]` | yes | |
2058
+ | `itemValue` | `ItemAccessor<Item> \| undefined` | | | |
2059
+ | `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 … |
2060
+ | `legend` | `string \| undefined` | | | Text content of the legend. |
2061
+ | `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… |
2062
+ | `loadingText` | `string \| undefined` | | | String displayed while items are being loaded. |
2063
+ | `max` | `number \| undefined` | `Infinity` | | Maximum number of items that can be selected. |
2064
+ | `modelValue` | `any` | `[]` | | Used by v-model. Can be any serializable type. |
2065
+ | `multiple` | `boolean \| undefined` | `true` | | Allows the selection of multiple items. |
2066
+ | `name` | `string \| undefined` | | | Defines the name of the input. |
2067
+ | `noDataText` | `string \| undefined` | | | String displayed when there are no items to display. |
2068
+ | `page` | `number \| undefined` | | | Current page number starting from 1, used for accessibility purposes. |
2069
+ | `perPage` | `number \| undefined` | | | Number of items per page, used for accessibility purposes. |
2070
+ | `readonly` | `boolean \| undefined` | | | Sets the input in a readonly state. |
2071
+ | `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. |
2072
+ | `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. |
2073
+ | `selectAll` | `boolean \| undefined` | `false` | | Defines whether the "Select all" checkbox is checked. |
2074
+ | `selectAllLabel` | `string \| undefined` | | | Text of the label used by the "Select all" boolean. |
2075
+ | `selectText` | `string \| undefined` | | | Text of the header used for the radio inputs when the table is not `multiple`. |
2076
+ | `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… |
2077
+ | `tdClass` | `ColumnClasses<Item> \| undefined` | | | Defines the classes to be passed to the `<td>` elements. Can also be a function for dynamic values. |
2078
+ | `thClass` | `Classes \| undefined` | | | Defines the classes to be passed to the `<th>`. |
2079
+ | `totalItems` | `number \| undefined` | | | Total number of items there is. |
2080
+ | `totalPages` | `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… |
2081
+ | `unselectedItems` | `any[] \| undefined` | `[]` | | Items that are not selected. |
2082
+
2083
+ ## Events
2084
+
2085
+ - `click:row` — `(e: "click:row", event: MouseEvent, item: any, selected: boolean): void`
2086
+ - `contextmenu:row` — `(e: "contextmenu:row", event: MouseEvent, item: any, selected: boolean): void`
2087
+ - `dblclick:row` — `(e: "dblclick:row", event: MouseEvent, item: any, selected: boolean): void`
2088
+ - `item:selected` — `(e: "item:selected", value: any): void`
2089
+ - `item:unselected` — `(e: "item:unselected", value: any): void`
2090
+ - `update:expandedItems` — `(e: "update:expandedItems", value: any[]): void`
2091
+ - `update:highlighted` — `(e: "update:highlighted", value: any): void`
2092
+ - `update:modelValue` — `(e: "update:modelValue", value: any): void`
2093
+ - `update:selectAll` — `(e: "update:selectAll", value: boolean): void`
2094
+ - `update:sort` — `(e: "update:sort", value: BbTableSortEntry[]): void`
2095
+ - `update:unselectedItems` — `(e: "update:unselectedItems", value: any[]): void`
2096
+
2097
+ ## Slots
2098
+
2099
+ - `actions` — scope: `{ expanded?: boolean \| undefined; expandProps?: ExpandControllerProps \| undefined; toggleExpanded?: (() => void) \| undefined; highlighted?: boolean \| undefi…` — Content rendered in the actions cell for each row.
2100
+ - `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.
2101
+ - `header:actions` — scope: `{ text: string; }` — Replaces the default actions header cell content.
2102
+ - `header:select` — scope: `{ multiple: boolean; selectAll: boolean; text: string; }` — Replaces the default selection header cell content (checkbox or radio label).
2103
+ - `loading` — scope: `{ items: Item[]; }` — Content shown while table rows are loading (replaces the skeleton rows).
2104
+ - `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…
2105
+ - `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.
2106
+ - `tbody` — scope: `object` — Replaces the entire `<tbody>` element. Use when you need full control over the body markup.
2107
+ - `tfoot` — scope: `{ columnCount: number; }` — Replaces the entire `<tfoot>` element. Use when you need full control over the footer markup.
2108
+ - `thead` — scope: `object` — Replaces the entire `<thead>` element. Use when you need full control over the header markup.
2109
+
2110
+ ## See Also
2111
+
2112
+ - [BbTree](./BbTree.md) — Renders hierarchical expandable tree structures.
2113
+ - [BbPagination](./BbPagination.md) — Navigates paginated data sets by page index.