From f7f540881484475f27d74ed607f85bb8f44e0c76 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 8 Feb 2026 20:12:39 -0800 Subject: [PATCH 1/3] init components --- apps/renderer/components.json | 25 ++ apps/renderer/package.json | 5 + apps/renderer/src/components/ui/accordion.tsx | 67 +++ .../src/components/ui/alert-dialog.tsx | 167 +++++++ apps/renderer/src/components/ui/alert.tsx | 80 ++++ .../src/components/ui/autocomplete.tsx | 308 +++++++++++++ apps/renderer/src/components/ui/avatar.tsx | 44 ++ apps/renderer/src/components/ui/badge.tsx | 58 +++ apps/renderer/src/components/ui/button.tsx | 71 +++ apps/renderer/src/components/ui/card.tsx | 242 ++++++++++ .../src/components/ui/checkbox-group.tsx | 14 + apps/renderer/src/components/ui/checkbox.tsx | 58 +++ .../src/components/ui/collapsible.tsx | 43 ++ apps/renderer/src/components/ui/combobox.tsx | 422 ++++++++++++++++++ apps/renderer/src/components/ui/command.tsx | 262 +++++++++++ apps/renderer/src/components/ui/dialog.tsx | 194 ++++++++ apps/renderer/src/components/ui/empty.tsx | 127 ++++++ apps/renderer/src/components/ui/field.tsx | 72 +++ apps/renderer/src/components/ui/fieldset.tsx | 27 ++ apps/renderer/src/components/ui/form.tsx | 15 + .../src/components/ui/input-group.tsx | 99 ++++ apps/renderer/src/components/ui/input.tsx | 64 +++ apps/renderer/src/components/ui/kbd.tsx | 28 ++ apps/renderer/src/components/ui/label.tsx | 26 ++ apps/renderer/src/components/ui/menu.tsx | 308 +++++++++++++ apps/renderer/src/components/ui/popover.tsx | 102 +++++ apps/renderer/src/components/ui/progress.tsx | 79 ++++ .../src/components/ui/radio-group.tsx | 34 ++ .../src/components/ui/scroll-area.tsx | 62 +++ apps/renderer/src/components/ui/select.tsx | 187 ++++++++ apps/renderer/src/components/ui/separator.tsx | 23 + apps/renderer/src/components/ui/sheet.tsx | 201 +++++++++ apps/renderer/src/components/ui/skeleton.tsx | 16 + apps/renderer/src/components/ui/spinner.tsx | 18 + apps/renderer/src/components/ui/switch.tsx | 25 ++ apps/renderer/src/components/ui/table.tsx | 126 ++++++ apps/renderer/src/components/ui/tabs.tsx | 85 ++++ apps/renderer/src/components/ui/textarea.tsx | 49 ++ apps/renderer/src/components/ui/toast.tsx | 267 +++++++++++ apps/renderer/src/hooks/use-mobile.ts | 21 + apps/renderer/src/index.css | 161 +++++++ apps/renderer/src/lib/utils.ts | 6 + apps/renderer/tsconfig.json | 6 +- bun.lock | 97 +++- 44 files changed, 4389 insertions(+), 2 deletions(-) create mode 100644 apps/renderer/components.json create mode 100644 apps/renderer/src/components/ui/accordion.tsx create mode 100644 apps/renderer/src/components/ui/alert-dialog.tsx create mode 100644 apps/renderer/src/components/ui/alert.tsx create mode 100644 apps/renderer/src/components/ui/autocomplete.tsx create mode 100644 apps/renderer/src/components/ui/avatar.tsx create mode 100644 apps/renderer/src/components/ui/badge.tsx create mode 100644 apps/renderer/src/components/ui/button.tsx create mode 100644 apps/renderer/src/components/ui/card.tsx create mode 100644 apps/renderer/src/components/ui/checkbox-group.tsx create mode 100644 apps/renderer/src/components/ui/checkbox.tsx create mode 100644 apps/renderer/src/components/ui/collapsible.tsx create mode 100644 apps/renderer/src/components/ui/combobox.tsx create mode 100644 apps/renderer/src/components/ui/command.tsx create mode 100644 apps/renderer/src/components/ui/dialog.tsx create mode 100644 apps/renderer/src/components/ui/empty.tsx create mode 100644 apps/renderer/src/components/ui/field.tsx create mode 100644 apps/renderer/src/components/ui/fieldset.tsx create mode 100644 apps/renderer/src/components/ui/form.tsx create mode 100644 apps/renderer/src/components/ui/input-group.tsx create mode 100644 apps/renderer/src/components/ui/input.tsx create mode 100644 apps/renderer/src/components/ui/kbd.tsx create mode 100644 apps/renderer/src/components/ui/label.tsx create mode 100644 apps/renderer/src/components/ui/menu.tsx create mode 100644 apps/renderer/src/components/ui/popover.tsx create mode 100644 apps/renderer/src/components/ui/progress.tsx create mode 100644 apps/renderer/src/components/ui/radio-group.tsx create mode 100644 apps/renderer/src/components/ui/scroll-area.tsx create mode 100644 apps/renderer/src/components/ui/select.tsx create mode 100644 apps/renderer/src/components/ui/separator.tsx create mode 100644 apps/renderer/src/components/ui/sheet.tsx create mode 100644 apps/renderer/src/components/ui/skeleton.tsx create mode 100644 apps/renderer/src/components/ui/spinner.tsx create mode 100644 apps/renderer/src/components/ui/switch.tsx create mode 100644 apps/renderer/src/components/ui/table.tsx create mode 100644 apps/renderer/src/components/ui/tabs.tsx create mode 100644 apps/renderer/src/components/ui/textarea.tsx create mode 100644 apps/renderer/src/components/ui/toast.tsx create mode 100644 apps/renderer/src/hooks/use-mobile.ts create mode 100644 apps/renderer/src/lib/utils.ts diff --git a/apps/renderer/components.json b/apps/renderer/components.json new file mode 100644 index 0000000000..8d7bf441ac --- /dev/null +++ b/apps/renderer/components.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "#/components", + "utils": "#/lib/utils", + "ui": "#/components/ui", + "lib": "#/lib", + "hooks": "#/hooks" + }, + "registries": { + "@coss": "https://coss.com/ui/r/{name}.json" + } +} diff --git a/apps/renderer/package.json b/apps/renderer/package.json index b91d3d1dae..8ab450ea8c 100644 --- a/apps/renderer/package.json +++ b/apps/renderer/package.json @@ -12,12 +12,17 @@ }, "dependencies": { "@acme/contracts": "workspace:*", + "@base-ui/react": "^1.1.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "highlight.js": "^11.11.1", + "lucide-react": "^0.563.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-markdown": "^10.1.0", "rehype-highlight": "^7.0.2", "remark-gfm": "^4.0.1", + "tailwind-merge": "^3.4.0", "zod": "^3.24.1" }, "devDependencies": { diff --git a/apps/renderer/src/components/ui/accordion.tsx b/apps/renderer/src/components/ui/accordion.tsx new file mode 100644 index 0000000000..7074a60588 --- /dev/null +++ b/apps/renderer/src/components/ui/accordion.tsx @@ -0,0 +1,67 @@ +import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion"; +import { ChevronDownIcon } from "lucide-react"; + +import { cn } from "#/lib/utils"; + +function Accordion(props: AccordionPrimitive.Root.Props) { + return ; +} + +function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) { + return ( + + ); +} + +function AccordionTrigger({ + className, + children, + ...props +}: AccordionPrimitive.Trigger.Props) { + return ( + + + {children} + + + + ); +} + +function AccordionPanel({ + className, + children, + ...props +}: AccordionPrimitive.Panel.Props) { + return ( + +
{children}
+
+ ); +} + +export { + Accordion, + AccordionItem, + AccordionTrigger, + AccordionPanel, + AccordionPanel as AccordionContent, +}; diff --git a/apps/renderer/src/components/ui/alert-dialog.tsx b/apps/renderer/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000000..5de779b3cd --- /dev/null +++ b/apps/renderer/src/components/ui/alert-dialog.tsx @@ -0,0 +1,167 @@ +import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog"; + +import { cn } from "#/lib/utils"; + +const AlertDialogCreateHandle = AlertDialogPrimitive.createHandle; + +const AlertDialog = AlertDialogPrimitive.Root; + +const AlertDialogPortal = AlertDialogPrimitive.Portal; + +function AlertDialogTrigger(props: AlertDialogPrimitive.Trigger.Props) { + return ( + + ); +} + +function AlertDialogBackdrop({ + className, + ...props +}: AlertDialogPrimitive.Backdrop.Props) { + return ( + + ); +} + +function AlertDialogViewport({ + className, + ...props +}: AlertDialogPrimitive.Viewport.Props) { + return ( + + ); +} + +function AlertDialogPopup({ + className, + bottomStickOnMobile = true, + ...props +}: AlertDialogPrimitive.Popup.Props & { + bottomStickOnMobile?: boolean; +}) { + return ( + + + + + + + ); +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AlertDialogFooter({ + className, + variant = "default", + ...props +}: React.ComponentProps<"div"> & { + variant?: "default" | "bare"; +}) { + return ( +
+ ); +} + +function AlertDialogTitle({ + className, + ...props +}: AlertDialogPrimitive.Title.Props) { + return ( + + ); +} + +function AlertDialogDescription({ + className, + ...props +}: AlertDialogPrimitive.Description.Props) { + return ( + + ); +} + +function AlertDialogClose(props: AlertDialogPrimitive.Close.Props) { + return ( + + ); +} + +export { + AlertDialogCreateHandle, + AlertDialog, + AlertDialogPortal, + AlertDialogBackdrop, + AlertDialogBackdrop as AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogPopup, + AlertDialogPopup as AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogClose, + AlertDialogViewport, +}; diff --git a/apps/renderer/src/components/ui/alert.tsx b/apps/renderer/src/components/ui/alert.tsx new file mode 100644 index 0000000000..196dde6c32 --- /dev/null +++ b/apps/renderer/src/components/ui/alert.tsx @@ -0,0 +1,80 @@ +import { cva, type VariantProps } from "class-variance-authority"; +import type * as React from "react"; + +import { cn } from "#/lib/utils"; + +const alertVariants = cva( + "relative grid w-full items-start gap-x-2 gap-y-0.5 rounded-xl border px-3.5 py-3 text-card-foreground text-sm has-[>svg]:has-data-[slot=alert-action]:grid-cols-[calc(var(--spacing)*4)_1fr_auto] has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-data-[slot=alert-action]:grid-cols-[1fr_auto] has-[>svg]:gap-x-2 [&>svg]:h-lh [&>svg]:w-4", + { + defaultVariants: { + variant: "default", + }, + variants: { + variant: { + default: + "bg-transparent dark:bg-input/32 [&>svg]:text-muted-foreground", + error: + "border-destructive/32 bg-destructive/4 [&>svg]:text-destructive", + info: "border-info/32 bg-info/4 [&>svg]:text-info", + success: "border-success/32 bg-success/4 [&>svg]:text-success", + warning: "border-warning/32 bg-warning/4 [&>svg]:text-warning", + }, + }, + }, +); + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ); +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AlertAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { Alert, AlertTitle, AlertDescription, AlertAction }; diff --git a/apps/renderer/src/components/ui/autocomplete.tsx b/apps/renderer/src/components/ui/autocomplete.tsx new file mode 100644 index 0000000000..3a505429ad --- /dev/null +++ b/apps/renderer/src/components/ui/autocomplete.tsx @@ -0,0 +1,308 @@ +import { Autocomplete as AutocompletePrimitive } from "@base-ui/react/autocomplete"; +import { ChevronsUpDownIcon, XIcon } from "lucide-react"; + +import { cn } from "#/lib/utils"; +import { Input } from "#/components/ui/input"; +import { ScrollArea } from "#/components/ui/scroll-area"; + +const Autocomplete = AutocompletePrimitive.Root; + +function AutocompleteInput({ + className, + showTrigger = false, + showClear = false, + startAddon, + size, + ...props +}: Omit & { + showTrigger?: boolean; + showClear?: boolean; + startAddon?: React.ReactNode; + size?: "sm" | "default" | "lg" | number; + ref?: React.Ref; +}) { + const sizeValue = (size ?? "default") as "sm" | "default" | "lg" | number; + + return ( +
+ {startAddon && ( + + )} + } + {...props} + /> + {showTrigger && ( + + + + )} + {showClear && ( + + + + )} +
+ ); +} + +function AutocompletePopup({ + className, + children, + side = "bottom", + sideOffset = 4, + alignOffset, + align = "start", + ...props +}: AutocompletePrimitive.Popup.Props & { + align?: AutocompletePrimitive.Positioner.Props["align"]; + sideOffset?: AutocompletePrimitive.Positioner.Props["sideOffset"]; + alignOffset?: AutocompletePrimitive.Positioner.Props["alignOffset"]; + side?: AutocompletePrimitive.Positioner.Props["side"]; +}) { + return ( + + + + + {children} + + + + + ); +} + +function AutocompleteItem({ + className, + children, + ...props +}: AutocompletePrimitive.Item.Props) { + return ( + + {children} + + ); +} + +function AutocompleteSeparator({ + className, + ...props +}: AutocompletePrimitive.Separator.Props) { + return ( + + ); +} + +function AutocompleteGroup({ + className, + ...props +}: AutocompletePrimitive.Group.Props) { + return ( + + ); +} + +function AutocompleteGroupLabel({ + className, + ...props +}: AutocompletePrimitive.GroupLabel.Props) { + return ( + + ); +} + +function AutocompleteEmpty({ + className, + ...props +}: AutocompletePrimitive.Empty.Props) { + return ( + + ); +} + +function AutocompleteRow({ + className, + ...props +}: AutocompletePrimitive.Row.Props) { + return ( + + ); +} + +function AutocompleteValue({ ...props }: AutocompletePrimitive.Value.Props) { + return ( + + ); +} + +function AutocompleteList({ + className, + ...props +}: AutocompletePrimitive.List.Props) { + return ( + + + + ); +} + +function AutocompleteClear({ + className, + ...props +}: AutocompletePrimitive.Clear.Props) { + return ( + + + + ); +} + +function AutocompleteStatus({ + className, + ...props +}: AutocompletePrimitive.Status.Props) { + return ( + + ); +} + +function AutocompleteCollection({ + ...props +}: AutocompletePrimitive.Collection.Props) { + return ( + + ); +} + +function AutocompleteTrigger({ + className, + ...props +}: AutocompletePrimitive.Trigger.Props) { + return ( + + ); +} + +const useAutocompleteFilter = AutocompletePrimitive.useFilter; + +export { + Autocomplete, + AutocompleteInput, + AutocompleteTrigger, + AutocompletePopup, + AutocompleteItem, + AutocompleteSeparator, + AutocompleteGroup, + AutocompleteGroupLabel, + AutocompleteEmpty, + AutocompleteValue, + AutocompleteList, + AutocompleteClear, + AutocompleteStatus, + AutocompleteRow, + AutocompleteCollection, + useAutocompleteFilter, +}; diff --git a/apps/renderer/src/components/ui/avatar.tsx b/apps/renderer/src/components/ui/avatar.tsx new file mode 100644 index 0000000000..8ea9820d03 --- /dev/null +++ b/apps/renderer/src/components/ui/avatar.tsx @@ -0,0 +1,44 @@ +import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"; + +import { cn } from "#/lib/utils"; + +function Avatar({ className, ...props }: AvatarPrimitive.Root.Props) { + return ( + + ); +} + +function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) { + return ( + + ); +} + +function AvatarFallback({ + className, + ...props +}: AvatarPrimitive.Fallback.Props) { + return ( + + ); +} + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/apps/renderer/src/components/ui/badge.tsx b/apps/renderer/src/components/ui/badge.tsx new file mode 100644 index 0000000000..458b4a8b3c --- /dev/null +++ b/apps/renderer/src/components/ui/badge.tsx @@ -0,0 +1,58 @@ +import { mergeProps } from "@base-ui/react/merge-props"; +import { useRender } from "@base-ui/react/use-render"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "#/lib/utils"; + +const badgeVariants = cva( + "relative inline-flex shrink-0 items-center justify-center gap-1 whitespace-nowrap rounded-sm border border-transparent font-medium outline-none transition-shadow focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-3.5 sm:[&_svg:not([class*='size-'])]:size-3 [&_svg]:pointer-events-none [&_svg]:shrink-0 [button,a&]:cursor-pointer [button,a&]:pointer-coarse:after:absolute [button,a&]:pointer-coarse:after:size-full [button,a&]:pointer-coarse:after:min-h-11 [button,a&]:pointer-coarse:after:min-w-11", + { + defaultVariants: { + size: "default", + variant: "default", + }, + variants: { + size: { + default: + "h-5.5 min-w-5.5 px-[calc(--spacing(1)-1px)] text-sm sm:h-4.5 sm:min-w-4.5 sm:text-xs", + lg: "h-6.5 min-w-6.5 px-[calc(--spacing(1.5)-1px)] text-base sm:h-5.5 sm:min-w-5.5 sm:text-sm", + sm: "h-5 min-w-5 rounded-[calc(var(--radius-sm)-2px)] px-[calc(--spacing(1)-1px)] text-xs sm:h-4 sm:min-w-4 sm:text-[.625rem]", + }, + variant: { + default: + "bg-primary text-primary-foreground [button,a&]:hover:bg-primary/90", + destructive: + "bg-destructive text-white [button,a&]:hover:bg-destructive/90", + error: + "bg-destructive/8 text-destructive-foreground dark:bg-destructive/16", + info: "bg-info/8 text-info-foreground dark:bg-info/16", + outline: + "border-input bg-background text-foreground dark:bg-input/32 [button,a&]:hover:bg-accent/50 dark:[button,a&]:hover:bg-input/48", + secondary: + "bg-secondary text-secondary-foreground [button,a&]:hover:bg-secondary/90", + success: "bg-success/8 text-success-foreground dark:bg-success/16", + warning: "bg-warning/8 text-warning-foreground dark:bg-warning/16", + }, + }, + }, +); + +interface BadgeProps extends useRender.ComponentProps<"span"> { + variant?: VariantProps["variant"]; + size?: VariantProps["size"]; +} + +function Badge({ className, variant, size, render, ...props }: BadgeProps) { + const defaultProps = { + className: cn(badgeVariants({ className, size, variant })), + "data-slot": "badge", + }; + + return useRender({ + defaultTagName: "span", + props: mergeProps<"span">(defaultProps, props), + render, + }); +} + +export { Badge, badgeVariants }; diff --git a/apps/renderer/src/components/ui/button.tsx b/apps/renderer/src/components/ui/button.tsx new file mode 100644 index 0000000000..4a110751b5 --- /dev/null +++ b/apps/renderer/src/components/ui/button.tsx @@ -0,0 +1,71 @@ +import { mergeProps } from "@base-ui/react/merge-props"; +import { useRender } from "@base-ui/react/use-render"; +import { cva, type VariantProps } from "class-variance-authority"; +import type * as React from "react"; + +import { cn } from "#/lib/utils"; + +const buttonVariants = cva( + "[&_svg]:-mx-0.5 relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-lg border font-medium text-base outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 sm:text-sm [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", + { + defaultVariants: { + size: "default", + variant: "default", + }, + variants: { + size: { + default: "h-9 px-[calc(--spacing(3)-1px)] sm:h-8", + icon: "size-9 sm:size-8", + "icon-lg": "size-10 sm:size-9", + "icon-sm": "size-8 sm:size-7", + "icon-xl": + "size-11 sm:size-10 [&_svg:not([class*='size-'])]:size-5 sm:[&_svg:not([class*='size-'])]:size-4.5", + "icon-xs": + "size-7 rounded-md before:rounded-[calc(var(--radius-md)-1px)] sm:size-6 not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-4 sm:not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-3.5", + lg: "h-10 px-[calc(--spacing(3.5)-1px)] sm:h-9", + sm: "h-8 gap-1.5 px-[calc(--spacing(2.5)-1px)] sm:h-7", + xl: "h-11 px-[calc(--spacing(4)-1px)] text-lg sm:h-10 sm:text-base [&_svg:not([class*='size-'])]:size-5 sm:[&_svg:not([class*='size-'])]:size-4.5", + xs: "h-7 gap-1 rounded-md px-[calc(--spacing(2)-1px)] text-sm before:rounded-[calc(var(--radius-md)-1px)] sm:h-6 sm:text-xs [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5", + }, + variant: { + default: + "not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-primary bg-primary text-primary-foreground shadow-primary/24 shadow-xs [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [:disabled,:active,[data-pressed]]:shadow-none [:hover,[data-pressed]]:bg-primary/90", + destructive: + "not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-destructive bg-destructive text-white shadow-destructive/24 shadow-xs [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [:disabled,:active,[data-pressed]]:shadow-none [:hover,[data-pressed]]:bg-destructive/90", + "destructive-outline": + "border-input bg-popover not-dark:bg-clip-padding text-destructive-foreground shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none [:hover,[data-pressed]]:border-destructive/32 [:hover,[data-pressed]]:bg-destructive/4", + ghost: + "border-transparent text-foreground data-pressed:bg-accent [:hover,[data-pressed]]:bg-accent", + link: "border-transparent underline-offset-4 [:hover,[data-pressed]]:underline", + outline: + "border-input bg-popover not-dark:bg-clip-padding text-foreground shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none [:hover,[data-pressed]]:bg-accent/50 dark:[:hover,[data-pressed]]:bg-input/64", + secondary: + "border-transparent bg-secondary text-secondary-foreground [:active,[data-pressed]]:bg-secondary/80 [:hover,[data-pressed]]:bg-secondary/90", + }, + }, + }, +); + +interface ButtonProps extends useRender.ComponentProps<"button"> { + variant?: VariantProps["variant"]; + size?: VariantProps["size"]; +} + +function Button({ className, variant, size, render, ...props }: ButtonProps) { + const typeValue: React.ButtonHTMLAttributes["type"] = + render ? undefined : "button"; + + const defaultProps = { + className: cn(buttonVariants({ className, size, variant })), + "data-slot": "button", + type: typeValue, + }; + + return useRender({ + defaultTagName: "button", + props: mergeProps<"button">(defaultProps, props), + render, + }); +} + +export { Button, buttonVariants }; diff --git a/apps/renderer/src/components/ui/card.tsx b/apps/renderer/src/components/ui/card.tsx new file mode 100644 index 0000000000..4023696281 --- /dev/null +++ b/apps/renderer/src/components/ui/card.tsx @@ -0,0 +1,242 @@ +import { mergeProps } from "@base-ui/react/merge-props"; +import { useRender } from "@base-ui/react/use-render"; + +import { cn } from "#/lib/utils"; + +function Card({ + className, + render, + ...props +}: useRender.ComponentProps<"div">) { + const defaultProps = { + className: cn( + "relative flex flex-col rounded-2xl border bg-card not-dark:bg-clip-padding text-card-foreground shadow-xs/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]", + className, + ), + "data-slot": "card", + }; + + return useRender({ + defaultTagName: "div", + props: mergeProps<"div">(defaultProps, props), + render, + }); +} + +function CardFrame({ + className, + render, + ...props +}: useRender.ComponentProps<"div">) { + const defaultProps = { + className: cn( + "flex flex-col relative rounded-2xl border bg-card before:bg-muted/72 not-dark:bg-clip-padding text-card-foreground shadow-xs/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)] *:data-[slot=card]:-m-px *:not-last:data-[slot=card]:rounded-b-xl *:not-last:data-[slot=card]:before:rounded-b-[calc(var(--radius-xl)-1px)] *:not-first:data-[slot=card]:rounded-t-xl *:not-first:data-[slot=card]:before:rounded-t-[calc(var(--radius-xl)-1px)] *:data-[slot=card]:[clip-path:inset(-1rem_1px)] *:data-[slot=card]:first:[clip-path:inset(1px_1px_-1rem_1px_round_calc(var(--radius-2xl)-1px))] *:data-[slot=card]:last:[clip-path:inset(-1rem_1px_1px_1px_round_calc(var(--radius-2xl)-1px))] *:data-[slot=card]:shadow-none *:data-[slot=card]:before:hidden *:data-[slot=card]:bg-clip-padding", + className, + ), + "data-slot": "card-frame", + }; + + return useRender({ + defaultTagName: "div", + props: mergeProps<"div">(defaultProps, props), + render, + }); +} + +function CardFrameHeader({ + className, + render, + ...props +}: useRender.ComponentProps<"div">) { + const defaultProps = { + className: cn("relative flex flex-col px-6 py-4", className), + "data-slot": "card-frame-header", + }; + + return useRender({ + defaultTagName: "div", + props: mergeProps<"div">(defaultProps, props), + render, + }); +} + +function CardFrameTitle({ + className, + render, + ...props +}: useRender.ComponentProps<"div">) { + const defaultProps = { + className: cn("font-semibold text-sm", className), + "data-slot": "card-frame-title", + }; + + return useRender({ + defaultTagName: "div", + props: mergeProps<"div">(defaultProps, props), + render, + }); +} + +function CardFrameDescription({ + className, + render, + ...props +}: useRender.ComponentProps<"div">) { + const defaultProps = { + className: cn("text-muted-foreground text-sm", className), + "data-slot": "card-frame-description", + }; + + return useRender({ + defaultTagName: "div", + props: mergeProps<"div">(defaultProps, props), + render, + }); +} + +function CardFrameFooter({ + className, + render, + ...props +}: useRender.ComponentProps<"div">) { + const defaultProps = { + className: cn("px-6 py-4", className), + "data-slot": "card-frame-footer", + }; + + return useRender({ + defaultTagName: "div", + props: mergeProps<"div">(defaultProps, props), + render, + }); +} + +function CardHeader({ + className, + render, + ...props +}: useRender.ComponentProps<"div">) { + const defaultProps = { + className: cn( + "grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 p-6 in-[[data-slot=card]:has(>[data-slot=card-panel])]:pb-4 has-data-[slot=card-action]:grid-cols-[1fr_auto]", + className, + ), + "data-slot": "card-header", + }; + + return useRender({ + defaultTagName: "div", + props: mergeProps<"div">(defaultProps, props), + render, + }); +} + +function CardTitle({ + className, + render, + ...props +}: useRender.ComponentProps<"div">) { + const defaultProps = { + className: cn("font-semibold text-lg leading-none", className), + "data-slot": "card-title", + }; + + return useRender({ + defaultTagName: "div", + props: mergeProps<"div">(defaultProps, props), + render, + }); +} + +function CardDescription({ + className, + render, + ...props +}: useRender.ComponentProps<"div">) { + const defaultProps = { + className: cn("text-muted-foreground text-sm", className), + "data-slot": "card-description", + }; + + return useRender({ + defaultTagName: "div", + props: mergeProps<"div">(defaultProps, props), + render, + }); +} + +function CardAction({ + className, + render, + ...props +}: useRender.ComponentProps<"div">) { + const defaultProps = { + className: cn( + "col-start-2 row-span-2 row-start-1 self-start justify-self-end inline-flex", + className, + ), + "data-slot": "card-action", + }; + + return useRender({ + defaultTagName: "div", + props: mergeProps<"div">(defaultProps, props), + render, + }); +} + +function CardPanel({ + className, + render, + ...props +}: useRender.ComponentProps<"div">) { + const defaultProps = { + className: cn( + "flex-1 p-6 in-[[data-slot=card]:has(>[data-slot=card-header]:not(.border-b))]:pt-0 in-[[data-slot=card]:has(>[data-slot=card-footer]:not(.border-t))]:pb-0", + className, + ), + "data-slot": "card-panel", + }; + + return useRender({ + defaultTagName: "div", + props: mergeProps<"div">(defaultProps, props), + render, + }); +} + +function CardFooter({ + className, + render, + ...props +}: useRender.ComponentProps<"div">) { + const defaultProps = { + className: cn( + "flex items-center p-6 in-[[data-slot=card]:has(>[data-slot=card-panel])]:pt-4", + className, + ), + "data-slot": "card-footer", + }; + + return useRender({ + defaultTagName: "div", + props: mergeProps<"div">(defaultProps, props), + render, + }); +} + +export { + Card, + CardFrame, + CardFrameHeader, + CardFrameTitle, + CardFrameDescription, + CardFrameFooter, + CardAction, + CardDescription, + CardFooter, + CardHeader, + CardPanel, + CardPanel as CardContent, + CardTitle, +}; diff --git a/apps/renderer/src/components/ui/checkbox-group.tsx b/apps/renderer/src/components/ui/checkbox-group.tsx new file mode 100644 index 0000000000..03879c5584 --- /dev/null +++ b/apps/renderer/src/components/ui/checkbox-group.tsx @@ -0,0 +1,14 @@ +import { CheckboxGroup as CheckboxGroupPrimitive } from "@base-ui/react/checkbox-group"; + +import { cn } from "#/lib/utils"; + +function CheckboxGroup({ className, ...props }: CheckboxGroupPrimitive.Props) { + return ( + + ); +} + +export { CheckboxGroup }; diff --git a/apps/renderer/src/components/ui/checkbox.tsx b/apps/renderer/src/components/ui/checkbox.tsx new file mode 100644 index 0000000000..d83dc9a8ca --- /dev/null +++ b/apps/renderer/src/components/ui/checkbox.tsx @@ -0,0 +1,58 @@ +import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"; + +import { cn } from "#/lib/utils"; + +function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) { + return ( + + ( + + {state.indeterminate ? ( + + + + ) : ( + + + + )} + + )} + /> + + ); +} + +export { Checkbox }; diff --git a/apps/renderer/src/components/ui/collapsible.tsx b/apps/renderer/src/components/ui/collapsible.tsx new file mode 100644 index 0000000000..2cf6810c81 --- /dev/null +++ b/apps/renderer/src/components/ui/collapsible.tsx @@ -0,0 +1,43 @@ +import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible"; + +import { cn } from "#/lib/utils"; + +function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) { + return ; +} + +function CollapsibleTrigger({ + className, + ...props +}: CollapsiblePrimitive.Trigger.Props) { + return ( + + ); +} + +function CollapsiblePanel({ + className, + ...props +}: CollapsiblePrimitive.Panel.Props) { + return ( + + ); +} + +export { + Collapsible, + CollapsibleTrigger, + CollapsiblePanel, + CollapsiblePanel as CollapsibleContent, +}; diff --git a/apps/renderer/src/components/ui/combobox.tsx b/apps/renderer/src/components/ui/combobox.tsx new file mode 100644 index 0000000000..79296dbd41 --- /dev/null +++ b/apps/renderer/src/components/ui/combobox.tsx @@ -0,0 +1,422 @@ +import { Combobox as ComboboxPrimitive } from "@base-ui/react/combobox"; +import { ChevronsUpDownIcon, XIcon } from "lucide-react"; +import * as React from "react"; + +import { cn } from "#/lib/utils"; +import { Input } from "#/components/ui/input"; +import { ScrollArea } from "#/components/ui/scroll-area"; + +const ComboboxContext = React.createContext<{ + chipsRef: React.RefObject | null; + multiple: boolean; +}>({ + chipsRef: null, + multiple: false, +}); + +function Combobox( + props: ComboboxPrimitive.Root.Props, +): React.JSX.Element { + const chipsRef = React.useRef(null); + return ( + + + + ); +} + +function ComboboxChipsInput({ + className, + size, + ...props +}: Omit & { + size?: "sm" | "default" | "lg" | number; + ref?: React.Ref; +}) { + const sizeValue = (size ?? "default") as "sm" | "default" | "lg" | number; + + return ( + + ); +} + +function ComboboxInput({ + className, + showTrigger = true, + showClear = false, + startAddon, + size, + ...props +}: Omit & { + showTrigger?: boolean; + showClear?: boolean; + startAddon?: React.ReactNode; + size?: "sm" | "default" | "lg" | number; + ref?: React.Ref; +}) { + const sizeValue = (size ?? "default") as "sm" | "default" | "lg" | number; + + return ( +
+ {startAddon && ( + + )} + + } + {...props} + /> + {showTrigger && ( + + + + )} + {showClear && ( + + + + )} +
+ ); +} + +function ComboboxTrigger({ + className, + ...props +}: ComboboxPrimitive.Trigger.Props) { + return ( + + ); +} + +function ComboboxPopup({ + className, + children, + side = "bottom", + sideOffset = 4, + alignOffset, + align = "start", + ...props +}: ComboboxPrimitive.Popup.Props & { + align?: ComboboxPrimitive.Positioner.Props["align"]; + sideOffset?: ComboboxPrimitive.Positioner.Props["sideOffset"]; + alignOffset?: ComboboxPrimitive.Positioner.Props["alignOffset"]; + side?: ComboboxPrimitive.Positioner.Props["side"]; +}) { + const { chipsRef } = React.useContext(ComboboxContext); + + return ( + + + + + {children} + + + + + ); +} + +function ComboboxItem({ + className, + children, + ...props +}: ComboboxPrimitive.Item.Props) { + return ( + + + + + + +
{children}
+
+ ); +} + +function ComboboxSeparator({ + className, + ...props +}: ComboboxPrimitive.Separator.Props) { + return ( + + ); +} + +function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) { + return ( + + ); +} + +function ComboboxGroupLabel({ + className, + ...props +}: ComboboxPrimitive.GroupLabel.Props) { + return ( + + ); +} + +function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) { + return ( + + ); +} + +function ComboboxRow({ className, ...props }: ComboboxPrimitive.Row.Props) { + return ( + + ); +} + +function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) { + return ; +} + +function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) { + return ( + + + + ); +} + +function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) { + return ( + + ); +} + +function ComboboxStatus({ + className, + ...props +}: ComboboxPrimitive.Status.Props) { + return ( + + ); +} + +function ComboboxCollection(props: ComboboxPrimitive.Collection.Props) { + return ( + + ); +} + +function ComboboxChips({ + className, + children, + startAddon, + ...props +}: ComboboxPrimitive.Chips.Props & { + startAddon?: React.ReactNode; +}) { + const { chipsRef } = React.useContext(ComboboxContext); + + return ( + { + const target = e.target as HTMLElement; + const isChip = target.closest('[data-slot="combobox-chip"]'); + if (isChip || !chipsRef?.current) return; + e.preventDefault(); + const input: HTMLInputElement | null = + chipsRef.current.querySelector("input"); + if (input && !chipsRef.current.querySelector("input:focus")) { + input.focus(); + } + }} + ref={chipsRef as React.Ref | null} + {...props} + > + {startAddon && ( + + )} + {children} + + ); +} + +function ComboboxChip({ children, ...props }: ComboboxPrimitive.Chip.Props) { + return ( + + {children} + + + ); +} + +function ComboboxChipRemove(props: ComboboxPrimitive.ChipRemove.Props) { + return ( + + + + ); +} + +const useComboboxFilter = ComboboxPrimitive.useFilter; + +export { + Combobox, + ComboboxChipsInput, + ComboboxInput, + ComboboxTrigger, + ComboboxPopup, + ComboboxItem, + ComboboxSeparator, + ComboboxGroup, + ComboboxGroupLabel, + ComboboxEmpty, + ComboboxValue, + ComboboxList, + ComboboxClear, + ComboboxStatus, + ComboboxRow, + ComboboxCollection, + ComboboxChips, + ComboboxChip, + useComboboxFilter, +}; diff --git a/apps/renderer/src/components/ui/command.tsx b/apps/renderer/src/components/ui/command.tsx new file mode 100644 index 0000000000..a94ee05f82 --- /dev/null +++ b/apps/renderer/src/components/ui/command.tsx @@ -0,0 +1,262 @@ +import { Dialog as CommandDialogPrimitive } from "@base-ui/react/dialog"; +import { SearchIcon } from "lucide-react"; +import type * as React from "react"; +import { cn } from "#/lib/utils"; +import { + Autocomplete, + AutocompleteCollection, + AutocompleteEmpty, + AutocompleteGroup, + AutocompleteGroupLabel, + AutocompleteInput, + AutocompleteItem, + AutocompleteList, + AutocompleteSeparator, +} from "#/components/ui/autocomplete"; + +const CommandDialog = CommandDialogPrimitive.Root; + +const CommandDialogPortal = CommandDialogPrimitive.Portal; + +const CommandCreateHandle = CommandDialogPrimitive.createHandle; + +function CommandDialogTrigger(props: CommandDialogPrimitive.Trigger.Props) { + return ( + + ); +} + +function CommandDialogBackdrop({ + className, + ...props +}: CommandDialogPrimitive.Backdrop.Props) { + return ( + + ); +} + +function CommandDialogViewport({ + className, + ...props +}: CommandDialogPrimitive.Viewport.Props) { + return ( + + ); +} + +function CommandDialogPopup({ + className, + children, + ...props +}: CommandDialogPrimitive.Popup.Props) { + return ( + + + + + {children} + + + + ); +} + +function Command({ + autoHighlight = "always", + keepHighlight = true, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandInput({ + className, + placeholder = undefined, + ...props +}: React.ComponentProps) { + return ( +
+ } + {...props} + /> +
+ ); +} + +function CommandList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandEmpty({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandPanel({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CommandGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandGroupLabel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandCollection({ + ...props +}: React.ComponentProps) { + return ; +} + +function CommandItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandShortcut({ className, ...props }: React.ComponentProps<"kbd">) { + return ( + + ); +} + +function CommandFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { + CommandCreateHandle, + Command, + CommandCollection, + CommandDialog, + CommandDialogPopup, + CommandDialogTrigger, + CommandEmpty, + CommandFooter, + CommandGroup, + CommandGroupLabel, + CommandInput, + CommandItem, + CommandList, + CommandPanel, + CommandSeparator, + CommandShortcut, +}; diff --git a/apps/renderer/src/components/ui/dialog.tsx b/apps/renderer/src/components/ui/dialog.tsx new file mode 100644 index 0000000000..50b6caa269 --- /dev/null +++ b/apps/renderer/src/components/ui/dialog.tsx @@ -0,0 +1,194 @@ +import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"; +import { XIcon } from "lucide-react"; +import { cn } from "#/lib/utils"; +import { Button } from "#/components/ui/button"; +import { ScrollArea } from "#/components/ui/scroll-area"; + +const DialogCreateHandle = DialogPrimitive.createHandle; + +const Dialog = DialogPrimitive.Root; + +const DialogPortal = DialogPrimitive.Portal; + +function DialogTrigger(props: DialogPrimitive.Trigger.Props) { + return ; +} + +function DialogClose(props: DialogPrimitive.Close.Props) { + return ; +} + +function DialogBackdrop({ + className, + ...props +}: DialogPrimitive.Backdrop.Props) { + return ( + + ); +} + +function DialogViewport({ + className, + ...props +}: DialogPrimitive.Viewport.Props) { + return ( + + ); +} + +function DialogPopup({ + className, + children, + showCloseButton = true, + bottomStickOnMobile = true, + ...props +}: DialogPrimitive.Popup.Props & { + showCloseButton?: boolean; + bottomStickOnMobile?: boolean; +}) { + return ( + + + + + {children} + {showCloseButton && ( + } + > + + + )} + + + + ); +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function DialogFooter({ + className, + variant = "default", + ...props +}: React.ComponentProps<"div"> & { + variant?: "default" | "bare"; +}) { + return ( +
+ ); +} + +function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) { + return ( + + ); +} + +function DialogDescription({ + className, + ...props +}: DialogPrimitive.Description.Props) { + return ( + + ); +} + +function DialogPanel({ + className, + scrollFade = true, + ...props +}: React.ComponentProps<"div"> & { scrollFade?: boolean }) { + return ( + +
+ + ); +} + +export { + DialogCreateHandle, + Dialog, + DialogTrigger, + DialogPortal, + DialogClose, + DialogBackdrop, + DialogBackdrop as DialogOverlay, + DialogPopup, + DialogPopup as DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, + DialogPanel, + DialogViewport, +}; diff --git a/apps/renderer/src/components/ui/empty.tsx b/apps/renderer/src/components/ui/empty.tsx new file mode 100644 index 0000000000..4e07c5deeb --- /dev/null +++ b/apps/renderer/src/components/ui/empty.tsx @@ -0,0 +1,127 @@ +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "#/lib/utils"; + +function Empty({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +const emptyMediaVariants = cva( + "flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0", + { + defaultVariants: { + variant: "default", + }, + variants: { + variant: { + default: "bg-transparent", + icon: "relative flex size-9 shrink-0 items-center justify-center rounded-md border bg-card not-dark:bg-clip-padding text-foreground shadow-sm/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-md)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)] [&_svg:not([class*='size-'])]:size-4.5", + }, + }, + }, +); + +function EmptyMedia({ + className, + variant = "default", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ {variant === "icon" && ( + <> +