@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,648 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SModal - Advanced Modal/Dialog Component
|
|
4
|
+
* A fully accessible, animated modal with keyboard navigation and focus trapping
|
|
5
|
+
*/
|
|
6
|
+
import { type InjectionKey, type Ref } from 'vue'
|
|
7
|
+
|
|
8
|
+
// Types
|
|
9
|
+
export type ModalSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full'
|
|
10
|
+
export type ModalVariant = 'default' | 'glass' | 'bordered' | 'elevated'
|
|
11
|
+
export type ModalAnimation = 'fade' | 'scale' | 'slide-up' | 'slide-down' | 'zoom' | 'flip'
|
|
12
|
+
export type ModalPosition = 'center' | 'top' | 'bottom' | 'left' | 'right'
|
|
13
|
+
|
|
14
|
+
export interface SModalContext {
|
|
15
|
+
close: () => void
|
|
16
|
+
isOpen: Ref<boolean>
|
|
17
|
+
size: ModalSize
|
|
18
|
+
hasHeader: Ref<boolean>
|
|
19
|
+
hasFooter: Ref<boolean>
|
|
20
|
+
setHasHeader: (value: boolean) => void
|
|
21
|
+
setHasFooter: (value: boolean) => void
|
|
22
|
+
titleId: string
|
|
23
|
+
descriptionId: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const SModalContextKey: InjectionKey<SModalContext> = Symbol('SModalContext')
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<script setup lang="ts">
|
|
30
|
+
defineOptions({ inheritAttrs: false })
|
|
31
|
+
|
|
32
|
+
import { ref, computed, provide, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
|
33
|
+
import { cn } from '../../../lib/utils'
|
|
34
|
+
|
|
35
|
+
export interface Props {
|
|
36
|
+
/** Control modal visibility */
|
|
37
|
+
modelValue?: boolean
|
|
38
|
+
/** Modal size variant */
|
|
39
|
+
size?: ModalSize
|
|
40
|
+
/** Visual variant */
|
|
41
|
+
variant?: ModalVariant
|
|
42
|
+
/** Animation style */
|
|
43
|
+
animation?: ModalAnimation
|
|
44
|
+
/** Position on screen */
|
|
45
|
+
position?: ModalPosition
|
|
46
|
+
/** Show close button */
|
|
47
|
+
closable?: boolean
|
|
48
|
+
/** Close on backdrop click */
|
|
49
|
+
closeOnBackdrop?: boolean
|
|
50
|
+
/** Close on escape key */
|
|
51
|
+
closeOnEscape?: boolean
|
|
52
|
+
/** Lock body scroll when open */
|
|
53
|
+
lockScroll?: boolean
|
|
54
|
+
/** Trap focus within modal */
|
|
55
|
+
trapFocus?: boolean
|
|
56
|
+
/** Show backdrop overlay */
|
|
57
|
+
backdrop?: boolean
|
|
58
|
+
/** Backdrop blur effect */
|
|
59
|
+
backdropBlur?: boolean
|
|
60
|
+
/** Custom backdrop class */
|
|
61
|
+
backdropClass?: string
|
|
62
|
+
/** Custom modal panel class */
|
|
63
|
+
panelClass?: string
|
|
64
|
+
/** Teleport target */
|
|
65
|
+
teleport?: boolean | string
|
|
66
|
+
/** Z-index for the modal */
|
|
67
|
+
zIndex?: number
|
|
68
|
+
/** Persistent mode (can't be closed by backdrop/escape) */
|
|
69
|
+
persistent?: boolean
|
|
70
|
+
/** Fullscreen mode */
|
|
71
|
+
fullscreen?: boolean
|
|
72
|
+
/** Modal title (alternative to header slot) */
|
|
73
|
+
title?: string
|
|
74
|
+
/** Modal description */
|
|
75
|
+
description?: string
|
|
76
|
+
/** Hide the default header */
|
|
77
|
+
hideHeader?: boolean
|
|
78
|
+
/** Initial focus selector */
|
|
79
|
+
initialFocus?: string
|
|
80
|
+
/** Auto-focus first interactive element (default: false, focuses panel instead) */
|
|
81
|
+
autoFocus?: boolean
|
|
82
|
+
/** Stacked modal order (for nested modals) */
|
|
83
|
+
stackOrder?: number
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
87
|
+
modelValue: false,
|
|
88
|
+
size: 'md',
|
|
89
|
+
variant: 'default',
|
|
90
|
+
animation: 'scale',
|
|
91
|
+
position: 'center',
|
|
92
|
+
closable: true,
|
|
93
|
+
closeOnBackdrop: true,
|
|
94
|
+
closeOnEscape: true,
|
|
95
|
+
lockScroll: true,
|
|
96
|
+
trapFocus: true,
|
|
97
|
+
backdrop: true,
|
|
98
|
+
backdropBlur: false,
|
|
99
|
+
backdropClass: '',
|
|
100
|
+
panelClass: '',
|
|
101
|
+
teleport: true,
|
|
102
|
+
zIndex: 1000,
|
|
103
|
+
persistent: false,
|
|
104
|
+
fullscreen: false,
|
|
105
|
+
title: undefined,
|
|
106
|
+
description: undefined,
|
|
107
|
+
hideHeader: false,
|
|
108
|
+
initialFocus: undefined,
|
|
109
|
+
autoFocus: false,
|
|
110
|
+
stackOrder: 0
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const emit = defineEmits<{
|
|
114
|
+
'update:modelValue': [value: boolean]
|
|
115
|
+
'open': []
|
|
116
|
+
'close': []
|
|
117
|
+
'opened': []
|
|
118
|
+
'closed': []
|
|
119
|
+
'before-open': []
|
|
120
|
+
'before-close': []
|
|
121
|
+
}>()
|
|
122
|
+
|
|
123
|
+
// Generate unique IDs for ARIA
|
|
124
|
+
const uid = Math.random().toString(36).slice(2, 9)
|
|
125
|
+
const titleId = `modal-title-${uid}`
|
|
126
|
+
const descriptionId = `modal-desc-${uid}`
|
|
127
|
+
|
|
128
|
+
// Refs
|
|
129
|
+
const modalRef = ref<HTMLElement | null>(null)
|
|
130
|
+
const backdropRef = ref<HTMLElement | null>(null)
|
|
131
|
+
const panelRef = ref<HTMLElement | null>(null)
|
|
132
|
+
const isOpen = ref(props.modelValue)
|
|
133
|
+
const isAnimating = ref(false)
|
|
134
|
+
const hasHeader = ref(false)
|
|
135
|
+
const hasFooter = ref(false)
|
|
136
|
+
const previousActiveElement = ref<HTMLElement | null>(null)
|
|
137
|
+
|
|
138
|
+
// Focus trap elements
|
|
139
|
+
const focusableSelector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
140
|
+
let scrollbarWidth = 0
|
|
141
|
+
|
|
142
|
+
// Computed
|
|
143
|
+
const teleportTarget = computed(() => {
|
|
144
|
+
if (props.teleport === true) return 'body'
|
|
145
|
+
if (typeof props.teleport === 'string') return props.teleport
|
|
146
|
+
return undefined
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const effectiveZIndex = computed(() => props.zIndex + (props.stackOrder * 10))
|
|
150
|
+
|
|
151
|
+
const sizeClasses = computed(() => {
|
|
152
|
+
if (props.fullscreen) return 'w-full h-full max-w-full max-h-full rounded-none'
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
xs: 'w-full max-w-xs',
|
|
156
|
+
sm: 'w-full max-w-sm',
|
|
157
|
+
md: 'w-full max-w-lg',
|
|
158
|
+
lg: 'w-full max-w-2xl',
|
|
159
|
+
xl: 'w-full max-w-4xl',
|
|
160
|
+
full: 'w-full max-w-[calc(100vw-2rem)] max-h-[calc(100vh-2rem)]'
|
|
161
|
+
}[props.size]
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
const positionClasses = computed(() => {
|
|
165
|
+
if (props.fullscreen) return 'items-stretch justify-center'
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
center: 'items-center justify-center',
|
|
169
|
+
top: 'items-start justify-center pt-16',
|
|
170
|
+
bottom: 'items-end justify-center pb-16',
|
|
171
|
+
left: 'items-center justify-start pl-4',
|
|
172
|
+
right: 'items-center justify-end pr-4'
|
|
173
|
+
}[props.position]
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
const variantClasses = computed(() => ({
|
|
177
|
+
default: 'bg-background border border-border shadow-2xl',
|
|
178
|
+
glass: 'bg-background/80 backdrop-blur-2xl border border-border/50 shadow-2xl',
|
|
179
|
+
bordered: 'bg-background border-2 border-primary/30 shadow-xl',
|
|
180
|
+
elevated: 'bg-background shadow-[0_25px_80px_-12px_rgba(0,0,0,0.4)] dark:shadow-[0_25px_80px_-12px_rgba(0,0,0,0.8)]'
|
|
181
|
+
}[props.variant]))
|
|
182
|
+
|
|
183
|
+
// Animation configurations
|
|
184
|
+
const panelAnimations = computed(() => {
|
|
185
|
+
const animations = {
|
|
186
|
+
fade: {
|
|
187
|
+
enter: 'transition-all duration-300 ease-out',
|
|
188
|
+
enterFrom: 'opacity-0',
|
|
189
|
+
enterTo: 'opacity-100',
|
|
190
|
+
leave: 'transition-all duration-200 ease-in',
|
|
191
|
+
leaveFrom: 'opacity-100',
|
|
192
|
+
leaveTo: 'opacity-0'
|
|
193
|
+
},
|
|
194
|
+
scale: {
|
|
195
|
+
enter: 'transition-all duration-300 ease-[cubic-bezier(0.34,1.56,0.64,1)]',
|
|
196
|
+
enterFrom: 'opacity-0 scale-90',
|
|
197
|
+
enterTo: 'opacity-100 scale-100',
|
|
198
|
+
leave: 'transition-all duration-200 ease-in',
|
|
199
|
+
leaveFrom: 'opacity-100 scale-100',
|
|
200
|
+
leaveTo: 'opacity-0 scale-95'
|
|
201
|
+
},
|
|
202
|
+
'slide-up': {
|
|
203
|
+
enter: 'transition-all duration-300 ease-out',
|
|
204
|
+
enterFrom: 'opacity-0 translate-y-12',
|
|
205
|
+
enterTo: 'opacity-100 translate-y-0',
|
|
206
|
+
leave: 'transition-all duration-200 ease-in',
|
|
207
|
+
leaveFrom: 'opacity-100 translate-y-0',
|
|
208
|
+
leaveTo: 'opacity-0 translate-y-8'
|
|
209
|
+
},
|
|
210
|
+
'slide-down': {
|
|
211
|
+
enter: 'transition-all duration-300 ease-out',
|
|
212
|
+
enterFrom: 'opacity-0 -translate-y-12',
|
|
213
|
+
enterTo: 'opacity-100 translate-y-0',
|
|
214
|
+
leave: 'transition-all duration-200 ease-in',
|
|
215
|
+
leaveFrom: 'opacity-100 translate-y-0',
|
|
216
|
+
leaveTo: 'opacity-0 -translate-y-8'
|
|
217
|
+
},
|
|
218
|
+
zoom: {
|
|
219
|
+
enter: 'transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)]',
|
|
220
|
+
enterFrom: 'opacity-0 scale-50',
|
|
221
|
+
enterTo: 'opacity-100 scale-100',
|
|
222
|
+
leave: 'transition-all duration-200 ease-in',
|
|
223
|
+
leaveFrom: 'opacity-100 scale-100',
|
|
224
|
+
leaveTo: 'opacity-0 scale-75'
|
|
225
|
+
},
|
|
226
|
+
flip: {
|
|
227
|
+
enter: 'transition-all duration-400 ease-out [transform-style:preserve-3d]',
|
|
228
|
+
enterFrom: 'opacity-0 [transform:rotateX(-15deg)_scale(0.95)]',
|
|
229
|
+
enterTo: 'opacity-100 [transform:rotateX(0deg)_scale(1)]',
|
|
230
|
+
leave: 'transition-all duration-200 ease-in [transform-style:preserve-3d]',
|
|
231
|
+
leaveFrom: 'opacity-100 [transform:rotateX(0deg)_scale(1)]',
|
|
232
|
+
leaveTo: 'opacity-0 [transform:rotateX(15deg)_scale(0.95)]'
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return animations[props.animation]
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
// Methods
|
|
239
|
+
const getScrollbarWidth = () => {
|
|
240
|
+
const outer = document.createElement('div')
|
|
241
|
+
outer.style.visibility = 'hidden'
|
|
242
|
+
outer.style.overflow = 'scroll'
|
|
243
|
+
document.body.appendChild(outer)
|
|
244
|
+
const inner = document.createElement('div')
|
|
245
|
+
outer.appendChild(inner)
|
|
246
|
+
const scrollbarWidth = outer.offsetWidth - inner.offsetWidth
|
|
247
|
+
outer.parentNode?.removeChild(outer)
|
|
248
|
+
return scrollbarWidth
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const lockBodyScroll = () => {
|
|
252
|
+
if (!props.lockScroll) return
|
|
253
|
+
|
|
254
|
+
scrollbarWidth = getScrollbarWidth()
|
|
255
|
+
const hasScrollbar = window.innerWidth > document.documentElement.clientWidth
|
|
256
|
+
|
|
257
|
+
document.body.style.overflow = 'hidden'
|
|
258
|
+
if (hasScrollbar) {
|
|
259
|
+
document.body.style.paddingRight = `${scrollbarWidth}px`
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const unlockBodyScroll = () => {
|
|
264
|
+
if (!props.lockScroll) return
|
|
265
|
+
|
|
266
|
+
document.body.style.overflow = ''
|
|
267
|
+
document.body.style.paddingRight = ''
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const getFocusableElements = (): HTMLElement[] => {
|
|
271
|
+
if (!panelRef.value) return []
|
|
272
|
+
return Array.from(panelRef.value.querySelectorAll(focusableSelector))
|
|
273
|
+
.filter(el => !el.hasAttribute('disabled') && el.getAttribute('tabindex') !== '-1') as HTMLElement[]
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const focusFirstElement = () => {
|
|
277
|
+
if (!props.trapFocus) return
|
|
278
|
+
|
|
279
|
+
nextTick(() => {
|
|
280
|
+
// If initialFocus is specified, focus that element
|
|
281
|
+
if (props.initialFocus && panelRef.value) {
|
|
282
|
+
const initialElement = panelRef.value.querySelector(props.initialFocus) as HTMLElement
|
|
283
|
+
if (initialElement) {
|
|
284
|
+
initialElement.focus()
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// If autoFocus is enabled, focus the first interactive element
|
|
290
|
+
if (props.autoFocus) {
|
|
291
|
+
const focusable = getFocusableElements()
|
|
292
|
+
// Skip close button (first element) if there are other focusable elements
|
|
293
|
+
const targetIndex = focusable.length > 1 ? 1 : 0
|
|
294
|
+
if (focusable.length > 0) {
|
|
295
|
+
focusable[targetIndex].focus()
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Otherwise, focus the panel itself (not the close button)
|
|
301
|
+
// This prevents the focus ring on the close button while maintaining accessibility
|
|
302
|
+
// Users can Tab to navigate to interactive elements
|
|
303
|
+
panelRef.value?.focus()
|
|
304
|
+
})
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const handleKeydown = (event: KeyboardEvent) => {
|
|
308
|
+
if (!isOpen.value) return
|
|
309
|
+
|
|
310
|
+
// Escape key
|
|
311
|
+
if (event.key === 'Escape' && props.closeOnEscape && !props.persistent) {
|
|
312
|
+
event.preventDefault()
|
|
313
|
+
event.stopPropagation()
|
|
314
|
+
close()
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Focus trap
|
|
319
|
+
if (event.key === 'Tab' && props.trapFocus) {
|
|
320
|
+
const focusable = getFocusableElements()
|
|
321
|
+
if (focusable.length === 0) return
|
|
322
|
+
|
|
323
|
+
const first = focusable[0]
|
|
324
|
+
const last = focusable[focusable.length - 1]
|
|
325
|
+
|
|
326
|
+
if (event.shiftKey) {
|
|
327
|
+
if (document.activeElement === first) {
|
|
328
|
+
event.preventDefault()
|
|
329
|
+
last.focus()
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
if (document.activeElement === last) {
|
|
333
|
+
event.preventDefault()
|
|
334
|
+
first.focus()
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const handleBackdropClick = (event: MouseEvent) => {
|
|
341
|
+
// Only close if clicking directly on the backdrop or modal wrapper, not the panel
|
|
342
|
+
const target = event.target as HTMLElement
|
|
343
|
+
const isBackdropClick = target === backdropRef.value || target === modalRef.value
|
|
344
|
+
|
|
345
|
+
if (isBackdropClick && props.closeOnBackdrop && !props.persistent) {
|
|
346
|
+
close()
|
|
347
|
+
} else if (isBackdropClick && props.persistent) {
|
|
348
|
+
// Shake animation for persistent modals
|
|
349
|
+
panelRef.value?.classList.add('animate-shake')
|
|
350
|
+
setTimeout(() => panelRef.value?.classList.remove('animate-shake'), 300)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const open = () => {
|
|
355
|
+
if (isOpen.value || isAnimating.value) return
|
|
356
|
+
|
|
357
|
+
emit('before-open')
|
|
358
|
+
previousActiveElement.value = document.activeElement as HTMLElement
|
|
359
|
+
isOpen.value = true
|
|
360
|
+
isAnimating.value = true
|
|
361
|
+
|
|
362
|
+
lockBodyScroll()
|
|
363
|
+
emit('update:modelValue', true)
|
|
364
|
+
emit('open')
|
|
365
|
+
|
|
366
|
+
nextTick(() => {
|
|
367
|
+
focusFirstElement()
|
|
368
|
+
setTimeout(() => {
|
|
369
|
+
isAnimating.value = false
|
|
370
|
+
emit('opened')
|
|
371
|
+
}, 300)
|
|
372
|
+
})
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const close = () => {
|
|
376
|
+
if (!isOpen.value || isAnimating.value) return
|
|
377
|
+
|
|
378
|
+
emit('before-close')
|
|
379
|
+
isAnimating.value = true
|
|
380
|
+
isOpen.value = false
|
|
381
|
+
|
|
382
|
+
unlockBodyScroll()
|
|
383
|
+
emit('update:modelValue', false)
|
|
384
|
+
emit('close')
|
|
385
|
+
|
|
386
|
+
setTimeout(() => {
|
|
387
|
+
isAnimating.value = false
|
|
388
|
+
if (previousActiveElement.value) {
|
|
389
|
+
previousActiveElement.value.focus()
|
|
390
|
+
}
|
|
391
|
+
emit('closed')
|
|
392
|
+
}, 200)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Watch modelValue
|
|
396
|
+
watch(() => props.modelValue, (value) => {
|
|
397
|
+
if (value) {
|
|
398
|
+
open()
|
|
399
|
+
} else {
|
|
400
|
+
close()
|
|
401
|
+
}
|
|
402
|
+
}, { immediate: true })
|
|
403
|
+
|
|
404
|
+
// Lifecycle
|
|
405
|
+
onMounted(() => {
|
|
406
|
+
document.addEventListener('keydown', handleKeydown)
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
onBeforeUnmount(() => {
|
|
410
|
+
document.removeEventListener('keydown', handleKeydown)
|
|
411
|
+
unlockBodyScroll()
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
// Provide context
|
|
415
|
+
provide(SModalContextKey, {
|
|
416
|
+
close,
|
|
417
|
+
isOpen,
|
|
418
|
+
size: props.size,
|
|
419
|
+
hasHeader,
|
|
420
|
+
hasFooter,
|
|
421
|
+
setHasHeader: (value: boolean) => hasHeader.value = value,
|
|
422
|
+
setHasFooter: (value: boolean) => hasFooter.value = value,
|
|
423
|
+
titleId,
|
|
424
|
+
descriptionId
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
// Expose for external control
|
|
428
|
+
defineExpose({
|
|
429
|
+
open,
|
|
430
|
+
close,
|
|
431
|
+
isOpen
|
|
432
|
+
})
|
|
433
|
+
</script>
|
|
434
|
+
|
|
435
|
+
<template>
|
|
436
|
+
<Teleport v-if="teleportTarget" :to="teleportTarget" :disabled="!teleportTarget">
|
|
437
|
+
<Transition
|
|
438
|
+
enter-active-class="transition-opacity duration-300 ease-out"
|
|
439
|
+
enter-from-class="opacity-0"
|
|
440
|
+
enter-to-class="opacity-100"
|
|
441
|
+
leave-active-class="transition-opacity duration-200 ease-in"
|
|
442
|
+
leave-from-class="opacity-100"
|
|
443
|
+
leave-to-class="opacity-0"
|
|
444
|
+
>
|
|
445
|
+
<div
|
|
446
|
+
v-if="isOpen"
|
|
447
|
+
ref="modalRef"
|
|
448
|
+
v-bind="$attrs"
|
|
449
|
+
:class="cn('s-modal fixed inset-0 flex overflow-hidden', positionClasses, fullscreen ? 'p-0' : 'p-4', $attrs.class ?? '')"
|
|
450
|
+
:style="{ zIndex: effectiveZIndex }"
|
|
451
|
+
role="dialog"
|
|
452
|
+
aria-modal="true"
|
|
453
|
+
:aria-labelledby="title ? titleId : undefined"
|
|
454
|
+
:aria-describedby="description ? descriptionId : undefined"
|
|
455
|
+
@mousedown="handleBackdropClick"
|
|
456
|
+
>
|
|
457
|
+
<!-- Backdrop -->
|
|
458
|
+
<div
|
|
459
|
+
v-if="backdrop"
|
|
460
|
+
ref="backdropRef"
|
|
461
|
+
class="s-modal-backdrop absolute inset-0 -z-1"
|
|
462
|
+
:class="[
|
|
463
|
+
'bg-black/60 dark:bg-black/70',
|
|
464
|
+
backdropBlur ? 'backdrop-blur-sm' : '',
|
|
465
|
+
backdropClass
|
|
466
|
+
]"
|
|
467
|
+
aria-hidden="true"
|
|
468
|
+
/>
|
|
469
|
+
|
|
470
|
+
<!-- Modal Panel -->
|
|
471
|
+
<Transition
|
|
472
|
+
appear
|
|
473
|
+
:enter-active-class="panelAnimations.enter"
|
|
474
|
+
:enter-from-class="panelAnimations.enterFrom"
|
|
475
|
+
:enter-to-class="panelAnimations.enterTo"
|
|
476
|
+
:leave-active-class="panelAnimations.leave"
|
|
477
|
+
:leave-from-class="panelAnimations.leaveFrom"
|
|
478
|
+
:leave-to-class="panelAnimations.leaveTo"
|
|
479
|
+
>
|
|
480
|
+
<div
|
|
481
|
+
v-show="isOpen"
|
|
482
|
+
ref="panelRef"
|
|
483
|
+
class="s-modal-panel relative flex flex-col overflow-hidden outline-none"
|
|
484
|
+
:class="[
|
|
485
|
+
sizeClasses,
|
|
486
|
+
variantClasses,
|
|
487
|
+
fullscreen ? '' : 'rounded-2xl',
|
|
488
|
+
'max-h-[calc(100vh-2rem)]',
|
|
489
|
+
panelClass
|
|
490
|
+
]"
|
|
491
|
+
tabindex="-1"
|
|
492
|
+
@mousedown.stop
|
|
493
|
+
>
|
|
494
|
+
<!-- Default Header (if title is provided and hideHeader is false) -->
|
|
495
|
+
<div
|
|
496
|
+
v-if="(title || closable) && !hideHeader && !$slots.header"
|
|
497
|
+
class="s-modal-header flex items-start justify-between gap-4 px-6 py-5 border-b border-border shrink-0"
|
|
498
|
+
>
|
|
499
|
+
<div class="flex-1 min-w-0">
|
|
500
|
+
<h2
|
|
501
|
+
v-if="title"
|
|
502
|
+
:id="titleId"
|
|
503
|
+
class="text-lg font-semibold text-foreground tracking-tight"
|
|
504
|
+
>
|
|
505
|
+
{{ title }}
|
|
506
|
+
</h2>
|
|
507
|
+
<p
|
|
508
|
+
v-if="description"
|
|
509
|
+
:id="descriptionId"
|
|
510
|
+
class="mt-1.5 text-sm text-muted-foreground"
|
|
511
|
+
>
|
|
512
|
+
{{ description }}
|
|
513
|
+
</p>
|
|
514
|
+
</div>
|
|
515
|
+
|
|
516
|
+
<button
|
|
517
|
+
v-if="closable"
|
|
518
|
+
type="button"
|
|
519
|
+
class="s-modal-close shrink-0 flex items-center justify-center w-8 h-8 -mt-1 -mr-2 rounded-lg text-muted-foreground hover:text-foreground hover:bg-accent transition-all duration-150 outline-none focus:ring-2 focus:ring-primary/30"
|
|
520
|
+
aria-label="Close modal"
|
|
521
|
+
@click="close"
|
|
522
|
+
>
|
|
523
|
+
<span class="mdi mdi-close text-xl" />
|
|
524
|
+
</button>
|
|
525
|
+
</div>
|
|
526
|
+
|
|
527
|
+
<!-- Header Slot -->
|
|
528
|
+
<slot name="header" />
|
|
529
|
+
|
|
530
|
+
<!-- Content -->
|
|
531
|
+
<div class="s-modal-body flex-1 overflow-y-auto overscroll-contain">
|
|
532
|
+
<slot />
|
|
533
|
+
</div>
|
|
534
|
+
|
|
535
|
+
<!-- Footer Slot -->
|
|
536
|
+
<slot name="footer" />
|
|
537
|
+
</div>
|
|
538
|
+
</Transition>
|
|
539
|
+
</div>
|
|
540
|
+
</Transition>
|
|
541
|
+
</Teleport>
|
|
542
|
+
|
|
543
|
+
<!-- Non-teleported fallback -->
|
|
544
|
+
<template v-else>
|
|
545
|
+
<Transition
|
|
546
|
+
enter-active-class="transition-opacity duration-300 ease-out"
|
|
547
|
+
enter-from-class="opacity-0"
|
|
548
|
+
enter-to-class="opacity-100"
|
|
549
|
+
leave-active-class="transition-opacity duration-200 ease-in"
|
|
550
|
+
leave-from-class="opacity-100"
|
|
551
|
+
leave-to-class="opacity-0"
|
|
552
|
+
>
|
|
553
|
+
<div
|
|
554
|
+
v-if="isOpen"
|
|
555
|
+
ref="modalRef"
|
|
556
|
+
v-bind="$attrs"
|
|
557
|
+
:class="cn('s-modal fixed inset-0 flex overflow-hidden', positionClasses, fullscreen ? 'p-0' : 'p-4', $attrs.class ?? '')"
|
|
558
|
+
:style="{ zIndex: effectiveZIndex }"
|
|
559
|
+
role="dialog"
|
|
560
|
+
aria-modal="true"
|
|
561
|
+
@mousedown="handleBackdropClick"
|
|
562
|
+
>
|
|
563
|
+
<div
|
|
564
|
+
v-if="backdrop"
|
|
565
|
+
ref="backdropRef"
|
|
566
|
+
class="s-modal-backdrop absolute inset-0 -z-1 bg-black/60 dark:bg-black/70"
|
|
567
|
+
:class="[backdropBlur ? 'backdrop-blur-sm' : '', backdropClass]"
|
|
568
|
+
aria-hidden="true"
|
|
569
|
+
/>
|
|
570
|
+
|
|
571
|
+
<Transition
|
|
572
|
+
appear
|
|
573
|
+
:enter-active-class="panelAnimations.enter"
|
|
574
|
+
:enter-from-class="panelAnimations.enterFrom"
|
|
575
|
+
:enter-to-class="panelAnimations.enterTo"
|
|
576
|
+
:leave-active-class="panelAnimations.leave"
|
|
577
|
+
:leave-from-class="panelAnimations.leaveFrom"
|
|
578
|
+
:leave-to-class="panelAnimations.leaveTo"
|
|
579
|
+
>
|
|
580
|
+
<div
|
|
581
|
+
v-show="isOpen"
|
|
582
|
+
ref="panelRef"
|
|
583
|
+
class="s-modal-panel relative flex flex-col overflow-hidden rounded-2xl outline-none"
|
|
584
|
+
:class="[sizeClasses, variantClasses, 'max-h-[calc(100vh-2rem)]', panelClass]"
|
|
585
|
+
tabindex="-1"
|
|
586
|
+
@mousedown.stop
|
|
587
|
+
>
|
|
588
|
+
<slot />
|
|
589
|
+
</div>
|
|
590
|
+
</Transition>
|
|
591
|
+
</div>
|
|
592
|
+
</Transition>
|
|
593
|
+
</template>
|
|
594
|
+
</template>
|
|
595
|
+
|
|
596
|
+
<style scoped>
|
|
597
|
+
.s-modal {
|
|
598
|
+
perspective: 1000px;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
.s-modal-panel {
|
|
602
|
+
transform-style: preserve-3d;
|
|
603
|
+
scrollbar-width: thin;
|
|
604
|
+
scrollbar-color: var(--s-border) transparent;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
.s-modal-panel::-webkit-scrollbar {
|
|
608
|
+
width: 6px;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.s-modal-panel::-webkit-scrollbar-track {
|
|
612
|
+
background: transparent;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.s-modal-panel::-webkit-scrollbar-thumb {
|
|
616
|
+
background: var(--s-border);
|
|
617
|
+
border-radius: 3px;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
.s-modal-body {
|
|
621
|
+
scrollbar-width: thin;
|
|
622
|
+
scrollbar-color: var(--s-border) transparent;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
.s-modal-body::-webkit-scrollbar {
|
|
626
|
+
width: 6px;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
.s-modal-body::-webkit-scrollbar-track {
|
|
630
|
+
background: transparent;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
.s-modal-body::-webkit-scrollbar-thumb {
|
|
634
|
+
background: var(--s-border);
|
|
635
|
+
border-radius: 3px;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/* Shake animation for persistent modals */
|
|
639
|
+
@keyframes shake {
|
|
640
|
+
0%, 100% { transform: translateX(0); }
|
|
641
|
+
10%, 30%, 50%, 70%, 90% { transform: translateX(-4px); }
|
|
642
|
+
20%, 40%, 60%, 80% { transform: translateX(4px); }
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.animate-shake {
|
|
646
|
+
animation: shake 0.3s ease-in-out;
|
|
647
|
+
}
|
|
648
|
+
</style>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SModalClose - Close button for modal
|
|
4
|
+
* Uses context to close the modal when clicked
|
|
5
|
+
*/
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<script setup lang="ts">
|
|
9
|
+
defineOptions({ inheritAttrs: false })
|
|
10
|
+
|
|
11
|
+
import { inject } from 'vue'
|
|
12
|
+
import { cn } from '../../../lib/utils'
|
|
13
|
+
import { SModalContextKey } from './SModal.vue'
|
|
14
|
+
|
|
15
|
+
export interface Props {
|
|
16
|
+
/** Additional class */
|
|
17
|
+
closeClass?: string
|
|
18
|
+
/** Disable the button */
|
|
19
|
+
disabled?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
23
|
+
closeClass: '',
|
|
24
|
+
disabled: false
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const context = inject(SModalContextKey)
|
|
28
|
+
|
|
29
|
+
const handleClose = () => {
|
|
30
|
+
if (!props.disabled) {
|
|
31
|
+
context?.close()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
v-bind="$attrs"
|
|
40
|
+
:class="cn('s-modal-close flex items-center justify-center w-8 h-8 rounded-lg text-muted-foreground hover:text-foreground hover:bg-accent transition-all duration-150 outline-none focus:ring-2 focus:ring-primary/30 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:text-muted-foreground', closeClass, $attrs.class ?? '')"
|
|
41
|
+
:disabled="disabled"
|
|
42
|
+
aria-label="Close modal"
|
|
43
|
+
@click="handleClose"
|
|
44
|
+
>
|
|
45
|
+
<slot>
|
|
46
|
+
<span class="mdi mdi-close text-xl" />
|
|
47
|
+
</slot>
|
|
48
|
+
</button>
|
|
49
|
+
</template>
|