@slexn/codecenter-ui 1.0.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/LICENSE +34 -0
- package/README.md +148 -0
- package/components.json +20 -0
- package/dist/codecenter-ui.cjs +10 -0
- package/dist/codecenter-ui.js +17995 -0
- package/dist/components/ui/accordion/Accordion.vue.d.ts +28 -0
- package/dist/components/ui/accordion/AccordionContent.vue.d.ts +22 -0
- package/dist/components/ui/accordion/AccordionItem.vue.d.ts +24 -0
- package/dist/components/ui/accordion/AccordionTrigger.vue.d.ts +22 -0
- package/dist/components/ui/accordion/index.d.ts +4 -0
- package/dist/components/ui/alert/Alert.vue.d.ts +32 -0
- package/dist/components/ui/alert/AlertDescription.vue.d.ts +24 -0
- package/dist/components/ui/alert/AlertTitle.vue.d.ts +24 -0
- package/dist/components/ui/alert/index.d.ts +4 -0
- package/dist/components/ui/alert/variants.d.ts +5 -0
- package/dist/components/ui/button/Button.vue.d.ts +27 -0
- package/dist/components/ui/button/index.d.ts +2 -0
- package/dist/components/ui/button/variants.d.ts +6 -0
- package/dist/components/ui/button-group/ButtonGroup.vue.d.ts +25 -0
- package/dist/components/ui/button-group/index.d.ts +2 -0
- package/dist/components/ui/card/Card.vue.d.ts +21 -0
- package/dist/components/ui/card/CardContent.vue.d.ts +21 -0
- package/dist/components/ui/card/CardDescription.vue.d.ts +21 -0
- package/dist/components/ui/card/CardFooter.vue.d.ts +21 -0
- package/dist/components/ui/card/CardHeader.vue.d.ts +21 -0
- package/dist/components/ui/card/CardTitle.vue.d.ts +21 -0
- package/dist/components/ui/card/index.d.ts +6 -0
- package/dist/components/ui/chart/Chart.vue.d.ts +92 -0
- package/dist/components/ui/chart/index.d.ts +2 -0
- package/dist/components/ui/chat/Chat.vue.d.ts +190 -0
- package/dist/components/ui/chat/ChatAttachments.vue.d.ts +11 -0
- package/dist/components/ui/chat/ChatCodeBlock.vue.d.ts +16 -0
- package/dist/components/ui/chat/code-block.d.ts +27 -0
- package/dist/components/ui/chat/index.d.ts +6 -0
- package/dist/components/ui/chat/types.d.ts +15 -0
- package/dist/components/ui/checkbox/Checkbox.vue.d.ts +29 -0
- package/dist/components/ui/checkbox/index.d.ts +1 -0
- package/dist/components/ui/commit/Commit.vue.d.ts +62 -0
- package/dist/components/ui/commit/index.d.ts +2 -0
- package/dist/components/ui/contribution-graph/ContributionGraph.vue.d.ts +87 -0
- package/dist/components/ui/contribution-graph/index.d.ts +2 -0
- package/dist/components/ui/data-table/DataTable.vue.d.ts +109 -0
- package/dist/components/ui/data-table/index.d.ts +2 -0
- package/dist/components/ui/date-picker/DatePicker.vue.d.ts +37 -0
- package/dist/components/ui/date-picker/index.d.ts +2 -0
- package/dist/components/ui/dialog/Dialog.vue.d.ts +25 -0
- package/dist/components/ui/dialog/DialogClose.vue.d.ts +18 -0
- package/dist/components/ui/dialog/DialogContent.vue.d.ts +39 -0
- package/dist/components/ui/dialog/DialogDescription.vue.d.ts +22 -0
- package/dist/components/ui/dialog/DialogFooter.vue.d.ts +21 -0
- package/dist/components/ui/dialog/DialogHeader.vue.d.ts +21 -0
- package/dist/components/ui/dialog/DialogOverlay.vue.d.ts +22 -0
- package/dist/components/ui/dialog/DialogScrollContent.vue.d.ts +36 -0
- package/dist/components/ui/dialog/DialogTitle.vue.d.ts +22 -0
- package/dist/components/ui/dialog/DialogTrigger.vue.d.ts +18 -0
- package/dist/components/ui/dialog/index.d.ts +10 -0
- package/dist/components/ui/diff/DiffTool.vue.d.ts +21 -0
- package/dist/components/ui/diff/diff-parser.d.ts +30 -0
- package/dist/components/ui/diff/diff-tool.d.ts +36 -0
- package/dist/components/ui/diff/index.d.ts +2 -0
- package/dist/components/ui/dropdown-menu/DropdownMenu.vue.d.ts +24 -0
- package/dist/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue.d.ts +29 -0
- package/dist/components/ui/dropdown-menu/DropdownMenuContent.vue.d.ts +36 -0
- package/dist/components/ui/dropdown-menu/DropdownMenuGroup.vue.d.ts +18 -0
- package/dist/components/ui/dropdown-menu/DropdownMenuItem.vue.d.ts +26 -0
- package/dist/components/ui/dropdown-menu/DropdownMenuLabel.vue.d.ts +23 -0
- package/dist/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue.d.ts +22 -0
- package/dist/components/ui/dropdown-menu/DropdownMenuRadioItem.vue.d.ts +27 -0
- package/dist/components/ui/dropdown-menu/DropdownMenuSeparator.vue.d.ts +7 -0
- package/dist/components/ui/dropdown-menu/DropdownMenuShortcut.vue.d.ts +21 -0
- package/dist/components/ui/dropdown-menu/DropdownMenuSub.vue.d.ts +22 -0
- package/dist/components/ui/dropdown-menu/DropdownMenuSubContent.vue.d.ts +38 -0
- package/dist/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue.d.ts +23 -0
- package/dist/components/ui/dropdown-menu/DropdownMenuTrigger.vue.d.ts +18 -0
- package/dist/components/ui/dropdown-menu/index.d.ts +15 -0
- package/dist/components/ui/gauge/Gauge.vue.d.ts +62 -0
- package/dist/components/ui/gauge/index.d.ts +2 -0
- package/dist/components/ui/git-graph/GitGraph.vue.d.ts +59 -0
- package/dist/components/ui/git-graph/index.d.ts +2 -0
- package/dist/components/ui/incident-timeline/IncidentTimeline.vue.d.ts +50 -0
- package/dist/components/ui/incident-timeline/index.d.ts +2 -0
- package/dist/components/ui/input/Input.vue.d.ts +14 -0
- package/dist/components/ui/input/InputControl.vue.d.ts +14 -0
- package/dist/components/ui/input/InputFieldGroup.vue.d.ts +21 -0
- package/dist/components/ui/input/index.d.ts +4 -0
- package/dist/components/ui/input/types.d.ts +31 -0
- package/dist/components/ui/kpi-card/KpiCard.vue.d.ts +46 -0
- package/dist/components/ui/kpi-card/index.d.ts +2 -0
- package/dist/components/ui/kpi-line-card/KpiLineCard.vue.d.ts +66 -0
- package/dist/components/ui/kpi-line-card/index.d.ts +2 -0
- package/dist/components/ui/model-selector/ModelSelector.vue.d.ts +41 -0
- package/dist/components/ui/model-selector/index.d.ts +2 -0
- package/dist/components/ui/model-selector/types.d.ts +12 -0
- package/dist/components/ui/network-graph/NetworkGraph.vue.d.ts +75 -0
- package/dist/components/ui/network-graph/index.d.ts +2 -0
- package/dist/components/ui/pagination/Pagination.vue.d.ts +29 -0
- package/dist/components/ui/pagination/PaginationContent.vue.d.ts +29 -0
- package/dist/components/ui/pagination/PaginationEllipsis.vue.d.ts +22 -0
- package/dist/components/ui/pagination/PaginationFirst.vue.d.ts +26 -0
- package/dist/components/ui/pagination/PaginationItem.vue.d.ts +28 -0
- package/dist/components/ui/pagination/PaginationLast.vue.d.ts +26 -0
- package/dist/components/ui/pagination/PaginationNext.vue.d.ts +26 -0
- package/dist/components/ui/pagination/PaginationPrevious.vue.d.ts +26 -0
- package/dist/components/ui/pagination/index.d.ts +8 -0
- package/dist/components/ui/profile/Profile.vue.d.ts +30 -0
- package/dist/components/ui/profile/ProfileGroup.vue.d.ts +37 -0
- package/dist/components/ui/profile/index.d.ts +4 -0
- package/dist/components/ui/progress/Progress.vue.d.ts +36 -0
- package/dist/components/ui/progress/index.d.ts +2 -0
- package/dist/components/ui/prompt-input/PromptInput.vue.d.ts +150 -0
- package/dist/components/ui/prompt-input/index.d.ts +2 -0
- package/dist/components/ui/prompt-input/types.d.ts +61 -0
- package/dist/components/ui/radio-group/RadioGroup.vue.d.ts +30 -0
- package/dist/components/ui/radio-group/RadioGroupItem.vue.d.ts +12 -0
- package/dist/components/ui/radio-group/RadioGroupOption.vue.d.ts +28 -0
- package/dist/components/ui/radio-group/index.d.ts +3 -0
- package/dist/components/ui/reasoning/Reasoning.vue.d.ts +40 -0
- package/dist/components/ui/reasoning/index.d.ts +2 -0
- package/dist/components/ui/reasoning/types.d.ts +26 -0
- package/dist/components/ui/select/Select.vue.d.ts +28 -0
- package/dist/components/ui/select/SelectContent.vue.d.ts +46 -0
- package/dist/components/ui/select/SelectGroup.vue.d.ts +18 -0
- package/dist/components/ui/select/SelectItem.vue.d.ts +26 -0
- package/dist/components/ui/select/SelectItemText.vue.d.ts +18 -0
- package/dist/components/ui/select/SelectLabel.vue.d.ts +22 -0
- package/dist/components/ui/select/SelectScrollDownButton.vue.d.ts +22 -0
- package/dist/components/ui/select/SelectScrollUpButton.vue.d.ts +22 -0
- package/dist/components/ui/select/SelectSeparator.vue.d.ts +7 -0
- package/dist/components/ui/select/SelectTrigger.vue.d.ts +25 -0
- package/dist/components/ui/select/SelectValue.vue.d.ts +18 -0
- package/dist/components/ui/select/index.d.ts +11 -0
- package/dist/components/ui/select/search.d.ts +18 -0
- package/dist/components/ui/separator/Separator.vue.d.ts +6 -0
- package/dist/components/ui/separator/index.d.ts +2 -0
- package/dist/components/ui/separator/types.d.ts +7 -0
- package/dist/components/ui/shimmer/Shimmer.vue.d.ts +37 -0
- package/dist/components/ui/shimmer/index.d.ts +2 -0
- package/dist/components/ui/sidebar/Sidebar.vue.d.ts +24 -0
- package/dist/components/ui/sidebar/SidebarContent.vue.d.ts +21 -0
- package/dist/components/ui/sidebar/SidebarFooter.vue.d.ts +21 -0
- package/dist/components/ui/sidebar/SidebarGroup.vue.d.ts +21 -0
- package/dist/components/ui/sidebar/SidebarGroupAction.vue.d.ts +24 -0
- package/dist/components/ui/sidebar/SidebarGroupContent.vue.d.ts +21 -0
- package/dist/components/ui/sidebar/SidebarGroupLabel.vue.d.ts +24 -0
- package/dist/components/ui/sidebar/SidebarHeader.vue.d.ts +21 -0
- package/dist/components/ui/sidebar/SidebarInput.vue.d.ts +6 -0
- package/dist/components/ui/sidebar/SidebarInset.vue.d.ts +21 -0
- package/dist/components/ui/sidebar/SidebarMenu.vue.d.ts +21 -0
- package/dist/components/ui/sidebar/SidebarMenuAction.vue.d.ts +25 -0
- package/dist/components/ui/sidebar/SidebarMenuBadge.vue.d.ts +21 -0
- package/dist/components/ui/sidebar/SidebarMenuButton.vue.d.ts +25 -0
- package/dist/components/ui/sidebar/SidebarMenuButtonChild.vue.d.ts +30 -0
- package/dist/components/ui/sidebar/SidebarMenuItem.vue.d.ts +21 -0
- package/dist/components/ui/sidebar/SidebarMenuSub.vue.d.ts +21 -0
- package/dist/components/ui/sidebar/SidebarMenuSubButton.vue.d.ts +27 -0
- package/dist/components/ui/sidebar/SidebarMenuSubItem.vue.d.ts +21 -0
- package/dist/components/ui/sidebar/SidebarProvider.vue.d.ts +36 -0
- package/dist/components/ui/sidebar/SidebarRail.vue.d.ts +21 -0
- package/dist/components/ui/sidebar/SidebarSeparator.vue.d.ts +6 -0
- package/dist/components/ui/sidebar/SidebarTrigger.vue.d.ts +6 -0
- package/dist/components/ui/sidebar/context.d.ts +19 -0
- package/dist/components/ui/sidebar/index.d.ts +26 -0
- package/dist/components/ui/sidebar/types.d.ts +11 -0
- package/dist/components/ui/sidebar/variants.d.ts +6 -0
- package/dist/components/ui/skeleton/Skeleton.vue.d.ts +18 -0
- package/dist/components/ui/skeleton/index.d.ts +2 -0
- package/dist/components/ui/sonner/Sonner.vue.d.ts +5 -0
- package/dist/components/ui/sonner/index.d.ts +2 -0
- package/dist/components/ui/spinner/Spinner.vue.d.ts +11 -0
- package/dist/components/ui/spinner/index.d.ts +1 -0
- package/dist/components/ui/stepper/Stepper.vue.d.ts +38 -0
- package/dist/components/ui/stepper/StepperDescription.vue.d.ts +22 -0
- package/dist/components/ui/stepper/StepperIndicator.vue.d.ts +30 -0
- package/dist/components/ui/stepper/StepperItem.vue.d.ts +24 -0
- package/dist/components/ui/stepper/StepperSeparator.vue.d.ts +7 -0
- package/dist/components/ui/stepper/StepperTitle.vue.d.ts +22 -0
- package/dist/components/ui/stepper/StepperTrigger.vue.d.ts +22 -0
- package/dist/components/ui/stepper/index.d.ts +7 -0
- package/dist/components/ui/switch/Switch.vue.d.ts +12 -0
- package/dist/components/ui/switch/index.d.ts +1 -0
- package/dist/components/ui/table/Table.vue.d.ts +22 -0
- package/dist/components/ui/table/TableBody.vue.d.ts +21 -0
- package/dist/components/ui/table/TableCaption.vue.d.ts +21 -0
- package/dist/components/ui/table/TableCell.vue.d.ts +22 -0
- package/dist/components/ui/table/TableEmpty.vue.d.ts +24 -0
- package/dist/components/ui/table/TableFooter.vue.d.ts +21 -0
- package/dist/components/ui/table/TableHead.vue.d.ts +21 -0
- package/dist/components/ui/table/TableHeader.vue.d.ts +21 -0
- package/dist/components/ui/table/TableRow.vue.d.ts +21 -0
- package/dist/components/ui/table/index.d.ts +9 -0
- package/dist/components/ui/tabs/Tabs.vue.d.ts +28 -0
- package/dist/components/ui/tabs/TabsContent.vue.d.ts +22 -0
- package/dist/components/ui/tabs/TabsList.vue.d.ts +22 -0
- package/dist/components/ui/tabs/TabsTrigger.vue.d.ts +22 -0
- package/dist/components/ui/tabs/index.d.ts +4 -0
- package/dist/components/ui/tag/Tag.vue.d.ts +35 -0
- package/dist/components/ui/tag/index.d.ts +2 -0
- package/dist/components/ui/tag/variants.d.ts +6 -0
- package/dist/components/ui/textarea/Textarea.vue.d.ts +14 -0
- package/dist/components/ui/textarea/TextareaControl.vue.d.ts +14 -0
- package/dist/components/ui/textarea/TextareaFieldGroup.vue.d.ts +21 -0
- package/dist/components/ui/textarea/index.d.ts +4 -0
- package/dist/components/ui/textarea/types.d.ts +32 -0
- package/dist/components/ui/tool/Tool.vue.d.ts +61 -0
- package/dist/components/ui/tool/index.d.ts +2 -0
- package/dist/components/ui/tooltip/Tooltip.vue.d.ts +24 -0
- package/dist/components/ui/tooltip/TooltipContent.vue.d.ts +32 -0
- package/dist/components/ui/tooltip/TooltipProvider.vue.d.ts +20 -0
- package/dist/components/ui/tooltip/TooltipTrigger.vue.d.ts +18 -0
- package/dist/components/ui/tooltip/index.d.ts +4 -0
- package/dist/docs/component-docs.d.ts +18 -0
- package/dist/docs/markdown.d.ts +27 -0
- package/dist/index.d.ts +45 -0
- package/dist/lib/code-highlight.d.ts +11 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/styles.css +3 -0
- package/package.json +76 -0
- package/public/r/accordion.json +52 -0
- package/public/r/alert.json +51 -0
- package/public/r/button-group.json +31 -0
- package/public/r/button.json +39 -0
- package/public/r/card.json +61 -0
- package/public/r/chart.json +31 -0
- package/public/r/chat.json +186 -0
- package/public/r/checkbox.json +34 -0
- package/public/r/commit.json +32 -0
- package/public/r/contribution-graph.json +63 -0
- package/public/r/data-table.json +197 -0
- package/public/r/date-picker.json +33 -0
- package/public/r/dialog.json +88 -0
- package/public/r/diff.json +71 -0
- package/public/r/dropdown-menu.json +112 -0
- package/public/r/gauge.json +31 -0
- package/public/r/git-graph.json +32 -0
- package/public/r/incident-timeline.json +64 -0
- package/public/r/input.json +49 -0
- package/public/r/kpi-card.json +32 -0
- package/public/r/kpi-line-card.json +32 -0
- package/public/r/model-selector.json +148 -0
- package/public/r/network-graph.json +33 -0
- package/public/r/pagination.json +95 -0
- package/public/r/profile.json +37 -0
- package/public/r/progress.json +31 -0
- package/public/r/prompt-input.json +293 -0
- package/public/r/radio-group.json +45 -0
- package/public/r/reasoning.json +38 -0
- package/public/r/registry.json +2512 -0
- package/public/r/select.json +100 -0
- package/public/r/separator.json +37 -0
- package/public/r/shimmer.json +31 -0
- package/public/r/sidebar.json +221 -0
- package/public/r/skeleton.json +31 -0
- package/public/r/sonner.json +33 -0
- package/public/r/spinner.json +31 -0
- package/public/r/stepper.json +70 -0
- package/public/r/switch.json +33 -0
- package/public/r/table.json +79 -0
- package/public/r/tabs.json +51 -0
- package/public/r/tag.json +39 -0
- package/public/r/textarea.json +49 -0
- package/public/r/tool.json +32 -0
- package/public/r/tooltip.json +51 -0
- package/registry.json +2512 -0
- package/src/components/docs/MarkdownContent.vue +106 -0
- package/src/components/ui/accordion/Accordion.vue +24 -0
- package/src/components/ui/accordion/AccordionContent.vue +62 -0
- package/src/components/ui/accordion/AccordionItem.vue +23 -0
- package/src/components/ui/accordion/AccordionTrigger.vue +38 -0
- package/src/components/ui/accordion/index.ts +4 -0
- package/src/components/ui/alert/Alert.vue +40 -0
- package/src/components/ui/alert/AlertDescription.vue +24 -0
- package/src/components/ui/alert/AlertTitle.vue +24 -0
- package/src/components/ui/alert/index.ts +4 -0
- package/src/components/ui/alert/variants.ts +19 -0
- package/src/components/ui/button/Button.vue +27 -0
- package/src/components/ui/button/index.ts +2 -0
- package/src/components/ui/button/variants.ts +32 -0
- package/src/components/ui/button-group/ButtonGroup.vue +31 -0
- package/src/components/ui/button-group/index.ts +2 -0
- package/src/components/ui/card/Card.vue +17 -0
- package/src/components/ui/card/CardContent.vue +14 -0
- package/src/components/ui/card/CardDescription.vue +14 -0
- package/src/components/ui/card/CardFooter.vue +14 -0
- package/src/components/ui/card/CardHeader.vue +17 -0
- package/src/components/ui/card/CardTitle.vue +14 -0
- package/src/components/ui/card/index.ts +6 -0
- package/src/components/ui/chart/Chart.vue +1042 -0
- package/src/components/ui/chart/index.ts +13 -0
- package/src/components/ui/chat/Chat.vue +1297 -0
- package/src/components/ui/chat/ChatAttachments.vue +278 -0
- package/src/components/ui/chat/ChatCodeBlock.vue +283 -0
- package/src/components/ui/chat/code-block.ts +30 -0
- package/src/components/ui/chat/index.ts +24 -0
- package/src/components/ui/chat/types.ts +23 -0
- package/src/components/ui/checkbox/Checkbox.vue +38 -0
- package/src/components/ui/checkbox/index.ts +1 -0
- package/src/components/ui/commit/Commit.vue +423 -0
- package/src/components/ui/commit/index.ts +9 -0
- package/src/components/ui/contribution-graph/ContributionGraph.vue +719 -0
- package/src/components/ui/contribution-graph/index.ts +9 -0
- package/src/components/ui/data-table/DataTable.vue +534 -0
- package/src/components/ui/data-table/index.ts +9 -0
- package/src/components/ui/date-picker/DatePicker.vue +649 -0
- package/src/components/ui/date-picker/index.ts +7 -0
- package/src/components/ui/dialog/Dialog.vue +19 -0
- package/src/components/ui/dialog/DialogClose.vue +17 -0
- package/src/components/ui/dialog/DialogContent.vue +60 -0
- package/src/components/ui/dialog/DialogDescription.vue +23 -0
- package/src/components/ui/dialog/DialogFooter.vue +17 -0
- package/src/components/ui/dialog/DialogHeader.vue +17 -0
- package/src/components/ui/dialog/DialogOverlay.vue +23 -0
- package/src/components/ui/dialog/DialogScrollContent.vue +69 -0
- package/src/components/ui/dialog/DialogTitle.vue +23 -0
- package/src/components/ui/dialog/DialogTrigger.vue +17 -0
- package/src/components/ui/dialog/index.ts +10 -0
- package/src/components/ui/diff/DiffTool.vue +513 -0
- package/src/components/ui/diff/diff-parser.ts +423 -0
- package/src/components/ui/diff/diff-tool.ts +39 -0
- package/src/components/ui/diff/index.ts +5 -0
- package/src/components/ui/dropdown-menu/DropdownMenu.vue +19 -0
- package/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +39 -0
- package/src/components/ui/dropdown-menu/DropdownMenuContent.vue +39 -0
- package/src/components/ui/dropdown-menu/DropdownMenuGroup.vue +15 -0
- package/src/components/ui/dropdown-menu/DropdownMenuItem.vue +31 -0
- package/src/components/ui/dropdown-menu/DropdownMenuLabel.vue +23 -0
- package/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +21 -0
- package/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +40 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue +23 -0
- package/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue +17 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSub.vue +18 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue +27 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +31 -0
- package/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue +17 -0
- package/src/components/ui/dropdown-menu/index.ts +16 -0
- package/src/components/ui/gauge/Gauge.vue +725 -0
- package/src/components/ui/gauge/index.ts +9 -0
- package/src/components/ui/git-graph/GitGraph.vue +715 -0
- package/src/components/ui/git-graph/index.ts +9 -0
- package/src/components/ui/incident-timeline/IncidentTimeline.vue +360 -0
- package/src/components/ui/incident-timeline/index.ts +7 -0
- package/src/components/ui/input/Input.vue +159 -0
- package/src/components/ui/input/InputControl.vue +135 -0
- package/src/components/ui/input/InputFieldGroup.vue +14 -0
- package/src/components/ui/input/index.ts +9 -0
- package/src/components/ui/input/types.ts +34 -0
- package/src/components/ui/kpi-card/KpiCard.vue +268 -0
- package/src/components/ui/kpi-card/index.ts +9 -0
- package/src/components/ui/kpi-line-card/KpiLineCard.vue +622 -0
- package/src/components/ui/kpi-line-card/index.ts +11 -0
- package/src/components/ui/model-selector/ModelSelector.vue +328 -0
- package/src/components/ui/model-selector/index.ts +6 -0
- package/src/components/ui/model-selector/types.ts +15 -0
- package/src/components/ui/network-graph/NetworkGraph.vue +902 -0
- package/src/components/ui/network-graph/index.ts +7 -0
- package/src/components/ui/pagination/Pagination.vue +26 -0
- package/src/components/ui/pagination/PaginationContent.vue +24 -0
- package/src/components/ui/pagination/PaginationEllipsis.vue +27 -0
- package/src/components/ui/pagination/PaginationFirst.vue +33 -0
- package/src/components/ui/pagination/PaginationItem.vue +39 -0
- package/src/components/ui/pagination/PaginationLast.vue +33 -0
- package/src/components/ui/pagination/PaginationNext.vue +33 -0
- package/src/components/ui/pagination/PaginationPrevious.vue +33 -0
- package/src/components/ui/pagination/index.ts +8 -0
- package/src/components/ui/profile/Profile.vue +226 -0
- package/src/components/ui/profile/ProfileGroup.vue +96 -0
- package/src/components/ui/profile/index.ts +8 -0
- package/src/components/ui/progress/Progress.vue +271 -0
- package/src/components/ui/progress/index.ts +9 -0
- package/src/components/ui/prompt-input/PromptInput.vue +1094 -0
- package/src/components/ui/prompt-input/index.ts +14 -0
- package/src/components/ui/prompt-input/types.ts +78 -0
- package/src/components/ui/radio-group/RadioGroup.vue +36 -0
- package/src/components/ui/radio-group/RadioGroupItem.vue +45 -0
- package/src/components/ui/radio-group/RadioGroupOption.vue +80 -0
- package/src/components/ui/radio-group/index.ts +3 -0
- package/src/components/ui/reasoning/Reasoning.vue +278 -0
- package/src/components/ui/reasoning/index.ts +8 -0
- package/src/components/ui/reasoning/types.ts +29 -0
- package/src/components/ui/select/Select.vue +19 -0
- package/src/components/ui/select/SelectContent.vue +166 -0
- package/src/components/ui/select/SelectGroup.vue +23 -0
- package/src/components/ui/select/SelectItem.vue +97 -0
- package/src/components/ui/select/SelectItemText.vue +15 -0
- package/src/components/ui/select/SelectLabel.vue +17 -0
- package/src/components/ui/select/SelectScrollDownButton.vue +26 -0
- package/src/components/ui/select/SelectScrollUpButton.vue +26 -0
- package/src/components/ui/select/SelectSeparator.vue +19 -0
- package/src/components/ui/select/SelectTrigger.vue +33 -0
- package/src/components/ui/select/SelectValue.vue +15 -0
- package/src/components/ui/select/index.ts +11 -0
- package/src/components/ui/select/search.ts +26 -0
- package/src/components/ui/separator/Separator.vue +30 -0
- package/src/components/ui/separator/index.ts +5 -0
- package/src/components/ui/separator/types.ts +9 -0
- package/src/components/ui/shimmer/Shimmer.vue +110 -0
- package/src/components/ui/shimmer/index.ts +5 -0
- package/src/components/ui/sidebar/Sidebar.vue +142 -0
- package/src/components/ui/sidebar/SidebarContent.vue +18 -0
- package/src/components/ui/sidebar/SidebarFooter.vue +18 -0
- package/src/components/ui/sidebar/SidebarGroup.vue +18 -0
- package/src/components/ui/sidebar/SidebarGroupAction.vue +31 -0
- package/src/components/ui/sidebar/SidebarGroupContent.vue +18 -0
- package/src/components/ui/sidebar/SidebarGroupLabel.vue +30 -0
- package/src/components/ui/sidebar/SidebarHeader.vue +18 -0
- package/src/components/ui/sidebar/SidebarInput.vue +26 -0
- package/src/components/ui/sidebar/SidebarInset.vue +23 -0
- package/src/components/ui/sidebar/SidebarMenu.vue +18 -0
- package/src/components/ui/sidebar/SidebarMenuAction.vue +34 -0
- package/src/components/ui/sidebar/SidebarMenuBadge.vue +25 -0
- package/src/components/ui/sidebar/SidebarMenuButton.vue +37 -0
- package/src/components/ui/sidebar/SidebarMenuButtonChild.vue +38 -0
- package/src/components/ui/sidebar/SidebarMenuItem.vue +18 -0
- package/src/components/ui/sidebar/SidebarMenuSub.vue +18 -0
- package/src/components/ui/sidebar/SidebarMenuSubButton.vue +36 -0
- package/src/components/ui/sidebar/SidebarMenuSubItem.vue +18 -0
- package/src/components/ui/sidebar/SidebarProvider.vue +119 -0
- package/src/components/ui/sidebar/SidebarRail.vue +35 -0
- package/src/components/ui/sidebar/SidebarSeparator.vue +18 -0
- package/src/components/ui/sidebar/SidebarTrigger.vue +28 -0
- package/src/components/ui/sidebar/context.ts +39 -0
- package/src/components/ui/sidebar/index.ts +43 -0
- package/src/components/ui/sidebar/types.ts +13 -0
- package/src/components/ui/sidebar/variants.ts +25 -0
- package/src/components/ui/skeleton/Skeleton.vue +53 -0
- package/src/components/ui/skeleton/index.ts +5 -0
- package/src/components/ui/sonner/Sonner.vue +69 -0
- package/src/components/ui/sonner/index.ts +12 -0
- package/src/components/ui/spinner/Spinner.vue +33 -0
- package/src/components/ui/spinner/index.ts +1 -0
- package/src/components/ui/stepper/Stepper.vue +29 -0
- package/src/components/ui/stepper/StepperDescription.vue +30 -0
- package/src/components/ui/stepper/StepperIndicator.vue +50 -0
- package/src/components/ui/stepper/StepperItem.vue +28 -0
- package/src/components/ui/stepper/StepperSeparator.vue +25 -0
- package/src/components/ui/stepper/StepperTitle.vue +27 -0
- package/src/components/ui/stepper/StepperTrigger.vue +27 -0
- package/src/components/ui/stepper/index.ts +7 -0
- package/src/components/ui/switch/Switch.vue +41 -0
- package/src/components/ui/switch/index.ts +1 -0
- package/src/components/ui/table/Table.vue +23 -0
- package/src/components/ui/table/TableBody.vue +17 -0
- package/src/components/ui/table/TableCaption.vue +17 -0
- package/src/components/ui/table/TableCell.vue +24 -0
- package/src/components/ui/table/TableEmpty.vue +31 -0
- package/src/components/ui/table/TableFooter.vue +17 -0
- package/src/components/ui/table/TableHead.vue +22 -0
- package/src/components/ui/table/TableHeader.vue +17 -0
- package/src/components/ui/table/TableRow.vue +22 -0
- package/src/components/ui/table/index.ts +9 -0
- package/src/components/ui/tabs/Tabs.vue +24 -0
- package/src/components/ui/tabs/TabsContent.vue +22 -0
- package/src/components/ui/tabs/TabsList.vue +27 -0
- package/src/components/ui/tabs/TabsTrigger.vue +27 -0
- package/src/components/ui/tabs/index.ts +4 -0
- package/src/components/ui/tag/Tag.vue +55 -0
- package/src/components/ui/tag/index.ts +2 -0
- package/src/components/ui/tag/variants.ts +29 -0
- package/src/components/ui/textarea/Textarea.vue +159 -0
- package/src/components/ui/textarea/TextareaControl.vue +120 -0
- package/src/components/ui/textarea/TextareaFieldGroup.vue +14 -0
- package/src/components/ui/textarea/index.ts +10 -0
- package/src/components/ui/textarea/types.ts +35 -0
- package/src/components/ui/tool/Tool.vue +304 -0
- package/src/components/ui/tool/index.ts +7 -0
- package/src/components/ui/tooltip/Tooltip.vue +19 -0
- package/src/components/ui/tooltip/TooltipContent.vue +44 -0
- package/src/components/ui/tooltip/TooltipProvider.vue +14 -0
- package/src/components/ui/tooltip/TooltipTrigger.vue +15 -0
- package/src/components/ui/tooltip/index.ts +4 -0
- package/src/lib/code-highlight.ts +220 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles.css +684 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://shadcn-vue.com/schema/registry-item.json",
|
|
3
|
+
"name": "incident-timeline",
|
|
4
|
+
"title": "Incident Timeline",
|
|
5
|
+
"description": "An uptime-style incident timeline with a service label, status icon, uptime percentage, and compact vertical status bars.",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"@lucide/vue",
|
|
8
|
+
"@vueuse/core",
|
|
9
|
+
"clsx",
|
|
10
|
+
"reka-ui",
|
|
11
|
+
"tailwind-merge"
|
|
12
|
+
],
|
|
13
|
+
"files": [
|
|
14
|
+
{
|
|
15
|
+
"path": "src/components/ui/incident-timeline/IncidentTimeline.vue",
|
|
16
|
+
"content": "<script setup lang=\"ts\">\nimport { computed, ref } from \"vue\"\nimport type { CSSProperties, HTMLAttributes } from \"vue\"\nimport { CheckIcon } from \"@lucide/vue\"\nimport { cn } from \"@/lib/utils\"\nimport {\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n} from \"../tooltip\"\n\nexport type IncidentTimelineStatus =\n | \"degraded\"\n | \"maintenance\"\n | \"operational\"\n | \"outage\"\n | \"partial-outage\"\n\nexport type IncidentTimelineSize = \"sm\" | \"default\" | \"lg\"\n\nexport interface IncidentTimelineItem {\n description?: string\n endedAt?: Date | string | null\n id?: number | string\n label?: string\n startedAt?: Date | string\n status?: IncidentTimelineStatus\n title?: string\n}\n\nexport interface IncidentTimelineSelectPayload {\n index: number\n item: IncidentTimelineItem\n status: IncidentTimelineStatus\n}\n\ninterface IncidentTimelineProps {\n barCount?: number\n class?: HTMLAttributes[\"class\"]\n emptyLabel?: string\n items?: IncidentTimelineItem[]\n label?: string\n modelValue?: string\n operationalLabel?: string\n selectable?: boolean\n showIcon?: boolean\n showUptime?: boolean\n size?: IncidentTimelineSize\n status?: IncidentTimelineStatus\n uptime?: number | string\n}\n\ninterface NormalizedIncidentTimelineItem {\n description?: string\n id: string\n index: number\n item: IncidentTimelineItem\n label: string\n status: IncidentTimelineStatus\n}\n\nconst props = withDefaults(defineProps<IncidentTimelineProps>(), {\n barCount: 90,\n emptyLabel: \"No timeline data.\",\n label: \"Pulsetic Website\",\n operationalLabel: \"Operational\",\n selectable: true,\n showIcon: true,\n showUptime: true,\n size: \"default\",\n status: \"operational\",\n})\n\nconst emit = defineEmits<{\n \"select\": [payload: IncidentTimelineSelectPayload]\n \"update:modelValue\": [value: string]\n}>()\n\nconst openTooltipId = ref<string | null>(null)\n\nconst sizeClasses = computed(() => {\n if (props.size === \"sm\") {\n return {\n bar: \"h-5 rounded-[3px]\",\n gap: \"gap-[clamp(1px,0.22vw,3px)]\",\n headerGap: \"gap-2\",\n icon: \"size-4\",\n iconGlyph: \"size-2.5\",\n label: \"text-xs\",\n labelWrap: \"gap-1.5\",\n root: \"gap-2\",\n uptime: \"text-xs\",\n }\n }\n\n if (props.size === \"lg\") {\n return {\n bar: \"h-9 rounded-[4px]\",\n gap: \"gap-[clamp(1px,0.32vw,5px)]\",\n headerGap: \"gap-4\",\n icon: \"size-6\",\n iconGlyph: \"size-4\",\n label: \"text-base\",\n labelWrap: \"gap-2.5\",\n root: \"gap-3\",\n uptime: \"text-sm\",\n }\n }\n\n return {\n bar: \"h-7 rounded-[4px]\",\n gap: \"gap-[clamp(1px,0.28vw,4px)]\",\n headerGap: \"gap-3\",\n icon: \"size-5\",\n iconGlyph: \"size-3.5\",\n label: \"text-sm\",\n labelWrap: \"gap-2\",\n root: \"gap-2.5\",\n uptime: \"text-sm\",\n }\n})\n\nconst normalizedItems = computed<NormalizedIncidentTimelineItem[]>(() => {\n const configuredItems = props.items ?? []\n\n if (configuredItems.length) {\n return configuredItems.map((item, index) => normalizeItem(item, index))\n }\n\n return Array.from({ length: normalizedBarCount.value }, (_, index) =>\n normalizeItem({\n id: index,\n status: props.status,\n }, index),\n )\n})\n\nconst normalizedBarCount = computed(() =>\n Math.max(1, Math.min(180, Math.trunc(props.barCount))),\n)\n\nconst visibleItems = computed(() => {\n if (!normalizedItems.value.length) return []\n\n if (normalizedItems.value.length === normalizedBarCount.value) return normalizedItems.value\n\n if (normalizedItems.value.length > normalizedBarCount.value) {\n return normalizedItems.value.slice(-normalizedBarCount.value)\n }\n\n const missingCount = normalizedBarCount.value - normalizedItems.value.length\n const generated = Array.from({ length: missingCount }, (_, index) =>\n normalizeItem({\n id: `generated-${index}`,\n status: props.status,\n }, index),\n )\n\n return [...generated, ...normalizedItems.value]\n})\n\nconst overallStatus = computed(() => props.status)\n\nconst uptimeLabel = computed(() => {\n if (props.uptime !== undefined) {\n return typeof props.uptime === \"number\" ? `${formatPercent(props.uptime)}%` : props.uptime\n }\n\n if (!visibleItems.value.length) return \"\"\n\n const operationalCount = visibleItems.value.filter((item) => item.status === \"operational\").length\n const value = (operationalCount / visibleItems.value.length) * 100\n\n return `${formatPercent(value)}%`\n})\n\nconst gridStyle = computed<CSSProperties>(() => ({\n gridTemplateColumns: `repeat(${visibleItems.value.length}, minmax(2px, 1fr))`,\n}))\n\nfunction normalizeItem(item: IncidentTimelineItem, index: number): NormalizedIncidentTimelineItem {\n const status = item.status ?? \"operational\"\n const label = item.label ?? item.title ?? `${props.operationalLabel} segment ${index + 1}`\n\n return {\n description: item.description,\n id: item.id === undefined ? String(index) : String(item.id),\n index,\n item,\n label,\n status,\n }\n}\n\nfunction formatPercent(value: number) {\n return new Intl.NumberFormat(undefined, {\n maximumFractionDigits: value >= 99 ? 2 : 1,\n minimumFractionDigits: value >= 99 ? 2 : 1,\n }).format(value)\n}\n\nfunction statusColor(status: IncidentTimelineStatus) {\n if (status === \"outage\") return \"oklch(0.58 0.22 27)\"\n if (status === \"partial-outage\") return \"oklch(0.66 0.18 60)\"\n if (status === \"degraded\") return \"oklch(0.75 0.18 90)\"\n if (status === \"maintenance\") return \"oklch(0.62 0.13 290)\"\n\n return \"oklch(0.58 0.15 150)\"\n}\n\nfunction itemStyle(item: NormalizedIncidentTimelineItem): CSSProperties {\n return {\n backgroundColor: statusColor(item.status),\n }\n}\n\nfunction isSelected(item: NormalizedIncidentTimelineItem) {\n return props.modelValue === item.id\n}\n\nfunction itemLabel(item: NormalizedIncidentTimelineItem) {\n const status = item.status.replace(/-/g, \" \")\n\n return item.description ? `${item.label}: ${status}. ${item.description}` : `${item.label}: ${status}`\n}\n\nfunction statusLabel(status: IncidentTimelineStatus) {\n if (status === \"partial-outage\") return \"Partial outage\"\n\n return status.charAt(0).toUpperCase() + status.slice(1)\n}\n\nfunction isTooltipOpen(item: NormalizedIncidentTimelineItem) {\n return openTooltipId.value === item.id\n}\n\nfunction handleTooltipOpen(item: NormalizedIncidentTimelineItem, open: boolean) {\n if (open) {\n openTooltipId.value = item.id\n return\n }\n\n if (openTooltipId.value === item.id) {\n openTooltipId.value = null\n }\n}\n\nfunction handleSelect(item: NormalizedIncidentTimelineItem) {\n openTooltipId.value = item.id\n\n if (!props.selectable) return\n\n emit(\"update:modelValue\", item.id)\n emit(\"select\", {\n index: item.index,\n item: item.item,\n status: item.status,\n })\n}\n</script>\n\n<template>\n <article\n data-slot=\"incident-timeline\"\n :data-size=\"props.size\"\n :class=\"cn('flex min-w-0 flex-col bg-background text-foreground', sizeClasses.root, props.class)\"\n >\n <header\n data-slot=\"incident-timeline-header\"\n :class=\"cn('flex min-w-0 items-center justify-between', sizeClasses.headerGap)\"\n >\n <div\n data-slot=\"incident-timeline-label-wrap\"\n :class=\"cn('flex min-w-0 items-center', sizeClasses.labelWrap)\"\n >\n <span\n v-if=\"props.showIcon\"\n data-slot=\"incident-timeline-status-icon\"\n :class=\"cn('inline-flex shrink-0 items-center justify-center rounded-full text-white', sizeClasses.icon)\"\n :style=\"{ backgroundColor: statusColor(overallStatus) }\"\n >\n <CheckIcon aria-hidden=\"true\" :class=\"sizeClasses.iconGlyph\" />\n </span>\n\n <h3\n data-slot=\"incident-timeline-label\"\n :class=\"cn('min-w-0 truncate font-medium leading-none tracking-normal', sizeClasses.label)\"\n >\n {{ props.label }}\n </h3>\n </div>\n\n <p\n v-if=\"props.showUptime && uptimeLabel\"\n data-slot=\"incident-timeline-uptime\"\n :class=\"cn('shrink-0 font-normal leading-none tracking-normal text-muted-foreground/45', sizeClasses.uptime)\"\n >\n {{ uptimeLabel }}\n </p>\n </header>\n\n <div\n v-if=\"visibleItems.length\"\n data-slot=\"incident-timeline-bars\"\n >\n <TooltipProvider>\n <div\n data-slot=\"incident-timeline-bar-grid\"\n :class=\"cn('grid min-w-0', sizeClasses.gap)\"\n :style=\"gridStyle\"\n role=\"list\"\n :aria-label=\"`${props.label} timeline`\"\n >\n <Tooltip\n v-for=\"item in visibleItems\"\n :key=\"item.id\"\n :open=\"isTooltipOpen(item)\"\n @update:open=\"handleTooltipOpen(item, $event)\"\n >\n <TooltipTrigger as-child>\n <component\n :is=\"props.selectable ? 'button' : 'span'\"\n data-slot=\"incident-timeline-bar\"\n :type=\"props.selectable ? 'button' : undefined\"\n role=\"listitem\"\n :aria-label=\"itemLabel(item)\"\n :aria-selected=\"props.selectable ? isSelected(item) : undefined\"\n :class=\"\n cn(\n 'block w-full border-0 p-0 outline-none transition-[opacity,transform] hover:opacity-80 focus-visible:ring-[3px] focus-visible:ring-ring/35',\n isSelected(item) && 'opacity-80',\n sizeClasses.bar,\n )\n \"\n :style=\"itemStyle(item)\"\n @click=\"handleSelect(item)\"\n />\n </TooltipTrigger>\n <TooltipContent class=\"max-w-56 text-left leading-snug\">\n <span class=\"block font-medium\">{{ item.label }}</span>\n <span class=\"block text-background/75\">{{ statusLabel(item.status) }}</span>\n <span v-if=\"item.description\" class=\"mt-1 block text-background/75\">\n {{ item.description }}\n </span>\n </TooltipContent>\n </Tooltip>\n </div>\n </TooltipProvider>\n </div>\n\n <p\n v-else\n data-slot=\"incident-timeline-empty\"\n class=\"text-sm text-muted-foreground\"\n >\n {{ props.emptyLabel }}\n </p>\n </article>\n</template>\n",
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"target": "components/ui/incident-timeline/IncidentTimeline.vue"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"path": "src/components/ui/incident-timeline/index.ts",
|
|
22
|
+
"content": "export { default as IncidentTimeline } from \"./IncidentTimeline.vue\"\nexport type {\n IncidentTimelineItem,\n IncidentTimelineSelectPayload,\n IncidentTimelineSize,\n IncidentTimelineStatus,\n} from \"./IncidentTimeline.vue\"\n",
|
|
23
|
+
"type": "registry:ui",
|
|
24
|
+
"target": "components/ui/incident-timeline/index.ts"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"path": "src/components/ui/tooltip/Tooltip.vue",
|
|
28
|
+
"content": "<script setup lang=\"ts\">\nimport type { TooltipRootEmits, TooltipRootProps } from \"reka-ui\"\nimport { TooltipRoot, useForwardPropsEmits } from \"reka-ui\"\n\nconst props = defineProps<TooltipRootProps>()\nconst emits = defineEmits<TooltipRootEmits>()\n\nconst forwarded = useForwardPropsEmits(props, emits)\n</script>\n\n<template>\n <TooltipRoot\n v-slot=\"slotProps\"\n data-slot=\"tooltip\"\n v-bind=\"forwarded\"\n >\n <slot v-bind=\"slotProps\" />\n </TooltipRoot>\n</template>\n",
|
|
29
|
+
"type": "registry:ui",
|
|
30
|
+
"target": "components/ui/tooltip/Tooltip.vue"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"path": "src/components/ui/tooltip/TooltipTrigger.vue",
|
|
34
|
+
"content": "<script setup lang=\"ts\">\nimport type { TooltipTriggerProps } from \"reka-ui\"\nimport { TooltipTrigger } from \"reka-ui\"\n\nconst props = defineProps<TooltipTriggerProps>()\n</script>\n\n<template>\n <TooltipTrigger\n data-slot=\"tooltip-trigger\"\n v-bind=\"props\"\n >\n <slot />\n </TooltipTrigger>\n</template>\n",
|
|
35
|
+
"type": "registry:ui",
|
|
36
|
+
"target": "components/ui/tooltip/TooltipTrigger.vue"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"path": "src/components/ui/tooltip/TooltipContent.vue",
|
|
40
|
+
"content": "<script setup lang=\"ts\">\nimport type { TooltipContentEmits, TooltipContentProps } from \"reka-ui\"\nimport type { HTMLAttributes } from \"vue\"\nimport { reactiveOmit } from \"@vueuse/core\"\nimport { TooltipArrow, TooltipContent, TooltipPortal, useForwardPropsEmits } from \"reka-ui\"\nimport { cn } from \"@/lib/utils\"\n\ndefineOptions({\n inheritAttrs: false,\n})\n\nconst props = withDefaults(\n defineProps<TooltipContentProps & {\n arrow?: boolean\n class?: HTMLAttributes[\"class\"]\n }>(),\n {\n arrow: true,\n sideOffset: 4,\n },\n)\n\nconst emits = defineEmits<TooltipContentEmits>()\n\nconst delegatedProps = reactiveOmit(props, \"arrow\", \"class\")\nconst forwarded = useForwardPropsEmits(delegatedProps, emits)\n</script>\n\n<template>\n <TooltipPortal>\n <TooltipContent\n data-slot=\"tooltip-content\"\n v-bind=\"{ ...forwarded, ...$attrs }\"\n :class=\"cn('bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance', props.class)\"\n >\n <slot />\n\n <TooltipArrow\n v-if=\"props.arrow\"\n class=\"bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]\"\n />\n </TooltipContent>\n </TooltipPortal>\n</template>\n",
|
|
41
|
+
"type": "registry:ui",
|
|
42
|
+
"target": "components/ui/tooltip/TooltipContent.vue"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"path": "src/components/ui/tooltip/TooltipProvider.vue",
|
|
46
|
+
"content": "<script setup lang=\"ts\">\nimport type { TooltipProviderProps } from \"reka-ui\"\nimport { TooltipProvider } from \"reka-ui\"\n\nconst props = withDefaults(defineProps<TooltipProviderProps>(), {\n delayDuration: 0,\n})\n</script>\n\n<template>\n <TooltipProvider v-bind=\"props\">\n <slot />\n </TooltipProvider>\n</template>\n",
|
|
47
|
+
"type": "registry:ui",
|
|
48
|
+
"target": "components/ui/tooltip/TooltipProvider.vue"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"path": "src/components/ui/tooltip/index.ts",
|
|
52
|
+
"content": "export { default as Tooltip } from \"./Tooltip.vue\"\nexport { default as TooltipContent } from \"./TooltipContent.vue\"\nexport { default as TooltipProvider } from \"./TooltipProvider.vue\"\nexport { default as TooltipTrigger } from \"./TooltipTrigger.vue\"\n",
|
|
53
|
+
"type": "registry:ui",
|
|
54
|
+
"target": "components/ui/tooltip/index.ts"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"path": "src/lib/utils.ts",
|
|
58
|
+
"content": "import { type ClassValue, clsx } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n",
|
|
59
|
+
"type": "registry:lib",
|
|
60
|
+
"target": "lib/utils.ts"
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"type": "registry:ui"
|
|
64
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://shadcn-vue.com/schema/registry-item.json",
|
|
3
|
+
"name": "input",
|
|
4
|
+
"title": "Input",
|
|
5
|
+
"description": "An input control with plain, field, field group, disabled, file, and icon states.",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "src/components/ui/input/Input.vue",
|
|
13
|
+
"content": "<script setup lang=\"ts\">\nimport { computed, useAttrs, useId } from \"vue\"\nimport { cn } from \"@/lib/utils\"\nimport InputControl from \"./InputControl.vue\"\nimport type { InputModelValue, InputProps } from \"./types\"\n\ndefineOptions({\n inheritAttrs: false,\n})\n\nconst props = withDefaults(defineProps<InputProps>(), {\n iconPosition: \"start\",\n type: \"text\",\n})\n\nconst emit = defineEmits<{\n \"update:modelValue\": [value: InputModelValue]\n input: [event: Event]\n change: [event: Event]\n}>()\n\nconst attrs = useAttrs()\nconst generatedId = useId()\n\nconst controlId = computed(() => props.id ?? generatedId)\nconst hasMessage = computed(() => Boolean(props.message || props.error))\nconst hasError = computed(() => Boolean(props.error))\nconst shouldRenderField = computed(() =>\n Boolean(\n props.field\n || props.fieldGroup\n || props.label\n || props.description\n || hasMessage.value,\n ),\n)\nconst descriptionId = computed(() =>\n props.description ? `${controlId.value}-description` : undefined,\n)\nconst messageId = computed(() =>\n hasMessage.value ? `${controlId.value}-message` : undefined,\n)\nconst describedBy = computed(() => {\n const value = [\n attrs[\"aria-describedby\"],\n descriptionId.value,\n messageId.value,\n ].filter(Boolean).join(\" \")\n\n return value || undefined\n})\nconst resolvedMessage = computed(() =>\n typeof props.error === \"string\" ? props.error : props.message,\n)\n\nfunction handleUpdate(value: InputModelValue) {\n emit(\"update:modelValue\", value)\n}\n</script>\n\n<template>\n <InputControl\n v-if=\"!shouldRenderField\"\n v-bind=\"attrs\"\n :id=\"controlId\"\n :class=\"props.class\"\n :default-value=\"props.defaultValue\"\n :disabled=\"props.disabled\"\n :icon=\"props.icon\"\n :icon-class=\"props.iconClass\"\n :icon-position=\"props.iconPosition\"\n :input-class=\"props.inputClass\"\n :invalid=\"props.invalid || hasError\"\n :model-value=\"props.modelValue\"\n :placeholder=\"props.placeholder\"\n :readonly=\"props.readonly\"\n :type=\"props.type\"\n @change=\"emit('change', $event)\"\n @input=\"emit('input', $event)\"\n @update:model-value=\"handleUpdate\"\n />\n\n <div\n v-else\n :data-slot=\"props.fieldGroup ? 'input-field-group' : 'input-field'\"\n :class=\"\n cn(\n props.fieldGroup ? 'grid gap-4' : 'grid gap-2',\n props.groupClass,\n )\n \"\n >\n <div\n data-slot=\"input-field\"\n :data-disabled=\"props.disabled ? '' : undefined\"\n :data-invalid=\"hasError ? '' : undefined\"\n :class=\"cn('grid gap-2', props.fieldClass)\"\n >\n <label\n v-if=\"props.label\"\n data-slot=\"input-label\"\n :for=\"controlId\"\n :class=\"\n cn(\n 'text-sm font-medium leading-none text-foreground peer-disabled:cursor-not-allowed peer-disabled:opacity-70 data-[disabled=true]:opacity-50',\n props.labelClass,\n )\n \"\n >\n {{ props.label }}\n </label>\n\n <InputControl\n v-bind=\"attrs\"\n :id=\"controlId\"\n :aria-describedby=\"describedBy\"\n :class=\"props.class\"\n :default-value=\"props.defaultValue\"\n :disabled=\"props.disabled\"\n :icon=\"props.icon\"\n :icon-class=\"props.iconClass\"\n :icon-position=\"props.iconPosition\"\n :input-class=\"props.inputClass\"\n :invalid=\"props.invalid || hasError\"\n :model-value=\"props.modelValue\"\n :placeholder=\"props.placeholder\"\n :readonly=\"props.readonly\"\n :type=\"props.type\"\n @change=\"emit('change', $event)\"\n @input=\"emit('input', $event)\"\n @update:model-value=\"handleUpdate\"\n />\n\n <p\n v-if=\"props.description\"\n :id=\"descriptionId\"\n data-slot=\"input-description\"\n :class=\"cn('text-xs leading-relaxed text-muted-foreground', props.descriptionClass)\"\n >\n {{ props.description }}\n </p>\n\n <p\n v-if=\"resolvedMessage\"\n :id=\"messageId\"\n data-slot=\"input-message\"\n :class=\"\n cn(\n 'text-xs font-medium leading-relaxed',\n hasError ? 'text-destructive' : 'text-muted-foreground',\n props.messageClass,\n )\n \"\n >\n {{ resolvedMessage }}\n </p>\n </div>\n </div>\n</template>\n",
|
|
14
|
+
"type": "registry:ui",
|
|
15
|
+
"target": "components/ui/input/Input.vue"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"path": "src/components/ui/input/InputControl.vue",
|
|
19
|
+
"content": "<script setup lang=\"ts\">\nimport { computed, useAttrs } from \"vue\"\nimport { cn } from \"@/lib/utils\"\nimport type { InputControlProps, InputModelValue } from \"./types\"\n\ndefineOptions({\n inheritAttrs: false,\n})\n\nconst props = withDefaults(defineProps<InputControlProps>(), {\n iconPosition: \"start\",\n type: \"text\",\n})\n\nconst emit = defineEmits<{\n \"update:modelValue\": [value: InputModelValue]\n input: [event: Event]\n change: [event: Event]\n}>()\n\nconst attrs = useAttrs()\n\nconst isFileInput = computed(() => props.type === \"file\")\nconst hasIcon = computed(() => Boolean(props.icon))\nconst inputValue = computed(() => {\n if (isFileInput.value) return undefined\n\n return props.modelValue ?? props.defaultValue ?? \"\"\n})\nconst valueBinding = computed(() =>\n isFileInput.value ? {} : { value: inputValue.value },\n)\nconst controlAttrs = computed(() => ({\n ...attrs,\n ...valueBinding.value,\n}))\n\nconst inputClasses = computed(() =>\n cn(\n \"border-input bg-background text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none file:mr-3 file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20\",\n isFileInput.value && \"cursor-pointer\",\n hasIcon.value\n && \"border-0 bg-transparent px-0 shadow-none focus-visible:border-transparent focus-visible:ring-0\",\n props.inputClass,\n !hasIcon.value && props.class,\n ),\n)\n\nconst wrapperClasses = computed(() =>\n cn(\n \"border-input bg-background text-foreground focus-within:border-ring focus-within:ring-ring/50 flex h-9 w-full min-w-0 items-center gap-2 rounded-md border px-3 shadow-xs transition-[color,box-shadow] focus-within:ring-[3px] has-disabled:cursor-not-allowed has-disabled:opacity-50 has-aria-invalid:border-destructive has-aria-invalid:ring-destructive/20\",\n props.class,\n ),\n)\n\nfunction getInputModelValue(target: HTMLInputElement) {\n if (isFileInput.value) return target.files\n if (props.type === \"number\") return target.value === \"\" ? \"\" : target.valueAsNumber\n\n return target.value\n}\n\nfunction handleInput(event: Event) {\n const target = event.target as HTMLInputElement\n\n if (!isFileInput.value) {\n emit(\"update:modelValue\", getInputModelValue(target))\n }\n\n emit(\"input\", event)\n}\n\nfunction handleChange(event: Event) {\n const target = event.target as HTMLInputElement\n\n if (isFileInput.value) {\n emit(\"update:modelValue\", getInputModelValue(target))\n }\n\n emit(\"change\", event)\n}\n</script>\n\n<template>\n <div\n v-if=\"hasIcon\"\n data-slot=\"input-wrapper\"\n :class=\"wrapperClasses\"\n >\n <component\n :is=\"props.icon\"\n v-if=\"props.iconPosition === 'start'\"\n aria-hidden=\"true\"\n data-slot=\"input-icon\"\n :class=\"cn('size-4 shrink-0 text-muted-foreground', props.iconClass)\"\n />\n\n <input\n v-bind=\"controlAttrs\"\n :id=\"props.id\"\n data-slot=\"input\"\n :aria-invalid=\"props.invalid ? true : undefined\"\n :class=\"inputClasses\"\n :disabled=\"props.disabled\"\n :placeholder=\"props.placeholder\"\n :readonly=\"props.readonly\"\n :type=\"props.type\"\n @change=\"handleChange\"\n @input=\"handleInput\"\n >\n\n <component\n :is=\"props.icon\"\n v-if=\"props.iconPosition === 'end'\"\n aria-hidden=\"true\"\n data-slot=\"input-icon\"\n :class=\"cn('size-4 shrink-0 text-muted-foreground', props.iconClass)\"\n />\n </div>\n\n <input\n v-else\n v-bind=\"controlAttrs\"\n :id=\"props.id\"\n data-slot=\"input\"\n :aria-invalid=\"props.invalid ? true : undefined\"\n :class=\"inputClasses\"\n :disabled=\"props.disabled\"\n :placeholder=\"props.placeholder\"\n :readonly=\"props.readonly\"\n :type=\"props.type\"\n @change=\"handleChange\"\n @input=\"handleInput\"\n >\n</template>\n",
|
|
20
|
+
"type": "registry:ui",
|
|
21
|
+
"target": "components/ui/input/InputControl.vue"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"path": "src/components/ui/input/InputFieldGroup.vue",
|
|
25
|
+
"content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from \"vue\"\nimport { cn } from \"@/lib/utils\"\n\nconst props = defineProps<{\n class?: HTMLAttributes[\"class\"]\n}>()\n</script>\n\n<template>\n <div data-slot=\"input-field-group\" :class=\"cn('grid gap-4', props.class)\">\n <slot />\n </div>\n</template>\n",
|
|
26
|
+
"type": "registry:ui",
|
|
27
|
+
"target": "components/ui/input/InputFieldGroup.vue"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"path": "src/components/ui/input/types.ts",
|
|
31
|
+
"content": "import type { Component, HTMLAttributes, InputHTMLAttributes } from \"vue\"\n\nexport type InputModelValue = string | number | FileList | null\nexport type InputIconPosition = \"start\" | \"end\"\n\nexport interface InputControlProps {\n class?: HTMLAttributes[\"class\"]\n defaultValue?: string | number\n disabled?: boolean\n icon?: Component\n iconClass?: HTMLAttributes[\"class\"]\n iconPosition?: InputIconPosition\n id?: string\n inputClass?: HTMLAttributes[\"class\"]\n invalid?: boolean\n modelValue?: InputModelValue\n placeholder?: string\n readonly?: boolean\n type?: InputHTMLAttributes[\"type\"]\n}\n\nexport interface InputProps extends InputControlProps {\n description?: string\n descriptionClass?: HTMLAttributes[\"class\"]\n error?: string | boolean\n field?: boolean\n fieldClass?: HTMLAttributes[\"class\"]\n fieldGroup?: boolean\n groupClass?: HTMLAttributes[\"class\"]\n label?: string\n labelClass?: HTMLAttributes[\"class\"]\n message?: string\n messageClass?: HTMLAttributes[\"class\"]\n}\n",
|
|
32
|
+
"type": "registry:ui",
|
|
33
|
+
"target": "components/ui/input/types.ts"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"path": "src/components/ui/input/index.ts",
|
|
37
|
+
"content": "export { default as Input } from \"./Input.vue\"\nexport { default as InputControl } from \"./InputControl.vue\"\nexport { default as InputFieldGroup } from \"./InputFieldGroup.vue\"\nexport type {\n InputControlProps,\n InputIconPosition,\n InputModelValue,\n InputProps,\n} from \"./types\"\n",
|
|
38
|
+
"type": "registry:ui",
|
|
39
|
+
"target": "components/ui/input/index.ts"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"path": "src/lib/utils.ts",
|
|
43
|
+
"content": "import { type ClassValue, clsx } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n",
|
|
44
|
+
"type": "registry:lib",
|
|
45
|
+
"target": "lib/utils.ts"
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"type": "registry:ui"
|
|
49
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://shadcn-vue.com/schema/registry-item.json",
|
|
3
|
+
"name": "kpi-card",
|
|
4
|
+
"title": "KPI Card",
|
|
5
|
+
"description": "A compact metric card with title, value, description, trend, loading state, and icon slots.",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"@lucide/vue",
|
|
8
|
+
"clsx",
|
|
9
|
+
"tailwind-merge"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "src/components/ui/kpi-card/KpiCard.vue",
|
|
14
|
+
"content": "<script setup lang=\"ts\">\nimport { computed, useSlots } from \"vue\"\nimport type { Component, HTMLAttributes } from \"vue\"\nimport { ArrowDownRightIcon, ArrowUpRightIcon, MinusIcon } from \"@lucide/vue\"\nimport { cn } from \"@/lib/utils\"\n\nexport type KpiCardSize = \"sm\" | \"default\" | \"lg\"\nexport type KpiCardTone = \"default\" | \"success\" | \"warning\" | \"danger\" | \"info\"\nexport type KpiCardTrend = \"up\" | \"down\" | \"neutral\"\nexport type KpiCardValue = number | string | null | undefined\nexport type KpiCardValueFormatter = (value: number | string) => number | string\n\nexport interface KpiCardProps {\n class?: HTMLAttributes[\"class\"]\n description?: string\n emptyLabel?: string\n loading?: boolean\n size?: KpiCardSize\n summary?: string\n title?: string\n tone?: KpiCardTone\n trend?: KpiCardTrend\n trendLabel?: string\n trendValue?: number | string | null\n value?: KpiCardValue\n valueFormatter?: KpiCardValueFormatter\n}\n\nconst props = withDefaults(defineProps<KpiCardProps>(), {\n emptyLabel: \"No data\",\n size: \"default\",\n tone: \"default\",\n})\n\nconst slots = useSlots()\n\nconst numberFormatter = computed(() =>\n new Intl.NumberFormat(undefined, {\n maximumFractionDigits: 2,\n }),\n)\n\nconst sizeClasses = computed(() => {\n if (props.size === \"sm\") {\n return {\n description: \"text-xs\",\n icon: \"size-7\",\n root: \"gap-2.5 p-3\",\n title: \"text-xs\",\n trend: \"text-[11px]\",\n value: \"text-xl\",\n }\n }\n\n if (props.size === \"lg\") {\n return {\n description: \"text-sm\",\n icon: \"size-10\",\n root: \"gap-4 p-5\",\n title: \"text-sm\",\n trend: \"text-xs\",\n value: \"text-3xl\",\n }\n }\n\n return {\n description: \"text-sm\",\n icon: \"size-8\",\n root: \"gap-3 p-4\",\n title: \"text-sm\",\n trend: \"text-xs\",\n value: \"text-2xl\",\n }\n})\n\nconst rootClass = computed(() =>\n cn(\n \"relative flex min-w-0 flex-col rounded-lg border bg-card text-card-foreground shadow-xs transition-colors\",\n sizeClasses.value.root,\n props.class,\n ),\n)\n\nconst trendDirection = computed<KpiCardTrend>(() => props.trend ?? \"neutral\")\n\nconst trendIcon = computed<Component>(() => {\n if (trendDirection.value === \"up\") return ArrowUpRightIcon\n if (trendDirection.value === \"down\") return ArrowDownRightIcon\n\n return MinusIcon\n})\n\nconst hasTrendValue = computed(() => hasDisplayValue(props.trendValue))\n\nconst hasTrend = computed(() =>\n Boolean(props.trend || props.trendLabel || hasTrendValue.value),\n)\n\nconst hasFooter = computed(() =>\n Boolean(slots.footer || props.summary || hasTrend.value),\n)\n\nconst formattedValue = computed(() => {\n const value = props.value\n\n if (!hasDisplayValue(value)) return props.emptyLabel\n if (props.valueFormatter) return props.valueFormatter(value)\n if (typeof value === \"number\") return numberFormatter.value.format(value)\n\n return value\n})\n\nconst formattedTrendValue = computed(() => {\n const value = props.trendValue\n\n if (!hasDisplayValue(value)) return \"\"\n if (typeof value === \"number\") return numberFormatter.value.format(value)\n\n return value\n})\n\nconst trendAccessibleLabel = computed(() =>\n [formattedTrendValue.value, props.trendLabel]\n .filter((part) => Boolean(part))\n .join(\" \"),\n)\n\nconst effectiveTrendTone = computed<KpiCardTone>(() => {\n if (props.tone !== \"default\") return props.tone\n if (trendDirection.value === \"up\") return \"success\"\n if (trendDirection.value === \"down\") return \"danger\"\n\n return \"default\"\n})\n\nconst iconClass = computed(() =>\n cn(\n \"inline-flex shrink-0 items-center justify-center rounded-md border bg-muted/60\",\n sizeClasses.value.icon,\n toneTextClass(props.tone),\n ),\n)\n\nconst trendClass = computed(() =>\n cn(\n \"inline-flex min-h-6 w-fit items-center gap-1 rounded-md border px-1.5 font-medium leading-none\",\n sizeClasses.value.trend,\n tonePillClass(effectiveTrendTone.value),\n ),\n)\n\nfunction hasDisplayValue(value: KpiCardValue): value is number | string {\n return value !== null && value !== undefined && value !== \"\"\n}\n\nfunction toneTextClass(tone: KpiCardTone) {\n const classes: Record<KpiCardTone, string> = {\n danger: \"text-red-600 dark:text-red-400\",\n default: \"text-muted-foreground\",\n info: \"text-sky-600 dark:text-sky-400\",\n success: \"text-emerald-600 dark:text-emerald-400\",\n warning: \"text-amber-600 dark:text-amber-400\",\n }\n\n return classes[tone]\n}\n\nfunction tonePillClass(tone: KpiCardTone) {\n const classes: Record<KpiCardTone, string> = {\n danger: \"border-red-500/20 bg-red-500/10 text-red-700 dark:text-red-300\",\n default: \"border-border bg-muted/50 text-muted-foreground\",\n info: \"border-sky-500/20 bg-sky-500/10 text-sky-700 dark:text-sky-300\",\n success: \"border-emerald-500/20 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300\",\n warning: \"border-amber-500/25 bg-amber-500/10 text-amber-700 dark:text-amber-300\",\n }\n\n return classes[tone]\n}\n</script>\n\n<template>\n <article\n data-slot=\"kpi-card\"\n :data-size=\"props.size\"\n :data-tone=\"props.tone\"\n :aria-busy=\"props.loading ? 'true' : undefined\"\n :class=\"rootClass\"\n >\n <div\n v-if=\"props.loading\"\n data-slot=\"kpi-card-loading\"\n class=\"flex flex-col gap-3\"\n >\n <div class=\"flex items-start justify-between gap-3\">\n <div class=\"h-3.5 w-24 rounded-sm bg-muted\" />\n <div :class=\"cn('rounded-md border bg-muted/60', sizeClasses.icon)\" />\n </div>\n <div class=\"h-8 w-32 rounded-sm bg-muted\" />\n <div class=\"h-3 w-40 rounded-sm bg-muted\" />\n </div>\n\n <template v-else>\n <div class=\"flex min-w-0 items-start justify-between gap-3\">\n <div class=\"min-w-0\">\n <p\n v-if=\"$slots.title || props.title\"\n data-slot=\"kpi-card-title\"\n :class=\"cn('truncate font-medium text-muted-foreground', sizeClasses.title)\"\n >\n <slot name=\"title\">{{ props.title }}</slot>\n </p>\n <div\n data-slot=\"kpi-card-value\"\n :class=\"cn('mt-2 truncate font-semibold leading-none tabular-nums', sizeClasses.value)\"\n >\n <slot name=\"value\">{{ formattedValue }}</slot>\n </div>\n </div>\n\n <div\n v-if=\"$slots.icon\"\n data-slot=\"kpi-card-icon\"\n :class=\"iconClass\"\n >\n <slot name=\"icon\" />\n </div>\n </div>\n\n <p\n v-if=\"$slots.description || props.description\"\n data-slot=\"kpi-card-description\"\n :class=\"cn('leading-relaxed text-muted-foreground', sizeClasses.description)\"\n >\n <slot name=\"description\">{{ props.description }}</slot>\n </p>\n\n <div\n v-if=\"hasFooter\"\n data-slot=\"kpi-card-footer\"\n class=\"mt-auto flex min-w-0 flex-wrap items-center justify-between gap-2\"\n >\n <slot name=\"footer\">\n <div\n v-if=\"hasTrend\"\n data-slot=\"kpi-card-trend\"\n :aria-label=\"trendAccessibleLabel || undefined\"\n :class=\"trendClass\"\n >\n <component\n :is=\"trendIcon\"\n class=\"size-3.5 shrink-0\"\n aria-hidden=\"true\"\n />\n <span v-if=\"hasTrendValue\">{{ formattedTrendValue }}</span>\n <span v-if=\"props.trendLabel\" class=\"text-current/75\">{{ props.trendLabel }}</span>\n </div>\n <p\n v-if=\"props.summary\"\n data-slot=\"kpi-card-summary\"\n class=\"min-w-0 truncate text-xs text-muted-foreground\"\n >\n {{ props.summary }}\n </p>\n </slot>\n </div>\n </template>\n </article>\n</template>\n",
|
|
15
|
+
"type": "registry:ui",
|
|
16
|
+
"target": "components/ui/kpi-card/KpiCard.vue"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "src/components/ui/kpi-card/index.ts",
|
|
20
|
+
"content": "export { default as KpiCard } from \"./KpiCard.vue\"\nexport type {\n KpiCardProps,\n KpiCardSize,\n KpiCardTone,\n KpiCardTrend,\n KpiCardValue,\n KpiCardValueFormatter,\n} from \"./KpiCard.vue\"\n",
|
|
21
|
+
"type": "registry:ui",
|
|
22
|
+
"target": "components/ui/kpi-card/index.ts"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"path": "src/lib/utils.ts",
|
|
26
|
+
"content": "import { type ClassValue, clsx } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n",
|
|
27
|
+
"type": "registry:lib",
|
|
28
|
+
"target": "lib/utils.ts"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"type": "registry:ui"
|
|
32
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://shadcn-vue.com/schema/registry-item.json",
|
|
3
|
+
"name": "kpi-line-card",
|
|
4
|
+
"title": "KPI Line Card",
|
|
5
|
+
"description": "A compact metric card with a configurable sparkline line chart position, trend state, loading state, and icon slots.",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"@lucide/vue",
|
|
8
|
+
"clsx",
|
|
9
|
+
"tailwind-merge"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "src/components/ui/kpi-line-card/KpiLineCard.vue",
|
|
14
|
+
"content": "<script setup lang=\"ts\">\nimport { computed, useSlots } from \"vue\"\nimport type { Component, HTMLAttributes } from \"vue\"\nimport { ArrowDownRightIcon, ArrowUpRightIcon, MinusIcon } from \"@lucide/vue\"\nimport { cn } from \"@/lib/utils\"\n\nexport type KpiLineCardSize = \"sm\" | \"default\" | \"lg\"\nexport type KpiLineCardChartPosition = \"bottom\" | \"right\"\nexport type KpiLineCardTone = \"default\" | \"success\" | \"warning\" | \"danger\" | \"info\"\nexport type KpiLineCardTrend = \"up\" | \"down\" | \"neutral\"\nexport type KpiLineCardValue = number | string | null | undefined\nexport type KpiLineCardValueFormatter = (value: number | string) => number | string\n\nexport interface KpiLineCardDataPoint extends Record<string, unknown> {\n id?: number | string\n label?: number | string\n value?: KpiLineCardValue\n}\n\nexport interface KpiLineCardProps {\n chartLabel?: string\n chartPosition?: KpiLineCardChartPosition\n class?: HTMLAttributes[\"class\"]\n data?: KpiLineCardDataPoint[]\n description?: string\n emptyLabel?: string\n loading?: boolean\n showArea?: boolean\n showPoints?: boolean\n size?: KpiLineCardSize\n summary?: string\n title?: string\n tone?: KpiLineCardTone\n trend?: KpiLineCardTrend\n trendLabel?: string\n trendValue?: number | string | null\n value?: KpiLineCardValue\n valueFormatter?: KpiLineCardValueFormatter\n xKey?: string\n yKey?: string\n}\n\ninterface NormalizedPoint {\n id: string\n label: string\n value: number\n x: number\n y: number\n}\n\nconst props = withDefaults(defineProps<KpiLineCardProps>(), {\n chartPosition: \"bottom\",\n data: () => [],\n emptyLabel: \"No data\",\n showArea: true,\n showPoints: false,\n size: \"default\",\n tone: \"default\",\n xKey: \"label\",\n yKey: \"value\",\n})\n\nconst slots = useSlots()\n\nconst chartWidth = 240\nconst chartHeight = 72\nconst chartPadding = {\n bottom: 8,\n left: 6,\n right: 6,\n top: 8,\n}\n\nconst isChartRight = computed(() => props.chartPosition === \"right\")\n\nconst numberFormatter = computed(() =>\n new Intl.NumberFormat(undefined, {\n maximumFractionDigits: 2,\n }),\n)\n\nconst sizeClasses = computed(() => {\n if (props.size === \"sm\") {\n return {\n chart: \"h-12\",\n chartBottom: \"h-14\",\n description: \"text-xs\",\n gap: \"gap-2.5\",\n icon: \"size-7\",\n root: \"min-h-40 p-3\",\n title: \"text-xs\",\n trend: \"text-[11px]\",\n value: \"text-xl\",\n }\n }\n\n if (props.size === \"lg\") {\n return {\n chart: \"h-16\",\n chartBottom: \"h-20\",\n description: \"text-sm\",\n gap: \"gap-4\",\n icon: \"size-10\",\n root: \"min-h-56 p-5\",\n title: \"text-sm\",\n trend: \"text-xs\",\n value: \"text-3xl\",\n }\n }\n\n return {\n chart: \"h-14\",\n chartBottom: \"h-16\",\n description: \"text-sm\",\n gap: \"gap-3\",\n icon: \"size-8\",\n root: \"min-h-48 p-4\",\n title: \"text-sm\",\n trend: \"text-xs\",\n value: \"text-2xl\",\n }\n})\n\nconst rootClass = computed(() =>\n cn(\n \"relative flex min-w-0 flex-col rounded-lg border bg-card text-card-foreground shadow-xs transition-colors\",\n sizeClasses.value.root,\n props.class,\n ),\n)\n\nconst contentClass = computed(() =>\n cn(\n \"flex min-w-0 flex-1 flex-col\",\n sizeClasses.value.gap,\n isChartRight.value && \"sm:flex-row sm:items-stretch\",\n ),\n)\n\nconst mainClass = computed(() =>\n cn(\n \"flex min-w-0 flex-col\",\n sizeClasses.value.gap,\n isChartRight.value && \"sm:min-w-0 sm:flex-1 sm:justify-between\",\n ),\n)\n\nconst chartSlotClass = computed(() =>\n cn(\n \"min-w-0\",\n isChartRight.value\n ? [sizeClasses.value.chart, \"sm:h-auto sm:w-2/5 sm:min-w-32 sm:self-stretch\"]\n : [sizeClasses.value.chartBottom, \"mt-auto\"],\n ),\n)\n\nconst chartClass = computed(() =>\n cn(\n \"block h-full w-full overflow-visible\",\n toneTextClass(effectiveTrendTone.value),\n ),\n)\n\nconst hasHeader = computed(() =>\n Boolean(slots.icon || slots.title || props.title),\n)\n\nconst rawPoints = computed(() =>\n props.data\n .map((point, index) => {\n const rawValue = getDataValue(point, props.yKey)\n const value = toFiniteNumber(rawValue)\n\n if (value === null) return null\n\n const rawLabel = getDataValue(point, props.xKey)\n const label = rawLabel === undefined || rawLabel === null || rawLabel === \"\"\n ? `Item ${index + 1}`\n : String(rawLabel)\n const id = point.id === undefined || point.id === null\n ? `${index}-${label}`\n : String(point.id)\n\n return {\n id,\n label,\n value,\n }\n })\n .filter((point): point is Omit<NormalizedPoint, \"x\" | \"y\"> => point !== null),\n)\n\nconst domain = computed(() => {\n const values = rawPoints.value.map((point) => point.value)\n\n if (!values.length) {\n return {\n max: 1,\n min: 0,\n }\n }\n\n let min = Math.min(...values)\n let max = Math.max(...values)\n\n if (min === max) {\n const delta = min === 0 ? 1 : Math.abs(min * 0.1)\n min -= delta\n max += delta\n }\n\n return {\n max,\n min,\n }\n})\n\nconst chartPoints = computed<NormalizedPoint[]>(() => {\n const points = rawPoints.value\n const plotWidth = chartWidth - chartPadding.left - chartPadding.right\n const plotHeight = chartHeight - chartPadding.top - chartPadding.bottom\n const denominator = Math.max(1, points.length - 1)\n\n return points.map((point, index) => {\n const valueRange = domain.value.max - domain.value.min || 1\n const x = chartPadding.left + ((index / denominator) * plotWidth)\n const y = chartPadding.top + (((domain.value.max - point.value) / valueRange) * plotHeight)\n\n return {\n ...point,\n x,\n y,\n }\n })\n})\n\nconst hasChart = computed(() => chartPoints.value.length > 1)\n\nconst linePath = computed(() => buildSmoothLinePath(chartPoints.value))\n\nconst areaPath = computed(() => {\n const points = chartPoints.value\n\n if (!props.showArea || !linePath.value || points.length < 2) return \"\"\n\n const first = points[0]\n const last = points[points.length - 1]\n const baseline = chartHeight - chartPadding.bottom\n\n if (!first || !last) return \"\"\n\n return `${linePath.value} L ${roundCoordinate(last.x)} ${baseline} L ${roundCoordinate(first.x)} ${baseline} Z`\n})\n\nconst inferredTrend = computed<KpiLineCardTrend>(() => {\n const points = rawPoints.value\n const first = points[0]\n const last = points[points.length - 1]\n\n if (!first || !last) return \"neutral\"\n if (last.value > first.value) return \"up\"\n if (last.value < first.value) return \"down\"\n\n return \"neutral\"\n})\n\nconst trendDirection = computed<KpiLineCardTrend>(() => props.trend ?? inferredTrend.value)\n\nconst trendIcon = computed<Component>(() => {\n if (trendDirection.value === \"up\") return ArrowUpRightIcon\n if (trendDirection.value === \"down\") return ArrowDownRightIcon\n\n return MinusIcon\n})\n\nconst hasTrendValue = computed(() => hasDisplayValue(props.trendValue))\n\nconst hasTrend = computed(() =>\n Boolean(props.trend || props.trendLabel || hasTrendValue.value),\n)\n\nconst hasFooter = computed(() =>\n Boolean(slots.footer || props.summary || hasTrend.value),\n)\n\nconst formattedValue = computed(() => {\n const value = props.value\n\n if (!hasDisplayValue(value)) return props.emptyLabel\n if (props.valueFormatter) return props.valueFormatter(value)\n if (typeof value === \"number\") return numberFormatter.value.format(value)\n\n return value\n})\n\nconst formattedTrendValue = computed(() => {\n const value = props.trendValue\n\n if (!hasDisplayValue(value)) return \"\"\n if (typeof value === \"number\") return numberFormatter.value.format(value)\n\n return value\n})\n\nconst trendAccessibleLabel = computed(() =>\n [formattedTrendValue.value, props.trendLabel]\n .filter((part) => Boolean(part))\n .join(\" \"),\n)\n\nconst chartAccessibleLabel = computed(() => {\n if (props.chartLabel) return props.chartLabel\n\n const title = props.title ?? \"KPI\"\n const first = rawPoints.value[0]\n const last = rawPoints.value[rawPoints.value.length - 1]\n\n if (!first || !last) return `${title} trend`\n\n return `${title} trend from ${formatNumber(first.value)} to ${formatNumber(last.value)}`\n})\n\nconst effectiveTrendTone = computed<KpiLineCardTone>(() => {\n if (props.tone !== \"default\") return props.tone\n if (trendDirection.value === \"up\") return \"success\"\n if (trendDirection.value === \"down\") return \"danger\"\n\n return \"default\"\n})\n\nconst iconClass = computed(() =>\n cn(\n \"inline-flex shrink-0 items-center justify-center rounded-md border bg-muted/60\",\n sizeClasses.value.icon,\n toneTextClass(props.tone),\n ),\n)\n\nconst trendClass = computed(() =>\n cn(\n \"inline-flex min-h-6 w-fit max-w-full items-center gap-1 rounded-md border px-1.5 font-medium leading-none tabular-nums\",\n sizeClasses.value.trend,\n tonePillClass(effectiveTrendTone.value),\n ),\n)\n\nfunction getDataValue(data: KpiLineCardDataPoint, key: string) {\n return (data as Record<string, unknown>)[key]\n}\n\nfunction toFiniteNumber(value: unknown) {\n if (typeof value === \"number\") {\n return Number.isFinite(value) ? value : null\n }\n\n if (typeof value === \"string\" && value.trim().length > 0) {\n const parsed = Number(value)\n\n return Number.isFinite(parsed) ? parsed : null\n }\n\n return null\n}\n\nfunction hasDisplayValue(value: KpiLineCardValue): value is number | string {\n return value !== null && value !== undefined && value !== \"\"\n}\n\nfunction buildSmoothLinePath(points: NormalizedPoint[]) {\n if (points.length < 2) return \"\"\n if (points.length < 3) return buildLinearLinePath(points)\n\n const commands = [`M ${roundCoordinate(points[0].x)} ${roundCoordinate(points[0].y)}`]\n\n for (let index = 0; index < points.length - 1; index += 1) {\n const previous = points[index - 1] ?? points[index]\n const current = points[index]\n const next = points[index + 1]\n const following = points[index + 2] ?? next\n\n if (!previous || !current || !next || !following) continue\n\n const controlPoint1X = current.x + ((next.x - previous.x) / 6)\n const controlPoint1Y = current.y + ((next.y - previous.y) / 6)\n const controlPoint2X = next.x - ((following.x - current.x) / 6)\n const controlPoint2Y = next.y - ((following.y - current.y) / 6)\n\n commands.push([\n \"C\",\n roundCoordinate(controlPoint1X),\n roundCoordinate(controlPoint1Y),\n roundCoordinate(controlPoint2X),\n roundCoordinate(controlPoint2Y),\n roundCoordinate(next.x),\n roundCoordinate(next.y),\n ].join(\" \"))\n }\n\n return commands.join(\" \")\n}\n\nfunction buildLinearLinePath(points: NormalizedPoint[]) {\n return points\n .map((point, index) => `${index === 0 ? \"M\" : \"L\"} ${roundCoordinate(point.x)} ${roundCoordinate(point.y)}`)\n .join(\" \")\n}\n\nfunction roundCoordinate(value: number) {\n return Math.round(value * 100) / 100\n}\n\nfunction formatNumber(value: number) {\n return numberFormatter.value.format(value)\n}\n\nfunction toneTextClass(tone: KpiLineCardTone) {\n const classes: Record<KpiLineCardTone, string> = {\n danger: \"text-red-600 dark:text-red-400\",\n default: \"text-muted-foreground\",\n info: \"text-sky-600 dark:text-sky-400\",\n success: \"text-emerald-600 dark:text-emerald-400\",\n warning: \"text-amber-600 dark:text-amber-400\",\n }\n\n return classes[tone]\n}\n\nfunction tonePillClass(tone: KpiLineCardTone) {\n const classes: Record<KpiLineCardTone, string> = {\n danger: \"border-red-500/20 bg-red-500/10 text-red-700 dark:text-red-300\",\n default: \"border-border bg-muted/50 text-muted-foreground\",\n info: \"border-sky-500/20 bg-sky-500/10 text-sky-700 dark:text-sky-300\",\n success: \"border-emerald-500/20 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300\",\n warning: \"border-amber-500/25 bg-amber-500/10 text-amber-700 dark:text-amber-300\",\n }\n\n return classes[tone]\n}\n</script>\n\n<template>\n <article\n data-slot=\"kpi-line-card\"\n :data-size=\"props.size\"\n :data-tone=\"props.tone\"\n :data-chart-position=\"props.chartPosition\"\n :aria-busy=\"props.loading ? 'true' : undefined\"\n :class=\"rootClass\"\n >\n <div\n v-if=\"props.loading\"\n data-slot=\"kpi-line-card-loading\"\n :class=\"contentClass\"\n >\n <div :class=\"mainClass\">\n <div class=\"flex min-w-0 items-start justify-between gap-3\">\n <div class=\"min-w-0 flex-1\">\n <div class=\"h-3.5 w-24 rounded-sm bg-muted\" />\n <div class=\"mt-2 h-8 w-32 rounded-sm bg-muted\" />\n </div>\n <div :class=\"cn('rounded-md border bg-muted/60', sizeClasses.icon)\" />\n </div>\n <div class=\"h-3 w-40 max-w-full rounded-sm bg-muted\" />\n <div class=\"h-6 w-36 max-w-full rounded-md bg-muted\" />\n </div>\n <div :class=\"cn('rounded-md bg-muted', chartSlotClass)\" />\n </div>\n\n <template v-else>\n <div\n data-slot=\"kpi-line-card-content\"\n :class=\"contentClass\"\n >\n <div\n data-slot=\"kpi-line-card-main\"\n :class=\"mainClass\"\n >\n <div\n v-if=\"hasHeader\"\n data-slot=\"kpi-line-card-header\"\n class=\"flex min-w-0 items-start justify-between gap-3\"\n >\n <div class=\"min-w-0\">\n <p\n v-if=\"$slots.title || props.title\"\n data-slot=\"kpi-line-card-title\"\n :class=\"cn('truncate font-medium text-muted-foreground', sizeClasses.title)\"\n >\n <slot name=\"title\">{{ props.title }}</slot>\n </p>\n <div\n data-slot=\"kpi-line-card-value\"\n :class=\"cn(($slots.title || props.title) ? 'mt-2' : '', 'truncate font-semibold leading-none tabular-nums', sizeClasses.value)\"\n >\n <slot name=\"value\">{{ formattedValue }}</slot>\n </div>\n </div>\n\n <div\n v-if=\"$slots.icon\"\n data-slot=\"kpi-line-card-icon\"\n :class=\"iconClass\"\n >\n <slot name=\"icon\" />\n </div>\n </div>\n\n <div\n v-else\n data-slot=\"kpi-line-card-value\"\n :class=\"cn('truncate font-semibold leading-none tabular-nums', sizeClasses.value)\"\n >\n <slot name=\"value\">{{ formattedValue }}</slot>\n </div>\n\n <p\n v-if=\"$slots.description || props.description\"\n data-slot=\"kpi-line-card-description\"\n :class=\"cn('leading-relaxed text-muted-foreground', sizeClasses.description)\"\n >\n <slot name=\"description\">{{ props.description }}</slot>\n </p>\n\n <div\n v-if=\"hasFooter\"\n data-slot=\"kpi-line-card-footer\"\n class=\"flex min-w-0 flex-wrap items-center gap-2\"\n >\n <slot name=\"footer\">\n <div\n v-if=\"hasTrend\"\n data-slot=\"kpi-line-card-trend\"\n :aria-label=\"trendAccessibleLabel || undefined\"\n :class=\"trendClass\"\n >\n <component\n :is=\"trendIcon\"\n class=\"size-3.5 shrink-0\"\n aria-hidden=\"true\"\n />\n <span v-if=\"hasTrendValue\">{{ formattedTrendValue }}</span>\n <span v-if=\"props.trendLabel\" class=\"text-current/75\">{{ props.trendLabel }}</span>\n </div>\n <p\n v-if=\"props.summary\"\n data-slot=\"kpi-line-card-summary\"\n class=\"min-w-0 truncate text-xs text-muted-foreground\"\n >\n {{ props.summary }}\n </p>\n </slot>\n </div>\n </div>\n\n <div\n data-slot=\"kpi-line-card-chart\"\n :class=\"chartSlotClass\"\n >\n <svg\n v-if=\"hasChart\"\n role=\"img\"\n :aria-label=\"chartAccessibleLabel\"\n :viewBox=\"`0 0 ${chartWidth} ${chartHeight}`\"\n preserveAspectRatio=\"none\"\n :class=\"chartClass\"\n >\n <line\n x1=\"0\"\n :y1=\"chartPadding.top\"\n :x2=\"chartWidth\"\n :y2=\"chartPadding.top\"\n stroke=\"currentColor\"\n stroke-width=\"1\"\n opacity=\"0.08\"\n />\n <line\n x1=\"0\"\n :y1=\"chartHeight - chartPadding.bottom\"\n :x2=\"chartWidth\"\n :y2=\"chartHeight - chartPadding.bottom\"\n stroke=\"currentColor\"\n stroke-width=\"1\"\n opacity=\"0.08\"\n />\n <path\n v-if=\"areaPath\"\n :d=\"areaPath\"\n fill=\"currentColor\"\n opacity=\"0.1\"\n />\n <path\n :d=\"linePath\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"3.5\"\n vector-effect=\"non-scaling-stroke\"\n />\n <circle\n v-for=\"point in props.showPoints ? chartPoints : []\"\n :key=\"point.id\"\n :cx=\"point.x\"\n :cy=\"point.y\"\n r=\"3\"\n fill=\"currentColor\"\n >\n <title>{{ point.label }}: {{ formatNumber(point.value) }}</title>\n </circle>\n </svg>\n <div\n v-else\n data-slot=\"kpi-line-card-empty-chart\"\n class=\"flex h-full items-center rounded-md bg-muted/40 px-3 text-xs text-muted-foreground\"\n >\n {{ props.emptyLabel }}\n </div>\n </div>\n </div>\n </template>\n </article>\n</template>\n",
|
|
15
|
+
"type": "registry:ui",
|
|
16
|
+
"target": "components/ui/kpi-line-card/KpiLineCard.vue"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "src/components/ui/kpi-line-card/index.ts",
|
|
20
|
+
"content": "export { default as KpiLineCard } from \"./KpiLineCard.vue\"\nexport type {\n KpiLineCardChartPosition,\n KpiLineCardDataPoint,\n KpiLineCardProps,\n KpiLineCardSize,\n KpiLineCardTone,\n KpiLineCardTrend,\n KpiLineCardValue,\n KpiLineCardValueFormatter,\n} from \"./KpiLineCard.vue\"\n",
|
|
21
|
+
"type": "registry:ui",
|
|
22
|
+
"target": "components/ui/kpi-line-card/index.ts"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"path": "src/lib/utils.ts",
|
|
26
|
+
"content": "import { type ClassValue, clsx } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n",
|
|
27
|
+
"type": "registry:lib",
|
|
28
|
+
"target": "lib/utils.ts"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"type": "registry:ui"
|
|
32
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://shadcn-vue.com/schema/registry-item.json",
|
|
3
|
+
"name": "model-selector",
|
|
4
|
+
"title": "Model Selector",
|
|
5
|
+
"description": "A Select-like model picker that switches to a searchable Dialog for large model catalogs.",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"@lucide/vue",
|
|
8
|
+
"@vueuse/core",
|
|
9
|
+
"clsx",
|
|
10
|
+
"reka-ui",
|
|
11
|
+
"tailwind-merge"
|
|
12
|
+
],
|
|
13
|
+
"files": [
|
|
14
|
+
{
|
|
15
|
+
"path": "src/components/ui/model-selector/ModelSelector.vue",
|
|
16
|
+
"content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from \"vue\"\nimport { computed, nextTick, ref, watch } from \"vue\"\nimport { Check, ChevronDown, Search, X } from \"@lucide/vue\"\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n} from \"@/components/ui/select\"\nimport { cn } from \"@/lib/utils\"\nimport Dialog from \"../dialog/Dialog.vue\"\nimport DialogContent from \"../dialog/DialogContent.vue\"\nimport DialogTitle from \"../dialog/DialogTitle.vue\"\nimport DialogTrigger from \"../dialog/DialogTrigger.vue\"\nimport type {\n ModelSelectorChangePayload,\n ModelSelectorMode,\n ModelSelectorOption,\n} from \"./types\"\n\ninterface ModelSelectorProps {\n class?: HTMLAttributes[\"class\"]\n contentClass?: HTMLAttributes[\"class\"]\n dialogClass?: HTMLAttributes[\"class\"]\n disabled?: boolean\n emptyText?: string\n label?: string\n modalThreshold?: number\n modalTitle?: string\n mode?: ModelSelectorMode\n modelValue?: string\n models?: ModelSelectorOption[]\n placeholder?: string\n searchPlaceholder?: string\n size?: \"default\" | \"sm\"\n triggerClass?: HTMLAttributes[\"class\"]\n}\n\nconst props = withDefaults(defineProps<ModelSelectorProps>(), {\n disabled: false,\n emptyText: \"No models found.\",\n label: \"Model\",\n modalThreshold: 8,\n modalTitle: \"Select model\",\n mode: \"auto\",\n modelValue: \"\",\n models: () => [],\n placeholder: \"Select model\",\n searchPlaceholder: \"Search models\",\n size: \"default\",\n})\n\nconst emit = defineEmits<{\n change: [payload: ModelSelectorChangePayload]\n \"update:modelValue\": [value: string]\n}>()\n\nconst dialogOpen = ref(false)\nconst searchQuery = ref(\"\")\nconst searchInputRef = ref<HTMLInputElement | null>(null)\n\nconst selectedValue = computed({\n get: () => props.modelValue,\n set: (value: string) => selectModel(value),\n})\nconst selectedModel = computed(() =>\n props.models.find((model) => model.value === props.modelValue),\n)\nconst shouldUseDialog = computed(() => {\n if (props.mode === \"dialog\") return true\n if (props.mode === \"select\") return false\n\n return props.models.length > props.modalThreshold\n})\nconst normalizedSearchQuery = computed(() => normalizeSearchText(searchQuery.value))\nconst filteredModels = computed(() => {\n if (!normalizedSearchQuery.value) return props.models\n\n return props.models.filter((model) =>\n normalizeSearchText([\n model.label,\n model.value,\n ].filter(Boolean).join(\" \")).includes(normalizedSearchQuery.value),\n )\n})\nconst hasSelection = computed(() => Boolean(selectedModel.value))\n\nwatch(dialogOpen, (open) => {\n if (!open) {\n searchQuery.value = \"\"\n return\n }\n\n void nextTick(() => searchInputRef.value?.focus())\n})\n\nfunction normalizeSearchText(value: string) {\n return value.trim().toLowerCase().replace(/\\s+/g, \" \")\n}\n\nfunction selectModel(value: string) {\n const option = props.models.find((model) => model.value === value)\n\n if (!option || option.disabled) return\n\n emit(\"update:modelValue\", value)\n emit(\"change\", {\n option,\n value,\n })\n}\n\nfunction selectDialogModel(model: ModelSelectorOption) {\n if (model.disabled) return\n\n selectModel(model.value)\n dialogOpen.value = false\n}\n\nfunction clearSearch() {\n searchQuery.value = \"\"\n void nextTick(() => searchInputRef.value?.focus())\n}\n\nfunction closeDialog() {\n dialogOpen.value = false\n}\n</script>\n\n<template>\n <div\n data-slot=\"model-selector\"\n :class=\"cn('inline-flex min-w-0', props.class)\"\n >\n <Select\n v-if=\"!shouldUseDialog\"\n v-model=\"selectedValue\"\n :disabled=\"props.disabled\"\n >\n <SelectTrigger\n :aria-label=\"props.label\"\n :size=\"props.size\"\n :class=\"cn('min-w-0 max-w-full', props.triggerClass)\"\n >\n <span\n data-slot=\"select-value\"\n :class=\"cn(\n 'inline-flex min-w-0 items-center gap-2',\n !hasSelection && 'text-muted-foreground',\n )\"\n >\n <component\n :is=\"selectedModel?.icon\"\n v-if=\"selectedModel?.icon\"\n data-slot=\"model-selector-icon\"\n />\n <span class=\"truncate\">\n {{ selectedModel?.label ?? props.placeholder }}\n </span>\n </span>\n </SelectTrigger>\n <SelectContent\n search\n :empty-text=\"props.emptyText\"\n :search-placeholder=\"props.searchPlaceholder\"\n :class=\"cn('w-72', props.contentClass)\"\n >\n <SelectItem\n v-for=\"model in props.models\"\n :key=\"model.value\"\n :disabled=\"model.disabled\"\n :text-value=\"model.label\"\n :value=\"model.value\"\n >\n <span class=\"inline-flex min-w-0 items-center gap-2\">\n <component\n :is=\"model.icon\"\n v-if=\"model.icon\"\n data-slot=\"model-selector-icon\"\n />\n <span class=\"truncate\">{{ model.label }}</span>\n </span>\n </SelectItem>\n </SelectContent>\n </Select>\n\n <Dialog\n v-else\n v-model:open=\"dialogOpen\"\n >\n <DialogTrigger as-child>\n <button\n data-slot=\"model-selector-trigger\"\n :data-size=\"props.size\"\n :data-placeholder=\"hasSelection ? undefined : ''\"\n type=\"button\"\n :aria-label=\"props.label\"\n :disabled=\"props.disabled\"\n :class=\"cn(\n 'border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*=text-])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit min-w-0 max-w-full items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4',\n props.triggerClass,\n )\"\n >\n <span\n data-slot=\"model-selector-value\"\n class=\"inline-flex min-w-0 items-center gap-2\"\n >\n <component\n :is=\"selectedModel?.icon\"\n v-if=\"selectedModel?.icon\"\n data-slot=\"model-selector-icon\"\n />\n <span class=\"truncate\">\n {{ selectedModel?.label ?? props.placeholder }}\n </span>\n </span>\n <ChevronDown class=\"size-4 opacity-50\" />\n </button>\n </DialogTrigger>\n\n <DialogContent\n :aria-describedby=\"undefined\"\n :show-close-button=\"false\"\n :class=\"\n cn(\n 'left-[50vw] w-[calc(100vw-4rem)] max-w-[calc(100vw-4rem)] gap-5 p-0 sm:max-w-2xl',\n props.dialogClass,\n )\n \"\n >\n <DialogTitle class=\"sr-only\">{{ props.modalTitle }}</DialogTitle>\n\n <div\n data-slot=\"model-selector-dialog-body\"\n class=\"flex min-h-0 flex-col\"\n >\n <div class=\"border-b px-4 py-3\">\n <div class=\"flex items-center gap-2\">\n <div class=\"relative min-w-0 flex-1\">\n <Search\n aria-hidden=\"true\"\n class=\"pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground\"\n />\n <input\n ref=\"searchInputRef\"\n v-model=\"searchQuery\"\n data-slot=\"model-selector-search\"\n type=\"text\"\n role=\"searchbox\"\n autocomplete=\"off\"\n :placeholder=\"props.searchPlaceholder\"\n class=\"h-9 w-full rounded-md border border-input bg-transparent py-1.5 pr-9 pl-9 text-sm shadow-xs outline-none transition-[color,box-shadow] placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50\"\n />\n <button\n v-if=\"searchQuery\"\n data-slot=\"model-selector-search-clear\"\n type=\"button\"\n aria-label=\"Clear search\"\n class=\"absolute top-1/2 right-1.5 flex size-7 -translate-y-1/2 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none\"\n @click=\"clearSearch\"\n >\n <X aria-hidden=\"true\" class=\"size-4\" />\n </button>\n </div>\n <button\n data-slot=\"model-selector-close\"\n type=\"button\"\n aria-label=\"Close\"\n class=\"flex size-9 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none\"\n @click=\"closeDialog\"\n >\n <X aria-hidden=\"true\" class=\"size-4\" />\n </button>\n </div>\n </div>\n\n <div\n data-slot=\"model-selector-list\"\n role=\"listbox\"\n :aria-label=\"props.label\"\n class=\"max-h-[min(60vh,28rem)] overflow-y-auto p-2\"\n >\n <button\n v-for=\"model in filteredModels\"\n :key=\"model.value\"\n data-slot=\"model-selector-option\"\n type=\"button\"\n role=\"option\"\n :aria-selected=\"model.value === props.modelValue\"\n :disabled=\"model.disabled\"\n :class=\"cn(\n 'group flex w-full min-w-0 items-center gap-3 rounded-md px-3 py-2 text-left text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 aria-selected:bg-accent aria-selected:text-accent-foreground',\n )\"\n @click=\"selectDialogModel(model)\"\n >\n <component\n :is=\"model.icon\"\n v-if=\"model.icon\"\n data-slot=\"model-selector-option-icon\"\n class=\"size-4 shrink-0\"\n />\n <span\n data-slot=\"model-selector-option-label\"\n class=\"min-w-0 flex-1 truncate font-medium\"\n >\n {{ model.label }}\n </span>\n <Check\n v-if=\"model.value === props.modelValue\"\n aria-hidden=\"true\"\n class=\"size-4 shrink-0\"\n />\n </button>\n\n <div\n v-if=\"!filteredModels.length\"\n data-slot=\"model-selector-empty\"\n class=\"px-3 py-10 text-center text-sm text-muted-foreground\"\n >\n {{ props.emptyText }}\n </div>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n </div>\n</template>\n",
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"target": "components/ui/model-selector/ModelSelector.vue"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"path": "src/components/ui/model-selector/types.ts",
|
|
22
|
+
"content": "import type { Component } from \"vue\"\n\nexport type ModelSelectorMode = \"auto\" | \"dialog\" | \"select\"\n\nexport interface ModelSelectorOption {\n disabled?: boolean\n icon?: Component\n label: string\n value: string\n}\n\nexport interface ModelSelectorChangePayload {\n option?: ModelSelectorOption\n value: string\n}\n",
|
|
23
|
+
"type": "registry:ui",
|
|
24
|
+
"target": "components/ui/model-selector/types.ts"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"path": "src/components/ui/model-selector/index.ts",
|
|
28
|
+
"content": "export { default as ModelSelector } from \"./ModelSelector.vue\"\nexport type {\n ModelSelectorChangePayload,\n ModelSelectorMode,\n ModelSelectorOption,\n} from \"./types\"\n",
|
|
29
|
+
"type": "registry:ui",
|
|
30
|
+
"target": "components/ui/model-selector/index.ts"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"path": "src/components/ui/select/Select.vue",
|
|
34
|
+
"content": "<script setup lang=\"ts\">\nimport type { SelectRootEmits, SelectRootProps } from \"reka-ui\"\nimport { SelectRoot, useForwardPropsEmits } from \"reka-ui\"\n\nconst props = defineProps<SelectRootProps>()\nconst emits = defineEmits<SelectRootEmits>()\n\nconst forwarded = useForwardPropsEmits(props, emits)\n</script>\n\n<template>\n <SelectRoot\n v-slot=\"slotProps\"\n data-slot=\"select\"\n v-bind=\"forwarded\"\n >\n <slot v-bind=\"slotProps\" />\n </SelectRoot>\n</template>\n",
|
|
35
|
+
"type": "registry:ui",
|
|
36
|
+
"target": "components/ui/select/Select.vue"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"path": "src/components/ui/select/SelectTrigger.vue",
|
|
40
|
+
"content": "<script setup lang=\"ts\">\nimport type { SelectTriggerProps } from \"reka-ui\"\nimport type { HTMLAttributes } from \"vue\"\nimport { ChevronDown } from \"@lucide/vue\"\nimport { reactiveOmit } from \"@vueuse/core\"\nimport { SelectIcon, SelectTrigger, useForwardProps } from \"reka-ui\"\nimport { cn } from \"@/lib/utils\"\n\nconst props = withDefaults(\n defineProps<SelectTriggerProps & { class?: HTMLAttributes[\"class\"], size?: \"sm\" | \"default\" }>(),\n { size: \"default\" },\n)\n\nconst delegatedProps = reactiveOmit(props, \"class\", \"size\")\nconst forwardedProps = useForwardProps(delegatedProps)\n</script>\n\n<template>\n <SelectTrigger\n data-slot=\"select-trigger\"\n :data-size=\"size\"\n v-bind=\"forwardedProps\"\n :class=\"cn(\n 'border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*=text-])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4',\n props.class,\n )\"\n >\n <slot />\n <SelectIcon as-child>\n <ChevronDown class=\"size-4 opacity-50\" />\n </SelectIcon>\n </SelectTrigger>\n</template>\n",
|
|
41
|
+
"type": "registry:ui",
|
|
42
|
+
"target": "components/ui/select/SelectTrigger.vue"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"path": "src/components/ui/select/SelectValue.vue",
|
|
46
|
+
"content": "<script setup lang=\"ts\">\nimport type { SelectValueProps } from \"reka-ui\"\nimport { SelectValue } from \"reka-ui\"\n\nconst props = defineProps<SelectValueProps>()\n</script>\n\n<template>\n <SelectValue\n data-slot=\"select-value\"\n v-bind=\"props\"\n >\n <slot />\n </SelectValue>\n</template>\n",
|
|
47
|
+
"type": "registry:ui",
|
|
48
|
+
"target": "components/ui/select/SelectValue.vue"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"path": "src/components/ui/select/SelectContent.vue",
|
|
52
|
+
"content": "<script setup lang=\"ts\">\nimport type { SelectContentEmits, SelectContentProps } from \"reka-ui\"\nimport type { HTMLAttributes } from \"vue\"\nimport { computed, nextTick, provide, ref, shallowReactive } from \"vue\"\nimport { Search, X } from \"@lucide/vue\"\nimport { reactiveOmit } from \"@vueuse/core\"\nimport {\n SelectContent,\n SelectPortal,\n SelectViewport,\n useForwardPropsEmits,\n} from \"reka-ui\"\nimport { cn } from \"@/lib/utils\"\nimport { SelectScrollDownButton, SelectScrollUpButton } from \".\"\nimport {\n normalizeSelectSearchText,\n selectSearchContextKey,\n type SelectSearchItem,\n} from \"./search\"\n\ndefineOptions({\n inheritAttrs: false,\n})\n\nconst props = withDefaults(\n defineProps<SelectContentProps & {\n class?: HTMLAttributes[\"class\"]\n emptyText?: string\n search?: boolean\n searchClearLabel?: string\n searchPlaceholder?: string\n 검색?: boolean\n }>(),\n {\n emptyText: \"No results found.\",\n position: \"popper\",\n search: false,\n searchClearLabel: \"Clear search\",\n searchPlaceholder: \"Search options\",\n 검색: false,\n },\n)\nconst emits = defineEmits<SelectContentEmits>()\n\nconst delegatedProps = reactiveOmit(\n props,\n \"class\",\n \"emptyText\",\n \"search\",\n \"searchClearLabel\",\n \"searchPlaceholder\",\n \"검색\",\n)\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits)\nconst searchQuery = ref(\"\")\nconst searchInputRef = ref<HTMLInputElement | null>(null)\nconst searchItems = shallowReactive(new Map<symbol, SelectSearchItem>())\nconst isSearchEnabled = computed(() => props.search || props.검색)\nconst normalizedSearchQuery = computed(() => normalizeSelectSearchText(searchQuery.value))\nconst hasSearchQuery = computed(() => normalizedSearchQuery.value.length > 0)\nconst hasVisibleItems = computed(() => {\n if (!isSearchEnabled.value || !hasSearchQuery.value) return true\n\n return Array.from(searchItems.values()).some((item) => item.matches.value)\n})\n\nfunction hasVisibleItemsInGroup(groupId: symbol) {\n if (!isSearchEnabled.value || !hasSearchQuery.value) return true\n\n return Array.from(searchItems.values()).some(\n (item) => item.groupId === groupId && item.matches.value,\n )\n}\n\nfunction handleSearchKeydown(event: KeyboardEvent) {\n if (event.key === \"Escape\" && searchQuery.value) {\n searchQuery.value = \"\"\n event.preventDefault()\n event.stopPropagation()\n return\n }\n\n if (event.key !== \"Escape\") {\n event.stopPropagation()\n }\n}\n\nfunction clearSearch() {\n searchQuery.value = \"\"\n\n void nextTick(() => searchInputRef.value?.focus())\n}\n\nprovide(selectSearchContextKey, {\n enabled: isSearchEnabled,\n hasQuery: hasSearchQuery,\n hasVisibleItems,\n query: normalizedSearchQuery,\n hasVisibleItemsInGroup,\n registerItem: (item) => searchItems.set(item.id, item),\n unregisterItem: (id) => searchItems.delete(id),\n})\n</script>\n\n<template>\n <SelectPortal>\n <SelectContent\n data-slot=\"select-content\"\n v-bind=\"{ ...$attrs, ...forwarded }\"\n :class=\"cn(\n 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--reka-select-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border shadow-md',\n position === 'popper'\n && 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',\n props.class,\n )\n \"\n >\n <SelectScrollUpButton />\n <div v-if=\"isSearchEnabled\" data-slot=\"select-search\" class=\"border-b p-1\">\n <div class=\"relative\">\n <Search\n aria-hidden=\"true\"\n class=\"pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 text-muted-foreground\"\n />\n <input\n ref=\"searchInputRef\"\n v-model=\"searchQuery\"\n data-slot=\"select-search-input\"\n type=\"text\"\n role=\"searchbox\"\n autocomplete=\"off\"\n :placeholder=\"props.searchPlaceholder\"\n class=\"h-8 w-full rounded-sm bg-transparent py-1.5 pr-8 pl-8 text-sm outline-none placeholder:text-muted-foreground focus:bg-accent focus:text-accent-foreground\"\n @click.stop\n @keydown=\"handleSearchKeydown\"\n @pointerdown.stop\n />\n <button\n v-if=\"searchQuery\"\n data-slot=\"select-search-clear\"\n type=\"button\"\n :aria-label=\"props.searchClearLabel\"\n class=\"absolute top-1/2 right-1 flex size-6 -translate-y-1/2 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none\"\n @click.stop.prevent=\"clearSearch\"\n @keydown.stop\n @pointerdown.stop.prevent\n >\n <X aria-hidden=\"true\" class=\"size-4\" />\n </button>\n </div>\n </div>\n <SelectViewport :class=\"cn('p-1', position === 'popper' && 'h-[var(--reka-select-trigger-height)] w-full min-w-[var(--reka-select-trigger-width)] scroll-my-1')\">\n <slot />\n <div\n v-if=\"isSearchEnabled && hasSearchQuery && !hasVisibleItems\"\n data-slot=\"select-empty\"\n class=\"px-2 py-6 text-center text-sm text-muted-foreground\"\n >\n {{ props.emptyText }}\n </div>\n </SelectViewport>\n <SelectScrollDownButton />\n </SelectContent>\n </SelectPortal>\n</template>\n",
|
|
53
|
+
"type": "registry:ui",
|
|
54
|
+
"target": "components/ui/select/SelectContent.vue"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"path": "src/components/ui/select/SelectGroup.vue",
|
|
58
|
+
"content": "<script setup lang=\"ts\">\nimport type { SelectGroupProps } from \"reka-ui\"\nimport { computed, inject, provide } from \"vue\"\nimport { SelectGroup } from \"reka-ui\"\nimport { selectSearchContextKey, selectSearchGroupKey } from \"./search\"\n\nconst props = defineProps<SelectGroupProps>()\nconst groupId = Symbol(\"select-group\")\nconst searchContext = inject(selectSearchContextKey, null)\nconst isVisible = computed(() => searchContext?.hasVisibleItemsInGroup(groupId) ?? true)\n\nprovide(selectSearchGroupKey, groupId)\n</script>\n\n<template>\n <SelectGroup\n v-show=\"isVisible\"\n data-slot=\"select-group\"\n v-bind=\"props\"\n >\n <slot />\n </SelectGroup>\n</template>\n",
|
|
59
|
+
"type": "registry:ui",
|
|
60
|
+
"target": "components/ui/select/SelectGroup.vue"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"path": "src/components/ui/select/SelectItem.vue",
|
|
64
|
+
"content": "<script setup lang=\"ts\">\nimport type { SelectItemProps } from \"reka-ui\"\nimport type { HTMLAttributes } from \"vue\"\nimport { computed, inject, onBeforeUnmount } from \"vue\"\nimport { Check } from \"@lucide/vue\"\nimport { reactiveOmit } from \"@vueuse/core\"\nimport {\n SelectItem,\n SelectItemIndicator,\n SelectItemText,\n useForwardProps,\n} from \"reka-ui\"\nimport { cn } from \"@/lib/utils\"\nimport {\n normalizeSelectSearchText,\n selectSearchContextKey,\n selectSearchGroupKey,\n} from \"./search\"\n\nconst props = defineProps<SelectItemProps & {\n class?: HTMLAttributes[\"class\"]\n description?: string\n descriptionClass?: HTMLAttributes[\"class\"]\n}>()\n\nconst delegatedProps = reactiveOmit(props, \"class\", \"description\", \"descriptionClass\")\n\nconst forwardedProps = useForwardProps(delegatedProps)\nconst searchContext = inject(selectSearchContextKey, null)\nconst groupId = inject(selectSearchGroupKey, undefined)\nconst itemId = Symbol(\"select-item\")\nconst itemSearchText = computed(() =>\n normalizeSelectSearchText(\n [props.textValue ?? props.value, props.description].filter(Boolean).join(\" \"),\n ),\n)\nconst matchesSearch = computed(() => {\n if (!searchContext?.enabled.value || !searchContext.hasQuery.value) return true\n\n return itemSearchText.value.includes(searchContext.query.value)\n})\nconst isVisible = computed(() => matchesSearch.value)\n\nif (searchContext) {\n searchContext.registerItem({\n groupId,\n id: itemId,\n matches: matchesSearch,\n })\n\n onBeforeUnmount(() => searchContext.unregisterItem(itemId))\n}\n</script>\n\n<template>\n <SelectItem\n v-if=\"isVisible\"\n data-slot=\"select-item\"\n v-bind=\"forwardedProps\"\n :class=\"\n cn(\n 'group focus:bg-accent focus:text-accent-foreground data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*=text-])]:text-muted-foreground relative flex w-full cursor-default items-start gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4',\n props.class,\n )\n \"\n >\n <span class=\"absolute top-1/2 right-2 flex size-3.5 -translate-y-1/2 items-center justify-center\">\n <SelectItemIndicator>\n <slot name=\"indicator-icon\">\n <Check class=\"size-4\" />\n </slot>\n </SelectItemIndicator>\n </span>\n\n <span class=\"grid min-w-0 gap-0.5\">\n <SelectItemText>\n <span data-slot=\"select-item-label\" class=\"truncate\">\n <slot />\n </span>\n </SelectItemText>\n <span\n v-if=\"props.description || $slots.description\"\n data-slot=\"select-item-description\"\n :class=\"\n cn(\n 'text-xs leading-snug text-muted-foreground group-data-[highlighted]:text-accent-foreground/80 group-focus:text-accent-foreground/80',\n props.descriptionClass,\n )\n \"\n >\n <slot name=\"description\">\n {{ props.description }}\n </slot>\n </span>\n </span>\n </SelectItem>\n</template>\n",
|
|
65
|
+
"type": "registry:ui",
|
|
66
|
+
"target": "components/ui/select/SelectItem.vue"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"path": "src/components/ui/select/SelectItemText.vue",
|
|
70
|
+
"content": "<script setup lang=\"ts\">\nimport type { SelectItemTextProps } from \"reka-ui\"\nimport { SelectItemText } from \"reka-ui\"\n\nconst props = defineProps<SelectItemTextProps>()\n</script>\n\n<template>\n <SelectItemText\n data-slot=\"select-item-text\"\n v-bind=\"props\"\n >\n <slot />\n </SelectItemText>\n</template>\n",
|
|
71
|
+
"type": "registry:ui",
|
|
72
|
+
"target": "components/ui/select/SelectItemText.vue"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"path": "src/components/ui/select/SelectLabel.vue",
|
|
76
|
+
"content": "<script setup lang=\"ts\">\nimport type { SelectLabelProps } from \"reka-ui\"\nimport type { HTMLAttributes } from \"vue\"\nimport { SelectLabel } from \"reka-ui\"\nimport { cn } from \"@/lib/utils\"\n\nconst props = defineProps<SelectLabelProps & { class?: HTMLAttributes[\"class\"] }>()\n</script>\n\n<template>\n <SelectLabel\n data-slot=\"select-label\"\n :class=\"cn('text-muted-foreground px-2 py-1.5 text-xs', props.class)\"\n >\n <slot />\n </SelectLabel>\n</template>\n",
|
|
77
|
+
"type": "registry:ui",
|
|
78
|
+
"target": "components/ui/select/SelectLabel.vue"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"path": "src/components/ui/select/SelectSeparator.vue",
|
|
82
|
+
"content": "<script setup lang=\"ts\">\nimport type { SelectSeparatorProps } from \"reka-ui\"\nimport type { HTMLAttributes } from \"vue\"\nimport { reactiveOmit } from \"@vueuse/core\"\nimport { SelectSeparator } from \"reka-ui\"\nimport { cn } from \"@/lib/utils\"\n\nconst props = defineProps<SelectSeparatorProps & { class?: HTMLAttributes[\"class\"] }>()\n\nconst delegatedProps = reactiveOmit(props, \"class\")\n</script>\n\n<template>\n <SelectSeparator\n data-slot=\"select-separator\"\n v-bind=\"delegatedProps\"\n :class=\"cn('bg-border pointer-events-none -mx-1 my-1 h-px', props.class)\"\n />\n</template>\n",
|
|
83
|
+
"type": "registry:ui",
|
|
84
|
+
"target": "components/ui/select/SelectSeparator.vue"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"path": "src/components/ui/select/SelectScrollUpButton.vue",
|
|
88
|
+
"content": "<script setup lang=\"ts\">\nimport type { SelectScrollUpButtonProps } from \"reka-ui\"\nimport type { HTMLAttributes } from \"vue\"\nimport { ChevronUp } from \"@lucide/vue\"\nimport { reactiveOmit } from \"@vueuse/core\"\nimport { SelectScrollUpButton, useForwardProps } from \"reka-ui\"\nimport { cn } from \"@/lib/utils\"\n\nconst props = defineProps<SelectScrollUpButtonProps & { class?: HTMLAttributes[\"class\"] }>()\n\nconst delegatedProps = reactiveOmit(props, \"class\")\n\nconst forwardedProps = useForwardProps(delegatedProps)\n</script>\n\n<template>\n <SelectScrollUpButton\n data-slot=\"select-scroll-up-button\"\n v-bind=\"forwardedProps\"\n :class=\"cn('flex cursor-default items-center justify-center py-1', props.class)\"\n >\n <slot>\n <ChevronUp class=\"size-4\" />\n </slot>\n </SelectScrollUpButton>\n</template>\n",
|
|
89
|
+
"type": "registry:ui",
|
|
90
|
+
"target": "components/ui/select/SelectScrollUpButton.vue"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"path": "src/components/ui/select/SelectScrollDownButton.vue",
|
|
94
|
+
"content": "<script setup lang=\"ts\">\nimport type { SelectScrollDownButtonProps } from \"reka-ui\"\nimport type { HTMLAttributes } from \"vue\"\nimport { ChevronDown } from \"@lucide/vue\"\nimport { reactiveOmit } from \"@vueuse/core\"\nimport { SelectScrollDownButton, useForwardProps } from \"reka-ui\"\nimport { cn } from \"@/lib/utils\"\n\nconst props = defineProps<SelectScrollDownButtonProps & { class?: HTMLAttributes[\"class\"] }>()\n\nconst delegatedProps = reactiveOmit(props, \"class\")\n\nconst forwardedProps = useForwardProps(delegatedProps)\n</script>\n\n<template>\n <SelectScrollDownButton\n data-slot=\"select-scroll-down-button\"\n v-bind=\"forwardedProps\"\n :class=\"cn('flex cursor-default items-center justify-center py-1', props.class)\"\n >\n <slot>\n <ChevronDown class=\"size-4\" />\n </slot>\n </SelectScrollDownButton>\n</template>\n",
|
|
95
|
+
"type": "registry:ui",
|
|
96
|
+
"target": "components/ui/select/SelectScrollDownButton.vue"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"path": "src/components/ui/select/search.ts",
|
|
100
|
+
"content": "import type { ComputedRef, InjectionKey } from \"vue\"\n\nexport interface SelectSearchItem {\n groupId?: symbol\n id: symbol\n matches: ComputedRef<boolean>\n}\n\nexport interface SelectSearchContext {\n enabled: ComputedRef<boolean>\n hasQuery: ComputedRef<boolean>\n hasVisibleItems: ComputedRef<boolean>\n query: ComputedRef<string>\n hasVisibleItemsInGroup: (groupId: symbol) => boolean\n registerItem: (item: SelectSearchItem) => void\n unregisterItem: (id: symbol) => void\n}\n\nexport const selectSearchContextKey: InjectionKey<SelectSearchContext> =\n Symbol(\"select-search-context\")\n\nexport const selectSearchGroupKey: InjectionKey<symbol> = Symbol(\"select-search-group\")\n\nexport function normalizeSelectSearchText(value: unknown) {\n return String(value ?? \"\").trim().toLocaleLowerCase()\n}\n",
|
|
101
|
+
"type": "registry:ui",
|
|
102
|
+
"target": "components/ui/select/search.ts"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"path": "src/components/ui/select/index.ts",
|
|
106
|
+
"content": "export { default as Select } from \"./Select.vue\"\nexport { default as SelectContent } from \"./SelectContent.vue\"\nexport { default as SelectGroup } from \"./SelectGroup.vue\"\nexport { default as SelectItem } from \"./SelectItem.vue\"\nexport { default as SelectItemText } from \"./SelectItemText.vue\"\nexport { default as SelectLabel } from \"./SelectLabel.vue\"\nexport { default as SelectScrollDownButton } from \"./SelectScrollDownButton.vue\"\nexport { default as SelectScrollUpButton } from \"./SelectScrollUpButton.vue\"\nexport { default as SelectSeparator } from \"./SelectSeparator.vue\"\nexport { default as SelectTrigger } from \"./SelectTrigger.vue\"\nexport { default as SelectValue } from \"./SelectValue.vue\"\n",
|
|
107
|
+
"type": "registry:ui",
|
|
108
|
+
"target": "components/ui/select/index.ts"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"path": "src/components/ui/dialog/Dialog.vue",
|
|
112
|
+
"content": "<script setup lang=\"ts\">\nimport type { DialogRootEmits, DialogRootProps } from \"reka-ui\"\nimport { DialogRoot, useForwardPropsEmits } from \"reka-ui\"\n\nconst props = defineProps<DialogRootProps>()\nconst emits = defineEmits<DialogRootEmits>()\n\nconst forwarded = useForwardPropsEmits(props, emits)\n</script>\n\n<template>\n <DialogRoot\n v-slot=\"slotProps\"\n data-slot=\"dialog\"\n v-bind=\"forwarded\"\n >\n <slot v-bind=\"slotProps\" />\n </DialogRoot>\n</template>\n",
|
|
113
|
+
"type": "registry:ui",
|
|
114
|
+
"target": "components/ui/dialog/Dialog.vue"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"path": "src/components/ui/dialog/DialogTrigger.vue",
|
|
118
|
+
"content": "<script setup lang=\"ts\">\nimport type { DialogTriggerProps } from \"reka-ui\"\nimport { DialogTrigger, useForwardProps } from \"reka-ui\"\n\nconst props = defineProps<DialogTriggerProps>()\n\nconst forwardedProps = useForwardProps(props)\n</script>\n\n<template>\n <DialogTrigger\n data-slot=\"dialog-trigger\"\n v-bind=\"forwardedProps\"\n >\n <slot />\n </DialogTrigger>\n</template>\n",
|
|
119
|
+
"type": "registry:ui",
|
|
120
|
+
"target": "components/ui/dialog/DialogTrigger.vue"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"path": "src/components/ui/dialog/DialogContent.vue",
|
|
124
|
+
"content": "<script setup lang=\"ts\">\nimport type { DialogContentEmits, DialogContentProps } from \"reka-ui\"\nimport type { HTMLAttributes } from \"vue\"\nimport { X } from \"@lucide/vue\"\nimport { reactiveOmit } from \"@vueuse/core\"\nimport {\n DialogClose,\n DialogContent,\n DialogPortal,\n useForwardPropsEmits,\n} from \"reka-ui\"\nimport { cn } from \"@/lib/utils\"\nimport DialogOverlay from \"./DialogOverlay.vue\"\n\ndefineOptions({\n inheritAttrs: false,\n})\n\nconst props = withDefaults(\n defineProps<DialogContentProps & {\n class?: HTMLAttributes[\"class\"]\n showCloseButton?: boolean\n }>(),\n {\n showCloseButton: true,\n },\n)\nconst emits = defineEmits<DialogContentEmits>()\n\nconst delegatedProps = reactiveOmit(props, \"class\", \"showCloseButton\")\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits)\n</script>\n\n<template>\n <DialogPortal>\n <DialogOverlay />\n <DialogContent\n data-slot=\"dialog-content\"\n v-bind=\"{ ...$attrs, ...forwarded }\"\n :class=\"\n cn(\n 'fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-lg border bg-background p-6 text-foreground shadow-lg duration-200 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg',\n props.class,\n )\n \"\n >\n <slot />\n\n <DialogClose\n v-if=\"props.showCloseButton\"\n data-slot=\"dialog-close\"\n class=\"absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background focus:outline-none disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n >\n <X aria-hidden=\"true\" />\n <span class=\"sr-only\">Close</span>\n </DialogClose>\n </DialogContent>\n </DialogPortal>\n</template>\n",
|
|
125
|
+
"type": "registry:ui",
|
|
126
|
+
"target": "components/ui/dialog/DialogContent.vue"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"path": "src/components/ui/dialog/DialogOverlay.vue",
|
|
130
|
+
"content": "<script setup lang=\"ts\">\nimport type { DialogOverlayProps } from \"reka-ui\"\nimport type { HTMLAttributes } from \"vue\"\nimport { reactiveOmit } from \"@vueuse/core\"\nimport { DialogOverlay, useForwardProps } from \"reka-ui\"\nimport { cn } from \"@/lib/utils\"\n\nconst props = defineProps<DialogOverlayProps & { class?: HTMLAttributes[\"class\"] }>()\n\nconst delegatedProps = reactiveOmit(props, \"class\")\n\nconst forwardedProps = useForwardProps(delegatedProps)\n</script>\n\n<template>\n <DialogOverlay\n data-slot=\"dialog-overlay\"\n v-bind=\"forwardedProps\"\n :class=\"cn('fixed inset-0 z-50 bg-[var(--overlay)] backdrop-blur-[4px] [-webkit-backdrop-filter:blur(4px)] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0', props.class)\"\n >\n <slot />\n </DialogOverlay>\n</template>\n",
|
|
131
|
+
"type": "registry:ui",
|
|
132
|
+
"target": "components/ui/dialog/DialogOverlay.vue"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"path": "src/components/ui/dialog/DialogTitle.vue",
|
|
136
|
+
"content": "<script setup lang=\"ts\">\nimport type { DialogTitleProps } from \"reka-ui\"\nimport type { HTMLAttributes } from \"vue\"\nimport { reactiveOmit } from \"@vueuse/core\"\nimport { DialogTitle, useForwardProps } from \"reka-ui\"\nimport { cn } from \"@/lib/utils\"\n\nconst props = defineProps<DialogTitleProps & { class?: HTMLAttributes[\"class\"] }>()\n\nconst delegatedProps = reactiveOmit(props, \"class\")\n\nconst forwardedProps = useForwardProps(delegatedProps)\n</script>\n\n<template>\n <DialogTitle\n data-slot=\"dialog-title\"\n v-bind=\"forwardedProps\"\n :class=\"cn('text-lg font-semibold leading-none', props.class)\"\n >\n <slot />\n </DialogTitle>\n</template>\n",
|
|
137
|
+
"type": "registry:ui",
|
|
138
|
+
"target": "components/ui/dialog/DialogTitle.vue"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"path": "src/lib/utils.ts",
|
|
142
|
+
"content": "import { type ClassValue, clsx } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n",
|
|
143
|
+
"type": "registry:lib",
|
|
144
|
+
"target": "lib/utils.ts"
|
|
145
|
+
}
|
|
146
|
+
],
|
|
147
|
+
"type": "registry:ui"
|
|
148
|
+
}
|