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