@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,1054 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed, watch, nextTick, onMounted, type CSSProperties } from 'vue'
|
|
3
|
+
import { cn } from '../../lib/utils'
|
|
4
|
+
|
|
5
|
+
defineOptions({ inheritAttrs: false })
|
|
6
|
+
|
|
7
|
+
// Props interface
|
|
8
|
+
export interface Props {
|
|
9
|
+
// Core
|
|
10
|
+
modelValue?: string | number
|
|
11
|
+
type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'search' | 'textarea'
|
|
12
|
+
|
|
13
|
+
// Visual Design
|
|
14
|
+
variant?: 'outlined' | 'filled' | 'underlined' | 'ghost'
|
|
15
|
+
size?: 'small' | 'medium' | 'large'
|
|
16
|
+
color?: string
|
|
17
|
+
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'full'
|
|
18
|
+
|
|
19
|
+
// Label & Placeholder
|
|
20
|
+
label?: string
|
|
21
|
+
placeholder?: string
|
|
22
|
+
labelPlacement?: 'top' | 'top-left' | 'top-center' | 'top-right' | 'bottom' | 'left' | 'right' | 'floating' | 'inside'
|
|
23
|
+
labelAnimation?: 'morph' | 'slide' | 'fade' | 'none'
|
|
24
|
+
|
|
25
|
+
// Icons
|
|
26
|
+
iconLeft?: string
|
|
27
|
+
iconRight?: string
|
|
28
|
+
iconColor?: string
|
|
29
|
+
|
|
30
|
+
// States
|
|
31
|
+
disabled?: boolean
|
|
32
|
+
readonly?: boolean
|
|
33
|
+
loading?: boolean
|
|
34
|
+
error?: string | boolean
|
|
35
|
+
success?: string | boolean
|
|
36
|
+
warning?: string | boolean
|
|
37
|
+
hint?: string
|
|
38
|
+
|
|
39
|
+
// Validation
|
|
40
|
+
required?: boolean
|
|
41
|
+
minLength?: number
|
|
42
|
+
maxLength?: number
|
|
43
|
+
pattern?: string | RegExp
|
|
44
|
+
validator?: (value: string | number) => string | boolean | Promise<string | boolean>
|
|
45
|
+
validateOn?: 'blur' | 'input' | 'submit' | 'never'
|
|
46
|
+
|
|
47
|
+
// Input Behavior
|
|
48
|
+
autocomplete?: string
|
|
49
|
+
autofocus?: boolean
|
|
50
|
+
spellcheck?: boolean
|
|
51
|
+
inputmode?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'
|
|
52
|
+
max?: number | string
|
|
53
|
+
min?: number | string
|
|
54
|
+
step?: number | string
|
|
55
|
+
|
|
56
|
+
// Advanced Features
|
|
57
|
+
clearable?: boolean
|
|
58
|
+
showPasswordToggle?: boolean
|
|
59
|
+
counter?: boolean
|
|
60
|
+
prefix?: string
|
|
61
|
+
suffix?: string
|
|
62
|
+
rows?: number
|
|
63
|
+
resize?: 'none' | 'vertical' | 'horizontal' | 'both'
|
|
64
|
+
|
|
65
|
+
// Autocomplete Suggestions
|
|
66
|
+
suggestions?: string[] // e.g., ['@gmail.com', '@outlook.com'] for email
|
|
67
|
+
showSuggestionsOnFocus?: boolean
|
|
68
|
+
|
|
69
|
+
// Input Filtering/Masking
|
|
70
|
+
allowOnly?: 'digits' | 'letters' | 'alphanumeric' | RegExp | ((char: string) => boolean)
|
|
71
|
+
decimalPlaces?: number // For number type, max decimal places allowed
|
|
72
|
+
|
|
73
|
+
// Accessibility
|
|
74
|
+
name?: string
|
|
75
|
+
id?: string
|
|
76
|
+
ariaLabel?: string
|
|
77
|
+
ariaDescribedBy?: string
|
|
78
|
+
|
|
79
|
+
// Styling Overrides
|
|
80
|
+
inputClass?: string
|
|
81
|
+
labelClass?: string
|
|
82
|
+
wrapperClass?: string
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
86
|
+
modelValue: '',
|
|
87
|
+
type: 'text',
|
|
88
|
+
variant: 'outlined',
|
|
89
|
+
size: 'medium',
|
|
90
|
+
color: undefined,
|
|
91
|
+
rounded: 'md',
|
|
92
|
+
labelPlacement: 'top',
|
|
93
|
+
labelAnimation: 'morph',
|
|
94
|
+
disabled: false,
|
|
95
|
+
readonly: false,
|
|
96
|
+
loading: false,
|
|
97
|
+
required: false,
|
|
98
|
+
validateOn: 'blur',
|
|
99
|
+
clearable: false,
|
|
100
|
+
showPasswordToggle: false,
|
|
101
|
+
counter: false,
|
|
102
|
+
rows: 3,
|
|
103
|
+
resize: 'vertical',
|
|
104
|
+
autofocus: false,
|
|
105
|
+
spellcheck: true,
|
|
106
|
+
suggestions: undefined,
|
|
107
|
+
showSuggestionsOnFocus: true,
|
|
108
|
+
allowOnly: undefined,
|
|
109
|
+
decimalPlaces: undefined
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const emit = defineEmits<{
|
|
113
|
+
'update:modelValue': [value: string | number]
|
|
114
|
+
'update:error': [error: string | null]
|
|
115
|
+
'focus': [event: FocusEvent]
|
|
116
|
+
'blur': [event: FocusEvent]
|
|
117
|
+
'input': [event: Event]
|
|
118
|
+
'change': [value: string | number, event: Event]
|
|
119
|
+
'clear': []
|
|
120
|
+
'enter': [event: KeyboardEvent]
|
|
121
|
+
'validate': [isValid: boolean, error: string | null]
|
|
122
|
+
'select-suggestion': [suggestion: string]
|
|
123
|
+
}>()
|
|
124
|
+
|
|
125
|
+
// Refs
|
|
126
|
+
const inputRef = ref<HTMLInputElement | HTMLTextAreaElement | null>(null)
|
|
127
|
+
const isFocused = ref(false)
|
|
128
|
+
const showPassword = ref(false)
|
|
129
|
+
const internalError = ref<string | null>(null)
|
|
130
|
+
const isValidating = ref(false)
|
|
131
|
+
const inputId = computed(() => props.id || `s-input-${Math.random().toString(36).slice(2, 9)}`)
|
|
132
|
+
const messageId = computed(() => `${inputId.value}-message`)
|
|
133
|
+
|
|
134
|
+
// Suggestions state
|
|
135
|
+
const showSuggestions = ref(false)
|
|
136
|
+
const selectedSuggestionIndex = ref(-1)
|
|
137
|
+
const suggestionsRef = ref<HTMLUListElement | null>(null)
|
|
138
|
+
|
|
139
|
+
// Debounce timer
|
|
140
|
+
let validateTimer: ReturnType<typeof setTimeout> | null = null
|
|
141
|
+
|
|
142
|
+
// Computed
|
|
143
|
+
const hasValue = computed(() => {
|
|
144
|
+
return props.modelValue !== null && props.modelValue !== undefined && String(props.modelValue).length > 0
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
const currentLength = computed(() => String(props.modelValue || '').length)
|
|
148
|
+
|
|
149
|
+
const computedType = computed(() => {
|
|
150
|
+
if (props.type === 'password' && showPassword.value) {
|
|
151
|
+
return 'text'
|
|
152
|
+
}
|
|
153
|
+
return props.type
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
const isFloatingLabel = computed(() => {
|
|
157
|
+
return props.labelPlacement === 'floating' || props.labelPlacement === 'inside'
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const isLabelFloated = computed(() => {
|
|
161
|
+
return isFocused.value || hasValue.value
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
const displayError = computed(() => {
|
|
165
|
+
if (typeof props.error === 'string') return props.error
|
|
166
|
+
if (props.error === true) return 'Invalid value'
|
|
167
|
+
return internalError.value
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
const displaySuccess = computed(() => {
|
|
171
|
+
if (typeof props.success === 'string') return props.success
|
|
172
|
+
return null
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const displayWarning = computed(() => {
|
|
176
|
+
if (typeof props.warning === 'string') return props.warning
|
|
177
|
+
return null
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
const validationState = computed<'error' | 'success' | 'warning' | null>(() => {
|
|
181
|
+
if (displayError.value || props.error) return 'error'
|
|
182
|
+
if (displaySuccess.value || props.success) return 'success'
|
|
183
|
+
if (displayWarning.value || props.warning) return 'warning'
|
|
184
|
+
return null
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const validationIcon = computed(() => {
|
|
188
|
+
switch (validationState.value) {
|
|
189
|
+
case 'error': return 'alert-circle'
|
|
190
|
+
case 'success': return 'check-circle'
|
|
191
|
+
case 'warning': return 'alert'
|
|
192
|
+
default: return null
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
// Filtered suggestions based on current value
|
|
197
|
+
const filteredSuggestions = computed(() => {
|
|
198
|
+
if (!props.suggestions || !props.suggestions.length) return []
|
|
199
|
+
const currentValue = String(props.modelValue || '')
|
|
200
|
+
|
|
201
|
+
// For email type, show suggestions if @ is typed
|
|
202
|
+
if (props.type === 'email') {
|
|
203
|
+
const atIndex = currentValue.indexOf('@')
|
|
204
|
+
if (atIndex === -1) return [] // No @ yet, don't show suggestions
|
|
205
|
+
|
|
206
|
+
const afterAt = currentValue.slice(atIndex)
|
|
207
|
+
// Filter suggestions that match what's typed after @
|
|
208
|
+
return props.suggestions.filter(s =>
|
|
209
|
+
s.toLowerCase().startsWith(afterAt.toLowerCase()) && s !== afterAt
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// For other types, filter by value
|
|
214
|
+
return props.suggestions.filter(s =>
|
|
215
|
+
s.toLowerCase().includes(currentValue.toLowerCase()) && s !== currentValue
|
|
216
|
+
)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
// Should show suggestions dropdown
|
|
220
|
+
const shouldShowSuggestions = computed(() => {
|
|
221
|
+
return showSuggestions.value &&
|
|
222
|
+
filteredSuggestions.value.length > 0 &&
|
|
223
|
+
!props.disabled &&
|
|
224
|
+
!props.readonly
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
// Size configurations
|
|
228
|
+
const sizeConfig = computed(() => {
|
|
229
|
+
const sizes = {
|
|
230
|
+
small: {
|
|
231
|
+
input: 'min-h-8 text-xs',
|
|
232
|
+
padding: 'px-2.5 py-1.5',
|
|
233
|
+
paddingWithIcon: 'px-2.5',
|
|
234
|
+
label: 'text-xs',
|
|
235
|
+
icon: 'text-sm',
|
|
236
|
+
floatLabel: 'text-xs',
|
|
237
|
+
floatLabelActive: '-top-2 text-[10px]'
|
|
238
|
+
},
|
|
239
|
+
medium: {
|
|
240
|
+
input: 'min-h-10 text-sm',
|
|
241
|
+
padding: 'px-3 py-2',
|
|
242
|
+
paddingWithIcon: 'px-3',
|
|
243
|
+
label: 'text-sm',
|
|
244
|
+
icon: 'text-base',
|
|
245
|
+
floatLabel: 'text-sm',
|
|
246
|
+
floatLabelActive: '-top-2.5 text-xs'
|
|
247
|
+
},
|
|
248
|
+
large: {
|
|
249
|
+
input: 'min-h-12 text-base',
|
|
250
|
+
padding: 'px-4 py-2.5',
|
|
251
|
+
paddingWithIcon: 'px-4',
|
|
252
|
+
label: 'text-base',
|
|
253
|
+
icon: 'text-lg',
|
|
254
|
+
floatLabel: 'text-base',
|
|
255
|
+
floatLabelActive: '-top-3 text-sm'
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return sizes[props.size]
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// Rounded classes
|
|
262
|
+
const roundedConfig = computed(() => {
|
|
263
|
+
const radii = {
|
|
264
|
+
none: 'rounded-none',
|
|
265
|
+
sm: 'rounded',
|
|
266
|
+
md: 'rounded-lg',
|
|
267
|
+
lg: 'rounded-xl',
|
|
268
|
+
full: 'rounded-full'
|
|
269
|
+
}
|
|
270
|
+
return radii[props.rounded]
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
// Variant styles
|
|
274
|
+
const variantClasses = computed(() => {
|
|
275
|
+
const base = {
|
|
276
|
+
outlined: 'border bg-background border-border hover:border-input',
|
|
277
|
+
filled: 'border-transparent bg-accent',
|
|
278
|
+
underlined: 'border-b border-t-0 border-l-0 border-r-0 rounded-none! bg-transparent border-border',
|
|
279
|
+
ghost: 'border-transparent bg-transparent hover:bg-accent'
|
|
280
|
+
}
|
|
281
|
+
return base[props.variant]
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
// Focus classes
|
|
285
|
+
const focusClasses = computed(() => {
|
|
286
|
+
if (!isFocused.value) return ''
|
|
287
|
+
if (props.color) return '' // handled by inline style
|
|
288
|
+
if (props.variant === 'underlined') {
|
|
289
|
+
return 'border-primary'
|
|
290
|
+
}
|
|
291
|
+
return 'ring-2 ring-ring/20 border-primary'
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
// Focus inline style when custom color is set
|
|
295
|
+
const focusStyle = computed<CSSProperties | undefined>(() => {
|
|
296
|
+
if (!isFocused.value || !props.color) return undefined
|
|
297
|
+
if (props.variant === 'underlined') {
|
|
298
|
+
return { borderColor: props.color }
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
borderColor: props.color,
|
|
302
|
+
boxShadow: `0 0 0 2px color-mix(in srgb, ${props.color} 20%, transparent)`
|
|
303
|
+
}
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
// Validation state border colors
|
|
307
|
+
const validationBorderClasses = computed(() => {
|
|
308
|
+
if (!validationState.value) return ''
|
|
309
|
+
const colors = {
|
|
310
|
+
error: 'border-red-500 hover:border-red-500',
|
|
311
|
+
success: 'border-green-500 hover:border-green-500',
|
|
312
|
+
warning: 'border-amber-500 hover:border-amber-500'
|
|
313
|
+
}
|
|
314
|
+
return colors[validationState.value]
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
// Character counter color
|
|
318
|
+
const counterColorClass = computed(() => {
|
|
319
|
+
if (!props.maxLength) return 'text-muted-foreground'
|
|
320
|
+
const ratio = currentLength.value / props.maxLength
|
|
321
|
+
if (ratio >= 1) return 'text-red-500'
|
|
322
|
+
if (ratio >= 0.9) return 'text-amber-500'
|
|
323
|
+
if (ratio >= 0.75) return 'text-amber-400'
|
|
324
|
+
return 'text-muted-foreground'
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
// Icon color classes
|
|
328
|
+
const iconColorClass = computed(() => {
|
|
329
|
+
if (props.iconColor) return ''
|
|
330
|
+
if (isFocused.value) return props.color ? '' : 'text-primary'
|
|
331
|
+
return 'text-muted-foreground'
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
// Icon color inline style when custom color is set
|
|
335
|
+
const iconFocusStyle = computed<CSSProperties | undefined>(() => {
|
|
336
|
+
if (!isFocused.value || props.iconColor || !props.color) return undefined
|
|
337
|
+
return { color: props.color }
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
// Label layout classes for non-floating labels
|
|
341
|
+
const labelLayoutClasses = computed(() => {
|
|
342
|
+
if (isFloatingLabel.value) return 'flex flex-col w-full'
|
|
343
|
+
|
|
344
|
+
const [side, align] = props.labelPlacement.split('-') as [string, string | undefined]
|
|
345
|
+
const classes = ['flex', 'w-full']
|
|
346
|
+
|
|
347
|
+
if (side === 'top' || side === 'bottom') {
|
|
348
|
+
classes.push('flex-col', 'gap-1.5')
|
|
349
|
+
if (side === 'bottom') classes.push('flex-col-reverse')
|
|
350
|
+
} else {
|
|
351
|
+
classes.push('gap-3')
|
|
352
|
+
if (side === 'right') classes.push('flex-row-reverse')
|
|
353
|
+
else classes.push('flex-row')
|
|
354
|
+
if (align === 'center') classes.push('items-center')
|
|
355
|
+
else if (align === 'bottom') classes.push('items-end')
|
|
356
|
+
else classes.push('items-start')
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return classes.join(' ')
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
// Label text alignment
|
|
363
|
+
const labelClasses = computed(() => {
|
|
364
|
+
const base = `font-medium text-muted-foreground ${sizeConfig.value.label}`
|
|
365
|
+
if (isFloatingLabel.value) return base
|
|
366
|
+
|
|
367
|
+
const [side, align] = props.labelPlacement.split('-')
|
|
368
|
+
if (side === 'top' || side === 'bottom') {
|
|
369
|
+
if (align === 'center') return `${base} text-center`
|
|
370
|
+
if (align === 'right') return `${base} text-right`
|
|
371
|
+
}
|
|
372
|
+
return base
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
// Input padding based on icons
|
|
376
|
+
const inputPaddingClasses = computed(() => {
|
|
377
|
+
const hasLeft = !!props.iconLeft || !!props.prefix
|
|
378
|
+
const hasRight = !!props.iconRight || !!props.suffix || props.clearable || props.showPasswordToggle || props.loading || validationIcon.value
|
|
379
|
+
|
|
380
|
+
let classes = sizeConfig.value.input
|
|
381
|
+
|
|
382
|
+
if (props.size === 'small') {
|
|
383
|
+
classes += hasLeft ? ' pl-8' : ' pl-2.5'
|
|
384
|
+
classes += hasRight ? ' pr-8' : ' pr-2.5'
|
|
385
|
+
} else if (props.size === 'large') {
|
|
386
|
+
classes += hasLeft ? ' pl-12' : ' pl-4'
|
|
387
|
+
classes += hasRight ? ' pr-12' : ' pr-4'
|
|
388
|
+
} else {
|
|
389
|
+
classes += hasLeft ? ' pl-10' : ' pl-3'
|
|
390
|
+
classes += hasRight ? ' pr-10' : ' pr-3'
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return classes
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
// Validation
|
|
397
|
+
const isValidEmail = (email: string): boolean => {
|
|
398
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const isValidUrl = (url: string): boolean => {
|
|
402
|
+
try {
|
|
403
|
+
new URL(url)
|
|
404
|
+
return true
|
|
405
|
+
} catch {
|
|
406
|
+
return false
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const validate = async (value: string | number): Promise<string | null> => {
|
|
411
|
+
const strValue = String(value)
|
|
412
|
+
|
|
413
|
+
// Required validation
|
|
414
|
+
if (props.required && strValue.trim() === '') {
|
|
415
|
+
return 'This field is required'
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Skip other validations if empty and not required
|
|
419
|
+
if (strValue === '') return null
|
|
420
|
+
|
|
421
|
+
// Min length
|
|
422
|
+
if (props.minLength && strValue.length < props.minLength) {
|
|
423
|
+
return `Minimum ${props.minLength} characters required`
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Max length
|
|
427
|
+
if (props.maxLength && strValue.length > props.maxLength) {
|
|
428
|
+
return `Maximum ${props.maxLength} characters allowed`
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Pattern
|
|
432
|
+
if (props.pattern) {
|
|
433
|
+
const regex = typeof props.pattern === 'string' ? new RegExp(props.pattern) : props.pattern
|
|
434
|
+
if (!regex.test(strValue)) {
|
|
435
|
+
return 'Invalid format'
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Type-specific validation
|
|
440
|
+
if (props.type === 'email' && strValue && !isValidEmail(strValue)) {
|
|
441
|
+
return 'Please enter a valid email address'
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (props.type === 'url' && strValue && !isValidUrl(strValue)) {
|
|
445
|
+
return 'Please enter a valid URL'
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Number range validation
|
|
449
|
+
if (props.type === 'number') {
|
|
450
|
+
const numValue = Number(value)
|
|
451
|
+
if (props.min !== undefined && numValue < Number(props.min)) {
|
|
452
|
+
return `Minimum value is ${props.min}`
|
|
453
|
+
}
|
|
454
|
+
if (props.max !== undefined && numValue > Number(props.max)) {
|
|
455
|
+
return `Maximum value is ${props.max}`
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Custom validator
|
|
460
|
+
if (props.validator) {
|
|
461
|
+
isValidating.value = true
|
|
462
|
+
try {
|
|
463
|
+
const result = await props.validator(value)
|
|
464
|
+
if (typeof result === 'string') return result
|
|
465
|
+
if (result === false) return 'Invalid value'
|
|
466
|
+
} finally {
|
|
467
|
+
isValidating.value = false
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return null
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const runValidation = async () => {
|
|
475
|
+
if (props.validateOn === 'never') return
|
|
476
|
+
|
|
477
|
+
const error = await validate(props.modelValue ?? '')
|
|
478
|
+
internalError.value = error
|
|
479
|
+
emit('update:error', error)
|
|
480
|
+
emit('validate', error === null, error)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Input character filtering
|
|
484
|
+
const isCharacterAllowed = (char: string, newValue: string): boolean => {
|
|
485
|
+
const allowOnly = props.allowOnly
|
|
486
|
+
if (!allowOnly) return true
|
|
487
|
+
|
|
488
|
+
if (allowOnly === 'digits') {
|
|
489
|
+
// Allow digits and one decimal point for numbers with decimalPlaces
|
|
490
|
+
if (props.decimalPlaces !== undefined) {
|
|
491
|
+
return /[\d.]/.test(char) && !(char === '.' && newValue.includes('.'))
|
|
492
|
+
}
|
|
493
|
+
return /\d/.test(char)
|
|
494
|
+
}
|
|
495
|
+
if (allowOnly === 'letters') return /[a-zA-Z]/.test(char)
|
|
496
|
+
if (allowOnly === 'alphanumeric') return /[a-zA-Z0-9]/.test(char)
|
|
497
|
+
if (allowOnly instanceof RegExp) return allowOnly.test(char)
|
|
498
|
+
if (typeof allowOnly === 'function') return allowOnly(char)
|
|
499
|
+
return true
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const enforceDecimalPlaces = (value: string): string => {
|
|
503
|
+
if (props.decimalPlaces === undefined) return value
|
|
504
|
+
|
|
505
|
+
const parts = value.split('.')
|
|
506
|
+
if (parts.length === 2 && parts[1].length > props.decimalPlaces) {
|
|
507
|
+
return parts[0] + '.' + parts[1].slice(0, props.decimalPlaces)
|
|
508
|
+
}
|
|
509
|
+
return value
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Handle keypress for character filtering (beforeinput)
|
|
513
|
+
const handleBeforeInput = (event: InputEvent) => {
|
|
514
|
+
if (!props.allowOnly && props.decimalPlaces === undefined) return
|
|
515
|
+
|
|
516
|
+
const char = event.data
|
|
517
|
+
if (!char) return // Allow control keys
|
|
518
|
+
|
|
519
|
+
const target = event.target as HTMLInputElement
|
|
520
|
+
const newValue = target.value + char
|
|
521
|
+
|
|
522
|
+
// Check if character is allowed
|
|
523
|
+
if (!isCharacterAllowed(char, target.value)) {
|
|
524
|
+
event.preventDefault()
|
|
525
|
+
return
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Check decimal places
|
|
529
|
+
if (props.decimalPlaces !== undefined && char !== '.') {
|
|
530
|
+
const parts = newValue.split('.')
|
|
531
|
+
if (parts.length === 2 && parts[1].length > props.decimalPlaces) {
|
|
532
|
+
event.preventDefault()
|
|
533
|
+
return
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Event handlers
|
|
539
|
+
const handleInput = (event: Event) => {
|
|
540
|
+
const target = event.target as HTMLInputElement | HTMLTextAreaElement
|
|
541
|
+
let value: string | number = target.value
|
|
542
|
+
|
|
543
|
+
// Enforce decimal places after paste
|
|
544
|
+
if (props.decimalPlaces !== undefined) {
|
|
545
|
+
value = enforceDecimalPlaces(String(value))
|
|
546
|
+
target.value = value
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (props.type === 'number' && value !== '') {
|
|
550
|
+
value = Number(value)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
emit('update:modelValue', value)
|
|
554
|
+
emit('input', event)
|
|
555
|
+
|
|
556
|
+
// Show suggestions on input
|
|
557
|
+
if (props.suggestions?.length) {
|
|
558
|
+
showSuggestions.value = true
|
|
559
|
+
selectedSuggestionIndex.value = -1
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Clear error on input if it was set
|
|
563
|
+
if (internalError.value && props.validateOn === 'input') {
|
|
564
|
+
if (validateTimer) clearTimeout(validateTimer)
|
|
565
|
+
validateTimer = setTimeout(() => {
|
|
566
|
+
runValidation()
|
|
567
|
+
}, 500)
|
|
568
|
+
} else if (props.validateOn === 'input') {
|
|
569
|
+
if (validateTimer) clearTimeout(validateTimer)
|
|
570
|
+
validateTimer = setTimeout(() => {
|
|
571
|
+
runValidation()
|
|
572
|
+
}, 350000)
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const handleFocus = (event: FocusEvent) => {
|
|
577
|
+
isFocused.value = true
|
|
578
|
+
emit('focus', event)
|
|
579
|
+
|
|
580
|
+
// Show suggestions on focus if configured
|
|
581
|
+
if (props.showSuggestionsOnFocus && props.suggestions?.length) {
|
|
582
|
+
showSuggestions.value = true
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const handleBlur = (event: FocusEvent) => {
|
|
587
|
+
isFocused.value = false
|
|
588
|
+
emit('blur', event)
|
|
589
|
+
|
|
590
|
+
// Delay hiding suggestions to allow click on suggestion
|
|
591
|
+
setTimeout(() => {
|
|
592
|
+
showSuggestions.value = false
|
|
593
|
+
selectedSuggestionIndex.value = -1
|
|
594
|
+
}, 300)
|
|
595
|
+
|
|
596
|
+
if (props.validateOn === 'blur') {
|
|
597
|
+
runValidation()
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const handleChange = (event: Event) => {
|
|
602
|
+
emit('change', props.modelValue ?? '', event)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const handleKeydown = (event: KeyboardEvent) => {
|
|
606
|
+
// Handle keyboard navigation for suggestions
|
|
607
|
+
if (shouldShowSuggestions.value) {
|
|
608
|
+
if (event.key === 'ArrowDown') {
|
|
609
|
+
event.preventDefault()
|
|
610
|
+
selectedSuggestionIndex.value = Math.min(
|
|
611
|
+
selectedSuggestionIndex.value + 1,
|
|
612
|
+
filteredSuggestions.value.length - 1
|
|
613
|
+
)
|
|
614
|
+
return
|
|
615
|
+
}
|
|
616
|
+
if (event.key === 'ArrowUp') {
|
|
617
|
+
event.preventDefault()
|
|
618
|
+
selectedSuggestionIndex.value = Math.max(selectedSuggestionIndex.value - 1, -1)
|
|
619
|
+
return
|
|
620
|
+
}
|
|
621
|
+
if (event.key === 'Enter' && selectedSuggestionIndex.value >= 0) {
|
|
622
|
+
event.preventDefault()
|
|
623
|
+
selectSuggestion(filteredSuggestions.value[selectedSuggestionIndex.value])
|
|
624
|
+
return
|
|
625
|
+
}
|
|
626
|
+
if (event.key === 'Escape') {
|
|
627
|
+
showSuggestions.value = false
|
|
628
|
+
selectedSuggestionIndex.value = -1
|
|
629
|
+
return
|
|
630
|
+
}
|
|
631
|
+
if (event.key === 'Tab' && filteredSuggestions.value.length > 0) {
|
|
632
|
+
// Auto-complete with first suggestion on Tab
|
|
633
|
+
event.preventDefault()
|
|
634
|
+
selectSuggestion(filteredSuggestions.value[0])
|
|
635
|
+
return
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (event.key === 'Enter' && props.type !== 'textarea') {
|
|
640
|
+
emit('enter', event)
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Select a suggestion
|
|
645
|
+
const selectSuggestion = (suggestion: string) => {
|
|
646
|
+
let newValue = suggestion
|
|
647
|
+
|
|
648
|
+
// For email type, append suggestion to the part before @
|
|
649
|
+
if (props.type === 'email') {
|
|
650
|
+
const currentValue = String(props.modelValue || '')
|
|
651
|
+
const atIndex = currentValue.indexOf('@')
|
|
652
|
+
if (atIndex >= 0) {
|
|
653
|
+
newValue = currentValue.slice(0, atIndex) + suggestion
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
emit('update:modelValue', newValue)
|
|
658
|
+
emit('select-suggestion', suggestion)
|
|
659
|
+
showSuggestions.value = false
|
|
660
|
+
selectedSuggestionIndex.value = -1
|
|
661
|
+
|
|
662
|
+
// Focus input after selection
|
|
663
|
+
nextTick(() => {
|
|
664
|
+
inputRef.value?.focus()
|
|
665
|
+
})
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const clear = () => {
|
|
669
|
+
emit('update:modelValue', '')
|
|
670
|
+
emit('clear')
|
|
671
|
+
internalError.value = null
|
|
672
|
+
nextTick(() => {
|
|
673
|
+
inputRef.value?.focus()
|
|
674
|
+
})
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const togglePassword = () => {
|
|
678
|
+
showPassword.value = !showPassword.value
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Focus method
|
|
682
|
+
const focus = () => {
|
|
683
|
+
inputRef.value?.focus()
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const blur = () => {
|
|
687
|
+
inputRef.value?.blur()
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Expose methods
|
|
691
|
+
defineExpose({
|
|
692
|
+
focus,
|
|
693
|
+
blur,
|
|
694
|
+
validate: runValidation,
|
|
695
|
+
inputElement: inputRef
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
// Lifecycle
|
|
699
|
+
onMounted(() => {
|
|
700
|
+
if (props.autofocus) {
|
|
701
|
+
nextTick(() => {
|
|
702
|
+
inputRef.value?.focus()
|
|
703
|
+
})
|
|
704
|
+
}
|
|
705
|
+
})
|
|
706
|
+
|
|
707
|
+
// Watch for external error changes
|
|
708
|
+
watch(() => props.error, (newError) => {
|
|
709
|
+
if (newError) {
|
|
710
|
+
internalError.value = null
|
|
711
|
+
}
|
|
712
|
+
})
|
|
713
|
+
</script>
|
|
714
|
+
|
|
715
|
+
<template>
|
|
716
|
+
<div
|
|
717
|
+
v-bind="$attrs"
|
|
718
|
+
:class="cn('s-input-wrapper relative w-full', labelLayoutClasses, wrapperClass, $attrs.class as string)"
|
|
719
|
+
>
|
|
720
|
+
<!-- Static Label (non-floating) -->
|
|
721
|
+
<label
|
|
722
|
+
v-if="label && !isFloatingLabel"
|
|
723
|
+
:for="inputId"
|
|
724
|
+
class="shrink-0"
|
|
725
|
+
:class="[labelClasses, labelClass]"
|
|
726
|
+
>
|
|
727
|
+
{{ label }}
|
|
728
|
+
<span v-if="required" class="text-red-500 ml-0.5">*</span>
|
|
729
|
+
</label>
|
|
730
|
+
|
|
731
|
+
<!-- Input Container -->
|
|
732
|
+
<div class="flex-1 min-w-0">
|
|
733
|
+
<div class="s-input-container relative">
|
|
734
|
+
<!-- Prefix / Left Icon -->
|
|
735
|
+
<span
|
|
736
|
+
v-if="iconLeft || prefix"
|
|
737
|
+
class="absolute left-0 top-1/2 -translate-y-1/2 flex items-center gap-1 pointer-events-none transition-colors duration-200"
|
|
738
|
+
:class="[
|
|
739
|
+
sizeConfig.icon,
|
|
740
|
+
iconColorClass,
|
|
741
|
+
size === 'small' ? 'pl-2.5' : size === 'large' ? 'pl-4' : 'pl-3'
|
|
742
|
+
]"
|
|
743
|
+
:style="iconColor ? { color: iconColor } : iconFocusStyle"
|
|
744
|
+
>
|
|
745
|
+
<slot name="prefix">
|
|
746
|
+
<span v-if="iconLeft" :class="['mdi', `mdi-${iconLeft}`]" />
|
|
747
|
+
<span v-if="prefix" class="text-muted-foreground text-sm">{{ prefix }}</span>
|
|
748
|
+
</slot>
|
|
749
|
+
</span>
|
|
750
|
+
|
|
751
|
+
<!-- Input Element -->
|
|
752
|
+
<input
|
|
753
|
+
v-if="type !== 'textarea'"
|
|
754
|
+
ref="inputRef"
|
|
755
|
+
:id="inputId"
|
|
756
|
+
:name="name"
|
|
757
|
+
:type="computedType"
|
|
758
|
+
:value="modelValue"
|
|
759
|
+
:placeholder="isFloatingLabel && !isLabelFloated ? '' : placeholder"
|
|
760
|
+
:disabled="disabled"
|
|
761
|
+
:readonly="readonly"
|
|
762
|
+
:required="required"
|
|
763
|
+
:autocomplete="autocomplete"
|
|
764
|
+
:spellcheck="spellcheck"
|
|
765
|
+
:inputmode="inputmode"
|
|
766
|
+
:min="min"
|
|
767
|
+
:max="max"
|
|
768
|
+
:step="step"
|
|
769
|
+
:maxlength="maxLength"
|
|
770
|
+
:aria-label="ariaLabel || label"
|
|
771
|
+
:aria-describedby="(displayError || displaySuccess || displayWarning || hint) ? messageId : ariaDescribedBy"
|
|
772
|
+
:aria-invalid="!!displayError"
|
|
773
|
+
:aria-required="required"
|
|
774
|
+
class="s-input w-full outline-none transition-all duration-200 text-foreground placeholder:text-muted-foreground"
|
|
775
|
+
:class="[
|
|
776
|
+
inputPaddingClasses,
|
|
777
|
+
roundedConfig,
|
|
778
|
+
variantClasses,
|
|
779
|
+
focusClasses,
|
|
780
|
+
validationBorderClasses,
|
|
781
|
+
inputClass,
|
|
782
|
+
{
|
|
783
|
+
'opacity-50 cursor-not-allowed': disabled,
|
|
784
|
+
'cursor-wait': loading,
|
|
785
|
+
'py-2': isFloatingLabel
|
|
786
|
+
}
|
|
787
|
+
]"
|
|
788
|
+
:style="focusStyle"
|
|
789
|
+
@input="handleInput"
|
|
790
|
+
@focus="handleFocus"
|
|
791
|
+
@blur="handleBlur"
|
|
792
|
+
@change="handleChange"
|
|
793
|
+
@keydown="handleKeydown"
|
|
794
|
+
@beforeinput="handleBeforeInput"
|
|
795
|
+
/>
|
|
796
|
+
|
|
797
|
+
<!-- Textarea Element -->
|
|
798
|
+
<textarea
|
|
799
|
+
v-else
|
|
800
|
+
ref="inputRef"
|
|
801
|
+
:id="inputId"
|
|
802
|
+
:name="name"
|
|
803
|
+
:value="modelValue"
|
|
804
|
+
:placeholder="isFloatingLabel && !isLabelFloated ? '' : placeholder"
|
|
805
|
+
:disabled="disabled"
|
|
806
|
+
:readonly="readonly"
|
|
807
|
+
:required="required"
|
|
808
|
+
:autocomplete="autocomplete"
|
|
809
|
+
:spellcheck="spellcheck"
|
|
810
|
+
:rows="rows"
|
|
811
|
+
:maxlength="maxLength"
|
|
812
|
+
:aria-label="ariaLabel || label"
|
|
813
|
+
:aria-describedby="(displayError || displaySuccess || displayWarning || hint) ? messageId : ariaDescribedBy"
|
|
814
|
+
:aria-invalid="!!displayError"
|
|
815
|
+
:aria-required="required"
|
|
816
|
+
class="s-input w-full outline-none transition-all duration-200 text-foreground placeholder:text-muted-foreground"
|
|
817
|
+
:class="[
|
|
818
|
+
sizeConfig.input,
|
|
819
|
+
size === 'small' ? 'px-2.5 py-1.5' : size === 'large' ? 'px-4 py-2.5' : 'px-3 py-2',
|
|
820
|
+
roundedConfig,
|
|
821
|
+
variantClasses,
|
|
822
|
+
focusClasses,
|
|
823
|
+
validationBorderClasses,
|
|
824
|
+
inputClass,
|
|
825
|
+
{
|
|
826
|
+
'opacity-50 cursor-not-allowed': disabled,
|
|
827
|
+
'cursor-wait': loading,
|
|
828
|
+
'resize-none': resize === 'none',
|
|
829
|
+
'resize-y': resize === 'vertical',
|
|
830
|
+
'resize-x': resize === 'horizontal',
|
|
831
|
+
'resize': resize === 'both'
|
|
832
|
+
}
|
|
833
|
+
]"
|
|
834
|
+
:style="{ ...focusStyle, minHeight: type === 'textarea' ? `${rows * 1.5 + 1}rem` : undefined }"
|
|
835
|
+
@input="handleInput"
|
|
836
|
+
@focus="handleFocus"
|
|
837
|
+
@blur="handleBlur"
|
|
838
|
+
@change="handleChange"
|
|
839
|
+
/>
|
|
840
|
+
|
|
841
|
+
<!-- Floating/Inside Label -->
|
|
842
|
+
<label
|
|
843
|
+
v-if="label && isFloatingLabel"
|
|
844
|
+
:for="inputId"
|
|
845
|
+
class="s-input-label-floating absolute left-0 pointer-events-none transition-all duration-200 ease-out origin-left"
|
|
846
|
+
:class="[
|
|
847
|
+
labelClass,
|
|
848
|
+
size === 'small' ? 'left-2.5' : size === 'large' ? 'left-4' : 'left-3',
|
|
849
|
+
iconLeft || prefix ? (size === 'small' ? 'left-8' : size === 'large' ? 'left-12' : 'left-10') : '',
|
|
850
|
+
isLabelFloated
|
|
851
|
+
? [sizeConfig.floatLabelActive, color ? '' : 'text-primary', 'bg-background px-1 -ml-1']
|
|
852
|
+
: ['top-1/2 -translate-y-1/2', sizeConfig.floatLabel, 'text-muted-foreground']
|
|
853
|
+
]"
|
|
854
|
+
:style="isLabelFloated && color ? { color } : undefined"
|
|
855
|
+
>
|
|
856
|
+
{{ label }}
|
|
857
|
+
<span v-if="required" class="text-red-500 ml-0.5">*</span>
|
|
858
|
+
</label>
|
|
859
|
+
|
|
860
|
+
<!-- Suffix / Right Icon / Actions -->
|
|
861
|
+
<span
|
|
862
|
+
v-if="iconRight || suffix || clearable || showPasswordToggle || loading || validationIcon"
|
|
863
|
+
class="absolute right-0 top-1/2 -translate-y-1/2 flex items-center gap-1 transition-colors duration-200"
|
|
864
|
+
:class="[
|
|
865
|
+
sizeConfig.icon,
|
|
866
|
+
size === 'small' ? 'pr-2.5' : size === 'large' ? 'pr-4' : 'pr-3'
|
|
867
|
+
]"
|
|
868
|
+
>
|
|
869
|
+
<!-- Loading spinner -->
|
|
870
|
+
<span
|
|
871
|
+
v-if="loading"
|
|
872
|
+
class="mdi mdi-loading animate-spin text-muted-foreground"
|
|
873
|
+
/>
|
|
874
|
+
|
|
875
|
+
<!-- Clear button -->
|
|
876
|
+
<button
|
|
877
|
+
v-else-if="clearable && hasValue && !disabled && !readonly"
|
|
878
|
+
type="button"
|
|
879
|
+
class="mdi mdi-close-circle text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
|
|
880
|
+
tabindex="-1"
|
|
881
|
+
@click="clear"
|
|
882
|
+
/>
|
|
883
|
+
|
|
884
|
+
<!-- Validation icon -->
|
|
885
|
+
<span
|
|
886
|
+
v-else-if="validationIcon && !iconRight"
|
|
887
|
+
:class="[
|
|
888
|
+
'mdi',
|
|
889
|
+
`mdi-${validationIcon}`,
|
|
890
|
+
validationState === 'error' ? 'text-red-500' : '',
|
|
891
|
+
validationState === 'success' ? 'text-green-500' : '',
|
|
892
|
+
validationState === 'warning' ? 'text-amber-500' : ''
|
|
893
|
+
]"
|
|
894
|
+
/>
|
|
895
|
+
|
|
896
|
+
<!-- Password toggle -->
|
|
897
|
+
<button
|
|
898
|
+
v-if="type === 'password' && showPasswordToggle && !loading"
|
|
899
|
+
type="button"
|
|
900
|
+
class="mdi transition-colors cursor-pointer text-muted-foreground hover:text-foreground"
|
|
901
|
+
:class="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
|
|
902
|
+
tabindex="-1"
|
|
903
|
+
@click="togglePassword"
|
|
904
|
+
/>
|
|
905
|
+
|
|
906
|
+
<!-- Custom suffix -->
|
|
907
|
+
<slot name="suffix">
|
|
908
|
+
<span
|
|
909
|
+
v-if="iconRight"
|
|
910
|
+
:class="['mdi', `mdi-${iconRight}`, iconColorClass]"
|
|
911
|
+
:style="iconColor ? { color: iconColor } : iconFocusStyle"
|
|
912
|
+
/>
|
|
913
|
+
<span v-if="suffix" class="text-muted-foreground text-sm">{{ suffix }}</span>
|
|
914
|
+
</slot>
|
|
915
|
+
</span>
|
|
916
|
+
|
|
917
|
+
<!-- Animated border line (underlined variant) -->
|
|
918
|
+
<div
|
|
919
|
+
v-if="variant === 'underlined'"
|
|
920
|
+
class="s-input-border-animated absolute bottom-0 left-1/2 h-0.5 transition-all duration-200 ease-out"
|
|
921
|
+
:class="[color ? '' : 'bg-primary', isFocused ? 'w-full -translate-x-1/2' : 'w-0 -translate-x-1/2']"
|
|
922
|
+
:style="color ? { backgroundColor: color } : undefined"
|
|
923
|
+
/>
|
|
924
|
+
|
|
925
|
+
<!-- Suggestions Dropdown -->
|
|
926
|
+
<Transition
|
|
927
|
+
enter-active-class="transition-all duration-150 ease-out"
|
|
928
|
+
enter-from-class="opacity-0 -translate-y-2 scale-95"
|
|
929
|
+
enter-to-class="opacity-100 translate-y-0 scale-100"
|
|
930
|
+
leave-active-class="transition-all duration-100 ease-in"
|
|
931
|
+
leave-from-class="opacity-100 translate-y-0 scale-100"
|
|
932
|
+
leave-to-class="opacity-0 -translate-y-2 scale-95"
|
|
933
|
+
>
|
|
934
|
+
<ul
|
|
935
|
+
v-if="shouldShowSuggestions"
|
|
936
|
+
ref="suggestionsRef"
|
|
937
|
+
class="s-input-suggestions absolute left-0 right-0 top-full mt-1 z-50 rounded-lg border border-border bg-background shadow-lg overflow-hidden"
|
|
938
|
+
role="listbox"
|
|
939
|
+
:aria-label="`Suggestions for ${label || 'input'}`"
|
|
940
|
+
>
|
|
941
|
+
<li
|
|
942
|
+
v-for="(suggestion, index) in filteredSuggestions"
|
|
943
|
+
:key="suggestion"
|
|
944
|
+
role="option"
|
|
945
|
+
:aria-selected="index === selectedSuggestionIndex"
|
|
946
|
+
class="px-3 py-2 text-sm cursor-pointer transition-colors"
|
|
947
|
+
:class="[
|
|
948
|
+
index === selectedSuggestionIndex
|
|
949
|
+
? 'bg-primary text-primary-foreground'
|
|
950
|
+
: 'text-foreground hover:bg-accent'
|
|
951
|
+
]"
|
|
952
|
+
@mousedown.prevent="selectSuggestion(suggestion)"
|
|
953
|
+
@mouseenter="selectedSuggestionIndex = index"
|
|
954
|
+
>
|
|
955
|
+
<template v-if="type === 'email'">
|
|
956
|
+
<span class="text-muted-foreground">{{ String(modelValue || '').split('@')[0] }}</span>
|
|
957
|
+
<span :class="index === selectedSuggestionIndex ? 'text-primary-foreground' : 'text-primary font-medium'">{{ suggestion }}</span>
|
|
958
|
+
</template>
|
|
959
|
+
<template v-else>
|
|
960
|
+
{{ suggestion }}
|
|
961
|
+
</template>
|
|
962
|
+
</li>
|
|
963
|
+
</ul>
|
|
964
|
+
</Transition>
|
|
965
|
+
</div>
|
|
966
|
+
|
|
967
|
+
<!-- Messages Row -->
|
|
968
|
+
<div
|
|
969
|
+
v-if="displayError || displaySuccess || displayWarning || hint || (counter && maxLength)"
|
|
970
|
+
:id="messageId"
|
|
971
|
+
class="s-input-messages flex items-start justify-between gap-2 mt-1.5"
|
|
972
|
+
>
|
|
973
|
+
<!-- Message -->
|
|
974
|
+
<Transition
|
|
975
|
+
enter-active-class="transition-all duration-200 ease-out"
|
|
976
|
+
enter-from-class="opacity-0 -translate-y-1"
|
|
977
|
+
enter-to-class="opacity-100 translate-y-0"
|
|
978
|
+
leave-active-class="transition-all duration-150 ease-in"
|
|
979
|
+
leave-from-class="opacity-100 translate-y-0"
|
|
980
|
+
leave-to-class="opacity-0 -translate-y-1"
|
|
981
|
+
mode="out-in"
|
|
982
|
+
>
|
|
983
|
+
<p
|
|
984
|
+
v-if="displayError"
|
|
985
|
+
key="error"
|
|
986
|
+
class="text-xs text-red-500 flex items-center gap-1"
|
|
987
|
+
>
|
|
988
|
+
<span class="mdi mdi-alert-circle text-sm" />
|
|
989
|
+
{{ displayError }}
|
|
990
|
+
</p>
|
|
991
|
+
<p
|
|
992
|
+
v-else-if="displaySuccess"
|
|
993
|
+
key="success"
|
|
994
|
+
class="text-xs text-green-500 flex items-center gap-1"
|
|
995
|
+
>
|
|
996
|
+
<span class="mdi mdi-check-circle text-sm" />
|
|
997
|
+
{{ displaySuccess }}
|
|
998
|
+
</p>
|
|
999
|
+
<p
|
|
1000
|
+
v-else-if="displayWarning"
|
|
1001
|
+
key="warning"
|
|
1002
|
+
class="text-xs text-amber-500 flex items-center gap-1"
|
|
1003
|
+
>
|
|
1004
|
+
<span class="mdi mdi-alert text-sm" />
|
|
1005
|
+
{{ displayWarning }}
|
|
1006
|
+
</p>
|
|
1007
|
+
<p
|
|
1008
|
+
v-else-if="hint"
|
|
1009
|
+
key="hint"
|
|
1010
|
+
class="text-xs text-muted-foreground"
|
|
1011
|
+
>
|
|
1012
|
+
{{ hint }}
|
|
1013
|
+
</p>
|
|
1014
|
+
<span v-else key="empty" />
|
|
1015
|
+
</Transition>
|
|
1016
|
+
|
|
1017
|
+
<!-- Character counter -->
|
|
1018
|
+
<span
|
|
1019
|
+
v-if="counter && maxLength"
|
|
1020
|
+
class="text-xs shrink-0 tabular-nums transition-colors duration-200"
|
|
1021
|
+
:class="counterColorClass"
|
|
1022
|
+
>
|
|
1023
|
+
{{ currentLength }} / {{ maxLength }}
|
|
1024
|
+
</span>
|
|
1025
|
+
</div>
|
|
1026
|
+
</div>
|
|
1027
|
+
</div>
|
|
1028
|
+
</template>
|
|
1029
|
+
|
|
1030
|
+
<style scoped>
|
|
1031
|
+
/* Ensure floating label background matches input */
|
|
1032
|
+
.s-input-label-floating {
|
|
1033
|
+
z-index: 1;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/* Remove default browser styling for search inputs */
|
|
1037
|
+
.s-input[type="search"]::-webkit-search-cancel-button,
|
|
1038
|
+
.s-input[type="search"]::-webkit-search-decoration {
|
|
1039
|
+
-webkit-appearance: none;
|
|
1040
|
+
appearance: none;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/* Remove number input spinners */
|
|
1044
|
+
.s-input[type="number"]::-webkit-inner-spin-button,
|
|
1045
|
+
.s-input[type="number"]::-webkit-outer-spin-button {
|
|
1046
|
+
-webkit-appearance: none;
|
|
1047
|
+
margin: 0;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
.s-input[type="number"] {
|
|
1051
|
+
-moz-appearance: textfield;
|
|
1052
|
+
appearance: textfield;
|
|
1053
|
+
}
|
|
1054
|
+
</style>
|