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