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