@sakoa/ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +171 -0
- package/dist/App.d.ts +2 -0
- package/dist/cli/index.js +9243 -0
- package/dist/components/DemoSection.d.ts +30 -0
- package/dist/components/SApiKeyboard.d.ts +22 -0
- package/dist/components/SApiSection.d.ts +21 -0
- package/dist/components/SApiTable.d.ts +46 -0
- package/dist/components/STableOfContents.d.ts +2 -0
- package/dist/components/ui/SAlert.d.ts +76 -0
- package/dist/components/ui/SBadge.d.ts +56 -0
- package/dist/components/ui/SButton.d.ts +67 -0
- package/dist/components/ui/SCheckbox.d.ts +64 -0
- package/dist/components/ui/SChip.d.ts +43 -0
- package/dist/components/ui/SDatePicker.d.ts +77 -0
- package/dist/components/ui/SGlassButton.d.ts +70 -0
- package/dist/components/ui/SIcon.d.ts +29 -0
- package/dist/components/ui/SInput.d.ts +129 -0
- package/dist/components/ui/SKbd.d.ts +24 -0
- package/dist/components/ui/SKbdShortcut.d.ts +14 -0
- package/dist/components/ui/SSelect.d.ts +148 -0
- package/dist/components/ui/SSkeleton.d.ts +37 -0
- package/dist/components/ui/SSwitch.d.ts +61 -0
- package/dist/components/ui/STooltip.d.ts +82 -0
- package/dist/components/ui/accordion/SAccordionContent.d.ts +23 -0
- package/dist/components/ui/accordion/SAccordionItem.d.ts +70 -0
- package/dist/components/ui/accordion/SAccordionTrigger.d.ts +37 -0
- package/dist/components/ui/accordion/index.d.ts +4 -0
- package/dist/components/ui/avatar/SAvatar.d.ts +36 -0
- package/dist/components/ui/avatar/SAvatarFallback.d.ts +26 -0
- package/dist/components/ui/avatar/SAvatarGroup.d.ts +30 -0
- package/dist/components/ui/avatar/SAvatarImage.d.ts +23 -0
- package/dist/components/ui/avatar/index.d.ts +4 -0
- package/dist/components/ui/breadcrumb/SBreadcrumb.d.ts +22 -0
- package/dist/components/ui/breadcrumb/SBreadcrumbEllipsis.d.ts +17 -0
- package/dist/components/ui/breadcrumb/SBreadcrumbItem.d.ts +17 -0
- package/dist/components/ui/breadcrumb/SBreadcrumbLink.d.ts +26 -0
- package/dist/components/ui/breadcrumb/SBreadcrumbList.d.ts +17 -0
- package/dist/components/ui/breadcrumb/SBreadcrumbPage.d.ts +17 -0
- package/dist/components/ui/breadcrumb/SBreadcrumbSeparator.d.ts +17 -0
- package/dist/components/ui/breadcrumb/index.d.ts +7 -0
- package/dist/components/ui/card/SCard.d.ts +103 -0
- package/dist/components/ui/card/SCardActions.d.ts +44 -0
- package/dist/components/ui/card/SCardContent.d.ts +35 -0
- package/dist/components/ui/card/SCardFooter.d.ts +38 -0
- package/dist/components/ui/card/SCardHeader.d.ts +53 -0
- package/dist/components/ui/card/SCardMedia.d.ts +83 -0
- package/dist/components/ui/card/SGlassCard.d.ts +103 -0
- package/dist/components/ui/card/SMorphingCardContent.d.ts +18 -0
- package/dist/components/ui/card/index.d.ts +24 -0
- package/dist/components/ui/carousel/SCarousel.d.ts +166 -0
- package/dist/components/ui/carousel/index.d.ts +2 -0
- package/dist/components/ui/color-picker/SColorPickerAlphaSlider.d.ts +4 -0
- package/dist/components/ui/color-picker/SColorPickerCopy.d.ts +19 -0
- package/dist/components/ui/color-picker/SColorPickerEyeDropper.d.ts +17 -0
- package/dist/components/ui/color-picker/SColorPickerHueSlider.d.ts +4 -0
- package/dist/components/ui/color-picker/SColorPickerInputs.d.ts +2 -0
- package/dist/components/ui/color-picker/SColorPickerPresets.d.ts +9 -0
- package/dist/components/ui/color-picker/SColorPickerPreview.d.ts +2 -0
- package/dist/components/ui/color-picker/SColorPickerRecent.d.ts +7 -0
- package/dist/components/ui/color-picker/SColorPickerSpectrum.d.ts +4 -0
- package/dist/components/ui/color-picker/index.d.ts +11 -0
- package/dist/components/ui/drawer/index.d.ts +11 -0
- package/dist/components/ui/dropdown/SDropdownDivider.d.ts +8 -0
- package/dist/components/ui/dropdown/SDropdownGroup.d.ts +25 -0
- package/dist/components/ui/dropdown/SDropdownItem.d.ts +56 -0
- package/dist/components/ui/dropdown/index.d.ts +4 -0
- package/dist/components/ui/form/SForm.d.ts +38 -0
- package/dist/components/ui/form/SFormField.d.ts +31 -0
- package/dist/components/ui/form/index.d.ts +5 -0
- package/dist/components/ui/modal/index.d.ts +19 -0
- package/dist/components/ui/option/SOption.d.ts +32 -0
- package/dist/components/ui/option/SOptionGroup.d.ts +28 -0
- package/dist/components/ui/option/index.d.ts +2 -0
- package/dist/components/ui/otp/SOTP.d.ts +122 -0
- package/dist/components/ui/otp/SOTPGroup.d.ts +23 -0
- package/dist/components/ui/otp/SOTPSeparator.d.ts +17 -0
- package/dist/components/ui/otp/SOTPSlot.d.ts +49 -0
- package/dist/components/ui/otp/index.d.ts +7 -0
- package/dist/components/ui/otp/types.d.ts +26 -0
- package/dist/components/ui/otp/useOTPContext.d.ts +42 -0
- package/dist/components/ui/pagination/SPagination.d.ts +151 -0
- package/dist/components/ui/pagination/index.d.ts +2 -0
- package/dist/components/ui/progress/SProgress.d.ts +62 -0
- package/dist/components/ui/progress/SProgressRange.d.ts +91 -0
- package/dist/components/ui/progress/index.d.ts +4 -0
- package/dist/components/ui/radio/SRadio.d.ts +58 -0
- package/dist/components/ui/radio/SRadioGroup.d.ts +52 -0
- package/dist/components/ui/radio/index.d.ts +2 -0
- package/dist/components/ui/stepper/SStepper.d.ts +83 -0
- package/dist/components/ui/stepper/SStepperContent.d.ts +24 -0
- package/dist/components/ui/stepper/SStepperDescription.d.ts +20 -0
- package/dist/components/ui/stepper/SStepperIndicator.d.ts +37 -0
- package/dist/components/ui/stepper/SStepperItem.d.ts +37 -0
- package/dist/components/ui/stepper/SStepperSeparator.d.ts +5 -0
- package/dist/components/ui/stepper/SStepperTitle.d.ts +20 -0
- package/dist/components/ui/stepper/SStepperTrigger.d.ts +22 -0
- package/dist/components/ui/stepper/index.d.ts +11 -0
- package/dist/components/ui/table/STableBody.d.ts +27 -0
- package/dist/components/ui/table/STableCell.d.ts +55 -0
- package/dist/components/ui/table/STableColumn.d.ts +87 -0
- package/dist/components/ui/table/STableEmpty.d.ts +54 -0
- package/dist/components/ui/table/STableHeader.d.ts +25 -0
- package/dist/components/ui/table/STableRow.d.ts +38 -0
- package/dist/components/ui/table/STableSkeleton.d.ts +29 -0
- package/dist/components/ui/table/index.d.ts +98 -0
- package/dist/components/ui/table/useDataTable.d.ts +80 -0
- package/dist/components/ui/tabs/STabPane.d.ts +31 -0
- package/dist/components/ui/tabs/STabsContent.d.ts +21 -0
- package/dist/components/ui/tabs/STabsIndicator.d.ts +9 -0
- package/dist/components/ui/tabs/STabsTrigger.d.ts +28 -0
- package/dist/components/ui/tabs/index.d.ts +6 -0
- package/dist/components/ui/toast/SToast.d.ts +49 -0
- package/dist/components/ui/toast/SToastContainer.d.ts +21 -0
- package/dist/components/ui/toast/index.d.ts +2 -0
- package/dist/composables/useAsync.d.ts +134 -0
- package/dist/composables/useClickOutside.d.ts +69 -0
- package/dist/composables/useClipboard.d.ts +46 -0
- package/dist/composables/useDebounce.d.ts +150 -0
- package/dist/composables/useDialog.d.ts +118 -0
- package/dist/composables/useForm.d.ts +204 -0
- package/dist/composables/useHotkey.d.ts +128 -0
- package/dist/composables/useIntersectionObserver.d.ts +156 -0
- package/dist/composables/useLocalStorage.d.ts +120 -0
- package/dist/composables/useMediaQuery.d.ts +115 -0
- package/dist/composables/useTheme.d.ts +8 -0
- package/dist/composables/useToast.d.ts +1619 -0
- package/dist/index.d.ts +71 -0
- package/dist/layouts/UILayout.d.ts +2 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/main.d.ts +0 -0
- package/dist/router.d.ts +2 -0
- package/dist/saka-ui.css +1 -0
- package/dist/saka-ui.js +18513 -0
- package/dist/saka-ui.umd.cjs +38 -0
- package/dist/views/docs/CustomizationView.d.ts +2 -0
- package/dist/views/docs/FormValidationView.d.ts +2 -0
- package/dist/views/docs/StylingGuideView.d.ts +2 -0
- package/dist/views/docs/UseAsyncView.d.ts +2 -0
- package/dist/views/docs/UseClickOutsideView.d.ts +124 -0
- package/dist/views/docs/UseClipboardView.d.ts +4 -0
- package/dist/views/docs/UseDebounceView.d.ts +2 -0
- package/dist/views/docs/UseHotkeyView.d.ts +205 -0
- package/dist/views/docs/UseIntersectionObserverView.d.ts +5 -0
- package/dist/views/docs/UseLocalStorageView.d.ts +2 -0
- package/dist/views/docs/UseMediaQueryView.d.ts +2 -0
- package/dist/views/docs/UseThemeView.d.ts +2 -0
- package/dist/views/examples/AuthFormView.d.ts +2 -0
- package/dist/views/examples/CreditCardFormView.d.ts +6 -0
- package/dist/views/examples/FormFieldExampleView.d.ts +2 -0
- package/dist/views/examples/ProjectFormView.d.ts +2 -0
- package/dist/views/ui/AccordionView.d.ts +2 -0
- package/dist/views/ui/AlertView.d.ts +2 -0
- package/dist/views/ui/AvatarView.d.ts +2 -0
- package/dist/views/ui/BadgeView.d.ts +2 -0
- package/dist/views/ui/BreadcrumbView.d.ts +2 -0
- package/dist/views/ui/ButtonView.d.ts +2 -0
- package/dist/views/ui/CardView.d.ts +2 -0
- package/dist/views/ui/CarouselView.d.ts +274 -0
- package/dist/views/ui/CheckboxView.d.ts +2 -0
- package/dist/views/ui/ChipView.d.ts +2 -0
- package/dist/views/ui/ColorPickerView.d.ts +2 -0
- package/dist/views/ui/DatePickerView.d.ts +2 -0
- package/dist/views/ui/DialogView.d.ts +2 -0
- package/dist/views/ui/DrawerView.d.ts +2 -0
- package/dist/views/ui/DropdownView.d.ts +2 -0
- package/dist/views/ui/GlassButtonView.d.ts +2 -0
- package/dist/views/ui/GlassCardView.d.ts +2 -0
- package/dist/views/ui/HomeView.d.ts +2 -0
- package/dist/views/ui/IconsView.d.ts +2 -0
- package/dist/views/ui/InputView.d.ts +2 -0
- package/dist/views/ui/KbdView.d.ts +2 -0
- package/dist/views/ui/ModalView.d.ts +2 -0
- package/dist/views/ui/MorphingCardView.d.ts +2 -0
- package/dist/views/ui/MorphingModalView.d.ts +2 -0
- package/dist/views/ui/OTPView.d.ts +206 -0
- package/dist/views/ui/PaginationView.d.ts +2 -0
- package/dist/views/ui/ProgressView.d.ts +2 -0
- package/dist/views/ui/RadioView.d.ts +2 -0
- package/dist/views/ui/SelectView.d.ts +2 -0
- package/dist/views/ui/SkeletonView.d.ts +2 -0
- package/dist/views/ui/StepperView.d.ts +2 -0
- package/dist/views/ui/SwitchView.d.ts +2 -0
- package/dist/views/ui/TableView.d.ts +2 -0
- package/dist/views/ui/TabsView.d.ts +2 -0
- package/dist/views/ui/ToastView.d.ts +2 -0
- package/dist/views/ui/TooltipView.d.ts +2 -0
- package/dist/vite.svg +1 -0
- package/package.json +64 -0
- package/registry/components/accordion.json +19 -0
- package/registry/components/alert.json +17 -0
- package/registry/components/avatar.json +18 -0
- package/registry/components/badge.json +14 -0
- package/registry/components/breadcrumb.json +24 -0
- package/registry/components/button.json +17 -0
- package/registry/components/card.json +23 -0
- package/registry/components/carousel.json +19 -0
- package/registry/components/checkbox.json +17 -0
- package/registry/components/chip.json +17 -0
- package/registry/components/color-picker.json +24 -0
- package/registry/components/date-picker.json +17 -0
- package/registry/components/drawer.json +26 -0
- package/registry/components/dropdown.json +21 -0
- package/registry/components/form.json +16 -0
- package/registry/components/glass-button.json +17 -0
- package/registry/components/icon.json +17 -0
- package/registry/components/input.json +17 -0
- package/registry/components/kbd.json +16 -0
- package/registry/components/modal.json +32 -0
- package/registry/components/option.json +16 -0
- package/registry/components/otp.json +23 -0
- package/registry/components/pagination.json +18 -0
- package/registry/components/progress.json +16 -0
- package/registry/components/radio.json +19 -0
- package/registry/components/select.json +17 -0
- package/registry/components/skeleton.json +14 -0
- package/registry/components/switch.json +17 -0
- package/registry/components/table.json +26 -0
- package/registry/components/tabs.json +19 -0
- package/registry/components/toast.json +19 -0
- package/registry/components/tooltip.json +14 -0
- package/registry/index.json +4 -0
- package/registry/source/components/ui/SAlert.vue +388 -0
- package/registry/source/components/ui/SBadge.vue +243 -0
- package/registry/source/components/ui/SButton.vue +387 -0
- package/registry/source/components/ui/SCheckbox.vue +310 -0
- package/registry/source/components/ui/SChip.vue +130 -0
- package/registry/source/components/ui/SDatePicker.vue +1290 -0
- package/registry/source/components/ui/SGlassButton.vue +547 -0
- package/registry/source/components/ui/SIcon.vue +78 -0
- package/registry/source/components/ui/SInput.vue +1054 -0
- package/registry/source/components/ui/SKbd.vue +96 -0
- package/registry/source/components/ui/SKbdShortcut.vue +36 -0
- package/registry/source/components/ui/SSelect.vue +1290 -0
- package/registry/source/components/ui/SSkeleton.vue +185 -0
- package/registry/source/components/ui/SSwitch.vue +275 -0
- package/registry/source/components/ui/STooltip.vue +491 -0
- package/registry/source/components/ui/accordion/SAccordion.vue +248 -0
- package/registry/source/components/ui/accordion/SAccordionItem.vue +353 -0
- package/registry/source/components/ui/accordion/index.ts +5 -0
- package/registry/source/components/ui/avatar/SAvatar.vue +169 -0
- package/registry/source/components/ui/avatar/SAvatarFallback.vue +66 -0
- package/registry/source/components/ui/avatar/SAvatarGroup.vue +69 -0
- package/registry/source/components/ui/avatar/SAvatarImage.vue +92 -0
- package/registry/source/components/ui/avatar/index.ts +5 -0
- package/registry/source/components/ui/breadcrumb/SBreadcrumb.vue +23 -0
- package/registry/source/components/ui/breadcrumb/SBreadcrumbEllipsis.vue +17 -0
- package/registry/source/components/ui/breadcrumb/SBreadcrumbItem.vue +14 -0
- package/registry/source/components/ui/breadcrumb/SBreadcrumbLink.vue +46 -0
- package/registry/source/components/ui/breadcrumb/SBreadcrumbList.vue +17 -0
- package/registry/source/components/ui/breadcrumb/SBreadcrumbPage.vue +15 -0
- package/registry/source/components/ui/breadcrumb/SBreadcrumbSeparator.vue +18 -0
- package/registry/source/components/ui/breadcrumb/index.ts +7 -0
- package/registry/source/components/ui/card/SCard.vue +517 -0
- package/registry/source/components/ui/card/SCardActions.vue +129 -0
- package/registry/source/components/ui/card/SCardContent.vue +117 -0
- package/registry/source/components/ui/card/SCardFooter.vue +103 -0
- package/registry/source/components/ui/card/SCardHeader.vue +163 -0
- package/registry/source/components/ui/card/SCardMedia.vue +312 -0
- package/registry/source/components/ui/card/index.ts +34 -0
- package/registry/source/components/ui/carousel/SCarousel.vue +1069 -0
- package/registry/source/components/ui/carousel/SCarouselSlide.vue +107 -0
- package/registry/source/components/ui/carousel/index.ts +3 -0
- package/registry/source/components/ui/color-picker/SColorPicker.vue +772 -0
- package/registry/source/components/ui/color-picker/SColorPickerAlphaSlider.vue +158 -0
- package/registry/source/components/ui/color-picker/SColorPickerCopy.vue +76 -0
- package/registry/source/components/ui/color-picker/SColorPickerEyeDropper.vue +68 -0
- package/registry/source/components/ui/color-picker/SColorPickerHueSlider.vue +138 -0
- package/registry/source/components/ui/color-picker/SColorPickerInputs.vue +227 -0
- package/registry/source/components/ui/color-picker/SColorPickerPresets.vue +87 -0
- package/registry/source/components/ui/color-picker/SColorPickerPreview.vue +46 -0
- package/registry/source/components/ui/color-picker/SColorPickerRecent.vue +74 -0
- package/registry/source/components/ui/color-picker/SColorPickerSpectrum.vue +149 -0
- package/registry/source/components/ui/color-picker/index.ts +11 -0
- package/registry/source/components/ui/drawer/SDrawer.vue +797 -0
- package/registry/source/components/ui/drawer/SDrawerClose.vue +64 -0
- package/registry/source/components/ui/drawer/SDrawerContent.vue +81 -0
- package/registry/source/components/ui/drawer/SDrawerDescription.vue +40 -0
- package/registry/source/components/ui/drawer/SDrawerFooter.vue +97 -0
- package/registry/source/components/ui/drawer/SDrawerHandle.vue +79 -0
- package/registry/source/components/ui/drawer/SDrawerHeader.vue +117 -0
- package/registry/source/components/ui/drawer/SDrawerTitle.vue +40 -0
- package/registry/source/components/ui/drawer/SDrawerTrigger.vue +51 -0
- package/registry/source/components/ui/drawer/index.ts +20 -0
- package/registry/source/components/ui/dropdown/SDropdown.vue +843 -0
- package/registry/source/components/ui/dropdown/SDropdownDivider.vue +23 -0
- package/registry/source/components/ui/dropdown/SDropdownGroup.vue +53 -0
- package/registry/source/components/ui/dropdown/SDropdownItem.vue +179 -0
- package/registry/source/components/ui/dropdown/index.ts +5 -0
- package/registry/source/components/ui/form/SForm.vue +84 -0
- package/registry/source/components/ui/form/SFormField.vue +78 -0
- package/registry/source/components/ui/form/index.ts +8 -0
- package/registry/source/components/ui/modal/SModal.vue +648 -0
- package/registry/source/components/ui/modal/SModalClose.vue +49 -0
- package/registry/source/components/ui/modal/SModalContent.vue +74 -0
- package/registry/source/components/ui/modal/SModalDescription.vue +39 -0
- package/registry/source/components/ui/modal/SModalFooter.vue +84 -0
- package/registry/source/components/ui/modal/SModalHeader.vue +107 -0
- package/registry/source/components/ui/modal/SModalTitle.vue +39 -0
- package/registry/source/components/ui/modal/SModalTrigger.vue +61 -0
- package/registry/source/components/ui/modal/SMorphingModal.vue +429 -0
- package/registry/source/components/ui/modal/SMorphingModalClose.vue +42 -0
- package/registry/source/components/ui/modal/SMorphingModalDescription.vue +49 -0
- package/registry/source/components/ui/modal/SMorphingModalImage.vue +44 -0
- package/registry/source/components/ui/modal/SMorphingModalSubtitle.vue +29 -0
- package/registry/source/components/ui/modal/SMorphingModalTitle.vue +34 -0
- package/registry/source/components/ui/modal/SMorphingModalTrigger.vue +95 -0
- package/registry/source/components/ui/modal/index.ts +32 -0
- package/registry/source/components/ui/option/SOption.vue +180 -0
- package/registry/source/components/ui/option/SOptionGroup.vue +77 -0
- package/registry/source/components/ui/option/index.ts +3 -0
- package/registry/source/components/ui/otp/SOTP.vue +843 -0
- package/registry/source/components/ui/otp/SOTPGroup.vue +29 -0
- package/registry/source/components/ui/otp/SOTPSeparator.vue +15 -0
- package/registry/source/components/ui/otp/SOTPSlot.vue +462 -0
- package/registry/source/components/ui/otp/index.ts +7 -0
- package/registry/source/components/ui/otp/types.ts +27 -0
- package/registry/source/components/ui/otp/useOTPContext.ts +62 -0
- package/registry/source/components/ui/pagination/SPagination.vue +923 -0
- package/registry/source/components/ui/pagination/index.ts +8 -0
- package/registry/source/components/ui/progress/SProgress.vue +635 -0
- package/registry/source/components/ui/progress/SProgressRange.vue +715 -0
- package/registry/source/components/ui/progress/index.ts +4 -0
- package/registry/source/components/ui/radio/SRadio.vue +407 -0
- package/registry/source/components/ui/radio/SRadioGroup.vue +200 -0
- package/registry/source/components/ui/radio/index.ts +3 -0
- package/registry/source/components/ui/table/SDataTable.vue +828 -0
- package/registry/source/components/ui/table/STableBody.vue +70 -0
- package/registry/source/components/ui/table/STableCell.vue +147 -0
- package/registry/source/components/ui/table/STableColumn.vue +120 -0
- package/registry/source/components/ui/table/STableEmpty.vue +159 -0
- package/registry/source/components/ui/table/STableHeader.vue +132 -0
- package/registry/source/components/ui/table/STableRow.vue +106 -0
- package/registry/source/components/ui/table/STableSkeleton.vue +208 -0
- package/registry/source/components/ui/table/index.ts +126 -0
- package/registry/source/components/ui/table/useDataTable.ts +519 -0
- package/registry/source/components/ui/tabs/STabPane.vue +130 -0
- package/registry/source/components/ui/tabs/STabs.vue +467 -0
- package/registry/source/components/ui/tabs/index.ts +7 -0
- package/registry/source/components/ui/toast/SToast.vue +261 -0
- package/registry/source/components/ui/toast/SToastContainer.vue +209 -0
- package/registry/source/components/ui/toast/index.ts +2 -0
- package/registry/source/composables/useForm.ts +960 -0
- package/registry/source/composables/useTheme.ts +86 -0
- package/registry/source/composables/useToast.ts +440 -0
- package/registry/source/lib/utils.ts +6 -0
|
@@ -0,0 +1,1290 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
|
3
|
+
import { cn } from '../../lib/utils'
|
|
4
|
+
|
|
5
|
+
defineOptions({ inheritAttrs: false })
|
|
6
|
+
|
|
7
|
+
export interface Props {
|
|
8
|
+
// Core
|
|
9
|
+
modelValue?: Date | Date[] | [Date, Date] | null
|
|
10
|
+
mode?: 'single' | 'range' | 'multiple'
|
|
11
|
+
|
|
12
|
+
// Display
|
|
13
|
+
variant?: 'outlined' | 'filled' | 'ghost'
|
|
14
|
+
size?: 'small' | 'medium' | 'large'
|
|
15
|
+
color?: string
|
|
16
|
+
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'full'
|
|
17
|
+
|
|
18
|
+
// Formatting
|
|
19
|
+
format?: string
|
|
20
|
+
placeholder?: string
|
|
21
|
+
|
|
22
|
+
// Constraints
|
|
23
|
+
minDate?: Date
|
|
24
|
+
maxDate?: Date
|
|
25
|
+
disabledDates?: Date[] | ((date: Date) => boolean)
|
|
26
|
+
disabledWeekdays?: number[]
|
|
27
|
+
|
|
28
|
+
// Behavior
|
|
29
|
+
disabled?: boolean
|
|
30
|
+
loading?: boolean
|
|
31
|
+
readonly?: boolean
|
|
32
|
+
clearable?: boolean
|
|
33
|
+
closeOnSelect?: boolean
|
|
34
|
+
teleport?: boolean | string
|
|
35
|
+
|
|
36
|
+
// Calendar Options
|
|
37
|
+
showWeekNumbers?: boolean
|
|
38
|
+
firstDayOfWeek?: 0 | 1 | 2 | 3 | 4 | 5 | 6
|
|
39
|
+
monthsToShow?: 1 | 2
|
|
40
|
+
showToday?: boolean
|
|
41
|
+
|
|
42
|
+
// Time Picker
|
|
43
|
+
enableTime?: boolean
|
|
44
|
+
timeFormat?: '12h' | '24h'
|
|
45
|
+
minuteStep?: number
|
|
46
|
+
|
|
47
|
+
// Labels
|
|
48
|
+
label?: string
|
|
49
|
+
labelPlacement?: 'top' | 'left'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
53
|
+
modelValue: null,
|
|
54
|
+
mode: 'single',
|
|
55
|
+
variant: 'outlined',
|
|
56
|
+
size: 'medium',
|
|
57
|
+
color: 'var(--s-primary)',
|
|
58
|
+
rounded: 'md',
|
|
59
|
+
format: 'MMM dd, yyyy',
|
|
60
|
+
placeholder: 'Select date...',
|
|
61
|
+
minDate: undefined,
|
|
62
|
+
maxDate: undefined,
|
|
63
|
+
disabledDates: undefined,
|
|
64
|
+
disabledWeekdays: () => [],
|
|
65
|
+
disabled: false,
|
|
66
|
+
loading: false,
|
|
67
|
+
readonly: false,
|
|
68
|
+
clearable: true,
|
|
69
|
+
closeOnSelect: true,
|
|
70
|
+
teleport: true,
|
|
71
|
+
showWeekNumbers: false,
|
|
72
|
+
firstDayOfWeek: 0,
|
|
73
|
+
monthsToShow: 1,
|
|
74
|
+
showToday: true,
|
|
75
|
+
enableTime: false,
|
|
76
|
+
timeFormat: '24h',
|
|
77
|
+
minuteStep: 5,
|
|
78
|
+
label: undefined,
|
|
79
|
+
labelPlacement: 'top'
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const emit = defineEmits<{
|
|
83
|
+
'update:modelValue': [value: Date | Date[] | [Date, Date] | null]
|
|
84
|
+
open: []
|
|
85
|
+
close: []
|
|
86
|
+
clear: []
|
|
87
|
+
monthChange: [month: number, year: number]
|
|
88
|
+
yearChange: [year: number]
|
|
89
|
+
}>()
|
|
90
|
+
|
|
91
|
+
// Active text class: use primary-foreground for default primary, white for custom colors
|
|
92
|
+
const activeTextClass = computed(() => props.color === 'var(--s-primary)' ? 'text-primary-foreground' : 'text-white')
|
|
93
|
+
|
|
94
|
+
// Refs
|
|
95
|
+
const triggerRef = ref<HTMLElement | null>(null)
|
|
96
|
+
const calendarRef = ref<HTMLElement | null>(null)
|
|
97
|
+
const isOpen = ref(false)
|
|
98
|
+
const isFocused = ref(false)
|
|
99
|
+
|
|
100
|
+
// Calendar state
|
|
101
|
+
const viewDate = ref(new Date())
|
|
102
|
+
const hoverDate = ref<Date | null>(null)
|
|
103
|
+
const rangeStart = ref<Date | null>(null)
|
|
104
|
+
|
|
105
|
+
// Month/Year picker state
|
|
106
|
+
const showMonthPicker = ref(false)
|
|
107
|
+
const showYearPicker = ref(false)
|
|
108
|
+
const yearPickerRef = ref<HTMLElement | null>(null)
|
|
109
|
+
|
|
110
|
+
// Time state
|
|
111
|
+
const selectedHour = ref(0)
|
|
112
|
+
const selectedMinute = ref(0)
|
|
113
|
+
const isAM = ref(true)
|
|
114
|
+
|
|
115
|
+
// Ripple state
|
|
116
|
+
const ripples = ref<{ id: number; x: number; y: number; size: number }[]>([])
|
|
117
|
+
let rippleId = 0
|
|
118
|
+
|
|
119
|
+
// Dropdown position
|
|
120
|
+
const dropdownPosition = ref<{
|
|
121
|
+
top?: number
|
|
122
|
+
bottom?: number
|
|
123
|
+
left: number
|
|
124
|
+
width: number
|
|
125
|
+
placement: 'top' | 'bottom'
|
|
126
|
+
}>({ top: 0, left: 0, width: 0, placement: 'bottom' })
|
|
127
|
+
|
|
128
|
+
// Month and weekday names
|
|
129
|
+
const monthNames = [
|
|
130
|
+
'January', 'February', 'March', 'April', 'May', 'June',
|
|
131
|
+
'July', 'August', 'September', 'October', 'November', 'December'
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
const monthNamesShort = [
|
|
135
|
+
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
|
136
|
+
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
const weekdayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
|
140
|
+
|
|
141
|
+
// Computed
|
|
142
|
+
const weekdaysOrdered = computed(() => {
|
|
143
|
+
const days = [...weekdayNames]
|
|
144
|
+
for (let i = 0; i < props.firstDayOfWeek; i++) {
|
|
145
|
+
days.push(days.shift()!)
|
|
146
|
+
}
|
|
147
|
+
return days
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const displayValue = computed(() => {
|
|
151
|
+
if (!props.modelValue) return ''
|
|
152
|
+
|
|
153
|
+
if (props.mode === 'single' && props.modelValue instanceof Date) {
|
|
154
|
+
return formatDate(props.modelValue)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (props.mode === 'range' && Array.isArray(props.modelValue) && props.modelValue.length === 2) {
|
|
158
|
+
const [start, end] = props.modelValue as [Date, Date]
|
|
159
|
+
return `${formatDate(start)} - ${formatDate(end)}`
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (props.mode === 'multiple' && Array.isArray(props.modelValue)) {
|
|
163
|
+
const dates = props.modelValue as Date[]
|
|
164
|
+
if (dates.length <= 2) {
|
|
165
|
+
return dates.map(d => formatDate(d)).join(', ')
|
|
166
|
+
}
|
|
167
|
+
return `${dates.length} dates selected`
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return ''
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const hasValue = computed(() => {
|
|
174
|
+
if (!props.modelValue) return false
|
|
175
|
+
if (Array.isArray(props.modelValue)) return props.modelValue.length > 0
|
|
176
|
+
return true
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
const calendarDays = computed(() => {
|
|
180
|
+
const year = viewDate.value.getFullYear()
|
|
181
|
+
const month = viewDate.value.getMonth()
|
|
182
|
+
|
|
183
|
+
const firstDay = new Date(year, month, 1)
|
|
184
|
+
const lastDay = new Date(year, month + 1, 0)
|
|
185
|
+
|
|
186
|
+
const days: { date: Date; isCurrentMonth: boolean; isToday: boolean; isSelected: boolean; isInRange: boolean; isRangeStart: boolean; isRangeEnd: boolean; isDisabled: boolean }[] = []
|
|
187
|
+
|
|
188
|
+
// Previous month days
|
|
189
|
+
let startDayOfWeek = firstDay.getDay() - props.firstDayOfWeek
|
|
190
|
+
if (startDayOfWeek < 0) startDayOfWeek += 7
|
|
191
|
+
|
|
192
|
+
for (let i = startDayOfWeek - 1; i >= 0; i--) {
|
|
193
|
+
const date = new Date(year, month, -i)
|
|
194
|
+
days.push({
|
|
195
|
+
date,
|
|
196
|
+
isCurrentMonth: false,
|
|
197
|
+
isToday: isToday(date),
|
|
198
|
+
isSelected: isSelected(date),
|
|
199
|
+
isInRange: isInRange(date),
|
|
200
|
+
isRangeStart: isRangeStart(date),
|
|
201
|
+
isRangeEnd: isRangeEnd(date),
|
|
202
|
+
isDisabled: isDateDisabled(date)
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Current month days
|
|
207
|
+
for (let i = 1; i <= lastDay.getDate(); i++) {
|
|
208
|
+
const date = new Date(year, month, i)
|
|
209
|
+
days.push({
|
|
210
|
+
date,
|
|
211
|
+
isCurrentMonth: true,
|
|
212
|
+
isToday: isToday(date),
|
|
213
|
+
isSelected: isSelected(date),
|
|
214
|
+
isInRange: isInRange(date),
|
|
215
|
+
isRangeStart: isRangeStart(date),
|
|
216
|
+
isRangeEnd: isRangeEnd(date),
|
|
217
|
+
isDisabled: isDateDisabled(date)
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Next month days
|
|
222
|
+
const remainingDays = 42 - days.length
|
|
223
|
+
for (let i = 1; i <= remainingDays; i++) {
|
|
224
|
+
const date = new Date(year, month + 1, i)
|
|
225
|
+
days.push({
|
|
226
|
+
date,
|
|
227
|
+
isCurrentMonth: false,
|
|
228
|
+
isToday: isToday(date),
|
|
229
|
+
isSelected: isSelected(date),
|
|
230
|
+
isInRange: isInRange(date),
|
|
231
|
+
isRangeStart: isRangeStart(date),
|
|
232
|
+
isRangeEnd: isRangeEnd(date),
|
|
233
|
+
isDisabled: isDateDisabled(date)
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return days
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
const weeks = computed(() => {
|
|
241
|
+
const result: typeof calendarDays.value[] = []
|
|
242
|
+
for (let i = 0; i < calendarDays.value.length; i += 7) {
|
|
243
|
+
result.push(calendarDays.value.slice(i, i + 7))
|
|
244
|
+
}
|
|
245
|
+
return result
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
const years = computed(() => {
|
|
249
|
+
const currentYear = new Date().getFullYear()
|
|
250
|
+
const years: number[] = []
|
|
251
|
+
|
|
252
|
+
// Default range: 100 years before, 50 years after
|
|
253
|
+
let startYear = currentYear - 100
|
|
254
|
+
let endYear = currentYear + 50
|
|
255
|
+
|
|
256
|
+
// Respect maxDate if provided
|
|
257
|
+
if (props.maxDate) {
|
|
258
|
+
endYear = Math.min(endYear, props.maxDate.getFullYear())
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Respect minDate if provided
|
|
262
|
+
if (props.minDate) {
|
|
263
|
+
startYear = Math.max(startYear, props.minDate.getFullYear())
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
for (let i = startYear; i <= endYear; i++) {
|
|
267
|
+
years.push(i)
|
|
268
|
+
}
|
|
269
|
+
return years
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
// Helper functions
|
|
273
|
+
function formatDate(date: Date): string {
|
|
274
|
+
if (props.enableTime) {
|
|
275
|
+
const hours = date.getHours()
|
|
276
|
+
const minutes = date.getMinutes()
|
|
277
|
+
const timeStr = props.timeFormat === '12h'
|
|
278
|
+
? `${hours % 12 || 12}:${String(minutes).padStart(2, '0')} ${hours >= 12 ? 'PM' : 'AM'}`
|
|
279
|
+
: `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`
|
|
280
|
+
return `${monthNamesShort[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()} ${timeStr}`
|
|
281
|
+
}
|
|
282
|
+
return `${monthNamesShort[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}`
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function isSameDay(date1: Date, date2: Date): boolean {
|
|
286
|
+
return date1.getFullYear() === date2.getFullYear() &&
|
|
287
|
+
date1.getMonth() === date2.getMonth() &&
|
|
288
|
+
date1.getDate() === date2.getDate()
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function isToday(date: Date): boolean {
|
|
292
|
+
return isSameDay(date, new Date())
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function isSelected(date: Date): boolean {
|
|
296
|
+
if (!props.modelValue) return false
|
|
297
|
+
|
|
298
|
+
if (props.mode === 'single' && props.modelValue instanceof Date) {
|
|
299
|
+
return isSameDay(date, props.modelValue)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (props.mode === 'range' && Array.isArray(props.modelValue)) {
|
|
303
|
+
const [start, end] = props.modelValue as [Date, Date]
|
|
304
|
+
if (start && isSameDay(date, start)) return true
|
|
305
|
+
if (end && isSameDay(date, end)) return true
|
|
306
|
+
return false
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (props.mode === 'multiple' && Array.isArray(props.modelValue)) {
|
|
310
|
+
return (props.modelValue as Date[]).some(d => isSameDay(date, d))
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return false
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function isInRange(date: Date): boolean {
|
|
317
|
+
if (props.mode !== 'range') return false
|
|
318
|
+
|
|
319
|
+
let start: Date | null = null
|
|
320
|
+
let end: Date | null = null
|
|
321
|
+
|
|
322
|
+
if (rangeStart.value && hoverDate.value) {
|
|
323
|
+
start = rangeStart.value < hoverDate.value ? rangeStart.value : hoverDate.value
|
|
324
|
+
end = rangeStart.value < hoverDate.value ? hoverDate.value : rangeStart.value
|
|
325
|
+
} else if (Array.isArray(props.modelValue) && props.modelValue.length === 2) {
|
|
326
|
+
[start, end] = props.modelValue as [Date, Date]
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!start || !end) return false
|
|
330
|
+
|
|
331
|
+
return date > start && date < end
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function isRangeStart(date: Date): boolean {
|
|
335
|
+
if (props.mode !== 'range') return false
|
|
336
|
+
|
|
337
|
+
if (rangeStart.value && !hoverDate.value) {
|
|
338
|
+
return isSameDay(date, rangeStart.value)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (Array.isArray(props.modelValue) && props.modelValue.length >= 1) {
|
|
342
|
+
const start = props.modelValue[0] as Date
|
|
343
|
+
return isSameDay(date, start)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return false
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function isRangeEnd(date: Date): boolean {
|
|
350
|
+
if (props.mode !== 'range') return false
|
|
351
|
+
|
|
352
|
+
if (Array.isArray(props.modelValue) && props.modelValue.length === 2) {
|
|
353
|
+
const end = props.modelValue[1] as Date
|
|
354
|
+
return isSameDay(date, end)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return false
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function isDateDisabled(date: Date): boolean {
|
|
361
|
+
// Check weekday
|
|
362
|
+
if (props.disabledWeekdays?.includes(date.getDay())) {
|
|
363
|
+
return true
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Check min date
|
|
367
|
+
if (props.minDate) {
|
|
368
|
+
const min = new Date(props.minDate)
|
|
369
|
+
min.setHours(0, 0, 0, 0)
|
|
370
|
+
const checkDate = new Date(date)
|
|
371
|
+
checkDate.setHours(0, 0, 0, 0)
|
|
372
|
+
if (checkDate < min) return true
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Check max date
|
|
376
|
+
if (props.maxDate) {
|
|
377
|
+
const max = new Date(props.maxDate)
|
|
378
|
+
max.setHours(0, 0, 0, 0)
|
|
379
|
+
const checkDate = new Date(date)
|
|
380
|
+
checkDate.setHours(0, 0, 0, 0)
|
|
381
|
+
if (checkDate > max) return true
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Check disabled dates
|
|
385
|
+
if (props.disabledDates) {
|
|
386
|
+
if (typeof props.disabledDates === 'function') {
|
|
387
|
+
return props.disabledDates(date)
|
|
388
|
+
}
|
|
389
|
+
return props.disabledDates.some(d => isSameDay(date, d))
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return false
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Actions
|
|
396
|
+
function createRipple(event: MouseEvent, element: HTMLElement) {
|
|
397
|
+
const rect = element.getBoundingClientRect()
|
|
398
|
+
const size = Math.max(rect.width, rect.height) * 2
|
|
399
|
+
const x = event.clientX - rect.left - size / 2
|
|
400
|
+
const y = event.clientY - rect.top - size / 2
|
|
401
|
+
|
|
402
|
+
const id = rippleId++
|
|
403
|
+
ripples.value.push({ id, x, y, size })
|
|
404
|
+
|
|
405
|
+
setTimeout(() => {
|
|
406
|
+
ripples.value = ripples.value.filter(r => r.id !== id)
|
|
407
|
+
}, 600)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function selectDate(date: Date, event?: MouseEvent) {
|
|
411
|
+
if (isDateDisabled(date)) return
|
|
412
|
+
|
|
413
|
+
if (event?.currentTarget) {
|
|
414
|
+
createRipple(event, event.currentTarget as HTMLElement)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const selectedDate = new Date(date)
|
|
418
|
+
if (props.enableTime) {
|
|
419
|
+
selectedDate.setHours(selectedHour.value, selectedMinute.value)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (props.mode === 'single') {
|
|
423
|
+
emit('update:modelValue', selectedDate)
|
|
424
|
+
if (props.closeOnSelect && !props.enableTime) {
|
|
425
|
+
close()
|
|
426
|
+
}
|
|
427
|
+
} else if (props.mode === 'range') {
|
|
428
|
+
if (!rangeStart.value) {
|
|
429
|
+
rangeStart.value = selectedDate
|
|
430
|
+
emit('update:modelValue', [selectedDate] as any)
|
|
431
|
+
} else {
|
|
432
|
+
const start = rangeStart.value
|
|
433
|
+
const end = selectedDate
|
|
434
|
+
const sortedRange: [Date, Date] = start < end ? [start, end] : [end, start]
|
|
435
|
+
emit('update:modelValue', sortedRange)
|
|
436
|
+
rangeStart.value = null
|
|
437
|
+
if (props.closeOnSelect && !props.enableTime) {
|
|
438
|
+
close()
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
} else if (props.mode === 'multiple') {
|
|
442
|
+
const current = (props.modelValue as Date[]) || []
|
|
443
|
+
const existingIndex = current.findIndex(d => isSameDay(d, selectedDate))
|
|
444
|
+
|
|
445
|
+
if (existingIndex >= 0) {
|
|
446
|
+
const newDates = [...current]
|
|
447
|
+
newDates.splice(existingIndex, 1)
|
|
448
|
+
emit('update:modelValue', newDates)
|
|
449
|
+
} else {
|
|
450
|
+
emit('update:modelValue', [...current, selectedDate])
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function previousMonth() {
|
|
456
|
+
const newDate = new Date(viewDate.value)
|
|
457
|
+
newDate.setMonth(newDate.getMonth() - 1)
|
|
458
|
+
viewDate.value = newDate
|
|
459
|
+
emit('monthChange', newDate.getMonth(), newDate.getFullYear())
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function nextMonth() {
|
|
463
|
+
const newDate = new Date(viewDate.value)
|
|
464
|
+
newDate.setMonth(newDate.getMonth() + 1)
|
|
465
|
+
viewDate.value = newDate
|
|
466
|
+
emit('monthChange', newDate.getMonth(), newDate.getFullYear())
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function setMonth(month: number) {
|
|
470
|
+
const newDate = new Date(viewDate.value)
|
|
471
|
+
newDate.setMonth(month)
|
|
472
|
+
viewDate.value = newDate
|
|
473
|
+
emit('monthChange', month, newDate.getFullYear())
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function setYear(year: number) {
|
|
477
|
+
const newDate = new Date(viewDate.value)
|
|
478
|
+
newDate.setFullYear(year)
|
|
479
|
+
viewDate.value = newDate
|
|
480
|
+
emit('yearChange', year)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function goToToday() {
|
|
484
|
+
viewDate.value = new Date()
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function clear(event?: Event) {
|
|
488
|
+
event?.stopPropagation()
|
|
489
|
+
emit('update:modelValue', null)
|
|
490
|
+
rangeStart.value = null
|
|
491
|
+
emit('clear')
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function updateDropdownPosition() {
|
|
495
|
+
if (!triggerRef.value) return
|
|
496
|
+
|
|
497
|
+
const rect = triggerRef.value.getBoundingClientRect()
|
|
498
|
+
const viewportHeight = window.innerHeight
|
|
499
|
+
const spaceBelow = viewportHeight - rect.bottom
|
|
500
|
+
const spaceAbove = rect.top
|
|
501
|
+
const dropdownHeight = 380
|
|
502
|
+
|
|
503
|
+
if (spaceBelow >= dropdownHeight || spaceBelow >= spaceAbove) {
|
|
504
|
+
dropdownPosition.value = {
|
|
505
|
+
top: rect.bottom + 8,
|
|
506
|
+
left: rect.left,
|
|
507
|
+
width: Math.max(rect.width, 320),
|
|
508
|
+
placement: 'bottom'
|
|
509
|
+
}
|
|
510
|
+
} else {
|
|
511
|
+
dropdownPosition.value = {
|
|
512
|
+
bottom: viewportHeight - rect.top + 8,
|
|
513
|
+
left: rect.left,
|
|
514
|
+
width: Math.max(rect.width, 320),
|
|
515
|
+
placement: 'top'
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function open() {
|
|
521
|
+
if (props.disabled || props.readonly) return
|
|
522
|
+
isOpen.value = true
|
|
523
|
+
updateDropdownPosition()
|
|
524
|
+
emit('open')
|
|
525
|
+
|
|
526
|
+
// Set view to current selection or today
|
|
527
|
+
if (props.modelValue instanceof Date) {
|
|
528
|
+
viewDate.value = new Date(props.modelValue)
|
|
529
|
+
if (props.enableTime) {
|
|
530
|
+
selectedHour.value = props.modelValue.getHours()
|
|
531
|
+
selectedMinute.value = props.modelValue.getMinutes()
|
|
532
|
+
}
|
|
533
|
+
} else if (Array.isArray(props.modelValue) && props.modelValue.length > 0) {
|
|
534
|
+
viewDate.value = new Date(props.modelValue[0])
|
|
535
|
+
if (props.enableTime && props.modelValue[0] instanceof Date) {
|
|
536
|
+
selectedHour.value = props.modelValue[0].getHours()
|
|
537
|
+
selectedMinute.value = props.modelValue[0].getMinutes()
|
|
538
|
+
}
|
|
539
|
+
} else {
|
|
540
|
+
viewDate.value = new Date()
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
nextTick(() => {
|
|
544
|
+
window.addEventListener('scroll', updateDropdownPosition, true)
|
|
545
|
+
window.addEventListener('resize', updateDropdownPosition)
|
|
546
|
+
})
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function close() {
|
|
550
|
+
isOpen.value = false
|
|
551
|
+
if (props.mode === 'range' && rangeStart.value) {
|
|
552
|
+
// If we only selected start, clear the selection
|
|
553
|
+
rangeStart.value = null
|
|
554
|
+
}
|
|
555
|
+
emit('close')
|
|
556
|
+
window.removeEventListener('scroll', updateDropdownPosition, true)
|
|
557
|
+
window.removeEventListener('resize', updateDropdownPosition)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function toggle() {
|
|
561
|
+
if (isOpen.value) {
|
|
562
|
+
close()
|
|
563
|
+
} else {
|
|
564
|
+
open()
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function handleFocus() {
|
|
569
|
+
isFocused.value = true
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function handleBlur(event: FocusEvent) {
|
|
573
|
+
const relatedTarget = event.relatedTarget as HTMLElement
|
|
574
|
+
if (calendarRef.value?.contains(relatedTarget)) return
|
|
575
|
+
isFocused.value = false
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function handleClickOutside(event: MouseEvent) {
|
|
579
|
+
const target = event.target as HTMLElement
|
|
580
|
+
if (triggerRef.value?.contains(target)) return
|
|
581
|
+
if (calendarRef.value?.contains(target)) return
|
|
582
|
+
close()
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
586
|
+
if (event.key === 'Escape') {
|
|
587
|
+
close()
|
|
588
|
+
triggerRef.value?.focus()
|
|
589
|
+
} else if (event.key === 'Enter' || event.key === ' ') {
|
|
590
|
+
if (!isOpen.value) {
|
|
591
|
+
event.preventDefault()
|
|
592
|
+
open()
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function handleDayHover(date: Date) {
|
|
598
|
+
if (props.mode === 'range' && rangeStart.value) {
|
|
599
|
+
hoverDate.value = date
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function handleDayLeave() {
|
|
604
|
+
hoverDate.value = null
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Time picker
|
|
608
|
+
function adjustHour(delta: number) {
|
|
609
|
+
selectedHour.value = (selectedHour.value + delta + 24) % 24
|
|
610
|
+
isAM.value = selectedHour.value < 12
|
|
611
|
+
updateTimeOnSelection()
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function adjustMinute(delta: number) {
|
|
615
|
+
selectedMinute.value = (selectedMinute.value + delta + 60) % 60
|
|
616
|
+
updateTimeOnSelection()
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function handleHourInput(event: Event) {
|
|
620
|
+
const input = event.target as HTMLInputElement
|
|
621
|
+
const rawValue = input.value.replace(/\D/g, '') // Only digits
|
|
622
|
+
|
|
623
|
+
if (!rawValue) {
|
|
624
|
+
return
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
let value = parseInt(rawValue, 10)
|
|
628
|
+
|
|
629
|
+
if (props.timeFormat === '12h') {
|
|
630
|
+
// For 12h format, input is 1-12
|
|
631
|
+
value = Math.max(1, Math.min(12, value))
|
|
632
|
+
// Convert to 24h format for internal storage
|
|
633
|
+
if (isAM.value) {
|
|
634
|
+
selectedHour.value = value === 12 ? 0 : value
|
|
635
|
+
} else {
|
|
636
|
+
selectedHour.value = value === 12 ? 12 : value + 12
|
|
637
|
+
}
|
|
638
|
+
} else {
|
|
639
|
+
// For 24h format, input is 0-23
|
|
640
|
+
value = Math.max(0, Math.min(23, value))
|
|
641
|
+
selectedHour.value = value
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
updateTimeOnSelection()
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function handleMinuteInput(event: Event) {
|
|
648
|
+
const input = event.target as HTMLInputElement
|
|
649
|
+
const rawValue = input.value.replace(/\D/g, '') // Only digits
|
|
650
|
+
|
|
651
|
+
if (!rawValue) {
|
|
652
|
+
return
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
let value = parseInt(rawValue, 10)
|
|
656
|
+
value = Math.max(0, Math.min(59, value))
|
|
657
|
+
selectedMinute.value = value
|
|
658
|
+
updateTimeOnSelection()
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function handleHourWheel(event: WheelEvent) {
|
|
662
|
+
event.preventDefault()
|
|
663
|
+
const delta = event.deltaY < 0 ? 1 : -1
|
|
664
|
+
adjustHour(delta)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function handleMinuteWheel(event: WheelEvent) {
|
|
668
|
+
event.preventDefault()
|
|
669
|
+
const delta = event.deltaY < 0 ? props.minuteStep : -props.minuteStep
|
|
670
|
+
adjustMinute(delta)
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function toggleAMPM() {
|
|
674
|
+
isAM.value = !isAM.value
|
|
675
|
+
if (isAM.value && selectedHour.value >= 12) {
|
|
676
|
+
selectedHour.value -= 12
|
|
677
|
+
} else if (!isAM.value && selectedHour.value < 12) {
|
|
678
|
+
selectedHour.value += 12
|
|
679
|
+
}
|
|
680
|
+
updateTimeOnSelection()
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function displayHour(): string {
|
|
684
|
+
if (props.timeFormat === '12h') {
|
|
685
|
+
const h = selectedHour.value % 12 || 12
|
|
686
|
+
return String(h).padStart(2, '0')
|
|
687
|
+
}
|
|
688
|
+
return String(selectedHour.value).padStart(2, '0')
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Month/Year picker methods
|
|
692
|
+
function selectMonthFromPicker(month: number) {
|
|
693
|
+
setMonth(month)
|
|
694
|
+
showMonthPicker.value = false
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function selectYearFromPicker(year: number) {
|
|
698
|
+
setYear(year)
|
|
699
|
+
showYearPicker.value = false
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function toggleMonthPicker() {
|
|
703
|
+
showYearPicker.value = false
|
|
704
|
+
showMonthPicker.value = !showMonthPicker.value
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function toggleYearPicker() {
|
|
708
|
+
showMonthPicker.value = false
|
|
709
|
+
showYearPicker.value = !showYearPicker.value
|
|
710
|
+
|
|
711
|
+
// Scroll to current year when opening
|
|
712
|
+
if (showYearPicker.value) {
|
|
713
|
+
nextTick(() => {
|
|
714
|
+
if (yearPickerRef.value) {
|
|
715
|
+
const currentYearBtn = yearPickerRef.value.querySelector('[data-current-year]') as HTMLElement
|
|
716
|
+
if (currentYearBtn) {
|
|
717
|
+
currentYearBtn.scrollIntoView({ block: 'center' })
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
})
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function updateTimeOnSelection() {
|
|
725
|
+
if (props.mode === 'single' && props.modelValue instanceof Date) {
|
|
726
|
+
const newDate = new Date(props.modelValue)
|
|
727
|
+
newDate.setHours(selectedHour.value, selectedMinute.value)
|
|
728
|
+
emit('update:modelValue', newDate)
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Watchers
|
|
733
|
+
watch(isOpen, (val) => {
|
|
734
|
+
if (val) {
|
|
735
|
+
nextTick(() => {
|
|
736
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
737
|
+
})
|
|
738
|
+
} else {
|
|
739
|
+
document.removeEventListener('mousedown', handleClickOutside)
|
|
740
|
+
}
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
// Lifecycle
|
|
744
|
+
onMounted(() => {
|
|
745
|
+
if (props.modelValue instanceof Date) {
|
|
746
|
+
viewDate.value = new Date(props.modelValue)
|
|
747
|
+
} else if (Array.isArray(props.modelValue) && props.modelValue.length > 0 && props.modelValue[0] instanceof Date) {
|
|
748
|
+
viewDate.value = new Date(props.modelValue[0])
|
|
749
|
+
}
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
onBeforeUnmount(() => {
|
|
753
|
+
document.removeEventListener('mousedown', handleClickOutside)
|
|
754
|
+
window.removeEventListener('scroll', updateDropdownPosition, true)
|
|
755
|
+
window.removeEventListener('resize', updateDropdownPosition)
|
|
756
|
+
})
|
|
757
|
+
|
|
758
|
+
// Size configurations
|
|
759
|
+
const sizeConfig = computed(() => {
|
|
760
|
+
const sizes = {
|
|
761
|
+
small: {
|
|
762
|
+
trigger: 'min-h-8 text-xs',
|
|
763
|
+
padding: 'px-2 py-0.5',
|
|
764
|
+
icon: 'text-sm',
|
|
765
|
+
day: 'w-7 h-7 text-xs',
|
|
766
|
+
label: 'text-xs mb-1'
|
|
767
|
+
},
|
|
768
|
+
medium: {
|
|
769
|
+
trigger: 'min-h-10 text-sm',
|
|
770
|
+
padding: 'px-2 py-0.5',
|
|
771
|
+
icon: 'text-base',
|
|
772
|
+
day: 'w-9 h-9 text-sm',
|
|
773
|
+
label: 'text-sm mb-1.5'
|
|
774
|
+
},
|
|
775
|
+
large: {
|
|
776
|
+
trigger: 'min-h-12 text-base',
|
|
777
|
+
padding: 'px-2.5 py-0.5',
|
|
778
|
+
icon: 'text-lg',
|
|
779
|
+
day: 'w-10 h-10 text-base',
|
|
780
|
+
label: 'text-base mb-2'
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return sizes[props.size]
|
|
784
|
+
})
|
|
785
|
+
|
|
786
|
+
const roundedConfig = computed(() => {
|
|
787
|
+
const radii = {
|
|
788
|
+
none: 'rounded-none',
|
|
789
|
+
sm: 'rounded',
|
|
790
|
+
md: 'rounded-lg',
|
|
791
|
+
lg: 'rounded-xl',
|
|
792
|
+
full: 'rounded-full'
|
|
793
|
+
}
|
|
794
|
+
return radii[props.rounded]
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
const variantStyles = computed(() => {
|
|
798
|
+
const color = props.color
|
|
799
|
+
|
|
800
|
+
if (props.variant === 'filled') {
|
|
801
|
+
return {
|
|
802
|
+
'--dp-bg': 'var(--s-accent)',
|
|
803
|
+
'--dp-bg-focus': 'var(--s-accent)',
|
|
804
|
+
'--dp-border': 'transparent',
|
|
805
|
+
'--dp-border-focus': color
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (props.variant === 'ghost') {
|
|
810
|
+
return {
|
|
811
|
+
'--dp-bg': 'transparent',
|
|
812
|
+
'--dp-bg-focus': 'var(--s-accent)',
|
|
813
|
+
'--dp-border': 'transparent',
|
|
814
|
+
'--dp-border-focus': color
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// outlined (default)
|
|
819
|
+
return {
|
|
820
|
+
'--dp-bg': 'var(--s-background)',
|
|
821
|
+
'--dp-bg-focus': 'var(--s-background)',
|
|
822
|
+
'--dp-border': 'var(--s-border)',
|
|
823
|
+
'--dp-border-focus': color
|
|
824
|
+
}
|
|
825
|
+
})
|
|
826
|
+
|
|
827
|
+
const layoutClasses = computed(() => {
|
|
828
|
+
if (props.labelPlacement === 'left') {
|
|
829
|
+
return 'flex items-center gap-3'
|
|
830
|
+
}
|
|
831
|
+
return 'flex flex-col'
|
|
832
|
+
})
|
|
833
|
+
|
|
834
|
+
const teleportTarget = computed(() => {
|
|
835
|
+
if (props.teleport === true) return 'body'
|
|
836
|
+
if (typeof props.teleport === 'string') return props.teleport
|
|
837
|
+
return undefined
|
|
838
|
+
})
|
|
839
|
+
</script>
|
|
840
|
+
|
|
841
|
+
<template>
|
|
842
|
+
<div v-bind="$attrs" :class="cn(layoutClasses)">
|
|
843
|
+
<!-- Label -->
|
|
844
|
+
<label
|
|
845
|
+
v-if="label"
|
|
846
|
+
:class="[
|
|
847
|
+
'font-medium text-foreground shrink-0',
|
|
848
|
+
sizeConfig.label,
|
|
849
|
+
labelPlacement === 'left' ? 'mb-0' : ''
|
|
850
|
+
]"
|
|
851
|
+
>
|
|
852
|
+
{{ label }}
|
|
853
|
+
</label>
|
|
854
|
+
|
|
855
|
+
<!-- Trigger -->
|
|
856
|
+
<div
|
|
857
|
+
ref="triggerRef"
|
|
858
|
+
tabindex="0"
|
|
859
|
+
role="combobox"
|
|
860
|
+
:aria-expanded="isOpen"
|
|
861
|
+
:aria-disabled="disabled"
|
|
862
|
+
class="s-datepicker-trigger relative flex items-center cursor-pointer transition-all duration-200 w-full"
|
|
863
|
+
:class="[
|
|
864
|
+
sizeConfig.trigger,
|
|
865
|
+
sizeConfig.padding,
|
|
866
|
+
roundedConfig,
|
|
867
|
+
{
|
|
868
|
+
'opacity-50 cursor-not-allowed': disabled,
|
|
869
|
+
'cursor-default': readonly
|
|
870
|
+
}
|
|
871
|
+
]"
|
|
872
|
+
:style="variantStyles"
|
|
873
|
+
@click="toggle"
|
|
874
|
+
@focus="handleFocus"
|
|
875
|
+
@blur="handleBlur"
|
|
876
|
+
@keydown="handleKeydown"
|
|
877
|
+
>
|
|
878
|
+
<!-- Calendar icon -->
|
|
879
|
+
<span
|
|
880
|
+
class="mdi mdi-calendar-blank-outline mr-2 text-muted-foreground"
|
|
881
|
+
:class="sizeConfig.icon"
|
|
882
|
+
/>
|
|
883
|
+
|
|
884
|
+
<!-- Display value -->
|
|
885
|
+
<span
|
|
886
|
+
v-if="hasValue"
|
|
887
|
+
class="flex-1 truncate text-foreground"
|
|
888
|
+
>
|
|
889
|
+
{{ displayValue }}
|
|
890
|
+
</span>
|
|
891
|
+
<span
|
|
892
|
+
v-else
|
|
893
|
+
class="flex-1 text-muted-foreground"
|
|
894
|
+
>
|
|
895
|
+
{{ placeholder }}
|
|
896
|
+
</span>
|
|
897
|
+
|
|
898
|
+
<!-- Loading spinner -->
|
|
899
|
+
<span
|
|
900
|
+
v-if="loading"
|
|
901
|
+
class="mdi mdi-loading animate-spin text-muted-foreground"
|
|
902
|
+
:class="sizeConfig.icon"
|
|
903
|
+
/>
|
|
904
|
+
|
|
905
|
+
<!-- Clear button -->
|
|
906
|
+
<button
|
|
907
|
+
v-else-if="clearable && hasValue && !disabled && !readonly"
|
|
908
|
+
type="button"
|
|
909
|
+
class="p-1 -mr-1 rounded-full hover:bg-accent transition-colors text-muted-foreground hover:text-foreground"
|
|
910
|
+
@click="clear"
|
|
911
|
+
>
|
|
912
|
+
<span class="mdi mdi-close text-sm" />
|
|
913
|
+
</button>
|
|
914
|
+
|
|
915
|
+
<!-- Dropdown arrow -->
|
|
916
|
+
<span
|
|
917
|
+
v-else
|
|
918
|
+
class="mdi transition-transform duration-200 text-muted-foreground"
|
|
919
|
+
:class="[sizeConfig.icon, isOpen ? 'mdi-chevron-up' : 'mdi-chevron-down']"
|
|
920
|
+
/>
|
|
921
|
+
</div>
|
|
922
|
+
|
|
923
|
+
<!-- Calendar Dropdown -->
|
|
924
|
+
<Teleport :to="teleportTarget" :disabled="!teleport">
|
|
925
|
+
<Transition
|
|
926
|
+
enter-active-class="transition duration-200 ease-out"
|
|
927
|
+
enter-from-class="opacity-0 scale-95"
|
|
928
|
+
enter-to-class="opacity-100 scale-100"
|
|
929
|
+
leave-active-class="transition duration-150 ease-in"
|
|
930
|
+
leave-from-class="opacity-100 scale-100"
|
|
931
|
+
leave-to-class="opacity-0 scale-95"
|
|
932
|
+
>
|
|
933
|
+
<div
|
|
934
|
+
v-if="isOpen"
|
|
935
|
+
ref="calendarRef"
|
|
936
|
+
class="s-datepicker-calendar fixed z-50 bg-background border border-border rounded-xl shadow-2xl overflow-hidden"
|
|
937
|
+
:style="{
|
|
938
|
+
top: dropdownPosition.top !== undefined ? `${dropdownPosition.top}px` : 'auto',
|
|
939
|
+
bottom: dropdownPosition.bottom !== undefined ? `${dropdownPosition.bottom}px` : 'auto',
|
|
940
|
+
left: `${dropdownPosition.left}px`,
|
|
941
|
+
width: `${dropdownPosition.width}px`,
|
|
942
|
+
minWidth: '320px',
|
|
943
|
+
transformOrigin: dropdownPosition.placement === 'bottom' ? 'top' : 'bottom'
|
|
944
|
+
}"
|
|
945
|
+
>
|
|
946
|
+
<!-- Header -->
|
|
947
|
+
<div class="flex items-center justify-between px-4 py-3 border-b border-border bg-muted">
|
|
948
|
+
<button
|
|
949
|
+
type="button"
|
|
950
|
+
class="p-1.5 rounded-lg hover:bg-accent transition-colors text-muted-foreground hover:text-foreground"
|
|
951
|
+
@click="previousMonth"
|
|
952
|
+
>
|
|
953
|
+
<span class="mdi mdi-chevron-left text-lg" />
|
|
954
|
+
</button>
|
|
955
|
+
|
|
956
|
+
<div class="flex items-center gap-1 relative">
|
|
957
|
+
<!-- Month Picker Button -->
|
|
958
|
+
<button
|
|
959
|
+
type="button"
|
|
960
|
+
class="px-2 py-1 rounded-lg hover:bg-accent transition-colors text-foreground font-semibold flex items-center gap-1"
|
|
961
|
+
@click="toggleMonthPicker"
|
|
962
|
+
>
|
|
963
|
+
{{ monthNames[viewDate.getMonth()] }}
|
|
964
|
+
<span class="mdi mdi-chevron-down text-sm text-muted-foreground" />
|
|
965
|
+
</button>
|
|
966
|
+
|
|
967
|
+
<!-- Month Picker Dropdown -->
|
|
968
|
+
<Transition
|
|
969
|
+
enter-active-class="transition duration-150 ease-out"
|
|
970
|
+
enter-from-class="opacity-0 translate-y-1"
|
|
971
|
+
enter-to-class="opacity-100 translate-y-0"
|
|
972
|
+
leave-active-class="transition duration-100 ease-in"
|
|
973
|
+
leave-from-class="opacity-100 translate-y-0"
|
|
974
|
+
leave-to-class="opacity-0 translate-y-1"
|
|
975
|
+
>
|
|
976
|
+
<div
|
|
977
|
+
v-if="showMonthPicker"
|
|
978
|
+
class="absolute top-full left-0 mt-1 z-10 bg-background border border-border rounded-lg shadow-xl p-2 grid grid-cols-3 gap-1 w-48"
|
|
979
|
+
>
|
|
980
|
+
<button
|
|
981
|
+
v-for="(month, index) in monthNamesShort"
|
|
982
|
+
:key="index"
|
|
983
|
+
type="button"
|
|
984
|
+
class="px-2 py-1.5 text-sm rounded-sm transition-colors"
|
|
985
|
+
:class="[
|
|
986
|
+
viewDate.getMonth() === index
|
|
987
|
+
? `${activeTextClass} font-semibold`
|
|
988
|
+
: 'text-foreground hover:bg-accent'
|
|
989
|
+
]"
|
|
990
|
+
:style="viewDate.getMonth() === index ? { backgroundColor: color } : undefined"
|
|
991
|
+
@click="selectMonthFromPicker(index)"
|
|
992
|
+
>
|
|
993
|
+
{{ month }}
|
|
994
|
+
</button>
|
|
995
|
+
</div>
|
|
996
|
+
</Transition>
|
|
997
|
+
|
|
998
|
+
<!-- Year Picker Button -->
|
|
999
|
+
<button
|
|
1000
|
+
type="button"
|
|
1001
|
+
class="px-2 py-1 rounded-lg hover:bg-accent transition-colors text-foreground font-semibold flex items-center gap-1"
|
|
1002
|
+
@click="toggleYearPicker"
|
|
1003
|
+
>
|
|
1004
|
+
{{ viewDate.getFullYear() }}
|
|
1005
|
+
<span class="mdi mdi-chevron-down text-sm text-muted-foreground" />
|
|
1006
|
+
</button>
|
|
1007
|
+
|
|
1008
|
+
<!-- Year Picker Dropdown -->
|
|
1009
|
+
<Transition
|
|
1010
|
+
enter-active-class="transition duration-150 ease-out"
|
|
1011
|
+
enter-from-class="opacity-0 translate-y-1"
|
|
1012
|
+
enter-to-class="opacity-100 translate-y-0"
|
|
1013
|
+
leave-active-class="transition duration-100 ease-in"
|
|
1014
|
+
leave-from-class="opacity-100 translate-y-0"
|
|
1015
|
+
leave-to-class="opacity-0 translate-y-1"
|
|
1016
|
+
>
|
|
1017
|
+
<div
|
|
1018
|
+
v-if="showYearPicker"
|
|
1019
|
+
ref="yearPickerRef"
|
|
1020
|
+
class="absolute top-full right-0 mt-1 z-10 bg-background border border-border rounded-lg shadow-xl p-2 max-h-48 overflow-y-auto w-24 scrollbar-thin"
|
|
1021
|
+
>
|
|
1022
|
+
<button
|
|
1023
|
+
v-for="year in years"
|
|
1024
|
+
:key="year"
|
|
1025
|
+
type="button"
|
|
1026
|
+
class="w-full px-2 py-1.5 text-sm rounded-sm transition-colors text-left"
|
|
1027
|
+
:class="[
|
|
1028
|
+
viewDate.getFullYear() === year
|
|
1029
|
+
? `${activeTextClass} font-semibold`
|
|
1030
|
+
: 'text-foreground hover:bg-accent'
|
|
1031
|
+
]"
|
|
1032
|
+
:style="viewDate.getFullYear() === year ? { backgroundColor: color } : undefined"
|
|
1033
|
+
:data-current-year="viewDate.getFullYear() === year ? true : undefined"
|
|
1034
|
+
@click="selectYearFromPicker(year)"
|
|
1035
|
+
>
|
|
1036
|
+
{{ year }}
|
|
1037
|
+
</button>
|
|
1038
|
+
</div>
|
|
1039
|
+
</Transition>
|
|
1040
|
+
</div>
|
|
1041
|
+
|
|
1042
|
+
<button
|
|
1043
|
+
type="button"
|
|
1044
|
+
class="p-1.5 rounded-lg hover:bg-accent transition-colors text-muted-foreground hover:text-foreground"
|
|
1045
|
+
@click="nextMonth"
|
|
1046
|
+
>
|
|
1047
|
+
<span class="mdi mdi-chevron-right text-lg" />
|
|
1048
|
+
</button>
|
|
1049
|
+
</div>
|
|
1050
|
+
|
|
1051
|
+
<!-- Weekday headers -->
|
|
1052
|
+
<div class="grid grid-cols-7 px-3 py-2 border-b border-border">
|
|
1053
|
+
<div
|
|
1054
|
+
v-for="day in weekdaysOrdered"
|
|
1055
|
+
:key="day"
|
|
1056
|
+
class="text-center text-xs font-medium text-muted-foreground py-1"
|
|
1057
|
+
>
|
|
1058
|
+
{{ day }}
|
|
1059
|
+
</div>
|
|
1060
|
+
</div>
|
|
1061
|
+
|
|
1062
|
+
<!-- Calendar grid -->
|
|
1063
|
+
<div class="p-3">
|
|
1064
|
+
<div
|
|
1065
|
+
v-for="(week, weekIndex) in weeks"
|
|
1066
|
+
:key="weekIndex"
|
|
1067
|
+
class="grid grid-cols-7 gap-0.5"
|
|
1068
|
+
>
|
|
1069
|
+
<button
|
|
1070
|
+
v-for="day in week"
|
|
1071
|
+
:key="day.date.toISOString()"
|
|
1072
|
+
type="button"
|
|
1073
|
+
class="relative flex items-center justify-center rounded-lg transition-all duration-150 overflow-hidden"
|
|
1074
|
+
:class="[
|
|
1075
|
+
sizeConfig.day,
|
|
1076
|
+
{
|
|
1077
|
+
'text-muted-foreground': !day.isCurrentMonth && !day.isSelected,
|
|
1078
|
+
'text-foreground': day.isCurrentMonth && !day.isSelected && !day.isDisabled,
|
|
1079
|
+
'font-bold ring-2 ring-inset': day.isToday && !day.isSelected,
|
|
1080
|
+
'opacity-40 cursor-not-allowed': day.isDisabled,
|
|
1081
|
+
'cursor-pointer hover:bg-accent': !day.isDisabled && !day.isSelected,
|
|
1082
|
+
[activeTextClass + ' font-semibold']: day.isSelected,
|
|
1083
|
+
'bg-primary/15': day.isInRange && !day.isSelected,
|
|
1084
|
+
'rounded-l-lg': day.isRangeStart,
|
|
1085
|
+
'rounded-r-lg': day.isRangeEnd
|
|
1086
|
+
}
|
|
1087
|
+
]"
|
|
1088
|
+
:style="{
|
|
1089
|
+
backgroundColor: day.isSelected ? color : undefined,
|
|
1090
|
+
'--tw-ring-color': day.isToday && !day.isSelected ? color : undefined
|
|
1091
|
+
}"
|
|
1092
|
+
:disabled="day.isDisabled"
|
|
1093
|
+
@click="selectDate(day.date, $event)"
|
|
1094
|
+
@mouseenter="handleDayHover(day.date)"
|
|
1095
|
+
@mouseleave="handleDayLeave"
|
|
1096
|
+
>
|
|
1097
|
+
<!-- Ripple -->
|
|
1098
|
+
<span
|
|
1099
|
+
v-for="ripple in ripples"
|
|
1100
|
+
:key="ripple.id"
|
|
1101
|
+
class="absolute rounded-full bg-white/30 animate-ripple pointer-events-none"
|
|
1102
|
+
:style="{
|
|
1103
|
+
left: `${ripple.x}px`,
|
|
1104
|
+
top: `${ripple.y}px`,
|
|
1105
|
+
width: `${ripple.size}px`,
|
|
1106
|
+
height: `${ripple.size}px`
|
|
1107
|
+
}"
|
|
1108
|
+
/>
|
|
1109
|
+
{{ day.date.getDate() }}
|
|
1110
|
+
</button>
|
|
1111
|
+
</div>
|
|
1112
|
+
</div>
|
|
1113
|
+
|
|
1114
|
+
<!-- Time picker section -->
|
|
1115
|
+
<div
|
|
1116
|
+
v-if="enableTime"
|
|
1117
|
+
class="px-4 py-3 border-t border-border bg-muted"
|
|
1118
|
+
>
|
|
1119
|
+
<div class="flex items-center justify-center gap-3">
|
|
1120
|
+
<!-- Hour input with arrows -->
|
|
1121
|
+
<div class="flex flex-col items-center" @wheel="handleHourWheel">
|
|
1122
|
+
<button
|
|
1123
|
+
type="button"
|
|
1124
|
+
class="p-0.5 rounded hover:bg-accent text-muted-foreground"
|
|
1125
|
+
@click="adjustHour(1)"
|
|
1126
|
+
>
|
|
1127
|
+
<span class="mdi mdi-chevron-up text-lg" />
|
|
1128
|
+
</button>
|
|
1129
|
+
<input
|
|
1130
|
+
type="text"
|
|
1131
|
+
inputmode="numeric"
|
|
1132
|
+
:value="displayHour()"
|
|
1133
|
+
class="w-12 h-10 text-center font-mono text-xl bg-background text-foreground border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
|
1134
|
+
maxlength="2"
|
|
1135
|
+
@keydown="(e: KeyboardEvent) => { if (!/^[0-9]$/.test(e.key) && !['Backspace', 'Delete', 'Tab', 'Enter', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) e.preventDefault() }"
|
|
1136
|
+
@change="handleHourInput"
|
|
1137
|
+
@focus="($event.target as HTMLInputElement).select()"
|
|
1138
|
+
/>
|
|
1139
|
+
<button
|
|
1140
|
+
type="button"
|
|
1141
|
+
class="p-0.5 rounded hover:bg-accent text-muted-foreground"
|
|
1142
|
+
@click="adjustHour(-1)"
|
|
1143
|
+
>
|
|
1144
|
+
<span class="mdi mdi-chevron-down text-lg" />
|
|
1145
|
+
</button>
|
|
1146
|
+
</div>
|
|
1147
|
+
|
|
1148
|
+
<span class="text-2xl font-bold text-foreground pb-1">:</span>
|
|
1149
|
+
|
|
1150
|
+
<!-- Minute input with arrows -->
|
|
1151
|
+
<div class="flex flex-col items-center" @wheel="handleMinuteWheel">
|
|
1152
|
+
<button
|
|
1153
|
+
type="button"
|
|
1154
|
+
class="p-0.5 rounded hover:bg-accent text-muted-foreground"
|
|
1155
|
+
@click="adjustMinute(minuteStep)"
|
|
1156
|
+
>
|
|
1157
|
+
<span class="mdi mdi-chevron-up text-lg" />
|
|
1158
|
+
</button>
|
|
1159
|
+
<input
|
|
1160
|
+
type="text"
|
|
1161
|
+
inputmode="numeric"
|
|
1162
|
+
:value="String(selectedMinute).padStart(2, '0')"
|
|
1163
|
+
class="w-12 h-10 text-center font-mono text-xl bg-background text-foreground border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
|
1164
|
+
maxlength="2"
|
|
1165
|
+
@keydown="(e: KeyboardEvent) => { if (!/^[0-9]$/.test(e.key) && !['Backspace', 'Delete', 'Tab', 'Enter', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) e.preventDefault() }"
|
|
1166
|
+
@change="handleMinuteInput"
|
|
1167
|
+
@focus="($event.target as HTMLInputElement).select()"
|
|
1168
|
+
/>
|
|
1169
|
+
<button
|
|
1170
|
+
type="button"
|
|
1171
|
+
class="p-0.5 rounded hover:bg-accent text-muted-foreground"
|
|
1172
|
+
@click="adjustMinute(-minuteStep)"
|
|
1173
|
+
>
|
|
1174
|
+
<span class="mdi mdi-chevron-down text-lg" />
|
|
1175
|
+
</button>
|
|
1176
|
+
</div>
|
|
1177
|
+
|
|
1178
|
+
<!-- AM/PM Toggle for 12h format -->
|
|
1179
|
+
<div v-if="timeFormat === '12h'" class="flex flex-col gap-1 ml-2">
|
|
1180
|
+
<button
|
|
1181
|
+
type="button"
|
|
1182
|
+
class="px-3 py-1.5 text-sm font-medium rounded-lg transition-colors"
|
|
1183
|
+
:class="[
|
|
1184
|
+
isAM
|
|
1185
|
+
? activeTextClass
|
|
1186
|
+
: 'text-muted-foreground hover:bg-accent'
|
|
1187
|
+
]"
|
|
1188
|
+
:style="isAM ? { backgroundColor: color } : undefined"
|
|
1189
|
+
@click="isAM || toggleAMPM()"
|
|
1190
|
+
>
|
|
1191
|
+
AM
|
|
1192
|
+
</button>
|
|
1193
|
+
<button
|
|
1194
|
+
type="button"
|
|
1195
|
+
class="px-3 py-1.5 text-sm font-medium rounded-lg transition-colors"
|
|
1196
|
+
:class="[
|
|
1197
|
+
!isAM
|
|
1198
|
+
? activeTextClass
|
|
1199
|
+
: 'text-muted-foreground hover:bg-accent'
|
|
1200
|
+
]"
|
|
1201
|
+
:style="!isAM ? { backgroundColor: color } : undefined"
|
|
1202
|
+
@click="isAM && toggleAMPM()"
|
|
1203
|
+
>
|
|
1204
|
+
PM
|
|
1205
|
+
</button>
|
|
1206
|
+
</div>
|
|
1207
|
+
</div>
|
|
1208
|
+
</div>
|
|
1209
|
+
|
|
1210
|
+
<!-- Footer -->
|
|
1211
|
+
<div
|
|
1212
|
+
v-if="showToday || enableTime"
|
|
1213
|
+
class="flex items-center justify-between px-4 py-2 border-t border-border"
|
|
1214
|
+
>
|
|
1215
|
+
<button
|
|
1216
|
+
v-if="showToday"
|
|
1217
|
+
type="button"
|
|
1218
|
+
class="text-sm font-medium transition-colors hover:underline"
|
|
1219
|
+
:style="{ color }"
|
|
1220
|
+
@click="goToToday"
|
|
1221
|
+
>
|
|
1222
|
+
Today
|
|
1223
|
+
</button>
|
|
1224
|
+
<div v-else />
|
|
1225
|
+
|
|
1226
|
+
<button
|
|
1227
|
+
v-if="enableTime"
|
|
1228
|
+
type="button"
|
|
1229
|
+
:class="['px-4 py-1.5 text-sm font-medium rounded-lg transition-colors', activeTextClass]"
|
|
1230
|
+
:style="{ backgroundColor: color }"
|
|
1231
|
+
@click="close"
|
|
1232
|
+
>
|
|
1233
|
+
Done
|
|
1234
|
+
</button>
|
|
1235
|
+
</div>
|
|
1236
|
+
</div>
|
|
1237
|
+
</Transition>
|
|
1238
|
+
</Teleport>
|
|
1239
|
+
</div>
|
|
1240
|
+
</template>
|
|
1241
|
+
|
|
1242
|
+
<style scoped>
|
|
1243
|
+
.s-datepicker-trigger {
|
|
1244
|
+
background-color: var(--dp-bg);
|
|
1245
|
+
border: 1.5px solid var(--dp-border);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
.s-datepicker-trigger:focus {
|
|
1249
|
+
background-color: var(--dp-bg-focus);
|
|
1250
|
+
border-color: var(--dp-border-focus);
|
|
1251
|
+
outline: none;
|
|
1252
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--dp-border-focus) 20%, transparent);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
/* Ripple animation */
|
|
1256
|
+
@keyframes ripple {
|
|
1257
|
+
0% {
|
|
1258
|
+
transform: scale(0);
|
|
1259
|
+
opacity: 0.5;
|
|
1260
|
+
}
|
|
1261
|
+
100% {
|
|
1262
|
+
transform: scale(1);
|
|
1263
|
+
opacity: 0;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
.animate-ripple {
|
|
1268
|
+
animation: ripple 0.6s ease-out forwards;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/* Custom scrollbar for dropdowns */
|
|
1272
|
+
.s-datepicker-calendar select {
|
|
1273
|
+
scrollbar-width: thin;
|
|
1274
|
+
scrollbar-color: var(--s-border) transparent;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
.s-datepicker-calendar select::-webkit-scrollbar {
|
|
1278
|
+
width: 6px;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
.s-datepicker-calendar select::-webkit-scrollbar-track {
|
|
1282
|
+
background: transparent;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
.s-datepicker-calendar select::-webkit-scrollbar-thumb {
|
|
1286
|
+
background-color: var(--s-border);
|
|
1287
|
+
border-radius: 3px;
|
|
1288
|
+
}
|
|
1289
|
+
</style>
|
|
1290
|
+
|