bitboss-ui 2.1.134 → 3.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +161 -59
- 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
|
@@ -0,0 +1,1847 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Upload Center (Nuxt)
|
|
3
|
+
summary: Full-anatomy expense-receipts upload center for Nuxt — multi-file dropzone into an inline-editable table (text, category popover, amount, date cells), per-file progress with inline retry, submit-all with count, bulk bottom panel, and a stacked receipt/merchant peek sidebar, filters URL-synced and drafts fetched with useAsyncData while the upload queue stays client-only, all through one coopting page context.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Recipe: Upload Center (Nuxt)
|
|
7
|
+
|
|
8
|
+
The Expensify-grade expense receipts submission page adapted for **Nuxt**:
|
|
9
|
+
the same multi-file dropzone feeding an inline-editable table, per-file
|
|
10
|
+
upload progress with inline retry, one-click category cells, bulk actions
|
|
11
|
+
in the floating bottom panel, and a stacked receipt/merchant peek sidebar —
|
|
12
|
+
but filters and the active tab live in the URL, the drafts page loads
|
|
13
|
+
through `useAsyncData` watching `route.query`, and the live upload queue
|
|
14
|
+
stays exactly what it is: client-only reactive state merged above the
|
|
15
|
+
fetched rows. Anatomy, components, the context shape, copy and data are
|
|
16
|
+
identical to the Vue recipe; only the data/URL/mutation layer changes.
|
|
17
|
+
|
|
18
|
+
Sibling recipes: [Vue](../vue/upload-center.md) ·
|
|
19
|
+
[Inertia](../inertia/upload-center.md).
|
|
20
|
+
|
|
21
|
+
## Prerequisites
|
|
22
|
+
|
|
23
|
+
- `<BbConfirm />` and `<BbToast />` mounted once in `app.vue`.
|
|
24
|
+
- [App Layout](../../guides/app-layout.md), [Page Shell](../../guides/page-shell.md)
|
|
25
|
+
and [Floating Bottom Panel](../../guides/floating-bottom-panel.md) copied
|
|
26
|
+
into `src/components/layout/` per their guides — every recipe page assumes
|
|
27
|
+
this chrome exists and renders inside it.
|
|
28
|
+
- A `useCategoriesStore()` Pinia store exposing the shared expense-category
|
|
29
|
+
catalog (Travel, Meals & entertainment, Lodging, Rideshare & transit,
|
|
30
|
+
Software & subscriptions, Office supplies, Fuel, Client entertainment).
|
|
31
|
+
- Server routes: `GET /api/receipts` (accepts `search`, `categoryIds[]`,
|
|
32
|
+
`merchant`, `spentBetween[]`, `tab`, `page`, `perPage`, returns
|
|
33
|
+
`{ data, total, tabCounts, readyCount }`), `GET /api/merchants/search`,
|
|
34
|
+
plus the mutation routes referenced below (`PATCH /api/receipts/:id`,
|
|
35
|
+
`POST /api/receipts/:id/submit`, `POST /api/receipts/submit-drafts`,
|
|
36
|
+
`DELETE /api/receipts/:id`, `PATCH /api/receipts/bulk/categorize`,
|
|
37
|
+
`POST /api/receipts/bulk/delete`). A dedicated `POST /api/receipts/upload`
|
|
38
|
+
endpoint backs `api.receipts.upload()` — see the upload note below.
|
|
39
|
+
|
|
40
|
+
## Key ideas
|
|
41
|
+
|
|
42
|
+
- **One coopting page context.** `provideUploadCenter()` joins the
|
|
43
|
+
page-shell stack and the table context once, coopts the shared category
|
|
44
|
+
catalog from `useCategoriesStore()`, owns the URL-backed filters/tab and
|
|
45
|
+
the **upload queue**, and exposes complete actions. Every leaf calls
|
|
46
|
+
`useUploadCenter()` and nothing else.
|
|
47
|
+
- **Filters and tab live in the URL, not in local refs.** They are seeded
|
|
48
|
+
from `route.query` at setup and every change writes back through
|
|
49
|
+
`navigateTo` — the page is bookmarkable and survives reload.
|
|
50
|
+
- **`useAsyncData` watches `route.query`.** That single watch source is
|
|
51
|
+
the whole refetch trigger for the _server_ page — filters, tab and page
|
|
52
|
+
changes all funnel through the URL first, so there is exactly one fetch
|
|
53
|
+
path for drafts/submitted/reimbursed rows.
|
|
54
|
+
- **The upload queue is untouched by the data-layer swap.** It stays
|
|
55
|
+
client-only reactive state (`uploads`), merged above the fetched server
|
|
56
|
+
rows in a computed (`rows`) passed straight to `BbTable`'s `:items` —
|
|
57
|
+
no `dependencies`, no `refreshKey`; progress ticks mutate the queue rows
|
|
58
|
+
in place and Vue's own reactivity re-renders the one cell reading them.
|
|
59
|
+
- **Capture and transfer are two layers.** `BbDropzone` only validates and
|
|
60
|
+
hands over `File[]`; the context runs the uploads (AbortController +
|
|
61
|
+
progress callback per file) and the dropzone leaf immediately drains its
|
|
62
|
+
model into `ctx.enqueueFiles()`. `$fetch` has no upload-progress events,
|
|
63
|
+
so the transfer itself posts `FormData` through a small XHR wrapper in
|
|
64
|
+
`@/api` — the call site (`api.receipts.upload(file, { signal, onProgress })`)
|
|
65
|
+
stays identical to the other platforms.
|
|
66
|
+
- **Every draft cell edits in place**, exactly as in the Vue recipe:
|
|
67
|
+
merchant (§6.3), category (§6.1), amount, date — each its own leaf with
|
|
68
|
+
a single `receipt` prop, failed saves revert via `refresh()` and toast
|
|
69
|
+
destructive.
|
|
70
|
+
- **Tabs are URL filters.** Drafts / Submitted / Reimbursed carry `href`
|
|
71
|
+
(`?tab=drafts`) and live counts from every fetch; the same table is the
|
|
72
|
+
pane, read-only outside Drafts.
|
|
73
|
+
- **Clicking the name of a thing peeks it.** Filename → receipt peek
|
|
74
|
+
(fresh stack); merchant name inside the peek → merchant peek **stacked**
|
|
75
|
+
with a back arrow. Peeks fetch their own JSON and re-run whenever
|
|
76
|
+
`ctx.rows.value` changes (the same signal a server `refresh()` updates).
|
|
77
|
+
- **Buttons are `md` by default**; `sm`/`xs` only in the table rows, the
|
|
78
|
+
filter pills, the peek footers and the bottom bar. One
|
|
79
|
+
`variant="primary"`: **Submit all** with its live count. Internal SPA
|
|
80
|
+
navigation (the merchant peek's "All receipts from…" link) uses `to`;
|
|
81
|
+
the file-download links (`Download original`) stay a raw `href` — they
|
|
82
|
+
serve a real file, not a Nuxt page, so they must not be intercepted by
|
|
83
|
+
the router.
|
|
84
|
+
|
|
85
|
+
## Page anatomy
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
UploadCenter/
|
|
89
|
+
Index.vue ← assembly: PageShell slots, dropzone, tabs, table, pagination
|
|
90
|
+
context.ts ← coopting page context (shell + table + categories + URL filters + upload queue + actions)
|
|
91
|
+
ReceiptDropzone.vue ← multi-file capture, constraint errors mapped to messages
|
|
92
|
+
FilterRail.vue ← pill filters + clear-all
|
|
93
|
+
ReceiptFileCell.vue ← file icon + name (peek link) + size, both row kinds
|
|
94
|
+
UploadProgressCell.vue ← per-file BbProgress / failed state with inline retry
|
|
95
|
+
MerchantCell.vue ← inline text edit, commit on blur/Enter (§6.3)
|
|
96
|
+
CategoryCell.vue ← one-click badge → BbSelectPopover (§6.1)
|
|
97
|
+
AmountCell.vue ← inline number edit with $ prefix
|
|
98
|
+
DateCell.vue ← date picker cell, commit on pick
|
|
99
|
+
RowActions.vue ← remove-from-queue / per-receipt "…" dropdown
|
|
100
|
+
SelectionBar.vue ← bulk categorize + bulk remove in the floating bottom panel
|
|
101
|
+
ReceiptPeek.vue ← sidebar workbench, stack level 1
|
|
102
|
+
MerchantPeek.vue ← stacked peek, back arrow via the shell
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Chrome assumed from the guides: [App Layout](../../guides/app-layout.md)
|
|
106
|
+
(left nav + fat footer), [Page Shell](../../guides/page-shell.md),
|
|
107
|
+
[Floating Bottom Panel](../../guides/floating-bottom-panel.md).
|
|
108
|
+
|
|
109
|
+
## Action inventory
|
|
110
|
+
|
|
111
|
+
| # | Action | Trigger | Mechanism |
|
|
112
|
+
| --- | --------------------------------- | ---------------------------------- | ----------------------------------------------------- |
|
|
113
|
+
| 1 | Upload receipts | dropzone (drag, click, Browse) | `enqueueFiles()` → per-file upload with progress |
|
|
114
|
+
| 2 | Retry a failed upload | inline in the progress cell | `retryUpload(id)` re-issues the request |
|
|
115
|
+
| 3 | Remove an in-flight upload | row actions (x) | `removeUpload(id)` aborts + drops the row |
|
|
116
|
+
| 4 | Page search | topbar right | `filters.search` → URL → `useAsyncData` refetch |
|
|
117
|
+
| 5 | Filter category / merchant / date | filter rail pills | `BbSelectPopover` multiple / query-aware / date range |
|
|
118
|
+
| 6 | Clear filters | rail right (only when active) | context `clearFilters()` |
|
|
119
|
+
| 7 | Switch tab | href tabs with counts | URL + `useAsyncData` refetch |
|
|
120
|
+
| 8 | Edit merchant inline | merchant cell | commit on blur/Enter, Esc reverts (§6.3) |
|
|
121
|
+
| 9 | Change category one-click | category badge in row & peek | `BbSelectPopover` on the badge (§6.1) |
|
|
122
|
+
| 10 | Edit amount inline | amount cell ($ prefix) | commit on blur/Enter, Esc reverts |
|
|
123
|
+
| 11 | Edit expense date inline | date cell | commit on pick |
|
|
124
|
+
| 12 | Submit one receipt | row `…`, peek footer | `submitReceipt()` ($fetch + refresh + toast) |
|
|
125
|
+
| 13 | Submit all with count | topbar, the page's one `primary` | `submitAll()` — count from every fetch |
|
|
126
|
+
| 14 | Remove a draft | row `…`, peek footer | destructive behind `useConfirm` |
|
|
127
|
+
| 15 | Bulk categorize | floating bottom panel popover | `bulkCategorize()` on `table.selected` |
|
|
128
|
+
| 16 | Bulk remove | floating bottom panel | destructive behind `useConfirm`, count in message |
|
|
129
|
+
| 17 | Select rows / select all | table checkboxes (drafts only) | table context, `selectable` predicate |
|
|
130
|
+
| 18 | Peek receipt | filename cell / merchant peek list | `peekReceipt(id)` → shell stack |
|
|
131
|
+
| 19 | Peek merchant (stacked) | merchant name inside the peek | `stackMerchant(name)` → back arrow |
|
|
132
|
+
| 20 | Download original | row `…`, peek footer | navigation (raw `href`, real file endpoint) |
|
|
133
|
+
|
|
134
|
+
## The code
|
|
135
|
+
|
|
136
|
+
### context.ts
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
// UploadCenter/context.ts
|
|
140
|
+
import {
|
|
141
|
+
computed,
|
|
142
|
+
inject,
|
|
143
|
+
provide,
|
|
144
|
+
reactive,
|
|
145
|
+
ref,
|
|
146
|
+
watch,
|
|
147
|
+
type ComputedRef,
|
|
148
|
+
type InjectionKey,
|
|
149
|
+
type Ref,
|
|
150
|
+
} from 'vue';
|
|
151
|
+
import {
|
|
152
|
+
useBbTableContext,
|
|
153
|
+
useConfirm,
|
|
154
|
+
useToast,
|
|
155
|
+
type BbTableContext,
|
|
156
|
+
} from 'bitboss-ui';
|
|
157
|
+
import {
|
|
158
|
+
usePageShellContext,
|
|
159
|
+
type PageShellContext,
|
|
160
|
+
} from '@/components/layout/usePageShellContext';
|
|
161
|
+
import { useCategoriesStore } from '@/stores/categories';
|
|
162
|
+
import { api } from '@/api';
|
|
163
|
+
import { watchDebounced } from '@vueuse/core';
|
|
164
|
+
import type { Category, Receipt } from '@/types';
|
|
165
|
+
|
|
166
|
+
/** What the stacked sidebar can show. Clicking a name pushes one of these. */
|
|
167
|
+
export type PeekItem =
|
|
168
|
+
| { type: 'receipt'; id: string }
|
|
169
|
+
| { type: 'merchant'; name: string };
|
|
170
|
+
|
|
171
|
+
export type UploadCenterTab = 'drafts' | 'submitted' | 'reimbursed';
|
|
172
|
+
|
|
173
|
+
export interface ReceiptFilters {
|
|
174
|
+
search: string;
|
|
175
|
+
categoryIds: string[];
|
|
176
|
+
merchant: string | null;
|
|
177
|
+
/**
|
|
178
|
+
* `[start, end]` as `YYYY-MM-DD`, or `[]` when unset. Range mode requires an
|
|
179
|
+
* array modelValue -- binding `null` throws, so the empty filter is `[]`.
|
|
180
|
+
*/
|
|
181
|
+
spentBetween: string[];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** A dropped file whose transfer is still in flight (or failed). Local-only. */
|
|
185
|
+
export type UploadRow = {
|
|
186
|
+
/** Local id (`upload-…`) — never collides with server receipt ids. */
|
|
187
|
+
id: string;
|
|
188
|
+
kind: 'upload';
|
|
189
|
+
filename: string;
|
|
190
|
+
size: number;
|
|
191
|
+
/** 0–100. Mutated in place per progress tick — reactive, no refetch. */
|
|
192
|
+
progress: number;
|
|
193
|
+
failed: boolean;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/** A server receipt as the table renders it. */
|
|
197
|
+
export type ReceiptRow = Receipt & { kind: 'receipt' };
|
|
198
|
+
|
|
199
|
+
/** What the table actually lists on the Drafts tab: uploads first. */
|
|
200
|
+
export type UploadCenterRow = UploadRow | ReceiptRow;
|
|
201
|
+
|
|
202
|
+
/** The only fields the inline cells may change. */
|
|
203
|
+
export type ReceiptPatch = Partial<
|
|
204
|
+
Pick<Receipt, 'merchant' | 'categoryId' | 'amount' | 'date'>
|
|
205
|
+
>;
|
|
206
|
+
|
|
207
|
+
type ReceiptListResponse = {
|
|
208
|
+
data: Receipt[];
|
|
209
|
+
total: number;
|
|
210
|
+
tabCounts: Record<UploadCenterTab, number>;
|
|
211
|
+
readyCount: number;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
export interface UploadCenterContext {
|
|
215
|
+
// -- Coopted contexts ------------------------------------------------------
|
|
216
|
+
shell: PageShellContext<PeekItem>;
|
|
217
|
+
table: BbTableContext;
|
|
218
|
+
/** Shared expense-category catalog, coopted from the store. */
|
|
219
|
+
categories: ComputedRef<Category[]>;
|
|
220
|
+
// -- Page-owned state ------------------------------------------------------
|
|
221
|
+
filters: ReceiptFilters;
|
|
222
|
+
hasActiveFilters: ComputedRef<boolean>;
|
|
223
|
+
tab: Ref<UploadCenterTab>;
|
|
224
|
+
tabCounts: ComputedRef<Record<UploadCenterTab, number>>;
|
|
225
|
+
/** Drafts that are complete (merchant + category + amount + date). */
|
|
226
|
+
readyCount: ComputedRef<number>;
|
|
227
|
+
/** The live upload queue — in-flight and failed transfers. */
|
|
228
|
+
uploads: Ref<UploadRow[]>;
|
|
229
|
+
// -- Data (server-rendered, URL-driven) -------------------------------------
|
|
230
|
+
/** Drafts tab: queue rows first, then the fetched server page. */
|
|
231
|
+
rows: ComputedRef<UploadCenterRow[]>;
|
|
232
|
+
rowsPending: Ref<boolean>;
|
|
233
|
+
searchMerchants: (query: string) => Promise<string[]>;
|
|
234
|
+
// -- Peek grammar ----------------------------------------------------------
|
|
235
|
+
peekReceipt: (id: string) => void;
|
|
236
|
+
stackReceipt: (id: string) => void;
|
|
237
|
+
stackMerchant: (name: string) => void;
|
|
238
|
+
// -- Actions (complete: API + refresh + toast) ------------------------------
|
|
239
|
+
clearFilters: () => void;
|
|
240
|
+
enqueueFiles: (files: File[]) => void;
|
|
241
|
+
retryUpload: (id: string) => void;
|
|
242
|
+
removeUpload: (id: string) => void;
|
|
243
|
+
updateReceipt: (receipt: Receipt, patch: ReceiptPatch) => Promise<void>;
|
|
244
|
+
submitReceipt: (receipt: Receipt) => Promise<void>;
|
|
245
|
+
submitAll: () => Promise<void>;
|
|
246
|
+
/** Resolves `true` when the receipt was actually deleted (confirm accepted). */
|
|
247
|
+
deleteReceipt: (receipt: Receipt) => Promise<boolean>;
|
|
248
|
+
bulkCategorize: (categoryId: string) => Promise<void>;
|
|
249
|
+
bulkRemove: () => Promise<void>;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const key: InjectionKey<UploadCenterContext> = Symbol('upload-center');
|
|
253
|
+
|
|
254
|
+
/** Comma-joined query value → string[] (or null when absent). */
|
|
255
|
+
const toArray = (value: unknown): string[] | null => {
|
|
256
|
+
if (value == null || value === '') return null;
|
|
257
|
+
return Array.isArray(value) ? value.map(String) : String(value).split(',');
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
export function provideUploadCenter(): UploadCenterContext {
|
|
261
|
+
const { toast } = useToast();
|
|
262
|
+
const { confirm } = useConfirm();
|
|
263
|
+
const route = useRoute();
|
|
264
|
+
|
|
265
|
+
// -- Coopt the specific contexts: once, here, for the whole page. ----------
|
|
266
|
+
const shell = usePageShellContext<PeekItem>();
|
|
267
|
+
const table = useBbTableContext('receipts-table', { perPage: 25 });
|
|
268
|
+
const categoriesStore = useCategoriesStore();
|
|
269
|
+
const categories = computed(() => categoriesStore.categories);
|
|
270
|
+
|
|
271
|
+
// -- Filters + tab: seeded from the URL, the single source of truth. -------
|
|
272
|
+
const filters = reactive<ReceiptFilters>({
|
|
273
|
+
search: (route.query.search as string) ?? '',
|
|
274
|
+
categoryIds: toArray(route.query.categoryIds) ?? [],
|
|
275
|
+
merchant: (route.query.merchant as string) ?? null,
|
|
276
|
+
spentBetween: toArray(route.query.spentBetween) ?? [],
|
|
277
|
+
});
|
|
278
|
+
const tab = ref<UploadCenterTab>(
|
|
279
|
+
(route.query.tab as UploadCenterTab) ?? 'drafts'
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const hasActiveFilters = computed(
|
|
283
|
+
() =>
|
|
284
|
+
filters.search !== '' ||
|
|
285
|
+
filters.categoryIds.length > 0 ||
|
|
286
|
+
filters.merchant !== null ||
|
|
287
|
+
filters.spentBetween.length > 0
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const clearFilters = () => {
|
|
291
|
+
filters.search = '';
|
|
292
|
+
filters.categoryIds = [];
|
|
293
|
+
filters.merchant = null;
|
|
294
|
+
filters.spentBetween = [];
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// Every filter/tab change mirrors into the query string; `useAsyncData`
|
|
298
|
+
// below watches `route.query`, so this is the whole refetch trigger.
|
|
299
|
+
const syncQuery = () =>
|
|
300
|
+
navigateTo({
|
|
301
|
+
query: {
|
|
302
|
+
search: filters.search || undefined,
|
|
303
|
+
categoryIds: filters.categoryIds.length
|
|
304
|
+
? filters.categoryIds.join(',')
|
|
305
|
+
: undefined,
|
|
306
|
+
merchant: filters.merchant ?? undefined,
|
|
307
|
+
spentBetween: filters.spentBetween.length
|
|
308
|
+
? filters.spentBetween.join(',')
|
|
309
|
+
: undefined,
|
|
310
|
+
tab: tab.value,
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
watchDebounced(filters, syncQuery, { deep: true, debounce: 300 });
|
|
314
|
+
watch(tab, () => {
|
|
315
|
+
table.page.value = 1;
|
|
316
|
+
syncQuery();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// -- Data ---------------------------------------------------------------------
|
|
320
|
+
// The one place the *server* page is fetched. A drafts page looks like:
|
|
321
|
+
// { id: 'rcp_3128', filename: 'uber-2026-07-08.pdf', merchant: 'Uber',
|
|
322
|
+
// categoryId: 'cat-rideshare', amount: 42.8, date: '2026-07-08', … }
|
|
323
|
+
// { id: 'rcp_3127', filename: 'IMG_4021.jpg', merchant: 'Starbucks Reserve',
|
|
324
|
+
// categoryId: 'cat-meals', amount: 11.4, date: '2026-07-08', … }
|
|
325
|
+
// { id: 'rcp_3120', filename: 'delta-ticket-0472.pdf', merchant: 'Delta Air Lines',
|
|
326
|
+
// categoryId: 'cat-travel', amount: 428.6, date: '2026-07-06', … }
|
|
327
|
+
// { id: 'rcp_3117', filename: 'marriott-folio.pdf', merchant: 'Marriott Marquis SF',
|
|
328
|
+
// categoryId: 'cat-lodging', amount: 612.33, date: '2026-07-05', … }
|
|
329
|
+
// The live upload queue is NOT part of this response — it is client-only
|
|
330
|
+
// reactive state merged in below, ahead of the fetched rows.
|
|
331
|
+
const { data, pending, refresh } = useAsyncData<ReceiptListResponse>(
|
|
332
|
+
'receipts',
|
|
333
|
+
() =>
|
|
334
|
+
$fetch<ReceiptListResponse>('/api/receipts', {
|
|
335
|
+
query: {
|
|
336
|
+
search: filters.search || undefined,
|
|
337
|
+
categoryIds: filters.categoryIds,
|
|
338
|
+
merchant: filters.merchant ?? undefined,
|
|
339
|
+
spentBetween: filters.spentBetween.length
|
|
340
|
+
? filters.spentBetween
|
|
341
|
+
: undefined,
|
|
342
|
+
tab: tab.value,
|
|
343
|
+
page: table.page.value,
|
|
344
|
+
perPage: table.perPage.value,
|
|
345
|
+
},
|
|
346
|
+
}),
|
|
347
|
+
{ watch: [() => route.query, table.page] }
|
|
348
|
+
);
|
|
349
|
+
const serverRows = computed<ReceiptRow[]>(
|
|
350
|
+
() =>
|
|
351
|
+
data.value?.data.map(
|
|
352
|
+
(receipt): ReceiptRow => ({ ...receipt, kind: 'receipt' })
|
|
353
|
+
) ?? []
|
|
354
|
+
);
|
|
355
|
+
// Drafts tab: queue rows render above the fetched page; a completed upload
|
|
356
|
+
// re-enters here once the next `refresh()` lands it as a server draft.
|
|
357
|
+
const uploads = ref<UploadRow[]>([]);
|
|
358
|
+
const rows = computed<UploadCenterRow[]>(() =>
|
|
359
|
+
tab.value === 'drafts'
|
|
360
|
+
? [...uploads.value, ...serverRows.value]
|
|
361
|
+
: serverRows.value
|
|
362
|
+
);
|
|
363
|
+
const rowsPending = pending;
|
|
364
|
+
const tabCounts = computed(
|
|
365
|
+
() => data.value?.tabCounts ?? { drafts: 0, submitted: 0, reimbursed: 0 }
|
|
366
|
+
);
|
|
367
|
+
const readyCount = computed(() => data.value?.readyCount ?? 0);
|
|
368
|
+
watch(
|
|
369
|
+
data,
|
|
370
|
+
(response) => {
|
|
371
|
+
if (response) table.total.value = response.total;
|
|
372
|
+
},
|
|
373
|
+
{ immediate: true }
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
const searchMerchants = (query: string): Promise<string[]> =>
|
|
377
|
+
$fetch<string[]>('/api/merchants/search', { query: { q: query } });
|
|
378
|
+
|
|
379
|
+
// -- Upload machinery ----------------------------------------------------------
|
|
380
|
+
// Untouched by the URL/useAsyncData swap: the queue is client-only state,
|
|
381
|
+
// mutated in place per progress tick — reactive, no refetch — and `rows`
|
|
382
|
+
// above re-derives on every push/filter. Files and abort controllers live
|
|
383
|
+
// in closure maps, not render state.
|
|
384
|
+
const uploadFiles = new Map<string, File>();
|
|
385
|
+
const controllers = new Map<string, AbortController>();
|
|
386
|
+
|
|
387
|
+
const startUpload = async (id: string) => {
|
|
388
|
+
const file = uploadFiles.get(id);
|
|
389
|
+
const row = uploads.value.find((upload) => upload.id === id);
|
|
390
|
+
if (!file || !row) return;
|
|
391
|
+
row.failed = false;
|
|
392
|
+
row.progress = 0;
|
|
393
|
+
const controller = new AbortController();
|
|
394
|
+
controllers.set(id, controller);
|
|
395
|
+
try {
|
|
396
|
+
// `$fetch` has no upload-progress events, so `api.receipts.upload`
|
|
397
|
+
// posts FormData through a small XHR wrapper in `@/api` instead of
|
|
398
|
+
// `$fetch` — the call shape here is identical to the other platforms.
|
|
399
|
+
await api.receipts.upload(file, {
|
|
400
|
+
signal: controller.signal,
|
|
401
|
+
onProgress: (percent) => {
|
|
402
|
+
row.progress = percent;
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
// Success: the file re-enters as a server draft with OCR-extracted
|
|
406
|
+
// merchant/amount/date prefilled — the refresh swaps the rows.
|
|
407
|
+
uploads.value = uploads.value.filter((upload) => upload.id !== id);
|
|
408
|
+
uploadFiles.delete(id);
|
|
409
|
+
await refresh();
|
|
410
|
+
} catch {
|
|
411
|
+
// Keep the row: the progress cell renders the inline Retry.
|
|
412
|
+
row.failed = true;
|
|
413
|
+
} finally {
|
|
414
|
+
controllers.delete(id);
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const enqueueFiles = (files: File[]) => {
|
|
419
|
+
for (const file of files) {
|
|
420
|
+
const id = `upload-${crypto.randomUUID()}`;
|
|
421
|
+
uploadFiles.set(id, file);
|
|
422
|
+
uploads.value.push({
|
|
423
|
+
id,
|
|
424
|
+
kind: 'upload',
|
|
425
|
+
filename: file.name,
|
|
426
|
+
size: file.size,
|
|
427
|
+
progress: 0,
|
|
428
|
+
failed: false,
|
|
429
|
+
});
|
|
430
|
+
void startUpload(id);
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const retryUpload = (id: string) => {
|
|
435
|
+
void startUpload(id);
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const removeUpload = (id: string) => {
|
|
439
|
+
controllers.get(id)?.abort();
|
|
440
|
+
controllers.delete(id);
|
|
441
|
+
uploadFiles.delete(id);
|
|
442
|
+
uploads.value = uploads.value.filter((upload) => upload.id !== id);
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// -- Peek grammar ----------------------------------------------------------------
|
|
446
|
+
// peekX opens a fresh stack (from the list); stackX drills down (from a peek).
|
|
447
|
+
const peekReceipt = (id: string) => shell.push({ type: 'receipt', id }, true);
|
|
448
|
+
const stackReceipt = (id: string) => shell.push({ type: 'receipt', id });
|
|
449
|
+
const stackMerchant = (name: string) =>
|
|
450
|
+
shell.push({ type: 'merchant', name });
|
|
451
|
+
|
|
452
|
+
// -- Actions --------------------------------------------------------------------
|
|
453
|
+
const updateReceipt = async (receipt: Receipt, patch: ReceiptPatch) => {
|
|
454
|
+
try {
|
|
455
|
+
await $fetch(`/api/receipts/${receipt.id}`, {
|
|
456
|
+
method: 'PATCH',
|
|
457
|
+
body: patch,
|
|
458
|
+
});
|
|
459
|
+
await refresh(); // silent on success — the cell already shows the new value
|
|
460
|
+
} catch {
|
|
461
|
+
await refresh(); // the refetch reverts the cell to the server value
|
|
462
|
+
toast({
|
|
463
|
+
title: `Could not save ${receipt.filename}. Changes were reverted.`,
|
|
464
|
+
variant: 'destructive',
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const submitReceipt = async (receipt: Receipt) => {
|
|
470
|
+
await $fetch(`/api/receipts/${receipt.id}/submit`, { method: 'POST' });
|
|
471
|
+
await refresh();
|
|
472
|
+
toast({
|
|
473
|
+
title: `${receipt.merchant ?? receipt.filename} submitted for approval.`,
|
|
474
|
+
variant: 'success',
|
|
475
|
+
});
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const submitAll = async () => {
|
|
479
|
+
const { submitted } = await $fetch<{ submitted: number }>(
|
|
480
|
+
'/api/receipts/submit-drafts',
|
|
481
|
+
{ method: 'POST' }
|
|
482
|
+
);
|
|
483
|
+
await refresh();
|
|
484
|
+
toast({
|
|
485
|
+
title: `${submitted} receipts submitted for approval.`,
|
|
486
|
+
variant: 'success',
|
|
487
|
+
});
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const deleteReceipt = async (receipt: Receipt): Promise<boolean> => {
|
|
491
|
+
const ok = await confirm({
|
|
492
|
+
variant: 'destructive',
|
|
493
|
+
title: 'Remove receipt',
|
|
494
|
+
text: `Remove ${receipt.filename}? The file is deleted and cannot be recovered.`,
|
|
495
|
+
yes: 'Remove',
|
|
496
|
+
no: 'Cancel',
|
|
497
|
+
});
|
|
498
|
+
if (!ok) return false;
|
|
499
|
+
await $fetch(`/api/receipts/${receipt.id}`, { method: 'DELETE' });
|
|
500
|
+
await refresh();
|
|
501
|
+
toast({ title: `${receipt.filename} removed.`, variant: 'success' });
|
|
502
|
+
return true;
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
// Bulk actions read and clear the coopted table context themselves. Only
|
|
506
|
+
// draft receipt rows are selectable, so `selected` holds receipt ids.
|
|
507
|
+
const bulkIds = (): string[] => table.selected.value;
|
|
508
|
+
const clearSelection = () => {
|
|
509
|
+
table.selected.value = [];
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const bulkCategorize = async (categoryId: string) => {
|
|
513
|
+
const ids = bulkIds();
|
|
514
|
+
await $fetch('/api/receipts/bulk/categorize', {
|
|
515
|
+
method: 'PATCH',
|
|
516
|
+
body: { ids, categoryId },
|
|
517
|
+
});
|
|
518
|
+
const category = categories.value.find((entry) => entry.id === categoryId);
|
|
519
|
+
clearSelection();
|
|
520
|
+
await refresh();
|
|
521
|
+
toast({
|
|
522
|
+
title: `${ids.length} receipts filed under ${category?.name ?? 'the selected category'}.`,
|
|
523
|
+
variant: 'success',
|
|
524
|
+
});
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const bulkRemove = async () => {
|
|
528
|
+
const ids = bulkIds();
|
|
529
|
+
const ok = await confirm({
|
|
530
|
+
variant: 'destructive',
|
|
531
|
+
title: 'Remove receipts',
|
|
532
|
+
text: `Remove ${ids.length} receipts? Their files are deleted and cannot be recovered.`,
|
|
533
|
+
yes: 'Remove',
|
|
534
|
+
no: 'Cancel',
|
|
535
|
+
});
|
|
536
|
+
if (!ok) return;
|
|
537
|
+
await $fetch('/api/receipts/bulk/delete', {
|
|
538
|
+
method: 'POST',
|
|
539
|
+
body: { ids },
|
|
540
|
+
});
|
|
541
|
+
clearSelection();
|
|
542
|
+
await refresh();
|
|
543
|
+
toast({ title: `${ids.length} receipts removed.`, variant: 'success' });
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
const ctx: UploadCenterContext = {
|
|
547
|
+
shell,
|
|
548
|
+
table,
|
|
549
|
+
categories,
|
|
550
|
+
filters,
|
|
551
|
+
hasActiveFilters,
|
|
552
|
+
tab,
|
|
553
|
+
tabCounts,
|
|
554
|
+
readyCount,
|
|
555
|
+
uploads,
|
|
556
|
+
rows,
|
|
557
|
+
rowsPending,
|
|
558
|
+
searchMerchants,
|
|
559
|
+
peekReceipt,
|
|
560
|
+
stackReceipt,
|
|
561
|
+
stackMerchant,
|
|
562
|
+
clearFilters,
|
|
563
|
+
enqueueFiles,
|
|
564
|
+
retryUpload,
|
|
565
|
+
removeUpload,
|
|
566
|
+
updateReceipt,
|
|
567
|
+
submitReceipt,
|
|
568
|
+
submitAll,
|
|
569
|
+
deleteReceipt,
|
|
570
|
+
bulkCategorize,
|
|
571
|
+
bulkRemove,
|
|
572
|
+
};
|
|
573
|
+
provide(key, ctx);
|
|
574
|
+
return ctx;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export function useUploadCenter(): UploadCenterContext {
|
|
578
|
+
const ctx = inject(key);
|
|
579
|
+
if (!ctx) throw new Error('useUploadCenter() called outside UploadCenter');
|
|
580
|
+
return ctx;
|
|
581
|
+
}
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
### Index.vue — the assembly
|
|
585
|
+
|
|
586
|
+
```vue
|
|
587
|
+
<!-- UploadCenter/Index.vue -->
|
|
588
|
+
<template>
|
|
589
|
+
<PageShell>
|
|
590
|
+
<!-- Topbar: breadcrumbs left, page search + submit-all right. -->
|
|
591
|
+
<template #topbar>
|
|
592
|
+
<div class="flex items-center justify-between gap-4 border-b px-4 py-2">
|
|
593
|
+
<!-- Breadcrumbs take the row's slack on a stable basis (min-w-0 flex-auto);
|
|
594
|
+
without it their overflow recompute fights the flex layout every frame. -->
|
|
595
|
+
<BbBreadcrumbs
|
|
596
|
+
class="min-w-0 flex-auto"
|
|
597
|
+
:items="[
|
|
598
|
+
{ key: 'dashboard', text: 'Dashboard', to: '/dashboard' },
|
|
599
|
+
{ key: 'expenses', text: 'Expenses', to: '/expenses' },
|
|
600
|
+
{ key: 'receipts', text: 'Receipts' },
|
|
601
|
+
]"
|
|
602
|
+
/>
|
|
603
|
+
<div class="flex shrink-0 items-center gap-2">
|
|
604
|
+
<BbTextInput
|
|
605
|
+
v-model="ctx.filters.search"
|
|
606
|
+
clearable
|
|
607
|
+
compact
|
|
608
|
+
hide-label
|
|
609
|
+
label="Search receipts"
|
|
610
|
+
placeholder="Search merchant or filename…"
|
|
611
|
+
prepend:icon="lucide:search"
|
|
612
|
+
/>
|
|
613
|
+
<!-- The page's single primary: submit-all with a live count.
|
|
614
|
+
BbButton tracks the async handler, so it shows its own
|
|
615
|
+
loading state while the batch submits. -->
|
|
616
|
+
<BbButton
|
|
617
|
+
:disabled="ctx.readyCount.value === 0"
|
|
618
|
+
prepend:icon="lucide:send"
|
|
619
|
+
variant="primary"
|
|
620
|
+
@click="ctx.submitAll"
|
|
621
|
+
>
|
|
622
|
+
{{ submitLabel }}
|
|
623
|
+
</BbButton>
|
|
624
|
+
</div>
|
|
625
|
+
</div>
|
|
626
|
+
</template>
|
|
627
|
+
|
|
628
|
+
<div class="flex flex-col gap-3 p-4">
|
|
629
|
+
<!-- Capture surface: drafts tab only. -->
|
|
630
|
+
<ReceiptDropzone v-if="ctx.tab.value === 'drafts'" />
|
|
631
|
+
|
|
632
|
+
<FilterRail />
|
|
633
|
+
|
|
634
|
+
<!-- Tabs as URL filters: href updates the query, the key feeds the
|
|
635
|
+
fetch. Counts come from the server on every fetch. -->
|
|
636
|
+
<BbTabs
|
|
637
|
+
v-model="ctx.tab.value"
|
|
638
|
+
compact
|
|
639
|
+
:items="[
|
|
640
|
+
{
|
|
641
|
+
key: 'drafts',
|
|
642
|
+
label: `Drafts · ${ctx.tabCounts.value.drafts}`,
|
|
643
|
+
href: '?tab=drafts',
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
key: 'submitted',
|
|
647
|
+
label: `Submitted · ${ctx.tabCounts.value.submitted}`,
|
|
648
|
+
href: '?tab=submitted',
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
key: 'reimbursed',
|
|
652
|
+
label: `Reimbursed · ${ctx.tabCounts.value.reimbursed}`,
|
|
653
|
+
href: '?tab=reimbursed',
|
|
654
|
+
},
|
|
655
|
+
]"
|
|
656
|
+
/>
|
|
657
|
+
|
|
658
|
+
<BbTable
|
|
659
|
+
id="receipts-table"
|
|
660
|
+
actions
|
|
661
|
+
caption="Expense receipts"
|
|
662
|
+
:columns="columns"
|
|
663
|
+
compact
|
|
664
|
+
item-value="id"
|
|
665
|
+
:items="ctx.rows.value"
|
|
666
|
+
:loading="ctx.rowsPending.value"
|
|
667
|
+
loading-text="Loading receipts…"
|
|
668
|
+
:row-class="rowClass"
|
|
669
|
+
:selectable="isDraft"
|
|
670
|
+
>
|
|
671
|
+
<!-- Clicking the NAME of a thing peeks it. -->
|
|
672
|
+
<template #filename="{ item }">
|
|
673
|
+
<ReceiptFileCell :row="item" />
|
|
674
|
+
</template>
|
|
675
|
+
|
|
676
|
+
<!-- Editable cells on draft rows; plain text everywhere else. -->
|
|
677
|
+
<template #merchant="{ item }">
|
|
678
|
+
<MerchantCell v-if="isDraft(item)" :receipt="item" />
|
|
679
|
+
<span v-else-if="item.kind === 'upload'" class="text-gray-400"
|
|
680
|
+
>—</span
|
|
681
|
+
>
|
|
682
|
+
<span v-else>{{ item.merchant ?? '—' }}</span>
|
|
683
|
+
</template>
|
|
684
|
+
|
|
685
|
+
<template #categoryId="{ item }">
|
|
686
|
+
<CategoryCell v-if="isDraft(item)" :receipt="item" />
|
|
687
|
+
<span v-else-if="item.kind === 'upload'" class="text-gray-400"
|
|
688
|
+
>—</span
|
|
689
|
+
>
|
|
690
|
+
<span v-else>{{ categoryName(item.categoryId) }}</span>
|
|
691
|
+
</template>
|
|
692
|
+
|
|
693
|
+
<template #amount="{ item }">
|
|
694
|
+
<AmountCell v-if="isDraft(item)" :receipt="item" />
|
|
695
|
+
<span v-else-if="item.kind === 'upload'" class="text-gray-400"
|
|
696
|
+
>—</span
|
|
697
|
+
>
|
|
698
|
+
<span v-else>{{
|
|
699
|
+
item.amount === null ? '—' : currency.format(item.amount)
|
|
700
|
+
}}</span>
|
|
701
|
+
</template>
|
|
702
|
+
|
|
703
|
+
<template #date="{ item }">
|
|
704
|
+
<DateCell v-if="isDraft(item)" :receipt="item" />
|
|
705
|
+
<span v-else-if="item.kind === 'upload'" class="text-gray-400"
|
|
706
|
+
>—</span
|
|
707
|
+
>
|
|
708
|
+
<span v-else>{{ item.date ? shortDate(item.date) : '—' }}</span>
|
|
709
|
+
</template>
|
|
710
|
+
|
|
711
|
+
<!-- Status column doubles as the transfer surface for queue rows. -->
|
|
712
|
+
<template #status="{ item }">
|
|
713
|
+
<UploadProgressCell v-if="item.kind === 'upload'" :upload="item" />
|
|
714
|
+
<BbBadge v-else variant="outline">{{ item.status }}</BbBadge>
|
|
715
|
+
</template>
|
|
716
|
+
|
|
717
|
+
<template #actions="{ item }">
|
|
718
|
+
<RowActions :row="item" />
|
|
719
|
+
</template>
|
|
720
|
+
|
|
721
|
+
<template #no-data>
|
|
722
|
+
<div class="flex flex-col items-center gap-2 p-6 text-center">
|
|
723
|
+
<p class="text-sm text-gray-500">
|
|
724
|
+
{{
|
|
725
|
+
ctx.hasActiveFilters.value
|
|
726
|
+
? 'No receipts match the current filters.'
|
|
727
|
+
: ctx.tab.value === 'drafts'
|
|
728
|
+
? 'No drafts — drop receipts above to get started.'
|
|
729
|
+
: 'Nothing here yet.'
|
|
730
|
+
}}
|
|
731
|
+
</p>
|
|
732
|
+
<BbButton
|
|
733
|
+
v-if="ctx.hasActiveFilters.value"
|
|
734
|
+
variant="ghost"
|
|
735
|
+
@click="ctx.clearFilters"
|
|
736
|
+
>
|
|
737
|
+
Clear filters
|
|
738
|
+
</BbButton>
|
|
739
|
+
</div>
|
|
740
|
+
</template>
|
|
741
|
+
</BbTable>
|
|
742
|
+
|
|
743
|
+
<!-- Joins the table context by id: no page/totalPages wiring at all. -->
|
|
744
|
+
<BbPagination table-id="receipts-table" />
|
|
745
|
+
</div>
|
|
746
|
+
|
|
747
|
+
<SelectionBar />
|
|
748
|
+
|
|
749
|
+
<!-- Stacked peek sidebar: the shell owns back/close; we render by type. -->
|
|
750
|
+
<template #sidebar>
|
|
751
|
+
<ReceiptPeek
|
|
752
|
+
v-if="ctx.shell.current.value?.type === 'receipt'"
|
|
753
|
+
:receipt-id="ctx.shell.current.value.id"
|
|
754
|
+
/>
|
|
755
|
+
<MerchantPeek
|
|
756
|
+
v-else-if="ctx.shell.current.value?.type === 'merchant'"
|
|
757
|
+
:merchant-name="ctx.shell.current.value.name"
|
|
758
|
+
/>
|
|
759
|
+
</template>
|
|
760
|
+
</PageShell>
|
|
761
|
+
</template>
|
|
762
|
+
|
|
763
|
+
<script setup lang="ts">
|
|
764
|
+
import { computed } from 'vue';
|
|
765
|
+
import {
|
|
766
|
+
BbBadge,
|
|
767
|
+
BbBreadcrumbs,
|
|
768
|
+
BbButton,
|
|
769
|
+
BbPagination,
|
|
770
|
+
BbTable,
|
|
771
|
+
BbTabs,
|
|
772
|
+
BbTextInput,
|
|
773
|
+
} from 'bitboss-ui';
|
|
774
|
+
import PageShell from '@/components/layout/PageShell.vue';
|
|
775
|
+
import {
|
|
776
|
+
provideUploadCenter,
|
|
777
|
+
type ReceiptRow,
|
|
778
|
+
type UploadCenterRow,
|
|
779
|
+
} from './context';
|
|
780
|
+
import AmountCell from './AmountCell.vue';
|
|
781
|
+
import CategoryCell from './CategoryCell.vue';
|
|
782
|
+
import DateCell from './DateCell.vue';
|
|
783
|
+
import FilterRail from './FilterRail.vue';
|
|
784
|
+
import MerchantCell from './MerchantCell.vue';
|
|
785
|
+
import MerchantPeek from './MerchantPeek.vue';
|
|
786
|
+
import ReceiptDropzone from './ReceiptDropzone.vue';
|
|
787
|
+
import ReceiptFileCell from './ReceiptFileCell.vue';
|
|
788
|
+
import ReceiptPeek from './ReceiptPeek.vue';
|
|
789
|
+
import RowActions from './RowActions.vue';
|
|
790
|
+
import SelectionBar from './SelectionBar.vue';
|
|
791
|
+
import UploadProgressCell from './UploadProgressCell.vue';
|
|
792
|
+
|
|
793
|
+
const ctx = provideUploadCenter();
|
|
794
|
+
|
|
795
|
+
const currency = new Intl.NumberFormat('en-US', {
|
|
796
|
+
style: 'currency',
|
|
797
|
+
currency: 'USD',
|
|
798
|
+
});
|
|
799
|
+
const shortDate = (value: string) =>
|
|
800
|
+
new Date(value).toLocaleDateString('en-US', {
|
|
801
|
+
month: 'short',
|
|
802
|
+
day: 'numeric',
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
// Every cell renders through a slot, so columns stay label-only definitions.
|
|
806
|
+
const columns = [
|
|
807
|
+
{ key: 'filename', label: 'Receipt' },
|
|
808
|
+
{ key: 'merchant', label: 'Merchant' },
|
|
809
|
+
{ key: 'categoryId', label: 'Category' },
|
|
810
|
+
{ key: 'amount', label: 'Amount', align: 'right' as const },
|
|
811
|
+
{ key: 'date', label: 'Date' },
|
|
812
|
+
{ key: 'status', label: 'Status' },
|
|
813
|
+
];
|
|
814
|
+
|
|
815
|
+
// Only server-side drafts are editable and selectable; queue rows and
|
|
816
|
+
// submitted/reimbursed receipts are not.
|
|
817
|
+
const isDraft = (item: UploadCenterRow): item is ReceiptRow =>
|
|
818
|
+
item.kind === 'receipt' && item.status === 'draft';
|
|
819
|
+
|
|
820
|
+
// Failed transfers get a tinted row — public row-class API, no internals.
|
|
821
|
+
const rowClass = (item: UploadCenterRow) =>
|
|
822
|
+
item.kind === 'upload' && item.failed
|
|
823
|
+
? 'bg-red-50 dark:bg-red-950/20'
|
|
824
|
+
: undefined;
|
|
825
|
+
|
|
826
|
+
const categoryName = (categoryId: string | null) =>
|
|
827
|
+
ctx.categories.value.find((category) => category.id === categoryId)?.name ??
|
|
828
|
+
'—';
|
|
829
|
+
|
|
830
|
+
const submitLabel = computed(() =>
|
|
831
|
+
ctx.readyCount.value === 0
|
|
832
|
+
? 'Submit receipts'
|
|
833
|
+
: `Submit ${ctx.readyCount.value} ${ctx.readyCount.value === 1 ? 'receipt' : 'receipts'}`
|
|
834
|
+
);
|
|
835
|
+
</script>
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
### ReceiptDropzone.vue
|
|
839
|
+
|
|
840
|
+
```vue
|
|
841
|
+
<!-- UploadCenter/ReceiptDropzone.vue -->
|
|
842
|
+
<template>
|
|
843
|
+
<BbDropzone
|
|
844
|
+
v-model="pending"
|
|
845
|
+
:accept="ACCEPT"
|
|
846
|
+
:errors="dropError"
|
|
847
|
+
:max-files="20"
|
|
848
|
+
:max-size="MAX_SIZE"
|
|
849
|
+
multiple
|
|
850
|
+
@error="onError"
|
|
851
|
+
>
|
|
852
|
+
<template #default="{ dragging, focused, open }">
|
|
853
|
+
<div
|
|
854
|
+
class="flex flex-col items-center gap-2 rounded-[var(--bb-radius)] border-2 border-dashed p-6 text-center transition-colors"
|
|
855
|
+
:class="[
|
|
856
|
+
dragging ? 'border-[var(--bb-primary)]' : 'border-[var(--bb-border)]',
|
|
857
|
+
focused ? 'ring-2 ring-[var(--bb-primary)] ring-offset-2' : '',
|
|
858
|
+
]"
|
|
859
|
+
>
|
|
860
|
+
<BbIcon class="text-gray-400" icon="lucide:receipt" size="lg" />
|
|
861
|
+
<p class="text-sm font-medium">
|
|
862
|
+
{{ dragging ? 'Drop to add receipts' : 'Drop receipts here' }}
|
|
863
|
+
</p>
|
|
864
|
+
<p class="text-xs text-gray-500">
|
|
865
|
+
JPG, PNG, HEIC or PDF · up to 10 MB each · 20 files per drop
|
|
866
|
+
</p>
|
|
867
|
+
<BbButton
|
|
868
|
+
prepend:icon="lucide:folder-open"
|
|
869
|
+
variant="ghost"
|
|
870
|
+
@click="open"
|
|
871
|
+
>
|
|
872
|
+
Browse files
|
|
873
|
+
</BbButton>
|
|
874
|
+
</div>
|
|
875
|
+
</template>
|
|
876
|
+
</BbDropzone>
|
|
877
|
+
</template>
|
|
878
|
+
|
|
879
|
+
<script setup lang="ts">
|
|
880
|
+
import { ref, watch } from 'vue';
|
|
881
|
+
import { BbButton, BbDropzone, BbIcon, type DropZoneError } from 'bitboss-ui';
|
|
882
|
+
import { useUploadCenter } from './context';
|
|
883
|
+
|
|
884
|
+
// Pair every extension with its MIME type: the MIME rule matches by content
|
|
885
|
+
// type, the .ext rule catches drag-drops where the OS leaves file.type empty.
|
|
886
|
+
const ACCEPT = [
|
|
887
|
+
'image/jpeg',
|
|
888
|
+
'.jpg',
|
|
889
|
+
'.jpeg',
|
|
890
|
+
'image/png',
|
|
891
|
+
'.png',
|
|
892
|
+
'image/heic',
|
|
893
|
+
'.heic',
|
|
894
|
+
'application/pdf',
|
|
895
|
+
'.pdf',
|
|
896
|
+
];
|
|
897
|
+
const MAX_SIZE = 10 * 1024 * 1024; // 10 MB per receipt
|
|
898
|
+
|
|
899
|
+
const ctx = useUploadCenter();
|
|
900
|
+
|
|
901
|
+
// BbDropzone captures and validates; the context uploads (posting FormData
|
|
902
|
+
// through an XHR wrapper — see context.ts). The model is a hand-off buffer:
|
|
903
|
+
// drain every validated batch into the queue, then clear it (which also
|
|
904
|
+
// resets the duplicate check for future corrections).
|
|
905
|
+
const pending = ref<File[]>([]);
|
|
906
|
+
watch(pending, (files) => {
|
|
907
|
+
if (files.length === 0) return;
|
|
908
|
+
dropError.value = undefined;
|
|
909
|
+
ctx.enqueueFiles(files);
|
|
910
|
+
pending.value = [];
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
// The errors display is a round-trip: map each typed reject to a sentence
|
|
914
|
+
// and feed it back through the `errors` prop (rendered in the built-in
|
|
915
|
+
// aria-live region). Batch drops emit one error per file; showing the last
|
|
916
|
+
// message keeps the surface calm.
|
|
917
|
+
const dropError = ref<string | undefined>();
|
|
918
|
+
const onError = (error: DropZoneError) => {
|
|
919
|
+
if (error.type === 'maxSize') {
|
|
920
|
+
dropError.value = `${error.filename} is ${error.formattedSize} — receipts are capped at ${error.formattedMaxSize}.`;
|
|
921
|
+
} else if (error.type === 'accept') {
|
|
922
|
+
dropError.value = `${error.filename} is not a JPG, PNG, HEIC or PDF.`;
|
|
923
|
+
} else if (error.type === 'uniqueness') {
|
|
924
|
+
dropError.value = `${error.filename} is already in this drop.`;
|
|
925
|
+
} else {
|
|
926
|
+
dropError.value = `That drop had ${error.totalFiles} files — add at most 20 at a time.`;
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
</script>
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
### FilterRail.vue
|
|
933
|
+
|
|
934
|
+
```vue
|
|
935
|
+
<!-- UploadCenter/FilterRail.vue -->
|
|
936
|
+
<template>
|
|
937
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
938
|
+
<!-- Category: multi-select pill over the coopted catalog. -->
|
|
939
|
+
<BbSelectPopover
|
|
940
|
+
v-model="ctx.filters.categoryIds"
|
|
941
|
+
item-text="name"
|
|
942
|
+
item-value="id"
|
|
943
|
+
:items="ctx.categories.value"
|
|
944
|
+
multiple
|
|
945
|
+
width="240px"
|
|
946
|
+
>
|
|
947
|
+
<template #activator="{ props }">
|
|
948
|
+
<BbButton
|
|
949
|
+
v-bind="props"
|
|
950
|
+
append:icon="lucide:chevron-down"
|
|
951
|
+
size="sm"
|
|
952
|
+
variant="outline"
|
|
953
|
+
>
|
|
954
|
+
Category{{
|
|
955
|
+
ctx.filters.categoryIds.length
|
|
956
|
+
? ` · ${ctx.filters.categoryIds.length}`
|
|
957
|
+
: ''
|
|
958
|
+
}}
|
|
959
|
+
</BbButton>
|
|
960
|
+
</template>
|
|
961
|
+
</BbSelectPopover>
|
|
962
|
+
|
|
963
|
+
<!-- Merchant: query-aware search. Merchant names are their own values,
|
|
964
|
+
so the pill labels itself straight from the filter state. -->
|
|
965
|
+
<BbSelectPopover
|
|
966
|
+
v-model="ctx.filters.merchant"
|
|
967
|
+
:items="ctx.searchMerchants"
|
|
968
|
+
width="240px"
|
|
969
|
+
>
|
|
970
|
+
<template #activator="{ props }">
|
|
971
|
+
<BbButton
|
|
972
|
+
v-bind="props"
|
|
973
|
+
append:icon="lucide:chevron-down"
|
|
974
|
+
size="sm"
|
|
975
|
+
variant="outline"
|
|
976
|
+
>
|
|
977
|
+
{{ ctx.filters.merchant ?? 'Merchant' }}
|
|
978
|
+
</BbButton>
|
|
979
|
+
</template>
|
|
980
|
+
</BbSelectPopover>
|
|
981
|
+
|
|
982
|
+
<!-- Spent-between range: a compact input is 28px — flush with the pills. -->
|
|
983
|
+
<BbDatePickerInput
|
|
984
|
+
v-model="ctx.filters.spentBetween"
|
|
985
|
+
clearable
|
|
986
|
+
compact
|
|
987
|
+
floating
|
|
988
|
+
hide-label
|
|
989
|
+
label="Spent between"
|
|
990
|
+
placeholder="Spent between…"
|
|
991
|
+
range
|
|
992
|
+
/>
|
|
993
|
+
|
|
994
|
+
<!-- Clear-all renders only while something is active. -->
|
|
995
|
+
<BbButton
|
|
996
|
+
v-if="ctx.hasActiveFilters.value"
|
|
997
|
+
class="ml-auto"
|
|
998
|
+
prepend:icon="lucide:x"
|
|
999
|
+
size="sm"
|
|
1000
|
+
variant="ghost"
|
|
1001
|
+
@click="ctx.clearFilters"
|
|
1002
|
+
>
|
|
1003
|
+
Clear filters
|
|
1004
|
+
</BbButton>
|
|
1005
|
+
</div>
|
|
1006
|
+
</template>
|
|
1007
|
+
|
|
1008
|
+
<script setup lang="ts">
|
|
1009
|
+
import { BbButton, BbDatePickerInput, BbSelectPopover } from 'bitboss-ui';
|
|
1010
|
+
import { useUploadCenter } from './context';
|
|
1011
|
+
|
|
1012
|
+
const ctx = useUploadCenter();
|
|
1013
|
+
</script>
|
|
1014
|
+
```
|
|
1015
|
+
|
|
1016
|
+
### ReceiptFileCell.vue
|
|
1017
|
+
|
|
1018
|
+
```vue
|
|
1019
|
+
<!-- UploadCenter/ReceiptFileCell.vue -->
|
|
1020
|
+
<template>
|
|
1021
|
+
<div class="flex items-center gap-2">
|
|
1022
|
+
<BbIcon class="shrink-0 text-gray-400" :icon="fileIcon" size="16" />
|
|
1023
|
+
<div class="flex min-w-0 flex-col">
|
|
1024
|
+
<!-- Server receipts peek on click; queue rows are not peekable yet. -->
|
|
1025
|
+
<BbButton
|
|
1026
|
+
v-if="row.kind === 'receipt'"
|
|
1027
|
+
class="justify-start"
|
|
1028
|
+
size="sm"
|
|
1029
|
+
variant="link"
|
|
1030
|
+
@click="() => ctx.peekReceipt(row.id)"
|
|
1031
|
+
>
|
|
1032
|
+
{{ row.filename }}
|
|
1033
|
+
</BbButton>
|
|
1034
|
+
<span v-else class="truncate text-sm">{{ row.filename }}</span>
|
|
1035
|
+
<span class="text-xs text-gray-500">{{ formattedSize }}</span>
|
|
1036
|
+
</div>
|
|
1037
|
+
</div>
|
|
1038
|
+
</template>
|
|
1039
|
+
|
|
1040
|
+
<script setup lang="ts">
|
|
1041
|
+
import { computed } from 'vue';
|
|
1042
|
+
import { BbButton, BbIcon } from 'bitboss-ui';
|
|
1043
|
+
import { useUploadCenter, type UploadCenterRow } from './context';
|
|
1044
|
+
|
|
1045
|
+
const props = defineProps<{ row: UploadCenterRow }>();
|
|
1046
|
+
|
|
1047
|
+
const ctx = useUploadCenter();
|
|
1048
|
+
|
|
1049
|
+
const fileIcon = computed(() =>
|
|
1050
|
+
props.row.filename.toLowerCase().endsWith('.pdf')
|
|
1051
|
+
? 'lucide:file-text'
|
|
1052
|
+
: 'lucide:image'
|
|
1053
|
+
);
|
|
1054
|
+
|
|
1055
|
+
const formattedSize = computed(() => {
|
|
1056
|
+
const bytes =
|
|
1057
|
+
props.row.kind === 'upload' ? props.row.size : props.row.fileSize;
|
|
1058
|
+
return bytes >= 1024 * 1024
|
|
1059
|
+
? `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
1060
|
+
: `${Math.max(1, Math.round(bytes / 1024))} KB`;
|
|
1061
|
+
});
|
|
1062
|
+
</script>
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
### UploadProgressCell.vue
|
|
1066
|
+
|
|
1067
|
+
```vue
|
|
1068
|
+
<!-- UploadCenter/UploadProgressCell.vue -->
|
|
1069
|
+
<template>
|
|
1070
|
+
<!-- In flight: a live bar. `upload.progress` is a reactive field mutated
|
|
1071
|
+
by the context's progress callback — no refetch per tick. -->
|
|
1072
|
+
<div v-if="!upload.failed" class="flex min-w-32 items-center gap-2">
|
|
1073
|
+
<BbProgress
|
|
1074
|
+
:aria-label="`Uploading ${upload.filename}`"
|
|
1075
|
+
class="flex-1"
|
|
1076
|
+
:model-value="upload.progress"
|
|
1077
|
+
/>
|
|
1078
|
+
<span class="text-xs tabular-nums text-gray-500"
|
|
1079
|
+
>{{ upload.progress }}%</span
|
|
1080
|
+
>
|
|
1081
|
+
</div>
|
|
1082
|
+
|
|
1083
|
+
<!-- Failed: say so and retry inline. `xs` is fine here — this is the
|
|
1084
|
+
densest surface on the page. -->
|
|
1085
|
+
<div v-else class="flex items-center gap-2">
|
|
1086
|
+
<span class="text-xs font-medium text-red-600 dark:text-red-400"
|
|
1087
|
+
>Upload failed</span
|
|
1088
|
+
>
|
|
1089
|
+
<BbButton
|
|
1090
|
+
prepend:icon="lucide:rotate-cw"
|
|
1091
|
+
size="xs"
|
|
1092
|
+
variant="outline"
|
|
1093
|
+
@click="() => ctx.retryUpload(upload.id)"
|
|
1094
|
+
>
|
|
1095
|
+
Retry
|
|
1096
|
+
</BbButton>
|
|
1097
|
+
</div>
|
|
1098
|
+
</template>
|
|
1099
|
+
|
|
1100
|
+
<script setup lang="ts">
|
|
1101
|
+
import { BbButton, BbProgress } from 'bitboss-ui';
|
|
1102
|
+
import { useUploadCenter, type UploadRow } from './context';
|
|
1103
|
+
|
|
1104
|
+
defineProps<{ upload: UploadRow }>();
|
|
1105
|
+
|
|
1106
|
+
const ctx = useUploadCenter();
|
|
1107
|
+
</script>
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
### MerchantCell.vue
|
|
1111
|
+
|
|
1112
|
+
```vue
|
|
1113
|
+
<!-- UploadCenter/MerchantCell.vue -->
|
|
1114
|
+
<template>
|
|
1115
|
+
<BbTextInput
|
|
1116
|
+
v-model="draft"
|
|
1117
|
+
compact
|
|
1118
|
+
hide-label
|
|
1119
|
+
label="Merchant"
|
|
1120
|
+
placeholder="Add merchant"
|
|
1121
|
+
@blur="commit"
|
|
1122
|
+
@keydown.enter="commit"
|
|
1123
|
+
@keydown.esc="revert"
|
|
1124
|
+
/>
|
|
1125
|
+
</template>
|
|
1126
|
+
|
|
1127
|
+
<script setup lang="ts">
|
|
1128
|
+
import { ref, watch } from 'vue';
|
|
1129
|
+
import { BbTextInput } from 'bitboss-ui';
|
|
1130
|
+
import { useUploadCenter } from './context';
|
|
1131
|
+
import type { Receipt } from '@/types';
|
|
1132
|
+
|
|
1133
|
+
const props = defineProps<{ receipt: Receipt }>();
|
|
1134
|
+
|
|
1135
|
+
const ctx = useUploadCenter();
|
|
1136
|
+
|
|
1137
|
+
// Inline-edit doctrine (§6.3): a local draft, commit on blur/Enter, revert
|
|
1138
|
+
// on Esc. The server value re-seeds the draft whenever a refetch updates the
|
|
1139
|
+
// row — which is also how a failed save reverts.
|
|
1140
|
+
const draft = ref<string | null>(props.receipt.merchant);
|
|
1141
|
+
watch(
|
|
1142
|
+
() => props.receipt.merchant,
|
|
1143
|
+
(value) => {
|
|
1144
|
+
draft.value = value;
|
|
1145
|
+
}
|
|
1146
|
+
);
|
|
1147
|
+
|
|
1148
|
+
const commit = () => {
|
|
1149
|
+
const merchant = draft.value?.trim() || null;
|
|
1150
|
+
if (merchant === props.receipt.merchant) return;
|
|
1151
|
+
void ctx.updateReceipt(props.receipt, { merchant });
|
|
1152
|
+
};
|
|
1153
|
+
const revert = () => {
|
|
1154
|
+
draft.value = props.receipt.merchant;
|
|
1155
|
+
};
|
|
1156
|
+
</script>
|
|
1157
|
+
```
|
|
1158
|
+
|
|
1159
|
+
### CategoryCell.vue
|
|
1160
|
+
|
|
1161
|
+
```vue
|
|
1162
|
+
<!-- UploadCenter/CategoryCell.vue -->
|
|
1163
|
+
<template>
|
|
1164
|
+
<BbSelectPopover
|
|
1165
|
+
compact
|
|
1166
|
+
disable-writing
|
|
1167
|
+
item-text="name"
|
|
1168
|
+
item-value="id"
|
|
1169
|
+
:items="ctx.categories.value"
|
|
1170
|
+
:model-value="receipt.categoryId"
|
|
1171
|
+
width="220px"
|
|
1172
|
+
@update:model-value="(categoryId) => pick(categoryId as string)"
|
|
1173
|
+
>
|
|
1174
|
+
<template #activator="{ props: activatorProps, shown }">
|
|
1175
|
+
<!-- One-click pattern (§6.1): the badge is the focusable trigger —
|
|
1176
|
+
BbBadgeButton takes over the badge body, the trailing chevron says
|
|
1177
|
+
"this can change", and while the save runs `loading` swaps the
|
|
1178
|
+
spinner into the chevron's place. A category is a label, not a
|
|
1179
|
+
semantic status, so a set category reads neutral (`secondary`);
|
|
1180
|
+
only the unset state stays `outline` so empty cells read as
|
|
1181
|
+
quieter to-dos, not values. -->
|
|
1182
|
+
<BbBadge
|
|
1183
|
+
append:icon="lucide:chevron-down"
|
|
1184
|
+
:class="{ 'category-cell--open': shown }"
|
|
1185
|
+
:loading="saving"
|
|
1186
|
+
:variant="receipt.categoryId ? 'secondary' : 'outline'"
|
|
1187
|
+
>
|
|
1188
|
+
<BbBadgeButton v-bind="activatorProps">{{
|
|
1189
|
+
categoryName
|
|
1190
|
+
}}</BbBadgeButton>
|
|
1191
|
+
</BbBadge>
|
|
1192
|
+
</template>
|
|
1193
|
+
</BbSelectPopover>
|
|
1194
|
+
</template>
|
|
1195
|
+
|
|
1196
|
+
<script setup lang="ts">
|
|
1197
|
+
import { computed, ref } from 'vue';
|
|
1198
|
+
import { BbBadge, BbBadgeButton, BbSelectPopover } from 'bitboss-ui';
|
|
1199
|
+
import { useUploadCenter } from './context';
|
|
1200
|
+
import type { Receipt } from '@/types';
|
|
1201
|
+
|
|
1202
|
+
const props = defineProps<{ receipt: Receipt }>();
|
|
1203
|
+
|
|
1204
|
+
const ctx = useUploadCenter();
|
|
1205
|
+
|
|
1206
|
+
// In-flight flag for the save feedback — rung-1 local state, not context.
|
|
1207
|
+
const saving = ref(false);
|
|
1208
|
+
const pick = async (categoryId: string) => {
|
|
1209
|
+
saving.value = true;
|
|
1210
|
+
try {
|
|
1211
|
+
await ctx.updateReceipt(props.receipt, { categoryId });
|
|
1212
|
+
} finally {
|
|
1213
|
+
saving.value = false;
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
1216
|
+
|
|
1217
|
+
// The trigger labels itself from the coopted catalog, never from the
|
|
1218
|
+
// popover's lazy `text` slot prop.
|
|
1219
|
+
const categoryName = computed(
|
|
1220
|
+
() =>
|
|
1221
|
+
ctx.categories.value.find(
|
|
1222
|
+
(category) => category.id === props.receipt.categoryId
|
|
1223
|
+
)?.name ?? 'Set category'
|
|
1224
|
+
);
|
|
1225
|
+
</script>
|
|
1226
|
+
|
|
1227
|
+
<style scoped>
|
|
1228
|
+
/* Open = the badge's own hue, deepened, plus its variant ring — the same
|
|
1229
|
+
grammar as every badge-activator cell. */
|
|
1230
|
+
.category-cell--open {
|
|
1231
|
+
--bg: color-mix(in oklab, var(--color) 24%, var(--bb-panel));
|
|
1232
|
+
--border-color: color-mix(in oklab, var(--color) 45%, var(--bb-panel));
|
|
1233
|
+
box-shadow: 0 0 0 var(--bb-ring-size) var(--ring);
|
|
1234
|
+
}
|
|
1235
|
+
</style>
|
|
1236
|
+
```
|
|
1237
|
+
|
|
1238
|
+
### AmountCell.vue
|
|
1239
|
+
|
|
1240
|
+
```vue
|
|
1241
|
+
<!-- UploadCenter/AmountCell.vue -->
|
|
1242
|
+
<template>
|
|
1243
|
+
<BbNumberInput
|
|
1244
|
+
v-model="draft"
|
|
1245
|
+
compact
|
|
1246
|
+
hide-label
|
|
1247
|
+
label="Amount"
|
|
1248
|
+
:max-precision="2"
|
|
1249
|
+
:min="0"
|
|
1250
|
+
placeholder="0.00"
|
|
1251
|
+
@blur="commit"
|
|
1252
|
+
@keydown.enter="commit"
|
|
1253
|
+
@keydown.esc="revert"
|
|
1254
|
+
>
|
|
1255
|
+
<template #prefix>$</template>
|
|
1256
|
+
</BbNumberInput>
|
|
1257
|
+
</template>
|
|
1258
|
+
|
|
1259
|
+
<script setup lang="ts">
|
|
1260
|
+
import { ref, watch } from 'vue';
|
|
1261
|
+
import { BbNumberInput } from 'bitboss-ui';
|
|
1262
|
+
import { useUploadCenter } from './context';
|
|
1263
|
+
import type { Receipt } from '@/types';
|
|
1264
|
+
|
|
1265
|
+
const props = defineProps<{ receipt: Receipt }>();
|
|
1266
|
+
|
|
1267
|
+
const ctx = useUploadCenter();
|
|
1268
|
+
|
|
1269
|
+
// Same commit/revert contract as the merchant cell, on the number model.
|
|
1270
|
+
const draft = ref<number | string | null>(props.receipt.amount);
|
|
1271
|
+
watch(
|
|
1272
|
+
() => props.receipt.amount,
|
|
1273
|
+
(value) => {
|
|
1274
|
+
draft.value = value;
|
|
1275
|
+
}
|
|
1276
|
+
);
|
|
1277
|
+
|
|
1278
|
+
const commit = () => {
|
|
1279
|
+
const amount =
|
|
1280
|
+
draft.value === null || draft.value === '' ? null : Number(draft.value);
|
|
1281
|
+
if (amount === props.receipt.amount) return;
|
|
1282
|
+
void ctx.updateReceipt(props.receipt, { amount });
|
|
1283
|
+
};
|
|
1284
|
+
const revert = () => {
|
|
1285
|
+
draft.value = props.receipt.amount;
|
|
1286
|
+
};
|
|
1287
|
+
</script>
|
|
1288
|
+
```
|
|
1289
|
+
|
|
1290
|
+
### DateCell.vue
|
|
1291
|
+
|
|
1292
|
+
```vue
|
|
1293
|
+
<!-- UploadCenter/DateCell.vue -->
|
|
1294
|
+
<template>
|
|
1295
|
+
<!-- Picking a date IS the commit — no blur dance for calendar input.
|
|
1296
|
+
`floating` keeps the model a plain YYYY-MM-DD string. -->
|
|
1297
|
+
<BbDatePickerInput
|
|
1298
|
+
compact
|
|
1299
|
+
floating
|
|
1300
|
+
hide-label
|
|
1301
|
+
label="Expense date"
|
|
1302
|
+
:model-value="receipt.date"
|
|
1303
|
+
placeholder="YYYY-MM-DD"
|
|
1304
|
+
@update:model-value="commit"
|
|
1305
|
+
/>
|
|
1306
|
+
</template>
|
|
1307
|
+
|
|
1308
|
+
<script setup lang="ts">
|
|
1309
|
+
import { BbDatePickerInput } from 'bitboss-ui';
|
|
1310
|
+
import { useUploadCenter } from './context';
|
|
1311
|
+
import type { Receipt } from '@/types';
|
|
1312
|
+
|
|
1313
|
+
const props = defineProps<{ receipt: Receipt }>();
|
|
1314
|
+
|
|
1315
|
+
const ctx = useUploadCenter();
|
|
1316
|
+
|
|
1317
|
+
const commit = (value: string | string[] | null) => {
|
|
1318
|
+
const date = Array.isArray(value) ? (value[0] ?? null) : value;
|
|
1319
|
+
if (date === props.receipt.date) return;
|
|
1320
|
+
void ctx.updateReceipt(props.receipt, { date });
|
|
1321
|
+
};
|
|
1322
|
+
</script>
|
|
1323
|
+
```
|
|
1324
|
+
|
|
1325
|
+
### RowActions.vue
|
|
1326
|
+
|
|
1327
|
+
```vue
|
|
1328
|
+
<!-- UploadCenter/RowActions.vue -->
|
|
1329
|
+
<template>
|
|
1330
|
+
<!-- Queue rows: one explicit remove (aborts the transfer). -->
|
|
1331
|
+
<BbButton
|
|
1332
|
+
v-if="row.kind === 'upload'"
|
|
1333
|
+
:aria-label="`Remove ${row.filename} from the queue`"
|
|
1334
|
+
icon="lucide:x"
|
|
1335
|
+
size="sm"
|
|
1336
|
+
variant="ghost"
|
|
1337
|
+
@click="() => ctx.removeUpload(row.id)"
|
|
1338
|
+
/>
|
|
1339
|
+
|
|
1340
|
+
<!-- Server receipts: contextual dropdown, entries vary by status. -->
|
|
1341
|
+
<BbDropdown v-else :items="items">
|
|
1342
|
+
<template #activator="{ props }">
|
|
1343
|
+
<BbButton v-bind="props" icon="lucide:ellipsis" size="sm" variant="ghost">
|
|
1344
|
+
Receipt actions
|
|
1345
|
+
</BbButton>
|
|
1346
|
+
</template>
|
|
1347
|
+
</BbDropdown>
|
|
1348
|
+
</template>
|
|
1349
|
+
|
|
1350
|
+
<script setup lang="ts">
|
|
1351
|
+
import { computed } from 'vue';
|
|
1352
|
+
import { BbButton, BbDropdown } from 'bitboss-ui';
|
|
1353
|
+
import { useUploadCenter, type UploadCenterRow } from './context';
|
|
1354
|
+
|
|
1355
|
+
const props = defineProps<{ row: UploadCenterRow }>();
|
|
1356
|
+
|
|
1357
|
+
const ctx = useUploadCenter();
|
|
1358
|
+
|
|
1359
|
+
const items = computed(() => {
|
|
1360
|
+
if (props.row.kind !== 'receipt') return [];
|
|
1361
|
+
const receipt = props.row;
|
|
1362
|
+
const isDraft = receipt.status === 'draft';
|
|
1363
|
+
return [
|
|
1364
|
+
...(isDraft
|
|
1365
|
+
? [
|
|
1366
|
+
{
|
|
1367
|
+
key: 'submit',
|
|
1368
|
+
text: 'Submit for approval',
|
|
1369
|
+
'prepend:icon': 'lucide:send',
|
|
1370
|
+
onClick: () => ctx.submitReceipt(receipt),
|
|
1371
|
+
},
|
|
1372
|
+
]
|
|
1373
|
+
: []),
|
|
1374
|
+
{
|
|
1375
|
+
// A real file download, not a Nuxt page — a raw `href` triggers a
|
|
1376
|
+
// normal browser navigation instead of being intercepted by the
|
|
1377
|
+
// router, which is what a client-side `to` would otherwise do.
|
|
1378
|
+
key: 'download',
|
|
1379
|
+
text: 'Download original',
|
|
1380
|
+
'prepend:icon': 'lucide:download',
|
|
1381
|
+
href: `/receipts/${receipt.id}/file`,
|
|
1382
|
+
},
|
|
1383
|
+
...(isDraft
|
|
1384
|
+
? [
|
|
1385
|
+
{
|
|
1386
|
+
key: 'remove',
|
|
1387
|
+
text: 'Remove',
|
|
1388
|
+
'prepend:icon': 'lucide:trash-2',
|
|
1389
|
+
variant: 'destructive' as const,
|
|
1390
|
+
onClick: () => ctx.deleteReceipt(receipt),
|
|
1391
|
+
},
|
|
1392
|
+
]
|
|
1393
|
+
: []),
|
|
1394
|
+
];
|
|
1395
|
+
});
|
|
1396
|
+
</script>
|
|
1397
|
+
```
|
|
1398
|
+
|
|
1399
|
+
### SelectionBar.vue
|
|
1400
|
+
|
|
1401
|
+
```vue
|
|
1402
|
+
<!-- UploadCenter/SelectionBar.vue -->
|
|
1403
|
+
<template>
|
|
1404
|
+
<FloatingBottomPanel :open="selectedCount > 0">
|
|
1405
|
+
<span class="px-2 text-sm">{{ selectedCount }} selected</span>
|
|
1406
|
+
|
|
1407
|
+
<!-- Bulk categorize: the picker lives right in the bar — a one-shot
|
|
1408
|
+
popover (model stays null) whose selection IS the action. -->
|
|
1409
|
+
<BbSelectPopover
|
|
1410
|
+
item-text="name"
|
|
1411
|
+
item-value="id"
|
|
1412
|
+
:items="ctx.categories.value"
|
|
1413
|
+
:model-value="null"
|
|
1414
|
+
placement="top"
|
|
1415
|
+
width="220px"
|
|
1416
|
+
@update:model-value="
|
|
1417
|
+
(categoryId) => categoryId && ctx.bulkCategorize(categoryId)
|
|
1418
|
+
"
|
|
1419
|
+
>
|
|
1420
|
+
<template #activator="{ props }">
|
|
1421
|
+
<BbButton
|
|
1422
|
+
v-bind="props"
|
|
1423
|
+
prepend:icon="lucide:tag"
|
|
1424
|
+
size="sm"
|
|
1425
|
+
variant="ghost"
|
|
1426
|
+
>
|
|
1427
|
+
Set category
|
|
1428
|
+
</BbButton>
|
|
1429
|
+
</template>
|
|
1430
|
+
</BbSelectPopover>
|
|
1431
|
+
|
|
1432
|
+
<div class="floating-bottom-panel__right flex items-center gap-1">
|
|
1433
|
+
<BbButton size="sm" variant="destructive" @click="ctx.bulkRemove"
|
|
1434
|
+
>Remove</BbButton
|
|
1435
|
+
>
|
|
1436
|
+
<BbButton
|
|
1437
|
+
aria-label="Clear selection"
|
|
1438
|
+
icon="lucide:x"
|
|
1439
|
+
size="sm"
|
|
1440
|
+
variant="ghost"
|
|
1441
|
+
@click="clearSelection"
|
|
1442
|
+
/>
|
|
1443
|
+
</div>
|
|
1444
|
+
</FloatingBottomPanel>
|
|
1445
|
+
</template>
|
|
1446
|
+
|
|
1447
|
+
<script setup lang="ts">
|
|
1448
|
+
import { computed } from 'vue';
|
|
1449
|
+
import { BbButton, BbSelectPopover } from 'bitboss-ui';
|
|
1450
|
+
import FloatingBottomPanel from '@/components/layout/FloatingBottomPanel.vue';
|
|
1451
|
+
import { useUploadCenter } from './context';
|
|
1452
|
+
|
|
1453
|
+
const ctx = useUploadCenter();
|
|
1454
|
+
|
|
1455
|
+
const selectedCount = computed(() => ctx.table.selected.value?.length ?? 0);
|
|
1456
|
+
const clearSelection = () => {
|
|
1457
|
+
ctx.table.selected.value = [];
|
|
1458
|
+
};
|
|
1459
|
+
</script>
|
|
1460
|
+
```
|
|
1461
|
+
|
|
1462
|
+
### ReceiptPeek.vue
|
|
1463
|
+
|
|
1464
|
+
```vue
|
|
1465
|
+
<!-- UploadCenter/ReceiptPeek.vue -->
|
|
1466
|
+
<template>
|
|
1467
|
+
<div v-if="receipt" class="flex flex-col gap-4 text-sm">
|
|
1468
|
+
<!-- Header: merchant identity + amount + status. The merchant name
|
|
1469
|
+
drills DOWN — stacked peek, back arrow via the shell. -->
|
|
1470
|
+
<header class="flex flex-col gap-1.5">
|
|
1471
|
+
<div class="flex items-center justify-between gap-2">
|
|
1472
|
+
<BbButton
|
|
1473
|
+
v-if="receipt.merchant"
|
|
1474
|
+
size="sm"
|
|
1475
|
+
variant="link"
|
|
1476
|
+
@click="() => ctx.stackMerchant(receipt!.merchant!)"
|
|
1477
|
+
>
|
|
1478
|
+
{{ receipt.merchant }}
|
|
1479
|
+
</BbButton>
|
|
1480
|
+
<span v-else class="text-gray-500">No merchant yet</span>
|
|
1481
|
+
<BbBadge variant="outline">{{ receipt.status }}</BbBadge>
|
|
1482
|
+
</div>
|
|
1483
|
+
<p class="text-xl font-semibold">
|
|
1484
|
+
{{
|
|
1485
|
+
receipt.amount === null
|
|
1486
|
+
? 'Amount missing'
|
|
1487
|
+
: currency.format(receipt.amount)
|
|
1488
|
+
}}
|
|
1489
|
+
</p>
|
|
1490
|
+
<p class="text-xs text-gray-500">
|
|
1491
|
+
{{ receipt.filename }} · spent
|
|
1492
|
+
{{ receipt.date ? longDate(receipt.date) : '—' }}
|
|
1493
|
+
</p>
|
|
1494
|
+
</header>
|
|
1495
|
+
|
|
1496
|
+
<!-- Preview placeholder: the real app renders the scanned image here. -->
|
|
1497
|
+
<div
|
|
1498
|
+
class="flex h-44 flex-col items-center justify-center gap-1 rounded-[var(--bb-radius)] border bg-[var(--bb-panel)] text-gray-400"
|
|
1499
|
+
>
|
|
1500
|
+
<BbIcon icon="lucide:receipt" size="lg" />
|
|
1501
|
+
<span class="text-xs">Receipt preview</span>
|
|
1502
|
+
</div>
|
|
1503
|
+
|
|
1504
|
+
<!-- Extracted fields: what OCR read off the scan, with confidence. -->
|
|
1505
|
+
<section class="flex flex-col gap-1.5">
|
|
1506
|
+
<div class="flex items-center justify-between">
|
|
1507
|
+
<h3 class="text-xs font-semibold uppercase tracking-wide text-gray-500">
|
|
1508
|
+
Extracted fields
|
|
1509
|
+
</h3>
|
|
1510
|
+
<BbBadge size="xs" variant="outline">{{
|
|
1511
|
+
`OCR · ${receipt.extracted.confidence}%`
|
|
1512
|
+
}}</BbBadge>
|
|
1513
|
+
</div>
|
|
1514
|
+
<dl class="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1.5">
|
|
1515
|
+
<dt class="text-gray-500">Merchant</dt>
|
|
1516
|
+
<dd>{{ receipt.extracted.merchant ?? '—' }}</dd>
|
|
1517
|
+
<dt class="text-gray-500">Total</dt>
|
|
1518
|
+
<dd>
|
|
1519
|
+
{{
|
|
1520
|
+
receipt.extracted.total === null
|
|
1521
|
+
? '—'
|
|
1522
|
+
: currency.format(receipt.extracted.total)
|
|
1523
|
+
}}
|
|
1524
|
+
</dd>
|
|
1525
|
+
<dt class="text-gray-500">Tax</dt>
|
|
1526
|
+
<dd>
|
|
1527
|
+
{{
|
|
1528
|
+
receipt.extracted.tax === null
|
|
1529
|
+
? '—'
|
|
1530
|
+
: currency.format(receipt.extracted.tax)
|
|
1531
|
+
}}
|
|
1532
|
+
</dd>
|
|
1533
|
+
<dt class="text-gray-500">Date</dt>
|
|
1534
|
+
<dd>
|
|
1535
|
+
{{ receipt.extracted.date ? longDate(receipt.extracted.date) : '—' }}
|
|
1536
|
+
</dd>
|
|
1537
|
+
<dt class="text-gray-500">Paid with</dt>
|
|
1538
|
+
<dd>{{ receipt.extracted.paymentMethod ?? '—' }}</dd>
|
|
1539
|
+
</dl>
|
|
1540
|
+
</section>
|
|
1541
|
+
|
|
1542
|
+
<!-- Properties: the peek is a workbench — the category is the same
|
|
1543
|
+
one-click cell as in the table. -->
|
|
1544
|
+
<section class="flex flex-col gap-1.5">
|
|
1545
|
+
<h3 class="text-xs font-semibold uppercase tracking-wide text-gray-500">
|
|
1546
|
+
Details
|
|
1547
|
+
</h3>
|
|
1548
|
+
<dl class="grid grid-cols-[auto_1fr] items-center gap-x-4 gap-y-1.5">
|
|
1549
|
+
<dt class="text-gray-500">Category</dt>
|
|
1550
|
+
<dd><CategoryCell :receipt="receipt" /></dd>
|
|
1551
|
+
<dt class="text-gray-500">Uploaded</dt>
|
|
1552
|
+
<dd>{{ longDate(receipt.uploadedAt) }}</dd>
|
|
1553
|
+
<dt class="text-gray-500">Report</dt>
|
|
1554
|
+
<dd>{{ receipt.reportName ?? 'Not on a report yet' }}</dd>
|
|
1555
|
+
</dl>
|
|
1556
|
+
</section>
|
|
1557
|
+
|
|
1558
|
+
<!-- Activity feed. -->
|
|
1559
|
+
<section class="flex flex-col gap-1.5">
|
|
1560
|
+
<h3 class="text-xs font-semibold uppercase tracking-wide text-gray-500">
|
|
1561
|
+
Activity
|
|
1562
|
+
</h3>
|
|
1563
|
+
<ul class="flex flex-col gap-2">
|
|
1564
|
+
<li
|
|
1565
|
+
v-for="entry in receipt.activity"
|
|
1566
|
+
:key="entry.id"
|
|
1567
|
+
class="flex gap-2"
|
|
1568
|
+
>
|
|
1569
|
+
<BbIcon
|
|
1570
|
+
class="mt-0.5 shrink-0 text-gray-400"
|
|
1571
|
+
:icon="entry.icon"
|
|
1572
|
+
size="14"
|
|
1573
|
+
/>
|
|
1574
|
+
<div class="flex flex-col">
|
|
1575
|
+
<span>{{ entry.text }}</span>
|
|
1576
|
+
<span class="text-xs text-gray-500"
|
|
1577
|
+
>{{ entry.actor }} · {{ longDate(entry.at) }}</span
|
|
1578
|
+
>
|
|
1579
|
+
</div>
|
|
1580
|
+
</li>
|
|
1581
|
+
</ul>
|
|
1582
|
+
</section>
|
|
1583
|
+
|
|
1584
|
+
<!-- Act here, or leave for the original file. -->
|
|
1585
|
+
<footer class="mt-2 flex items-center gap-2 border-t pt-3">
|
|
1586
|
+
<BbButton
|
|
1587
|
+
v-if="receipt.status === 'draft'"
|
|
1588
|
+
prepend:icon="lucide:send"
|
|
1589
|
+
size="sm"
|
|
1590
|
+
variant="secondary"
|
|
1591
|
+
@click="() => ctx.submitReceipt(receipt!)"
|
|
1592
|
+
>
|
|
1593
|
+
Submit
|
|
1594
|
+
</BbButton>
|
|
1595
|
+
<!-- A real file download, not a Nuxt page — stays a raw `href`. -->
|
|
1596
|
+
<BbButton
|
|
1597
|
+
append:icon="lucide:arrow-up-right"
|
|
1598
|
+
:href="`/receipts/${receipt.id}/file`"
|
|
1599
|
+
size="sm"
|
|
1600
|
+
variant="ghost"
|
|
1601
|
+
>
|
|
1602
|
+
Download original
|
|
1603
|
+
</BbButton>
|
|
1604
|
+
<BbButton
|
|
1605
|
+
v-if="receipt.status === 'draft'"
|
|
1606
|
+
class="ml-auto"
|
|
1607
|
+
prepend:icon="lucide:trash-2"
|
|
1608
|
+
size="sm"
|
|
1609
|
+
variant="destructive"
|
|
1610
|
+
@click="removeAndClose"
|
|
1611
|
+
>
|
|
1612
|
+
Remove
|
|
1613
|
+
</BbButton>
|
|
1614
|
+
</footer>
|
|
1615
|
+
</div>
|
|
1616
|
+
|
|
1617
|
+
<div v-else class="flex justify-center p-8">
|
|
1618
|
+
<BbSpinner />
|
|
1619
|
+
</div>
|
|
1620
|
+
</template>
|
|
1621
|
+
|
|
1622
|
+
<script setup lang="ts">
|
|
1623
|
+
import { ref, watchEffect } from 'vue';
|
|
1624
|
+
import { BbBadge, BbButton, BbIcon, BbSpinner } from 'bitboss-ui';
|
|
1625
|
+
import CategoryCell from './CategoryCell.vue';
|
|
1626
|
+
import { useUploadCenter } from './context';
|
|
1627
|
+
import type { ReceiptDetail } from '@/types';
|
|
1628
|
+
|
|
1629
|
+
const props = defineProps<{ receiptId: string }>();
|
|
1630
|
+
|
|
1631
|
+
const ctx = useUploadCenter();
|
|
1632
|
+
|
|
1633
|
+
// Re-fetches whenever the stack shows a different receipt; `ctx.rows` changes
|
|
1634
|
+
// on every server `refresh()` (mutations) or upload completion, so watching
|
|
1635
|
+
// it makes inline edits (category, submit) reflect here immediately.
|
|
1636
|
+
const receipt = ref<ReceiptDetail | null>(null);
|
|
1637
|
+
watchEffect(async () => {
|
|
1638
|
+
void ctx.rows.value;
|
|
1639
|
+
receipt.value = await $fetch<ReceiptDetail>(
|
|
1640
|
+
`/api/receipts/${props.receiptId}`
|
|
1641
|
+
);
|
|
1642
|
+
});
|
|
1643
|
+
|
|
1644
|
+
// deleteReceipt resolves true only when the confirm was accepted — don't
|
|
1645
|
+
// close the peek on a cancelled dialog.
|
|
1646
|
+
const removeAndClose = async () => {
|
|
1647
|
+
if (!receipt.value) return;
|
|
1648
|
+
if (await ctx.deleteReceipt(receipt.value)) ctx.shell.close();
|
|
1649
|
+
};
|
|
1650
|
+
|
|
1651
|
+
const currency = new Intl.NumberFormat('en-US', {
|
|
1652
|
+
style: 'currency',
|
|
1653
|
+
currency: 'USD',
|
|
1654
|
+
});
|
|
1655
|
+
const longDate = (value: string) =>
|
|
1656
|
+
new Date(value).toLocaleDateString('en-US', {
|
|
1657
|
+
month: 'short',
|
|
1658
|
+
day: 'numeric',
|
|
1659
|
+
year: 'numeric',
|
|
1660
|
+
});
|
|
1661
|
+
</script>
|
|
1662
|
+
```
|
|
1663
|
+
|
|
1664
|
+
### MerchantPeek.vue
|
|
1665
|
+
|
|
1666
|
+
```vue
|
|
1667
|
+
<!-- UploadCenter/MerchantPeek.vue -->
|
|
1668
|
+
<template>
|
|
1669
|
+
<div v-if="merchant" class="flex flex-col gap-4 text-sm">
|
|
1670
|
+
<header class="flex items-center gap-3">
|
|
1671
|
+
<div
|
|
1672
|
+
class="grid h-9 w-9 shrink-0 place-items-center rounded-[var(--bb-radius)] border"
|
|
1673
|
+
>
|
|
1674
|
+
<BbIcon class="text-gray-400" icon="lucide:store" size="18" />
|
|
1675
|
+
</div>
|
|
1676
|
+
<div class="flex flex-col">
|
|
1677
|
+
<h2 class="text-base font-semibold">{{ merchant.name }}</h2>
|
|
1678
|
+
<p class="text-xs text-gray-500">
|
|
1679
|
+
{{ merchant.defaultCategoryName }} · this year
|
|
1680
|
+
</p>
|
|
1681
|
+
</div>
|
|
1682
|
+
</header>
|
|
1683
|
+
|
|
1684
|
+
<!-- Stat strip. -->
|
|
1685
|
+
<div class="grid grid-cols-3 gap-2">
|
|
1686
|
+
<div class="rounded-[var(--bb-radius)] border p-2">
|
|
1687
|
+
<p class="text-xs text-gray-500">Total spent</p>
|
|
1688
|
+
<p class="font-semibold">{{ currency.format(merchant.totalSpent) }}</p>
|
|
1689
|
+
</div>
|
|
1690
|
+
<div class="rounded-[var(--bb-radius)] border p-2">
|
|
1691
|
+
<p class="text-xs text-gray-500">Receipts</p>
|
|
1692
|
+
<p class="font-semibold">{{ merchant.receiptCount }}</p>
|
|
1693
|
+
</div>
|
|
1694
|
+
<div class="rounded-[var(--bb-radius)] border p-2">
|
|
1695
|
+
<p class="text-xs text-gray-500">Average</p>
|
|
1696
|
+
<p class="font-semibold">
|
|
1697
|
+
{{ currency.format(merchant.averageAmount) }}
|
|
1698
|
+
</p>
|
|
1699
|
+
</div>
|
|
1700
|
+
</div>
|
|
1701
|
+
|
|
1702
|
+
<!-- Recent receipts: names drill down again — the stack keeps growing. -->
|
|
1703
|
+
<section class="flex flex-col gap-1.5">
|
|
1704
|
+
<h3 class="text-xs font-semibold uppercase tracking-wide text-gray-500">
|
|
1705
|
+
Recent receipts
|
|
1706
|
+
</h3>
|
|
1707
|
+
<ul class="flex flex-col">
|
|
1708
|
+
<li
|
|
1709
|
+
v-for="recent in merchant.recentReceipts"
|
|
1710
|
+
:key="recent.id"
|
|
1711
|
+
class="flex items-center justify-between gap-2 border-b py-1.5 last:border-b-0"
|
|
1712
|
+
>
|
|
1713
|
+
<BbButton
|
|
1714
|
+
size="sm"
|
|
1715
|
+
variant="link"
|
|
1716
|
+
@click="() => ctx.stackReceipt(recent.id)"
|
|
1717
|
+
>
|
|
1718
|
+
{{ recent.filename }}
|
|
1719
|
+
</BbButton>
|
|
1720
|
+
<BbBadge variant="outline">{{ recent.status }}</BbBadge>
|
|
1721
|
+
<span class="text-xs tabular-nums text-gray-500">
|
|
1722
|
+
{{ currency.format(recent.amount) }}
|
|
1723
|
+
</span>
|
|
1724
|
+
</li>
|
|
1725
|
+
</ul>
|
|
1726
|
+
</section>
|
|
1727
|
+
|
|
1728
|
+
<footer class="mt-2 flex items-center gap-2 border-t pt-3">
|
|
1729
|
+
<!-- An internal listing page — a real Nuxt route, so this uses `to`. -->
|
|
1730
|
+
<BbButton
|
|
1731
|
+
append:icon="lucide:arrow-up-right"
|
|
1732
|
+
size="sm"
|
|
1733
|
+
:to="`/expenses/receipts?merchant=${encodeURIComponent(merchant.name)}`"
|
|
1734
|
+
variant="ghost"
|
|
1735
|
+
>
|
|
1736
|
+
All receipts from {{ merchant.name }}
|
|
1737
|
+
</BbButton>
|
|
1738
|
+
</footer>
|
|
1739
|
+
</div>
|
|
1740
|
+
|
|
1741
|
+
<div v-else class="flex justify-center p-8">
|
|
1742
|
+
<BbSpinner />
|
|
1743
|
+
</div>
|
|
1744
|
+
</template>
|
|
1745
|
+
|
|
1746
|
+
<script setup lang="ts">
|
|
1747
|
+
import { ref, watchEffect } from 'vue';
|
|
1748
|
+
import { BbBadge, BbButton, BbIcon, BbSpinner } from 'bitboss-ui';
|
|
1749
|
+
import { useUploadCenter } from './context';
|
|
1750
|
+
import type { MerchantSummary } from '@/types';
|
|
1751
|
+
|
|
1752
|
+
const props = defineProps<{ merchantName: string }>();
|
|
1753
|
+
|
|
1754
|
+
const ctx = useUploadCenter();
|
|
1755
|
+
|
|
1756
|
+
const merchant = ref<MerchantSummary | null>(null);
|
|
1757
|
+
watchEffect(async () => {
|
|
1758
|
+
merchant.value = await $fetch<MerchantSummary>(
|
|
1759
|
+
`/api/merchants/${encodeURIComponent(props.merchantName)}`
|
|
1760
|
+
);
|
|
1761
|
+
});
|
|
1762
|
+
|
|
1763
|
+
const currency = new Intl.NumberFormat('en-US', {
|
|
1764
|
+
style: 'currency',
|
|
1765
|
+
currency: 'USD',
|
|
1766
|
+
});
|
|
1767
|
+
</script>
|
|
1768
|
+
```
|
|
1769
|
+
|
|
1770
|
+
## Why it is built this way
|
|
1771
|
+
|
|
1772
|
+
- **The context coopts, leaves consume.** `shell`, `table` **and the
|
|
1773
|
+
category catalog** are joined once in `provideUploadCenter()`; thirteen
|
|
1774
|
+
components share the filters, the queue, selection, the peek stack and
|
|
1775
|
+
every action through one `useUploadCenter()` call. No leaf imports
|
|
1776
|
+
`$fetch`, `@/api` or a store directly — even the query-aware merchant
|
|
1777
|
+
pill gets its provider (`ctx.searchMerchants`) from the context, so the
|
|
1778
|
+
page has exactly one import surface.
|
|
1779
|
+
- **The URL is the filter store, `useAsyncData` is the fetch.** Filters
|
|
1780
|
+
and tab are seeded from `route.query` once at setup, mirrored back with
|
|
1781
|
+
`navigateTo` on every change (debounced for free text), and
|
|
1782
|
+
`useAsyncData('receipts', …, { watch: [() => route.query, table.page] })`
|
|
1783
|
+
is the single refetch trigger for the _server_ page — there is exactly
|
|
1784
|
+
one place server data loads.
|
|
1785
|
+
- **The upload queue crosses the data-layer swap untouched.** It was
|
|
1786
|
+
always client-only reactive state, so it needed no adaptation: `uploads`
|
|
1787
|
+
stays a plain `ref`, progress ticks still mutate the row objects in
|
|
1788
|
+
place, and `rows` (a computed merging `uploads` ahead of the fetched
|
|
1789
|
+
`serverRows`) is what `BbTable` binds to directly — no items function, no
|
|
1790
|
+
`dependencies`, no `refreshKey`. A completed upload's own `refresh()`
|
|
1791
|
+
call is what swaps it into `serverRows` on the next tick.
|
|
1792
|
+
- **`$fetch` can't stream upload progress, so the transfer itself doesn't
|
|
1793
|
+
use it.** `api.receipts.upload()` posts `FormData` through a small XHR
|
|
1794
|
+
wrapper instead — the only place this recipe steps outside `$fetch` —
|
|
1795
|
+
while every other mutation (`updateReceipt`, `submitReceipt`, the bulk
|
|
1796
|
+
actions) is a plain `$fetch` call followed by `await refresh()`.
|
|
1797
|
+
- **The table context still owns paging and selection.** `useBbTableContext`
|
|
1798
|
+
is unchanged from the Vue recipe: it seeds `perPage`, `BbPagination`
|
|
1799
|
+
joins by `table-id`, and `SelectionBar` reads `table.selected` — only the
|
|
1800
|
+
_server_ rows move from an items-function to a resolved array merged
|
|
1801
|
+
into `rows`.
|
|
1802
|
+
- **Peeks watch `ctx.rows.value`, not a manual refresh counter.** Since
|
|
1803
|
+
every mutation already calls the list's `refresh()`, and `rows` recomputes
|
|
1804
|
+
whenever the underlying `data` changes, a peek's `watchEffect(() => {
|
|
1805
|
+
void ctx.rows.value; … })` re-fetches its own JSON detail for free — the
|
|
1806
|
+
same signal drives both surfaces.
|
|
1807
|
+
- **`peekX` vs `stackX` is the whole stack grammar.** Filename →
|
|
1808
|
+
`peekReceipt` (replaces the stack); merchant name inside the peek →
|
|
1809
|
+
`stackMerchant` (pushes, back arrow appears); a recent receipt inside
|
|
1810
|
+
the merchant peek → `stackReceipt` (deeper still). The grammar lives in
|
|
1811
|
+
the context — a leaf cannot accidentally nuke the stack.
|
|
1812
|
+
- **Tabs carry `href` and live counts**, exactly as in the Vue recipe — the
|
|
1813
|
+
client-side pushState they perform keeps `route.query.tab` in sync, which
|
|
1814
|
+
is what seeds `tab` on load and what `useAsyncData`'s `route.query` watch
|
|
1815
|
+
reacts to.
|
|
1816
|
+
- **Internal links use `to`; real file downloads stay `href`.** The
|
|
1817
|
+
merchant peek's "All receipts from…" link is a genuine Nuxt route, so it
|
|
1818
|
+
uses `to` for client-side navigation. "Download original" (row `…`, peek
|
|
1819
|
+
footer) hits a raw file-serving endpoint, not a page — it deliberately
|
|
1820
|
+
keeps a plain `href` so the browser performs a real download instead of
|
|
1821
|
+
the router trying to resolve it as a route.
|
|
1822
|
+
- **Sizing:** topbar follows `md`-first (`compact` search at 28px flush
|
|
1823
|
+
beside the default-size primary); `sm` appears exactly where the
|
|
1824
|
+
doctrine allows — row actions, pills, peek footers, the bulk bar — and
|
|
1825
|
+
the one `xs` (inline Retry) sits inside a table cell, the densest
|
|
1826
|
+
surface on the page. **One `variant="primary"`** (Submit all, with its
|
|
1827
|
+
live count and auto-tracked loading). Destructive actions — single,
|
|
1828
|
+
bulk, in-peek — all ride `useConfirm` with the filename or count in the
|
|
1829
|
+
message.
|
|
1830
|
+
|
|
1831
|
+
## Variations
|
|
1832
|
+
|
|
1833
|
+
- **Expense reports**: group receipts into named reports (a `reportId`
|
|
1834
|
+
column + a "Add to report" bulk action feeding a `BbSelectPopover` of
|
|
1835
|
+
open reports); the Submit-all primary becomes "Submit report".
|
|
1836
|
+
- **Policy checks**: server flags out-of-policy drafts
|
|
1837
|
+
(`receipt.policyWarning`); tint the row, badge the amount cell, and
|
|
1838
|
+
render a `BbAlert` above the table summarizing the count.
|
|
1839
|
+
- **Camera-first mobile**: keep `accept` image-heavy and rely on the
|
|
1840
|
+
native picker offering the camera; the dropzone slot collapses to a
|
|
1841
|
+
single "Snap a receipt" affordance under the mobile breakpoint.
|
|
1842
|
+
- **Auto-submit on high confidence**: when `extracted.confidence` ≥ 95 and
|
|
1843
|
+
all four fields resolved, submit server-side on upload and toast an
|
|
1844
|
+
undo — the Drafts tab then only holds receipts needing review.
|
|
1845
|
+
- **SSR-populated peeks**: swap the peeks' `$fetch` calls for
|
|
1846
|
+
`useAsyncData` keyed by id if the peek content should be part of the
|
|
1847
|
+
server-rendered payload rather than a client-side follow-up fetch.
|