@vestow/vue 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 +233 -0
- package/index.ts +8 -0
- package/package.json +68 -0
- package/src/components/accordion/Accordion.vue +45 -0
- package/src/components/accordion/AccordionContent.vue +31 -0
- package/src/components/accordion/AccordionItem.vue +26 -0
- package/src/components/accordion/AccordionTrigger.vue +39 -0
- package/src/components/accordion/index.ts +18 -0
- package/src/components/alert/Alert.vue +17 -0
- package/src/components/alert/AlertDescription.vue +12 -0
- package/src/components/alert/AlertTitle.vue +12 -0
- package/src/components/alert/index.ts +5 -0
- package/src/components/avatar/Avatar.vue +32 -0
- package/src/components/avatar/AvatarFallback.vue +23 -0
- package/src/components/avatar/AvatarImage.vue +18 -0
- package/src/components/avatar/index.ts +3 -0
- package/src/components/badge/Badge.vue +25 -0
- package/src/components/badge/index.ts +3 -0
- package/src/components/breadcrumb/Breadcrumb.vue +14 -0
- package/src/components/breadcrumb/BreadcrumbItem.vue +12 -0
- package/src/components/breadcrumb/BreadcrumbLink.vue +19 -0
- package/src/components/breadcrumb/BreadcrumbPage.vue +12 -0
- package/src/components/breadcrumb/BreadcrumbSeparator.vue +15 -0
- package/src/components/breadcrumb/index.ts +5 -0
- package/src/components/button/Button.vue +55 -0
- package/src/components/button/index.ts +3 -0
- package/src/components/button-group/ButtonGroup.vue +29 -0
- package/src/components/button-group/ButtonGroupSeparator.vue +30 -0
- package/src/components/button-group/ButtonGroupText.vue +29 -0
- package/src/components/button-group/SplitButton.vue +103 -0
- package/src/components/button-group/SplitButtonItems.vue +50 -0
- package/src/components/button-group/index.ts +34 -0
- package/src/components/calendar/Calendar.vue +120 -0
- package/src/components/calendar/RangeCalendar.vue +127 -0
- package/src/components/calendar/index.ts +2 -0
- package/src/components/card/Card.vue +12 -0
- package/src/components/card/CardAction.vue +12 -0
- package/src/components/card/CardContent.vue +12 -0
- package/src/components/card/CardDescription.vue +12 -0
- package/src/components/card/CardFooter.vue +12 -0
- package/src/components/card/CardHeader.vue +15 -0
- package/src/components/card/CardTitle.vue +12 -0
- package/src/components/card/index.ts +7 -0
- package/src/components/carousel/Carousel.vue +164 -0
- package/src/components/carousel/CarouselContent.vue +43 -0
- package/src/components/carousel/CarouselDots.vue +42 -0
- package/src/components/carousel/CarouselItem.vue +58 -0
- package/src/components/carousel/CarouselNext.vue +45 -0
- package/src/components/carousel/CarouselPrevious.vue +45 -0
- package/src/components/carousel/index.ts +44 -0
- package/src/components/cascade-select/CascadeSelect.vue +314 -0
- package/src/components/cascade-select/index.ts +11 -0
- package/src/components/chart/AreaChart.vue +201 -0
- package/src/components/chart/BarChart.vue +389 -0
- package/src/components/chart/ChartContainer.vue +127 -0
- package/src/components/chart/ChartCrosshair.vue +92 -0
- package/src/components/chart/ChartLegend.vue +63 -0
- package/src/components/chart/ChartTooltipCard.vue +49 -0
- package/src/components/chart/LineChart.vue +214 -0
- package/src/components/chart/PieChart.vue +280 -0
- package/src/components/chart/RadarChart.vue +291 -0
- package/src/components/chart/RadialChart.vue +354 -0
- package/src/components/chart/index.ts +30 -0
- package/src/components/chart/polar.ts +5 -0
- package/src/components/chart/tooltip.ts +5 -0
- package/src/components/chart/types.ts +52 -0
- package/src/components/chart/useChartSeries.ts +138 -0
- package/src/components/chart/xy-label.ts +5 -0
- package/src/components/checkbox/Checkbox.vue +81 -0
- package/src/components/checkbox/CheckboxField.vue +129 -0
- package/src/components/checkbox/CheckboxGroup.vue +62 -0
- package/src/components/checkbox/index.ts +21 -0
- package/src/components/chip/Chip.vue +73 -0
- package/src/components/chip/index.ts +3 -0
- package/src/components/code-block/CodeBlock.vue +100 -0
- package/src/components/code-block/index.ts +4 -0
- package/src/components/collapsible/Collapsible.vue +15 -0
- package/src/components/collapsible/CollapsibleContent.vue +26 -0
- package/src/components/collapsible/CollapsibleTrigger.vue +12 -0
- package/src/components/collapsible/index.ts +3 -0
- package/src/components/combobox/Combobox.vue +23 -0
- package/src/components/combobox/ComboboxAnchor.vue +20 -0
- package/src/components/combobox/ComboboxEmpty.vue +21 -0
- package/src/components/combobox/ComboboxGroup.vue +23 -0
- package/src/components/combobox/ComboboxInput.vue +32 -0
- package/src/components/combobox/ComboboxItem.vue +38 -0
- package/src/components/combobox/ComboboxList.vue +43 -0
- package/src/components/combobox/ComboboxSeparator.vue +18 -0
- package/src/components/combobox/ComboboxTrigger.vue +12 -0
- package/src/components/combobox/index.ts +9 -0
- package/src/components/command/Command.vue +54 -0
- package/src/components/command/CommandDialog.vue +41 -0
- package/src/components/command/CommandEmpty.vue +19 -0
- package/src/components/command/CommandGroup.vue +28 -0
- package/src/components/command/CommandInput.vue +25 -0
- package/src/components/command/CommandItem.vue +40 -0
- package/src/components/command/CommandList.vue +13 -0
- package/src/components/command/CommandSeparator.vue +11 -0
- package/src/components/command/CommandShortcut.vue +13 -0
- package/src/components/command/index.ts +34 -0
- package/src/components/command-block/CommandBlock.vue +90 -0
- package/src/components/command-block/index.ts +2 -0
- package/src/components/confirm-dialog/ConfirmDialog.vue +37 -0
- package/src/components/confirm-dialog/index.ts +1 -0
- package/src/components/confirm-popup/ConfirmPopup.vue +123 -0
- package/src/components/confirm-popup/index.ts +1 -0
- package/src/components/context-menu/ContextMenu.vue +15 -0
- package/src/components/context-menu/ContextMenuCheckboxItem.vue +30 -0
- package/src/components/context-menu/ContextMenuContent.vue +36 -0
- package/src/components/context-menu/ContextMenuGroup.vue +12 -0
- package/src/components/context-menu/ContextMenuItem.vue +32 -0
- package/src/components/context-menu/ContextMenuLabel.vue +21 -0
- package/src/components/context-menu/ContextMenuRadioGroup.vue +15 -0
- package/src/components/context-menu/ContextMenuRadioItem.vue +30 -0
- package/src/components/context-menu/ContextMenuSeparator.vue +19 -0
- package/src/components/context-menu/ContextMenuShortcut.vue +13 -0
- package/src/components/context-menu/ContextMenuSub.vue +15 -0
- package/src/components/context-menu/ContextMenuSubContent.vue +26 -0
- package/src/components/context-menu/ContextMenuSubTrigger.vue +26 -0
- package/src/components/context-menu/ContextMenuTrigger.vue +12 -0
- package/src/components/context-menu/index.ts +14 -0
- package/src/components/data-table/DataTable.vue +458 -0
- package/src/components/data-table/DataTableFacetedFilter.vue +97 -0
- package/src/components/data-table/DataTablePagination.vue +103 -0
- package/src/components/data-table/DataTableViewOptions.vue +69 -0
- package/src/components/data-table/features.ts +87 -0
- package/src/components/data-table/index.ts +27 -0
- package/src/components/data-table/pagination-adapter.ts +67 -0
- package/src/components/data-table/selection-column.ts +48 -0
- package/src/components/data-table/types.ts +36 -0
- package/src/components/data-view/DataView.vue +136 -0
- package/src/components/data-view/index.ts +1 -0
- package/src/components/date-picker/DatePicker.vue +117 -0
- package/src/components/date-picker/DateRangePicker.vue +122 -0
- package/src/components/date-picker/index.ts +2 -0
- package/src/components/deferred/Deferred.vue +80 -0
- package/src/components/deferred/index.ts +1 -0
- package/src/components/dialog/Dialog.vue +15 -0
- package/src/components/dialog/DialogClose.vue +12 -0
- package/src/components/dialog/DialogContent.vue +46 -0
- package/src/components/dialog/DialogDescription.vue +19 -0
- package/src/components/dialog/DialogFooter.vue +12 -0
- package/src/components/dialog/DialogHeader.vue +12 -0
- package/src/components/dialog/DialogTitle.vue +19 -0
- package/src/components/dialog/DialogTrigger.vue +12 -0
- package/src/components/dialog/index.ts +10 -0
- package/src/components/doc-example/DocExample.vue +41 -0
- package/src/components/doc-example/index.ts +1 -0
- package/src/components/doc-text/DocText.vue +64 -0
- package/src/components/doc-text/index.ts +1 -0
- package/src/components/dropdown-menu/DropdownMenu.vue +15 -0
- package/src/components/dropdown-menu/DropdownMenuCheckboxItem.vue +33 -0
- package/src/components/dropdown-menu/DropdownMenuContent.vue +36 -0
- package/src/components/dropdown-menu/DropdownMenuGroup.vue +12 -0
- package/src/components/dropdown-menu/DropdownMenuItem.vue +32 -0
- package/src/components/dropdown-menu/DropdownMenuLabel.vue +24 -0
- package/src/components/dropdown-menu/DropdownMenuRadioGroup.vue +15 -0
- package/src/components/dropdown-menu/DropdownMenuRadioItem.vue +33 -0
- package/src/components/dropdown-menu/DropdownMenuSeparator.vue +19 -0
- package/src/components/dropdown-menu/DropdownMenuShortcut.vue +13 -0
- package/src/components/dropdown-menu/DropdownMenuSub.vue +15 -0
- package/src/components/dropdown-menu/DropdownMenuSubContent.vue +29 -0
- package/src/components/dropdown-menu/DropdownMenuSubTrigger.vue +26 -0
- package/src/components/dropdown-menu/DropdownMenuTrigger.vue +12 -0
- package/src/components/dropdown-menu/index.ts +14 -0
- package/src/components/editable/Editable.vue +41 -0
- package/src/components/editable/EditableArea.vue +22 -0
- package/src/components/editable/EditableCancelTrigger.vue +40 -0
- package/src/components/editable/EditableEditTrigger.vue +40 -0
- package/src/components/editable/EditableInput.vue +28 -0
- package/src/components/editable/EditablePreview.vue +34 -0
- package/src/components/editable/EditableSubmitTrigger.vue +40 -0
- package/src/components/editable/index.ts +17 -0
- package/src/components/empty/Empty.vue +17 -0
- package/src/components/empty/EmptyActions.vue +14 -0
- package/src/components/empty/EmptyDescription.vue +12 -0
- package/src/components/empty/EmptyMedia.vue +16 -0
- package/src/components/empty/EmptyTitle.vue +17 -0
- package/src/components/empty/index.ts +7 -0
- package/src/components/field/Field.vue +22 -0
- package/src/components/field/FieldDescription.vue +20 -0
- package/src/components/field/FieldGroup.vue +22 -0
- package/src/components/field/FieldLabel.vue +27 -0
- package/src/components/field/FieldSeparator.vue +28 -0
- package/src/components/field/index.ts +7 -0
- package/src/components/fieldset/Fieldset.vue +126 -0
- package/src/components/fieldset/index.ts +1 -0
- package/src/components/file-upload/FileUpload.vue +149 -0
- package/src/components/file-upload/index.ts +8 -0
- package/src/components/float-label/FloatLabel.vue +26 -0
- package/src/components/float-label/index.ts +3 -0
- package/src/components/form/Form.vue +126 -0
- package/src/components/form/FormDescription.vue +12 -0
- package/src/components/form/FormField.vue +127 -0
- package/src/components/form/FormItem.vue +12 -0
- package/src/components/form/FormLabel.vue +14 -0
- package/src/components/form/FormMessage.vue +31 -0
- package/src/components/form/context.ts +54 -0
- package/src/components/form/errors.ts +6 -0
- package/src/components/form/index.ts +7 -0
- package/src/components/form/types.ts +55 -0
- package/src/components/hover-card/HoverCard.vue +27 -0
- package/src/components/hover-card/HoverCardContent.vue +36 -0
- package/src/components/hover-card/HoverCardTrigger.vue +12 -0
- package/src/components/hover-card/index.ts +3 -0
- package/src/components/ifta-label/IftaLabel.vue +26 -0
- package/src/components/ifta-label/index.ts +1 -0
- package/src/components/index.ts +89 -0
- package/src/components/input/Input.vue +87 -0
- package/src/components/input/index.ts +1 -0
- package/src/components/input-color/ColorArea.vue +36 -0
- package/src/components/input-color/ColorField.vue +32 -0
- package/src/components/input-color/ColorSlider.vue +31 -0
- package/src/components/input-color/InputColor.vue +65 -0
- package/src/components/input-color/index.ts +16 -0
- package/src/components/input-group/InputGroup.vue +26 -0
- package/src/components/input-group/InputGroupAddon.vue +32 -0
- package/src/components/input-group/InputGroupInput.vue +33 -0
- package/src/components/input-group/index.ts +17 -0
- package/src/components/input-number/InputNumber.vue +82 -0
- package/src/components/input-number/index.ts +1 -0
- package/src/components/input-otp/InputOtp.vue +186 -0
- package/src/components/input-otp/index.ts +1 -0
- package/src/components/input-password/InputPassword.vue +116 -0
- package/src/components/input-password/index.ts +5 -0
- package/src/components/input-tags/InputTags.vue +38 -0
- package/src/components/input-tags/InputTagsClear.vue +39 -0
- package/src/components/input-tags/InputTagsInput.vue +27 -0
- package/src/components/input-tags/InputTagsItem.vue +40 -0
- package/src/components/input-tags/InputTagsItemDelete.vue +32 -0
- package/src/components/input-tags/InputTagsItemText.vue +18 -0
- package/src/components/input-tags/index.ts +16 -0
- package/src/components/kbd/Kbd.vue +17 -0
- package/src/components/kbd/KbdGroup.vue +15 -0
- package/src/components/kbd/index.ts +4 -0
- package/src/components/label/Label.vue +23 -0
- package/src/components/label/index.ts +1 -0
- package/src/components/listbox/Listbox.vue +42 -0
- package/src/components/listbox/ListboxContent.vue +22 -0
- package/src/components/listbox/ListboxFilter.vue +34 -0
- package/src/components/listbox/ListboxGroup.vue +31 -0
- package/src/components/listbox/ListboxItem.vue +46 -0
- package/src/components/listbox/index.ts +19 -0
- package/src/components/mask/Mask.vue +205 -0
- package/src/components/mask/index.ts +15 -0
- package/src/components/menubar/Menubar.vue +26 -0
- package/src/components/menubar/MenubarCheckboxItem.vue +30 -0
- package/src/components/menubar/MenubarContent.vue +52 -0
- package/src/components/menubar/MenubarGroup.vue +12 -0
- package/src/components/menubar/MenubarItem.vue +32 -0
- package/src/components/menubar/MenubarLabel.vue +21 -0
- package/src/components/menubar/MenubarMenu.vue +12 -0
- package/src/components/menubar/MenubarRadioGroup.vue +15 -0
- package/src/components/menubar/MenubarRadioItem.vue +30 -0
- package/src/components/menubar/MenubarSeparator.vue +12 -0
- package/src/components/menubar/MenubarShortcut.vue +13 -0
- package/src/components/menubar/MenubarSub.vue +15 -0
- package/src/components/menubar/MenubarSubContent.vue +26 -0
- package/src/components/menubar/MenubarSubTrigger.vue +26 -0
- package/src/components/menubar/MenubarTrigger.vue +30 -0
- package/src/components/menubar/index.ts +29 -0
- package/src/components/navigation-menu/NavigationMenu.vue +43 -0
- package/src/components/navigation-menu/NavigationMenuContent.vue +63 -0
- package/src/components/navigation-menu/NavigationMenuIndicator.vue +31 -0
- package/src/components/navigation-menu/NavigationMenuItem.vue +20 -0
- package/src/components/navigation-menu/NavigationMenuLink.vue +34 -0
- package/src/components/navigation-menu/NavigationMenuList.vue +23 -0
- package/src/components/navigation-menu/NavigationMenuTrigger.vue +38 -0
- package/src/components/navigation-menu/NavigationMenuViewport.vue +34 -0
- package/src/components/navigation-menu/index.ts +18 -0
- package/src/components/order-list/OrderList.vue +168 -0
- package/src/components/order-list/SelectableList.vue +200 -0
- package/src/components/order-list/index.ts +1 -0
- package/src/components/pagination/Pagination.vue +133 -0
- package/src/components/pagination/index.ts +1 -0
- package/src/components/panel/Panel.vue +69 -0
- package/src/components/panel/index.ts +1 -0
- package/src/components/pick-list/PickList.vue +211 -0
- package/src/components/pick-list/index.ts +1 -0
- package/src/components/popover/Popover.vue +15 -0
- package/src/components/popover/PopoverAnchor.vue +12 -0
- package/src/components/popover/PopoverContent.vue +39 -0
- package/src/components/popover/PopoverTrigger.vue +12 -0
- package/src/components/popover/index.ts +4 -0
- package/src/components/progress/Progress.vue +39 -0
- package/src/components/progress/index.ts +1 -0
- package/src/components/props-table/PropsTable.vue +62 -0
- package/src/components/props-table/index.ts +2 -0
- package/src/components/radio-group/RadioGroup.vue +52 -0
- package/src/components/radio-group/RadioGroupItem.vue +80 -0
- package/src/components/radio-group/RadioGroupItemField.vue +128 -0
- package/src/components/radio-group/index.ts +21 -0
- package/src/components/rating/Rating.vue +70 -0
- package/src/components/rating/RatingItem.vue +26 -0
- package/src/components/rating/RatingItemIndicator.vue +28 -0
- package/src/components/rating/RatingStar.vue +16 -0
- package/src/components/rating/index.ts +13 -0
- package/src/components/scroll-area/ScrollArea.vue +52 -0
- package/src/components/scroll-area/index.ts +1 -0
- package/src/components/search-input/SearchInput.vue +46 -0
- package/src/components/search-input/index.ts +1 -0
- package/src/components/select/Select.vue +15 -0
- package/src/components/select/SelectContent.vue +67 -0
- package/src/components/select/SelectGroup.vue +20 -0
- package/src/components/select/SelectItem.vue +59 -0
- package/src/components/select/SelectLabel.vue +20 -0
- package/src/components/select/SelectSeparator.vue +18 -0
- package/src/components/select/SelectTrigger.vue +101 -0
- package/src/components/select/SelectValue.vue +46 -0
- package/src/components/select/index.ts +8 -0
- package/src/components/separator/Separator.vue +25 -0
- package/src/components/separator/index.ts +1 -0
- package/src/components/sheet/Sheet.vue +15 -0
- package/src/components/sheet/SheetClose.vue +12 -0
- package/src/components/sheet/SheetContent.vue +45 -0
- package/src/components/sheet/SheetDescription.vue +19 -0
- package/src/components/sheet/SheetFooter.vue +12 -0
- package/src/components/sheet/SheetHeader.vue +12 -0
- package/src/components/sheet/SheetTitle.vue +19 -0
- package/src/components/sheet/SheetTrigger.vue +12 -0
- package/src/components/sheet/index.ts +10 -0
- package/src/components/sidebar/Sidebar.vue +108 -0
- package/src/components/sidebar/SidebarContent.vue +19 -0
- package/src/components/sidebar/SidebarFooter.vue +19 -0
- package/src/components/sidebar/SidebarGroup.vue +19 -0
- package/src/components/sidebar/SidebarGroupAction.vue +28 -0
- package/src/components/sidebar/SidebarGroupContent.vue +19 -0
- package/src/components/sidebar/SidebarGroupLabel.vue +25 -0
- package/src/components/sidebar/SidebarHeader.vue +19 -0
- package/src/components/sidebar/SidebarInput.vue +15 -0
- package/src/components/sidebar/SidebarInset.vue +22 -0
- package/src/components/sidebar/SidebarMenu.vue +19 -0
- package/src/components/sidebar/SidebarMenuAction.vue +41 -0
- package/src/components/sidebar/SidebarMenuBadge.vue +24 -0
- package/src/components/sidebar/SidebarMenuButton.vue +94 -0
- package/src/components/sidebar/SidebarMenuItem.vue +19 -0
- package/src/components/sidebar/SidebarMenuSkeleton.vue +36 -0
- package/src/components/sidebar/SidebarMenuSub.vue +22 -0
- package/src/components/sidebar/SidebarMenuSubButton.vue +39 -0
- package/src/components/sidebar/SidebarMenuSubItem.vue +19 -0
- package/src/components/sidebar/SidebarProvider.vue +114 -0
- package/src/components/sidebar/SidebarRail.vue +37 -0
- package/src/components/sidebar/SidebarSeparator.vue +15 -0
- package/src/components/sidebar/SidebarTrigger.vue +27 -0
- package/src/components/sidebar/context.ts +39 -0
- package/src/components/sidebar/index.ts +34 -0
- package/src/components/skeleton/Skeleton.vue +10 -0
- package/src/components/skeleton/index.ts +1 -0
- package/src/components/skip-link/SkipLink.vue +45 -0
- package/src/components/skip-link/index.ts +1 -0
- package/src/components/slider/Slider.vue +57 -0
- package/src/components/slider/index.ts +3 -0
- package/src/components/speed-dial/SpeedDial.vue +362 -0
- package/src/components/speed-dial/index.ts +28 -0
- package/src/components/spinner/Spinner.vue +20 -0
- package/src/components/spinner/index.ts +1 -0
- package/src/components/splitter/Splitter.vue +24 -0
- package/src/components/splitter/SplitterHandle.vue +51 -0
- package/src/components/splitter/SplitterPanel.vue +30 -0
- package/src/components/splitter/index.ts +14 -0
- package/src/components/stepper/Stepper.vue +34 -0
- package/src/components/stepper/StepperItem.vue +113 -0
- package/src/components/stepper/index.ts +2 -0
- package/src/components/switch/Switch.vue +35 -0
- package/src/components/switch/index.ts +1 -0
- package/src/components/table/Table.vue +24 -0
- package/src/components/table/TableBody.vue +12 -0
- package/src/components/table/TableCell.vue +12 -0
- package/src/components/table/TableEmpty.vue +20 -0
- package/src/components/table/TableHead.vue +17 -0
- package/src/components/table/TableHeader.vue +12 -0
- package/src/components/table/TableRow.vue +19 -0
- package/src/components/table/index.ts +7 -0
- package/src/components/tabs/Tabs.vue +23 -0
- package/src/components/tabs/TabsContent.vue +23 -0
- package/src/components/tabs/TabsList.vue +26 -0
- package/src/components/tabs/TabsTrigger.vue +30 -0
- package/src/components/tabs/index.ts +4 -0
- package/src/components/textarea/Textarea.vue +25 -0
- package/src/components/textarea/index.ts +1 -0
- package/src/components/theme-controls/LocaleMenu.vue +95 -0
- package/src/components/theme-controls/PaletteMenu.vue +155 -0
- package/src/components/theme-controls/ThemeToggle.vue +21 -0
- package/src/components/theme-controls/index.ts +15 -0
- package/src/components/timeline/Timeline.vue +20 -0
- package/src/components/timeline/TimelineItem.vue +68 -0
- package/src/components/timeline/index.ts +15 -0
- package/src/components/toast/Toaster.vue +62 -0
- package/src/components/toast/index.ts +4 -0
- package/src/components/toast/toast.ts +57 -0
- package/src/components/toggle/Toggle.vue +32 -0
- package/src/components/toggle/ToggleGroup.vue +56 -0
- package/src/components/toggle/ToggleGroupItem.vue +33 -0
- package/src/components/toggle/index.ts +18 -0
- package/src/components/token-table/TokenTable.vue +67 -0
- package/src/components/token-table/index.ts +2 -0
- package/src/components/toolbar/Toolbar.vue +34 -0
- package/src/components/toolbar/ToolbarButton.vue +33 -0
- package/src/components/toolbar/ToolbarSeparator.vue +21 -0
- package/src/components/toolbar/index.ts +3 -0
- package/src/components/tooltip/Tooltip.vue +15 -0
- package/src/components/tooltip/TooltipContent.vue +38 -0
- package/src/components/tooltip/TooltipProvider.vue +16 -0
- package/src/components/tooltip/TooltipTrigger.vue +12 -0
- package/src/components/tooltip/index.ts +4 -0
- package/src/components/tree/Tree.vue +53 -0
- package/src/components/tree/TreeItem.vue +59 -0
- package/src/components/tree/index.ts +21 -0
- package/src/components/tree-select/TreeSelect.vue +141 -0
- package/src/components/tree-select/index.ts +1 -0
- package/src/components/tree-table/TreeTable.vue +302 -0
- package/src/components/tree-table/features.ts +49 -0
- package/src/components/tree-table/index.ts +3 -0
- package/src/components/video-player/VideoPlayer.vue +472 -0
- package/src/components/video-player/index.ts +20 -0
- package/src/composables/index.ts +15 -0
- package/src/composables/useBase.ts +32 -0
- package/src/composables/useChartPalette.ts +33 -0
- package/src/composables/useColorMode.ts +29 -0
- package/src/composables/useConfirm.ts +81 -0
- package/src/composables/useDelayedFlag.ts +62 -0
- package/src/composables/useI18n.ts +107 -0
- package/src/composables/useIsMobile.ts +34 -0
- package/src/composables/useMenuAccent.ts +32 -0
- package/src/composables/useMenuAppearance.ts +31 -0
- package/src/composables/useMenuColor.ts +31 -0
- package/src/composables/usePalette.ts +30 -0
- package/src/composables/useStyle.ts +35 -0
- package/src/composables/useSurface.ts +30 -0
- package/src/composables/useTableQuery.ts +73 -0
- package/src/composables/useThemeController.ts +48 -0
- package/src/i18n/README.md +68 -0
- package/src/i18n/controller.ts +233 -0
- package/src/i18n/dictionary.ts +43 -0
- package/src/i18n/index.ts +34 -0
- package/src/i18n/registry.ts +64 -0
- package/src/i18n/ui/en/ui.json +114 -0
- package/src/i18n/ui/en/zod.json +16 -0
- package/src/i18n/ui/index.ts +48 -0
- package/src/i18n/ui/vi/ui.json +114 -0
- package/src/i18n/ui/vi/zod.json +16 -0
- package/src/lib/date.ts +24 -0
- package/src/lib/focus.ts +12 -0
- package/src/lib/icons.ts +345 -0
- package/src/lib/menu-styles.ts +16 -0
- package/src/lib/menu.ts +20 -0
- package/src/lib/platform.ts +5 -0
- package/src/lib/types.ts +6 -0
- package/src/lib/utils.ts +9 -0
- package/src/lib/zod-messages.ts +127 -0
- package/src/styles/chart.css +30 -0
- package/src/styles/index.css +18 -0
- package/src/variants/README.md +55 -0
- package/src/variants/components/accordion.ts +71 -0
- package/src/variants/components/alert.ts +20 -0
- package/src/variants/components/badge.ts +76 -0
- package/src/variants/components/button-group.ts +136 -0
- package/src/variants/components/button.ts +160 -0
- package/src/variants/components/checkbox.ts +67 -0
- package/src/variants/components/chip.ts +51 -0
- package/src/variants/components/code-block.ts +22 -0
- package/src/variants/components/dialog.ts +28 -0
- package/src/variants/components/empty.ts +60 -0
- package/src/variants/components/field.ts +35 -0
- package/src/variants/components/float-label.ts +95 -0
- package/src/variants/components/input-group.ts +73 -0
- package/src/variants/components/input-password.ts +68 -0
- package/src/variants/components/kbd.ts +47 -0
- package/src/variants/components/picker-trigger.ts +30 -0
- package/src/variants/components/radio-group.ts +63 -0
- package/src/variants/components/sheet.ts +23 -0
- package/src/variants/components/sidebar.ts +33 -0
- package/src/variants/components/slider.ts +49 -0
- package/src/variants/components/timeline.ts +45 -0
- package/src/variants/components/toggle.ts +58 -0
- package/src/variants/index.ts +35 -0
- package/src/variants/lib/chart/polar.ts +139 -0
- package/src/variants/lib/chart/tooltip.ts +101 -0
- package/src/variants/lib/chart/types.ts +172 -0
- package/src/variants/lib/chart/xy-label.ts +42 -0
- package/src/variants/lib/focus.ts +66 -0
- package/src/variants/lib/form-errors.ts +59 -0
- package/src/variants/lib/highlight.ts +548 -0
- package/src/variants/lib/list-move.ts +63 -0
- package/src/variants/lib/mask.ts +106 -0
- package/src/variants/lib/media.ts +56 -0
- package/src/variants/lib/menu-styles.ts +71 -0
- package/src/variants/lib/platform.ts +68 -0
- package/src/variants/lib/tree.ts +119 -0
- package/src/variants/lib/upload.ts +58 -0
- package/src/variants/lib/utils.ts +11 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ClassProps } from '../../lib/types'
|
|
3
|
+
import { useSlots } from 'vue'
|
|
4
|
+
import { cn } from '../../lib/utils'
|
|
5
|
+
import Separator from '../separator/Separator.vue'
|
|
6
|
+
|
|
7
|
+
const props = defineProps<ClassProps>()
|
|
8
|
+
const slots = useSlots()
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<!-- Chữ nằm ĐÈ lên đường kẻ và tự che nó bằng nền — không cắt đường kẻ làm
|
|
13
|
+
hai đoạn, nên chữ dài ngắn thế nào cũng không phải tính lại. -->
|
|
14
|
+
<div
|
|
15
|
+
data-slot="field-separator"
|
|
16
|
+
:data-content="Boolean(slots.default)"
|
|
17
|
+
:class="cn('relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2', props.class)"
|
|
18
|
+
>
|
|
19
|
+
<Separator class="absolute inset-0 top-1/2" />
|
|
20
|
+
<span
|
|
21
|
+
v-if="slots.default"
|
|
22
|
+
data-slot="field-separator-content"
|
|
23
|
+
class="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
|
|
24
|
+
>
|
|
25
|
+
<slot />
|
|
26
|
+
</span>
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { fieldVariants, type FieldVariants } from '../../variants/components/field'
|
|
2
|
+
export { default as Field } from './Field.vue'
|
|
3
|
+
export { default as FieldDescription } from './FieldDescription.vue'
|
|
4
|
+
export { default as FieldGroup } from './FieldGroup.vue'
|
|
5
|
+
export { default as FieldLabel } from './FieldLabel.vue'
|
|
6
|
+
|
|
7
|
+
export { default as FieldSeparator } from './FieldSeparator.vue'
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ClassProps } from '../../lib/types'
|
|
3
|
+
import { computed, ref, useId } from 'vue'
|
|
4
|
+
import { ChevronDown } from '../../lib/icons'
|
|
5
|
+
import { cn } from '../../lib/utils'
|
|
6
|
+
|
|
7
|
+
const props = withDefaults(
|
|
8
|
+
defineProps<ClassProps & {
|
|
9
|
+
legend?: string
|
|
10
|
+
/** Cho gập lại. Không bật thì phần chú giải không phải là nút. */
|
|
11
|
+
collapsible?: boolean
|
|
12
|
+
defaultOpen?: boolean
|
|
13
|
+
/** Điều khiển từ ngoài. `v-model:open`. */
|
|
14
|
+
open?: boolean
|
|
15
|
+
/**
|
|
16
|
+
* Khoá MỌI control bên trong — thuộc tính thật của `<fieldset>`, không phải
|
|
17
|
+
* một class. Không một cái `<div>` nào làm được việc này.
|
|
18
|
+
*/
|
|
19
|
+
disabled?: boolean
|
|
20
|
+
}>(),
|
|
21
|
+
{
|
|
22
|
+
collapsible: false,
|
|
23
|
+
defaultOpen: true,
|
|
24
|
+
disabled: false,
|
|
25
|
+
/**
|
|
26
|
+
* Dòng này là bắt buộc, không phải cho gọn.
|
|
27
|
+
*
|
|
28
|
+
* Prop kiểu Boolean vắng mặt bị Vue ép thành `false`, nên thiếu nó thì mọi
|
|
29
|
+
* `<Fieldset>` không truyền `open` đều bị hiểu là "đang được điều khiển và
|
|
30
|
+
* đang đóng" — đúng cái bẫy đã ghi cho `SidebarProvider`, và cũng im lặng
|
|
31
|
+
* y như vậy. `defineModel` không nhận được default này nên phải khai tay
|
|
32
|
+
* prop cộng emit.
|
|
33
|
+
*/
|
|
34
|
+
open: undefined,
|
|
35
|
+
},
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const emit = defineEmits<{ 'update:open': [value: boolean] }>()
|
|
39
|
+
|
|
40
|
+
const internal = ref(props.defaultOpen)
|
|
41
|
+
|
|
42
|
+
const isOpen = computed({
|
|
43
|
+
get: () => props.open ?? internal.value,
|
|
44
|
+
set: (value: boolean) => {
|
|
45
|
+
internal.value = value
|
|
46
|
+
emit('update:open', value)
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const contentId = useId()
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<template>
|
|
54
|
+
<fieldset
|
|
55
|
+
:disabled="props.disabled || undefined"
|
|
56
|
+
:class="cn(
|
|
57
|
+
// `min-w-0` là BẮT BUỘC. Mặc định của trình duyệt cho `<fieldset>` là
|
|
58
|
+
// `min-inline-size: min-content`, nên một bảng rộng hay một chuỗi không
|
|
59
|
+
// ngắt được nằm bên trong sẽ nong cả nhóm ra rồi đẩy tràn cả biểu mẫu.
|
|
60
|
+
// Không `<div>` nào có luật đó, nên nó rất dễ bị bỏ sót.
|
|
61
|
+
'min-w-0 rounded-xl border border-border px-4 pb-4',
|
|
62
|
+
props.disabled && 'opacity-60',
|
|
63
|
+
props.class,
|
|
64
|
+
)"
|
|
65
|
+
>
|
|
66
|
+
<!--
|
|
67
|
+
`<legend>` phải là con TRỰC TIẾP đầu tiên của `<fieldset>`.
|
|
68
|
+
|
|
69
|
+
Đó là điều kiện để trình duyệt cắt một khoảng trống trên đường viền cho
|
|
70
|
+
nó, và cũng là điều kiện để trình đọc màn hình ghép chú giải này vào TỪNG
|
|
71
|
+
control bên trong. Bọc nó vào một `<div>` là mất cả hai, mà nhìn thì chỉ
|
|
72
|
+
thấy mất cái khoảng trống.
|
|
73
|
+
-->
|
|
74
|
+
<legend class="px-1.5 text-sm font-medium">
|
|
75
|
+
<button
|
|
76
|
+
v-if="props.collapsible"
|
|
77
|
+
type="button"
|
|
78
|
+
:aria-expanded="isOpen"
|
|
79
|
+
:aria-controls="contentId"
|
|
80
|
+
class="-mx-1 flex items-center gap-1.5 rounded-md px-1 py-0.5 transition-colors hover:text-foreground/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
81
|
+
@click="isOpen = !isOpen"
|
|
82
|
+
>
|
|
83
|
+
<ChevronDown
|
|
84
|
+
class="size-4 shrink-0 text-muted-foreground transition-transform duration-(--duration-base) motion-reduce:transition-none"
|
|
85
|
+
:class="!isOpen && '-rotate-90'"
|
|
86
|
+
aria-hidden="true"
|
|
87
|
+
/>
|
|
88
|
+
<slot name="legend">
|
|
89
|
+
{{ props.legend }}
|
|
90
|
+
</slot>
|
|
91
|
+
</button>
|
|
92
|
+
|
|
93
|
+
<template v-else>
|
|
94
|
+
<slot name="legend">
|
|
95
|
+
{{ props.legend }}
|
|
96
|
+
</slot>
|
|
97
|
+
</template>
|
|
98
|
+
</legend>
|
|
99
|
+
|
|
100
|
+
<!--
|
|
101
|
+
Gập bằng `grid-template-rows: 0fr → 1fr`, KHÔNG dùng Collapsible của reka.
|
|
102
|
+
|
|
103
|
+
Không phải vì thích tự làm: `CollapsibleTrigger` bắt buộc nằm trong
|
|
104
|
+
`CollapsibleRoot`, mà cái nút thì phải nằm trong `<legend>`, còn `<legend>`
|
|
105
|
+
phải là con trực tiếp của `<fieldset>`. Ba ràng buộc đó không cùng thoả
|
|
106
|
+
được. Mẹo `0fr → 1fr` bù lại còn không cần đo chiều cao bằng JS.
|
|
107
|
+
|
|
108
|
+
`inert` chứ không phải `hidden`: hộp vẫn phải vẽ ra thì mới có gì để thu
|
|
109
|
+
lại, nhưng nội dung đang gập thì không được nhận Tab và không được đọc lên.
|
|
110
|
+
-->
|
|
111
|
+
<div
|
|
112
|
+
:id="contentId"
|
|
113
|
+
class="grid transition-[grid-template-rows] duration-(--duration-base) motion-reduce:transition-none"
|
|
114
|
+
:class="props.collapsible && !isOpen ? 'grid-rows-[0fr]' : 'grid-rows-[1fr]'"
|
|
115
|
+
:inert="props.collapsible && !isOpen ? true : undefined"
|
|
116
|
+
>
|
|
117
|
+
<!-- Khoảng đệm trên nằm ở ĐÂY chứ không ở `<fieldset>`: đặt ngoài thì
|
|
118
|
+
lúc gập nó không co lại và cái hộp vẫn cao lù lù một dải trống. -->
|
|
119
|
+
<div class="min-h-0 overflow-hidden">
|
|
120
|
+
<div class="pt-2">
|
|
121
|
+
<slot />
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</fieldset>
|
|
126
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Fieldset } from './Fieldset.vue'
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ClassProps } from '../../lib/types'
|
|
3
|
+
import type { UploadFile } from '../../variants/lib/upload'
|
|
4
|
+
import { computed, ref } from 'vue'
|
|
5
|
+
import { useI18n } from '../../composables/useI18n'
|
|
6
|
+
import { focusRingWithin } from '../../lib/focus'
|
|
7
|
+
import { File as FileIcon, Upload, X } from '../../lib/icons'
|
|
8
|
+
import { cn } from '../../lib/utils'
|
|
9
|
+
import { formatFileSize, splitBySize, uploadFileId } from '../../variants/lib/upload'
|
|
10
|
+
import { Button } from '../button'
|
|
11
|
+
import { Progress } from '../progress'
|
|
12
|
+
|
|
13
|
+
const props = withDefaults(
|
|
14
|
+
defineProps<ClassProps & {
|
|
15
|
+
/** Giống thuộc tính `accept` của input, ví dụ `image/*,.pdf`. */
|
|
16
|
+
accept?: string
|
|
17
|
+
multiple?: boolean
|
|
18
|
+
/** Chặn tệp lớn hơn mức này, tính bằng byte. 0 nghĩa là không giới hạn. */
|
|
19
|
+
maxSize?: number
|
|
20
|
+
disabled?: boolean
|
|
21
|
+
hint?: string
|
|
22
|
+
}>(),
|
|
23
|
+
{ multiple: false, maxSize: 0, disabled: false },
|
|
24
|
+
)
|
|
25
|
+
const emit = defineEmits<{
|
|
26
|
+
/** Tệp bị loại vì quá lớn — bên gọi tự quyết báo lỗi thế nào. */
|
|
27
|
+
reject: [files: File[]]
|
|
28
|
+
remove: [file: UploadFile]
|
|
29
|
+
}>()
|
|
30
|
+
const { t } = useI18n()
|
|
31
|
+
const files = defineModel<UploadFile[]>({ default: () => [] })
|
|
32
|
+
|
|
33
|
+
const dragging = ref(false)
|
|
34
|
+
|
|
35
|
+
const hintText = computed(() => {
|
|
36
|
+
if (props.hint)
|
|
37
|
+
return props.hint
|
|
38
|
+
const parts: string[] = []
|
|
39
|
+
if (props.accept)
|
|
40
|
+
parts.push(props.accept)
|
|
41
|
+
if (props.maxSize)
|
|
42
|
+
parts.push(t('ui.fileUpload.maxSize', { size: formatFileSize(props.maxSize) }))
|
|
43
|
+
return parts.join(' · ')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
function add(incoming: FileList | null): void {
|
|
47
|
+
if (!incoming?.length)
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
const { accepted, tooBig } = splitBySize(Array.from(incoming), props.maxSize)
|
|
51
|
+
|
|
52
|
+
if (tooBig.length)
|
|
53
|
+
emit('reject', tooBig)
|
|
54
|
+
|
|
55
|
+
const next = accepted.map(file => ({ id: uploadFileId(file), file }))
|
|
56
|
+
|
|
57
|
+
// Lọc trùng theo tên + cỡ + thời điểm sửa: kéo cùng một tệp vào hai lần là
|
|
58
|
+
// thao tác nhầm, không phải ý định tải hai bản.
|
|
59
|
+
files.value = props.multiple
|
|
60
|
+
? [...files.value, ...next.filter(n => !files.value.some(f => f.id === n.id))]
|
|
61
|
+
: next.slice(0, 1)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function onDrop(event: DragEvent): void {
|
|
65
|
+
dragging.value = false
|
|
66
|
+
if (props.disabled)
|
|
67
|
+
return
|
|
68
|
+
add(event.dataTransfer?.files ?? null)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function onPick(event: Event): void {
|
|
72
|
+
const target = event.target as HTMLInputElement
|
|
73
|
+
add(target.files)
|
|
74
|
+
// Xoá giá trị để chọn lại đúng tệp vừa gỡ vẫn kích hoạt `change`.
|
|
75
|
+
target.value = ''
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function remove(file: UploadFile): void {
|
|
79
|
+
files.value = files.value.filter(f => f.id !== file.id)
|
|
80
|
+
emit('remove', file)
|
|
81
|
+
}
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<template>
|
|
85
|
+
<div :class="cn('flex w-full flex-col gap-3', props.class)">
|
|
86
|
+
<!--
|
|
87
|
+
Vùng thả là `<label>`, không phải `<div>` gắn click.
|
|
88
|
+
Nhờ vậy bấm vào đâu trong vùng cũng mở hộp chọn tệp, và cái `<input>` bên
|
|
89
|
+
trong vẫn là control thật — vào được chuỗi Tab, nhận được Enter, và trình
|
|
90
|
+
đọc màn hình đọc ra "chọn tệp" chứ không phải một khối trống.
|
|
91
|
+
-->
|
|
92
|
+
<label
|
|
93
|
+
:class="cn(
|
|
94
|
+
'flex cursor-pointer flex-col items-center justify-center gap-2 rounded-xl border border-dashed px-6 py-8 text-center transition-colors',
|
|
95
|
+
focusRingWithin,
|
|
96
|
+
dragging ? 'border-primary bg-primary/5' : 'border-border hover:bg-accent/40',
|
|
97
|
+
props.disabled && 'pointer-events-none opacity-50',
|
|
98
|
+
)"
|
|
99
|
+
@dragover.prevent="dragging = true"
|
|
100
|
+
@dragleave.prevent="dragging = false"
|
|
101
|
+
@drop.prevent="onDrop"
|
|
102
|
+
>
|
|
103
|
+
<span class="grid size-10 place-items-center rounded-full bg-muted text-muted-foreground">
|
|
104
|
+
<Upload class="size-4" />
|
|
105
|
+
</span>
|
|
106
|
+
<span class="text-sm font-medium">
|
|
107
|
+
<slot name="label">{{ t('ui.fileUpload.label') }}</slot>
|
|
108
|
+
</span>
|
|
109
|
+
<span v-if="hintText" class="text-xs text-muted-foreground">{{ hintText }}</span>
|
|
110
|
+
|
|
111
|
+
<input
|
|
112
|
+
type="file"
|
|
113
|
+
class="sr-only"
|
|
114
|
+
:accept="props.accept"
|
|
115
|
+
:multiple="props.multiple"
|
|
116
|
+
:disabled="props.disabled"
|
|
117
|
+
@change="onPick"
|
|
118
|
+
>
|
|
119
|
+
</label>
|
|
120
|
+
|
|
121
|
+
<ul v-if="files.length" class="flex flex-col gap-2">
|
|
122
|
+
<li
|
|
123
|
+
v-for="item in files"
|
|
124
|
+
:key="item.id"
|
|
125
|
+
class="flex items-center gap-3 rounded-lg border border-border bg-card px-3 py-2"
|
|
126
|
+
>
|
|
127
|
+
<FileIcon class="size-4 shrink-0 text-muted-foreground" />
|
|
128
|
+
|
|
129
|
+
<div class="flex min-w-0 flex-1 flex-col gap-1">
|
|
130
|
+
<div class="flex items-baseline justify-between gap-3">
|
|
131
|
+
<span class="truncate text-sm">{{ item.file.name }}</span>
|
|
132
|
+
<span class="shrink-0 text-xs tabular-nums text-muted-foreground">{{ formatFileSize(item.file.size) }}</span>
|
|
133
|
+
</div>
|
|
134
|
+
<Progress v-if="item.progress !== undefined && !item.error" :model-value="item.progress" class="h-1" />
|
|
135
|
+
<span v-if="item.error" class="text-xs text-destructive">{{ item.error }}</span>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<Button
|
|
139
|
+
variant="ghost"
|
|
140
|
+
size="icon-sm"
|
|
141
|
+
:aria-label="t('ui.fileUpload.remove', { name: item.file.name })"
|
|
142
|
+
@click="remove(item)"
|
|
143
|
+
>
|
|
144
|
+
<X />
|
|
145
|
+
</Button>
|
|
146
|
+
</li>
|
|
147
|
+
</ul>
|
|
148
|
+
</div>
|
|
149
|
+
</template>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kiểu và hai hàm thuần lên `src/variants` — bản React chép lại chúng thì
|
|
3
|
+
* hai cây có hai cách đặt id tệp và hai cách làm tròn cỡ tệp, mà không có gì
|
|
4
|
+
* phát hiện khi chúng lệch.
|
|
5
|
+
*/
|
|
6
|
+
export { formatFileSize, splitBySize, type UploadFile, uploadFileId } from '../../variants/lib/upload'
|
|
7
|
+
|
|
8
|
+
export { default as FileUpload } from './FileUpload.vue'
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ClassProps } from '../../lib/types'
|
|
3
|
+
import type { FloatLabelVariants } from './index'
|
|
4
|
+
import { cn } from '../../lib/utils'
|
|
5
|
+
import { floatLabelVariants } from './index'
|
|
6
|
+
|
|
7
|
+
const props = withDefaults(
|
|
8
|
+
defineProps<ClassProps & {
|
|
9
|
+
variant?: FloatLabelVariants['variant']
|
|
10
|
+
}>(),
|
|
11
|
+
{ variant: 'over' },
|
|
12
|
+
)
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<!--
|
|
17
|
+
Ô nhập trước, nhãn sau — thứ tự trong DOM là thứ tự đọc.
|
|
18
|
+
|
|
19
|
+
Nhãn được đặt tuyệt đối nên nhìn thì nó ở trên, nhưng `<label for>` mới là
|
|
20
|
+
thứ nối hai bên lại, và trình đọc màn hình đọc theo cái nối đó chứ không
|
|
21
|
+
theo toạ độ. Đảo lại cho "đúng thứ tự nhìn thấy" là thừa.
|
|
22
|
+
-->
|
|
23
|
+
<div :class="cn(floatLabelVariants({ variant: props.variant }), props.class)">
|
|
24
|
+
<slot />
|
|
25
|
+
</div>
|
|
26
|
+
</template>
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<script setup lang="ts" generic="TValues extends Record<string, unknown>">
|
|
2
|
+
import type { ZodType } from 'zod'
|
|
3
|
+
import type { ClassProps } from '../../lib/types'
|
|
4
|
+
import type { FieldErrorMap } from './errors'
|
|
5
|
+
import { useForm } from '@tanstack/vue-form'
|
|
6
|
+
import { computed, ref } from 'vue'
|
|
7
|
+
import { cn } from '../../lib/utils'
|
|
8
|
+
import { provideFormContext } from './context'
|
|
9
|
+
import { firstErrorMessage } from './errors'
|
|
10
|
+
|
|
11
|
+
const props = defineProps<ClassProps & {
|
|
12
|
+
/**
|
|
13
|
+
* Schema zod. Truyền thẳng, không qua adapter nào: TanStack Form đọc
|
|
14
|
+
* **Standard Schema**, mà zod đã cài giao diện đó từ 3.24.
|
|
15
|
+
*
|
|
16
|
+
* PHẢI viết đủ hai tham số. Ô thứ hai của `ZodType` đổi nghĩa giữa hai bản
|
|
17
|
+
* lớn: v3 là `Def` nội bộ và `Input` nằm ở ô thứ ba, mặc định bằng `Output`;
|
|
18
|
+
* v4 ô thứ hai CHÍNH LÀ `Input`, và mặc định là `unknown` chứ không còn ăn
|
|
19
|
+
* theo `Output`. Nên `ZodType<TValues>` vẫn biên dịch được ở cả hai bản mà
|
|
20
|
+
* nghĩa đã khác hẳn — ở v4 nó là "nhận vào bất cứ thứ gì", trong khi
|
|
21
|
+
* TanStack Form đòi `StandardSchemaV1<TValues, unknown>`, tức đầu vào phải
|
|
22
|
+
* đúng `TValues`.
|
|
23
|
+
*/
|
|
24
|
+
schema: ZodType<TValues, TValues>
|
|
25
|
+
initialValues?: Record<string, unknown>
|
|
26
|
+
}>()
|
|
27
|
+
|
|
28
|
+
const emit = defineEmits<{
|
|
29
|
+
submit: [values: TValues]
|
|
30
|
+
}>()
|
|
31
|
+
|
|
32
|
+
const form = useForm({
|
|
33
|
+
defaultValues: (props.initialValues ?? {}) as TValues,
|
|
34
|
+
/**
|
|
35
|
+
* Chỉ khai `onChange` là đủ cho cả lúc bấm gửi: logic mặc định của form-core
|
|
36
|
+
* chạy onChange + onBlur + onSubmit khi nguyên nhân là `submit`.
|
|
37
|
+
*
|
|
38
|
+
* `onChange` ở đây KHÔNG có nghĩa là người dùng bị báo lỗi từng phím gõ —
|
|
39
|
+
* `FormField` chỉ hiện lỗi sau khi field đã blur hoặc form đã gửi. Tính sớm
|
|
40
|
+
* mà hiện muộn là cố ý: nhờ tính sớm nên lúc đang sửa, lỗi biến mất ngay
|
|
41
|
+
* khoảnh khắc gõ đúng. Xem chú thích `showValidation` trong `FormField.vue`.
|
|
42
|
+
*/
|
|
43
|
+
validators: { onChange: props.schema },
|
|
44
|
+
onSubmit: ({ value }) => {
|
|
45
|
+
emit('submit', value)
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Lỗi do server trả về giữ **ở đây**, không ghi vào errorMap của TanStack.
|
|
51
|
+
*
|
|
52
|
+
* Thư viện có sẵn khoá `onServer` cho đúng việc này, nhưng kiểu của nó suy ra
|
|
53
|
+
* từ tham số generic `TOnServer`, mà `TOnServer` chỉ đến từ tham số type tường
|
|
54
|
+
* minh — `FormValidators` không có trường `onServer` để suy ra. Muốn gọi
|
|
55
|
+
* `setErrorMap({ onServer })` thì phải viết đủ **12 tham số generic** cho
|
|
56
|
+
* `useForm`. Một `ref` đứng ngoài rẻ hơn nhiều.
|
|
57
|
+
*
|
|
58
|
+
* Còn được thêm một thứ: `setErrorMap` chỉ ghi vào field **đang mount** và bỏ
|
|
59
|
+
* qua tên không khớp, nên lỗi cho field nằm trong tab chưa mở sẽ rơi mất.
|
|
60
|
+
* Map riêng thì field nhận được lỗi ngay khi nó xuất hiện.
|
|
61
|
+
*/
|
|
62
|
+
const serverErrors = ref<Record<string, string>>({})
|
|
63
|
+
|
|
64
|
+
const submissionAttempts = form.useSelector(state => state.submissionAttempts)
|
|
65
|
+
const submitted = computed(() => submissionAttempts.value > 0)
|
|
66
|
+
|
|
67
|
+
provideFormContext({ api: form, serverErrors, submitted })
|
|
68
|
+
|
|
69
|
+
const isSubmitting = form.useSelector(state => state.isSubmitting)
|
|
70
|
+
const values = form.useSelector(state => state.values)
|
|
71
|
+
const fieldMeta = form.useSelector(state => state.fieldMeta)
|
|
72
|
+
|
|
73
|
+
/** Gộp về `{ tên field: thông báo }` — giữ nguyên hình dạng slot prop cũ. */
|
|
74
|
+
const errors = computed<Record<string, string | undefined>>(() => {
|
|
75
|
+
const result: Record<string, string | undefined> = { ...serverErrors.value }
|
|
76
|
+
const entries = Object.entries(fieldMeta.value) as Array<[string, { errorMap?: FieldErrorMap }]>
|
|
77
|
+
|
|
78
|
+
for (const [name, meta] of entries) {
|
|
79
|
+
if (result[name])
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
const message = firstErrorMessage(meta.errorMap)
|
|
83
|
+
if (message)
|
|
84
|
+
result[name] = message
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return result
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
function onSubmit(event: Event): void {
|
|
91
|
+
event.preventDefault()
|
|
92
|
+
void form.handleSubmit()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Map lỗi validation của .NET (ModelState / ProblemDetails.errors) ngược về
|
|
97
|
+
* từng field.
|
|
98
|
+
*/
|
|
99
|
+
function setErrors(fields: Record<string, string>): void {
|
|
100
|
+
serverErrors.value = { ...serverErrors.value, ...fields }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function setFieldError(name: string, message: string): void {
|
|
104
|
+
setErrors({ [name]: message })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function resetForm(): void {
|
|
108
|
+
serverErrors.value = {}
|
|
109
|
+
form.reset()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
defineExpose({
|
|
113
|
+
resetForm,
|
|
114
|
+
setErrors,
|
|
115
|
+
setFieldError,
|
|
116
|
+
values,
|
|
117
|
+
errors,
|
|
118
|
+
isSubmitting,
|
|
119
|
+
})
|
|
120
|
+
</script>
|
|
121
|
+
|
|
122
|
+
<template>
|
|
123
|
+
<form novalidate :class="cn('flex flex-col gap-4', props.class)" @submit="onSubmit">
|
|
124
|
+
<slot :is-submitting="isSubmitting" :errors="errors" :values="values" />
|
|
125
|
+
</form>
|
|
126
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ClassProps } from '../../lib/types'
|
|
3
|
+
import { cn } from '../../lib/utils'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<ClassProps & { id?: string }>()
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<template>
|
|
9
|
+
<p :id="props.id" :class="cn('text-xs text-muted-foreground', props.class)">
|
|
10
|
+
<slot />
|
|
11
|
+
</p>
|
|
12
|
+
</template>
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<script setup lang="ts" generic="T = string">
|
|
2
|
+
import type { ClassProps } from '../../lib/types'
|
|
3
|
+
import type { FieldBindings } from './types'
|
|
4
|
+
import { useField } from '@tanstack/vue-form'
|
|
5
|
+
import { computed, useId } from 'vue'
|
|
6
|
+
import { cn } from '../../lib/utils'
|
|
7
|
+
import { useFormContext } from './context'
|
|
8
|
+
import { firstErrorMessage } from './errors'
|
|
9
|
+
import FormDescription from './FormDescription.vue'
|
|
10
|
+
import FormItem from './FormItem.vue'
|
|
11
|
+
import FormLabel from './FormLabel.vue'
|
|
12
|
+
import FormMessage from './FormMessage.vue'
|
|
13
|
+
|
|
14
|
+
const props = defineProps<ClassProps & {
|
|
15
|
+
/** Tên field trong schema. Hỗ trợ path lồng nhau: "address.city" */
|
|
16
|
+
name: string
|
|
17
|
+
label?: string
|
|
18
|
+
description?: string
|
|
19
|
+
required?: boolean
|
|
20
|
+
/** Giá trị khởi tạo. Cũng là chỗ để TS suy ra kiểu T cho slot prop. */
|
|
21
|
+
defaultValue?: T
|
|
22
|
+
}>()
|
|
23
|
+
|
|
24
|
+
const { api: form, serverErrors, submitted } = useFormContext()
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Đọc `props` **một lần**: `useField` trải opts vào `new FieldApi()` ngay lúc
|
|
28
|
+
* gọi, và cái `watch` bên trong nó theo dõi chính object opts nên object tĩnh
|
|
29
|
+
* không bao giờ kích hoạt. Đổi `name` giữa chừng sẽ không đổi field — muốn vậy
|
|
30
|
+
* thì đặt `:key` để dựng lại component.
|
|
31
|
+
*/
|
|
32
|
+
const { api } = useField({
|
|
33
|
+
form,
|
|
34
|
+
name: props.name,
|
|
35
|
+
defaultValue: props.defaultValue,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const controlId = useId()
|
|
39
|
+
const descriptionId = computed(() => (props.description ? `${controlId}-description` : undefined))
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Lỗi được TÍNH liên tục nhưng chỉ được HIỆN sau khi field đã rời focus một
|
|
43
|
+
* lần, hoặc sau khi form đã bấm gửi.
|
|
44
|
+
*
|
|
45
|
+
* Đây là chỗ đặt "thời điểm validate", và nó nằm ở tầng hiển thị chứ không
|
|
46
|
+
* phải ở validator. Kết quả ra đúng ba giai đoạn:
|
|
47
|
+
*
|
|
48
|
+
* | lúc | người dùng thấy |
|
|
49
|
+
* |---|---|
|
|
50
|
+
* | đang gõ lần đầu | **không gì cả** — chưa gõ xong đã báo sai là quấy rầy |
|
|
51
|
+
* | vừa rời ô | lỗi hiện ra |
|
|
52
|
+
* | đang sửa lỗi | lỗi **biến mất ngay** khi gõ đúng, không phải chờ blur |
|
|
53
|
+
*
|
|
54
|
+
* Giai đoạn ba là lý do không chuyển thẳng validator sang `onBlur`: làm vậy
|
|
55
|
+
* thì lúc sửa, người dùng gõ đúng rồi mà thông báo đỏ vẫn đứng đó tới khi họ
|
|
56
|
+
* bấm ra ngoài — không biết mình đã sửa xong chưa.
|
|
57
|
+
*
|
|
58
|
+
* Cách này còn tránh được một chuyện: validator ở TanStack là **cấp form**,
|
|
59
|
+
* nên không thể bật `onChange` cho riêng một field. Gate ở tầng hiển thị thì
|
|
60
|
+
* mỗi FormField tự quyết định phần của mình.
|
|
61
|
+
*/
|
|
62
|
+
const showValidation = computed(() => api.state.meta.isBlurred || submitted.value)
|
|
63
|
+
|
|
64
|
+
// Lỗi server thắng: nó là thông tin mà client không tự suy ra được, và nó chỉ
|
|
65
|
+
// đến sau một lần gửi nên không bao giờ là "quá sớm".
|
|
66
|
+
const errorMessage = computed(() => (
|
|
67
|
+
serverErrors.value[props.name]
|
|
68
|
+
?? (showValidation.value ? firstErrorMessage(api.state.meta.errorMap) : undefined)
|
|
69
|
+
))
|
|
70
|
+
|
|
71
|
+
const messageId = computed(() => (errorMessage.value ? `${controlId}-message` : undefined))
|
|
72
|
+
const invalid = computed(() => Boolean(errorMessage.value))
|
|
73
|
+
|
|
74
|
+
const value = computed(() => api.state.value as T)
|
|
75
|
+
|
|
76
|
+
const describedBy = computed(() => {
|
|
77
|
+
const ids = [descriptionId.value, messageId.value].filter(Boolean)
|
|
78
|
+
return ids.length > 0 ? ids.join(' ') : undefined
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
function setValue(next: unknown): void {
|
|
82
|
+
api.handleChange(next)
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Lỗi server phải mất ngay khi người dùng sửa field, nếu không "email đã tồn
|
|
86
|
+
* tại" bám lại cả sau khi họ gõ email khác. Client không có cách nào biết lỗi
|
|
87
|
+
* đó còn đúng hay không, nên bỏ đi là lựa chọn duy nhất trung thực.
|
|
88
|
+
*/
|
|
89
|
+
if (serverErrors.value[props.name] !== undefined) {
|
|
90
|
+
const { [props.name]: _removed, ...rest } = serverErrors.value
|
|
91
|
+
serverErrors.value = rest
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const field = computed<FieldBindings<T>>(() => ({
|
|
96
|
+
'modelValue': value.value,
|
|
97
|
+
'onUpdate:modelValue': setValue,
|
|
98
|
+
'onBlur': () => api.handleBlur(),
|
|
99
|
+
'name': props.name,
|
|
100
|
+
'id': controlId,
|
|
101
|
+
'aria-describedby': describedBy.value,
|
|
102
|
+
'aria-invalid': invalid.value ? true : undefined,
|
|
103
|
+
}))
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<template>
|
|
107
|
+
<FormItem :class="cn(props.class)">
|
|
108
|
+
<FormLabel v-if="props.label" :for="controlId" :required="props.required">
|
|
109
|
+
{{ props.label }}
|
|
110
|
+
</FormLabel>
|
|
111
|
+
|
|
112
|
+
<slot
|
|
113
|
+
:id="controlId"
|
|
114
|
+
:field="field"
|
|
115
|
+
:value="value"
|
|
116
|
+
:set-value="setValue"
|
|
117
|
+
:error-message="errorMessage"
|
|
118
|
+
:invalid="invalid"
|
|
119
|
+
/>
|
|
120
|
+
|
|
121
|
+
<FormDescription v-if="props.description" :id="descriptionId">
|
|
122
|
+
{{ props.description }}
|
|
123
|
+
</FormDescription>
|
|
124
|
+
|
|
125
|
+
<FormMessage :id="messageId" :message="errorMessage" />
|
|
126
|
+
</FormItem>
|
|
127
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ClassProps } from '../../lib/types'
|
|
3
|
+
import { cn } from '../../lib/utils'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<ClassProps>()
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<template>
|
|
9
|
+
<div :class="cn('flex flex-col gap-1.5', props.class)">
|
|
10
|
+
<slot />
|
|
11
|
+
</div>
|
|
12
|
+
</template>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ClassProps } from '../../lib/types'
|
|
3
|
+
import { cn } from '../../lib/utils'
|
|
4
|
+
import Label from '../label/Label.vue'
|
|
5
|
+
|
|
6
|
+
const props = defineProps<ClassProps & { for?: string, required?: boolean }>()
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<Label :for="props.for" :class="cn('text-sm', props.class)">
|
|
11
|
+
<slot />
|
|
12
|
+
<span v-if="props.required" class="ml-0.5 text-destructive" aria-hidden="true">*</span>
|
|
13
|
+
</Label>
|
|
14
|
+
</template>
|