The recipes.blue monorepo recipes.blue
recipes appview atproto
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: shadcn init

+2103 -26
+22
apps/web/components.json
··· 1 + { 2 + "$schema": "https://ui.shadcn.com/schema.json", 3 + "style": "new-york", 4 + "rsc": false, 5 + "tsx": true, 6 + "tailwind": { 7 + "config": "", 8 + "css": "src/index.css", 9 + "baseColor": "zinc", 10 + "cssVariables": true, 11 + "prefix": "" 12 + }, 13 + "iconLibrary": "lucide", 14 + "aliases": { 15 + "components": "@/components", 16 + "utils": "@/lib/utils", 17 + "ui": "@/components/ui", 18 + "lib": "@/lib", 19 + "hooks": "@/hooks" 20 + }, 21 + "registries": {} 22 + }
+12
apps/web/package.json
··· 10 10 "preview": "vite preview" 11 11 }, 12 12 "dependencies": { 13 + "@radix-ui/react-avatar": "^1.1.11", 14 + "@radix-ui/react-dialog": "^1.1.15", 15 + "@radix-ui/react-dropdown-menu": "^2.1.16", 16 + "@radix-ui/react-label": "^2.1.8", 17 + "@radix-ui/react-separator": "^1.1.8", 18 + "@radix-ui/react-slot": "^1.2.4", 19 + "@radix-ui/react-tooltip": "^1.2.8", 20 + "class-variance-authority": "^0.7.1", 21 + "clsx": "^2.1.1", 22 + "lucide-react": "^0.556.0", 13 23 "react": "^19.2.0", 14 24 "react-dom": "^19.2.0", 25 + "tailwind-merge": "^3.4.0", 15 26 "tailwindcss": "^4.1.17" 16 27 }, 17 28 "devDependencies": { ··· 26 37 "eslint-plugin-react-hooks": "^7.0.1", 27 38 "eslint-plugin-react-refresh": "^0.4.24", 28 39 "globals": "^16.5.0", 40 + "tw-animate-css": "^1.4.0", 29 41 "typescript": "~5.9.3", 30 42 "typescript-eslint": "^8.46.4", 31 43 "vite": "npm:rolldown-vite@7.2.5"
-1
apps/web/src/assets/react.svg
··· 1 - <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
+51
apps/web/src/components/ui/avatar.tsx
··· 1 + import * as React from "react" 2 + import * as AvatarPrimitive from "@radix-ui/react-avatar" 3 + 4 + import { cn } from "@/lib/utils" 5 + 6 + function Avatar({ 7 + className, 8 + ...props 9 + }: React.ComponentProps<typeof AvatarPrimitive.Root>) { 10 + return ( 11 + <AvatarPrimitive.Root 12 + data-slot="avatar" 13 + className={cn( 14 + "relative flex size-8 shrink-0 overflow-hidden rounded-full", 15 + className 16 + )} 17 + {...props} 18 + /> 19 + ) 20 + } 21 + 22 + function AvatarImage({ 23 + className, 24 + ...props 25 + }: React.ComponentProps<typeof AvatarPrimitive.Image>) { 26 + return ( 27 + <AvatarPrimitive.Image 28 + data-slot="avatar-image" 29 + className={cn("aspect-square size-full", className)} 30 + {...props} 31 + /> 32 + ) 33 + } 34 + 35 + function AvatarFallback({ 36 + className, 37 + ...props 38 + }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) { 39 + return ( 40 + <AvatarPrimitive.Fallback 41 + data-slot="avatar-fallback" 42 + className={cn( 43 + "bg-muted flex size-full items-center justify-center rounded-full", 44 + className 45 + )} 46 + {...props} 47 + /> 48 + ) 49 + } 50 + 51 + export { Avatar, AvatarImage, AvatarFallback }
+60
apps/web/src/components/ui/button.tsx
··· 1 + import * as React from "react" 2 + import { Slot } from "@radix-ui/react-slot" 3 + import { cva, type VariantProps } from "class-variance-authority" 4 + 5 + import { cn } from "@/lib/utils" 6 + 7 + const buttonVariants = cva( 8 + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 + { 10 + variants: { 11 + variant: { 12 + default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 + destructive: 14 + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 15 + outline: 16 + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 17 + secondary: 18 + "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 + ghost: 20 + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 21 + link: "text-primary underline-offset-4 hover:underline", 22 + }, 23 + size: { 24 + default: "h-9 px-4 py-2 has-[>svg]:px-3", 25 + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 26 + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 27 + icon: "size-9", 28 + "icon-sm": "size-8", 29 + "icon-lg": "size-10", 30 + }, 31 + }, 32 + defaultVariants: { 33 + variant: "default", 34 + size: "default", 35 + }, 36 + } 37 + ) 38 + 39 + function Button({ 40 + className, 41 + variant, 42 + size, 43 + asChild = false, 44 + ...props 45 + }: React.ComponentProps<"button"> & 46 + VariantProps<typeof buttonVariants> & { 47 + asChild?: boolean 48 + }) { 49 + const Comp = asChild ? Slot : "button" 50 + 51 + return ( 52 + <Comp 53 + data-slot="button" 54 + className={cn(buttonVariants({ variant, size, className }))} 55 + {...props} 56 + /> 57 + ) 58 + } 59 + 60 + export { Button, buttonVariants }
+92
apps/web/src/components/ui/card.tsx
··· 1 + import * as React from "react" 2 + 3 + import { cn } from "@/lib/utils" 4 + 5 + function Card({ className, ...props }: React.ComponentProps<"div">) { 6 + return ( 7 + <div 8 + data-slot="card" 9 + className={cn( 10 + "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", 11 + className 12 + )} 13 + {...props} 14 + /> 15 + ) 16 + } 17 + 18 + function CardHeader({ className, ...props }: React.ComponentProps<"div">) { 19 + return ( 20 + <div 21 + data-slot="card-header" 22 + className={cn( 23 + "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", 24 + className 25 + )} 26 + {...props} 27 + /> 28 + ) 29 + } 30 + 31 + function CardTitle({ className, ...props }: React.ComponentProps<"div">) { 32 + return ( 33 + <div 34 + data-slot="card-title" 35 + className={cn("leading-none font-semibold", className)} 36 + {...props} 37 + /> 38 + ) 39 + } 40 + 41 + function CardDescription({ className, ...props }: React.ComponentProps<"div">) { 42 + return ( 43 + <div 44 + data-slot="card-description" 45 + className={cn("text-muted-foreground text-sm", className)} 46 + {...props} 47 + /> 48 + ) 49 + } 50 + 51 + function CardAction({ className, ...props }: React.ComponentProps<"div">) { 52 + return ( 53 + <div 54 + data-slot="card-action" 55 + className={cn( 56 + "col-start-2 row-span-2 row-start-1 self-start justify-self-end", 57 + className 58 + )} 59 + {...props} 60 + /> 61 + ) 62 + } 63 + 64 + function CardContent({ className, ...props }: React.ComponentProps<"div">) { 65 + return ( 66 + <div 67 + data-slot="card-content" 68 + className={cn("px-6", className)} 69 + {...props} 70 + /> 71 + ) 72 + } 73 + 74 + function CardFooter({ className, ...props }: React.ComponentProps<"div">) { 75 + return ( 76 + <div 77 + data-slot="card-footer" 78 + className={cn("flex items-center px-6 [.border-t]:pt-6", className)} 79 + {...props} 80 + /> 81 + ) 82 + } 83 + 84 + export { 85 + Card, 86 + CardHeader, 87 + CardFooter, 88 + CardTitle, 89 + CardAction, 90 + CardDescription, 91 + CardContent, 92 + }
+257
apps/web/src/components/ui/dropdown-menu.tsx
··· 1 + "use client" 2 + 3 + import * as React from "react" 4 + import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" 5 + import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" 6 + 7 + import { cn } from "@/lib/utils" 8 + 9 + function DropdownMenu({ 10 + ...props 11 + }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) { 12 + return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} /> 13 + } 14 + 15 + function DropdownMenuPortal({ 16 + ...props 17 + }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) { 18 + return ( 19 + <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} /> 20 + ) 21 + } 22 + 23 + function DropdownMenuTrigger({ 24 + ...props 25 + }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) { 26 + return ( 27 + <DropdownMenuPrimitive.Trigger 28 + data-slot="dropdown-menu-trigger" 29 + {...props} 30 + /> 31 + ) 32 + } 33 + 34 + function DropdownMenuContent({ 35 + className, 36 + sideOffset = 4, 37 + ...props 38 + }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) { 39 + return ( 40 + <DropdownMenuPrimitive.Portal> 41 + <DropdownMenuPrimitive.Content 42 + data-slot="dropdown-menu-content" 43 + sideOffset={sideOffset} 44 + className={cn( 45 + "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 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md", 46 + className 47 + )} 48 + {...props} 49 + /> 50 + </DropdownMenuPrimitive.Portal> 51 + ) 52 + } 53 + 54 + function DropdownMenuGroup({ 55 + ...props 56 + }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) { 57 + return ( 58 + <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} /> 59 + ) 60 + } 61 + 62 + function DropdownMenuItem({ 63 + className, 64 + inset, 65 + variant = "default", 66 + ...props 67 + }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & { 68 + inset?: boolean 69 + variant?: "default" | "destructive" 70 + }) { 71 + return ( 72 + <DropdownMenuPrimitive.Item 73 + data-slot="dropdown-menu-item" 74 + data-inset={inset} 75 + data-variant={variant} 76 + className={cn( 77 + "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", 78 + className 79 + )} 80 + {...props} 81 + /> 82 + ) 83 + } 84 + 85 + function DropdownMenuCheckboxItem({ 86 + className, 87 + children, 88 + checked, 89 + ...props 90 + }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) { 91 + return ( 92 + <DropdownMenuPrimitive.CheckboxItem 93 + data-slot="dropdown-menu-checkbox-item" 94 + className={cn( 95 + "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 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", 96 + className 97 + )} 98 + checked={checked} 99 + {...props} 100 + > 101 + <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> 102 + <DropdownMenuPrimitive.ItemIndicator> 103 + <CheckIcon className="size-4" /> 104 + </DropdownMenuPrimitive.ItemIndicator> 105 + </span> 106 + {children} 107 + </DropdownMenuPrimitive.CheckboxItem> 108 + ) 109 + } 110 + 111 + function DropdownMenuRadioGroup({ 112 + ...props 113 + }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) { 114 + return ( 115 + <DropdownMenuPrimitive.RadioGroup 116 + data-slot="dropdown-menu-radio-group" 117 + {...props} 118 + /> 119 + ) 120 + } 121 + 122 + function DropdownMenuRadioItem({ 123 + className, 124 + children, 125 + ...props 126 + }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) { 127 + return ( 128 + <DropdownMenuPrimitive.RadioItem 129 + data-slot="dropdown-menu-radio-item" 130 + className={cn( 131 + "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 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", 132 + className 133 + )} 134 + {...props} 135 + > 136 + <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> 137 + <DropdownMenuPrimitive.ItemIndicator> 138 + <CircleIcon className="size-2 fill-current" /> 139 + </DropdownMenuPrimitive.ItemIndicator> 140 + </span> 141 + {children} 142 + </DropdownMenuPrimitive.RadioItem> 143 + ) 144 + } 145 + 146 + function DropdownMenuLabel({ 147 + className, 148 + inset, 149 + ...props 150 + }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & { 151 + inset?: boolean 152 + }) { 153 + return ( 154 + <DropdownMenuPrimitive.Label 155 + data-slot="dropdown-menu-label" 156 + data-inset={inset} 157 + className={cn( 158 + "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", 159 + className 160 + )} 161 + {...props} 162 + /> 163 + ) 164 + } 165 + 166 + function DropdownMenuSeparator({ 167 + className, 168 + ...props 169 + }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) { 170 + return ( 171 + <DropdownMenuPrimitive.Separator 172 + data-slot="dropdown-menu-separator" 173 + className={cn("bg-border -mx-1 my-1 h-px", className)} 174 + {...props} 175 + /> 176 + ) 177 + } 178 + 179 + function DropdownMenuShortcut({ 180 + className, 181 + ...props 182 + }: React.ComponentProps<"span">) { 183 + return ( 184 + <span 185 + data-slot="dropdown-menu-shortcut" 186 + className={cn( 187 + "text-muted-foreground ml-auto text-xs tracking-widest", 188 + className 189 + )} 190 + {...props} 191 + /> 192 + ) 193 + } 194 + 195 + function DropdownMenuSub({ 196 + ...props 197 + }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) { 198 + return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} /> 199 + } 200 + 201 + function DropdownMenuSubTrigger({ 202 + className, 203 + inset, 204 + children, 205 + ...props 206 + }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & { 207 + inset?: boolean 208 + }) { 209 + return ( 210 + <DropdownMenuPrimitive.SubTrigger 211 + data-slot="dropdown-menu-sub-trigger" 212 + data-inset={inset} 213 + className={cn( 214 + "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", 215 + className 216 + )} 217 + {...props} 218 + > 219 + {children} 220 + <ChevronRightIcon className="ml-auto size-4" /> 221 + </DropdownMenuPrimitive.SubTrigger> 222 + ) 223 + } 224 + 225 + function DropdownMenuSubContent({ 226 + className, 227 + ...props 228 + }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) { 229 + return ( 230 + <DropdownMenuPrimitive.SubContent 231 + data-slot="dropdown-menu-sub-content" 232 + className={cn( 233 + "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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg", 234 + className 235 + )} 236 + {...props} 237 + /> 238 + ) 239 + } 240 + 241 + export { 242 + DropdownMenu, 243 + DropdownMenuPortal, 244 + DropdownMenuTrigger, 245 + DropdownMenuContent, 246 + DropdownMenuGroup, 247 + DropdownMenuLabel, 248 + DropdownMenuItem, 249 + DropdownMenuCheckboxItem, 250 + DropdownMenuRadioGroup, 251 + DropdownMenuRadioItem, 252 + DropdownMenuSeparator, 253 + DropdownMenuShortcut, 254 + DropdownMenuSub, 255 + DropdownMenuSubTrigger, 256 + DropdownMenuSubContent, 257 + }
+248
apps/web/src/components/ui/field.tsx
··· 1 + "use client" 2 + 3 + import { useMemo } from "react" 4 + import { cva, type VariantProps } from "class-variance-authority" 5 + 6 + import { cn } from "@/lib/utils" 7 + import { Label } from "@/components/ui/label" 8 + import { Separator } from "@/components/ui/separator" 9 + 10 + function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { 11 + return ( 12 + <fieldset 13 + data-slot="field-set" 14 + className={cn( 15 + "flex flex-col gap-6", 16 + "has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", 17 + className 18 + )} 19 + {...props} 20 + /> 21 + ) 22 + } 23 + 24 + function FieldLegend({ 25 + className, 26 + variant = "legend", 27 + ...props 28 + }: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) { 29 + return ( 30 + <legend 31 + data-slot="field-legend" 32 + data-variant={variant} 33 + className={cn( 34 + "mb-3 font-medium", 35 + "data-[variant=legend]:text-base", 36 + "data-[variant=label]:text-sm", 37 + className 38 + )} 39 + {...props} 40 + /> 41 + ) 42 + } 43 + 44 + function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { 45 + return ( 46 + <div 47 + data-slot="field-group" 48 + className={cn( 49 + "group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4", 50 + className 51 + )} 52 + {...props} 53 + /> 54 + ) 55 + } 56 + 57 + const fieldVariants = cva( 58 + "group/field flex w-full gap-3 data-[invalid=true]:text-destructive", 59 + { 60 + variants: { 61 + orientation: { 62 + vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"], 63 + horizontal: [ 64 + "flex-row items-center", 65 + "[&>[data-slot=field-label]]:flex-auto", 66 + "has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", 67 + ], 68 + responsive: [ 69 + "flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto", 70 + "@md/field-group:[&>[data-slot=field-label]]:flex-auto", 71 + "@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", 72 + ], 73 + }, 74 + }, 75 + defaultVariants: { 76 + orientation: "vertical", 77 + }, 78 + } 79 + ) 80 + 81 + function Field({ 82 + className, 83 + orientation = "vertical", 84 + ...props 85 + }: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) { 86 + return ( 87 + <div 88 + role="group" 89 + data-slot="field" 90 + data-orientation={orientation} 91 + className={cn(fieldVariants({ orientation }), className)} 92 + {...props} 93 + /> 94 + ) 95 + } 96 + 97 + function FieldContent({ className, ...props }: React.ComponentProps<"div">) { 98 + return ( 99 + <div 100 + data-slot="field-content" 101 + className={cn( 102 + "group/field-content flex flex-1 flex-col gap-1.5 leading-snug", 103 + className 104 + )} 105 + {...props} 106 + /> 107 + ) 108 + } 109 + 110 + function FieldLabel({ 111 + className, 112 + ...props 113 + }: React.ComponentProps<typeof Label>) { 114 + return ( 115 + <Label 116 + data-slot="field-label" 117 + className={cn( 118 + "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50", 119 + "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4", 120 + "has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10", 121 + className 122 + )} 123 + {...props} 124 + /> 125 + ) 126 + } 127 + 128 + function FieldTitle({ className, ...props }: React.ComponentProps<"div">) { 129 + return ( 130 + <div 131 + data-slot="field-label" 132 + className={cn( 133 + "flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50", 134 + className 135 + )} 136 + {...props} 137 + /> 138 + ) 139 + } 140 + 141 + function FieldDescription({ className, ...props }: React.ComponentProps<"p">) { 142 + return ( 143 + <p 144 + data-slot="field-description" 145 + className={cn( 146 + "text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance", 147 + "last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5", 148 + "[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", 149 + className 150 + )} 151 + {...props} 152 + /> 153 + ) 154 + } 155 + 156 + function FieldSeparator({ 157 + children, 158 + className, 159 + ...props 160 + }: React.ComponentProps<"div"> & { 161 + children?: React.ReactNode 162 + }) { 163 + return ( 164 + <div 165 + data-slot="field-separator" 166 + data-content={!!children} 167 + className={cn( 168 + "relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2", 169 + className 170 + )} 171 + {...props} 172 + > 173 + <Separator className="absolute inset-0 top-1/2" /> 174 + {children && ( 175 + <span 176 + className="bg-background text-muted-foreground relative mx-auto block w-fit px-2" 177 + data-slot="field-separator-content" 178 + > 179 + {children} 180 + </span> 181 + )} 182 + </div> 183 + ) 184 + } 185 + 186 + function FieldError({ 187 + className, 188 + children, 189 + errors, 190 + ...props 191 + }: React.ComponentProps<"div"> & { 192 + errors?: Array<{ message?: string } | undefined> 193 + }) { 194 + const content = useMemo(() => { 195 + if (children) { 196 + return children 197 + } 198 + 199 + if (!errors?.length) { 200 + return null 201 + } 202 + 203 + const uniqueErrors = [ 204 + ...new Map(errors.map((error) => [error?.message, error])).values(), 205 + ] 206 + 207 + if (uniqueErrors?.length == 1) { 208 + return uniqueErrors[0]?.message 209 + } 210 + 211 + return ( 212 + <ul className="ml-4 flex list-disc flex-col gap-1"> 213 + {uniqueErrors.map( 214 + (error, index) => 215 + error?.message && <li key={index}>{error.message}</li> 216 + )} 217 + </ul> 218 + ) 219 + }, [children, errors]) 220 + 221 + if (!content) { 222 + return null 223 + } 224 + 225 + return ( 226 + <div 227 + role="alert" 228 + data-slot="field-error" 229 + className={cn("text-destructive text-sm font-normal", className)} 230 + {...props} 231 + > 232 + {content} 233 + </div> 234 + ) 235 + } 236 + 237 + export { 238 + Field, 239 + FieldLabel, 240 + FieldDescription, 241 + FieldError, 242 + FieldGroup, 243 + FieldLegend, 244 + FieldSeparator, 245 + FieldSet, 246 + FieldContent, 247 + FieldTitle, 248 + }
+168
apps/web/src/components/ui/input-group.tsx
··· 1 + import * as React from "react" 2 + import { cva, type VariantProps } from "class-variance-authority" 3 + 4 + import { cn } from "@/lib/utils" 5 + import { Button } from "@/components/ui/button" 6 + import { Input } from "@/components/ui/input" 7 + import { Textarea } from "@/components/ui/textarea" 8 + 9 + function InputGroup({ className, ...props }: React.ComponentProps<"div">) { 10 + return ( 11 + <div 12 + data-slot="input-group" 13 + role="group" 14 + className={cn( 15 + "group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none", 16 + "h-9 min-w-0 has-[>textarea]:h-auto", 17 + 18 + // Variants based on alignment. 19 + "has-[>[data-align=inline-start]]:[&>input]:pl-2", 20 + "has-[>[data-align=inline-end]]:[&>input]:pr-2", 21 + "has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3", 22 + "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3", 23 + 24 + // Focus state. 25 + "has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]", 26 + 27 + // Error state. 28 + "has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40", 29 + 30 + className 31 + )} 32 + {...props} 33 + /> 34 + ) 35 + } 36 + 37 + const inputGroupAddonVariants = cva( 38 + "text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50", 39 + { 40 + variants: { 41 + align: { 42 + "inline-start": 43 + "order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]", 44 + "inline-end": 45 + "order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]", 46 + "block-start": 47 + "order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5", 48 + "block-end": 49 + "order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5", 50 + }, 51 + }, 52 + defaultVariants: { 53 + align: "inline-start", 54 + }, 55 + } 56 + ) 57 + 58 + function InputGroupAddon({ 59 + className, 60 + align = "inline-start", 61 + ...props 62 + }: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) { 63 + return ( 64 + <div 65 + role="group" 66 + data-slot="input-group-addon" 67 + data-align={align} 68 + className={cn(inputGroupAddonVariants({ align }), className)} 69 + onClick={(e) => { 70 + if ((e.target as HTMLElement).closest("button")) { 71 + return 72 + } 73 + e.currentTarget.parentElement?.querySelector("input")?.focus() 74 + }} 75 + {...props} 76 + /> 77 + ) 78 + } 79 + 80 + const inputGroupButtonVariants = cva( 81 + "text-sm shadow-none flex gap-2 items-center", 82 + { 83 + variants: { 84 + size: { 85 + xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2", 86 + sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5", 87 + "icon-xs": 88 + "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0", 89 + "icon-sm": "size-8 p-0 has-[>svg]:p-0", 90 + }, 91 + }, 92 + defaultVariants: { 93 + size: "xs", 94 + }, 95 + } 96 + ) 97 + 98 + function InputGroupButton({ 99 + className, 100 + type = "button", 101 + variant = "ghost", 102 + size = "xs", 103 + ...props 104 + }: Omit<React.ComponentProps<typeof Button>, "size"> & 105 + VariantProps<typeof inputGroupButtonVariants>) { 106 + return ( 107 + <Button 108 + type={type} 109 + data-size={size} 110 + variant={variant} 111 + className={cn(inputGroupButtonVariants({ size }), className)} 112 + {...props} 113 + /> 114 + ) 115 + } 116 + 117 + function InputGroupText({ className, ...props }: React.ComponentProps<"span">) { 118 + return ( 119 + <span 120 + className={cn( 121 + "text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4", 122 + className 123 + )} 124 + {...props} 125 + /> 126 + ) 127 + } 128 + 129 + function InputGroupInput({ 130 + className, 131 + ...props 132 + }: React.ComponentProps<"input">) { 133 + return ( 134 + <Input 135 + data-slot="input-group-control" 136 + className={cn( 137 + "flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent", 138 + className 139 + )} 140 + {...props} 141 + /> 142 + ) 143 + } 144 + 145 + function InputGroupTextarea({ 146 + className, 147 + ...props 148 + }: React.ComponentProps<"textarea">) { 149 + return ( 150 + <Textarea 151 + data-slot="input-group-control" 152 + className={cn( 153 + "flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent", 154 + className 155 + )} 156 + {...props} 157 + /> 158 + ) 159 + } 160 + 161 + export { 162 + InputGroup, 163 + InputGroupAddon, 164 + InputGroupButton, 165 + InputGroupText, 166 + InputGroupInput, 167 + InputGroupTextarea, 168 + }
+21
apps/web/src/components/ui/input.tsx
··· 1 + import * as React from "react" 2 + 3 + import { cn } from "@/lib/utils" 4 + 5 + function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 + return ( 7 + <input 8 + type={type} 9 + data-slot="input" 10 + className={cn( 11 + "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", 12 + "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", 13 + "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 14 + className 15 + )} 16 + {...props} 17 + /> 18 + ) 19 + } 20 + 21 + export { Input }
+22
apps/web/src/components/ui/label.tsx
··· 1 + import * as React from "react" 2 + import * as LabelPrimitive from "@radix-ui/react-label" 3 + 4 + import { cn } from "@/lib/utils" 5 + 6 + function Label({ 7 + className, 8 + ...props 9 + }: React.ComponentProps<typeof LabelPrimitive.Root>) { 10 + return ( 11 + <LabelPrimitive.Root 12 + data-slot="label" 13 + className={cn( 14 + "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", 15 + className 16 + )} 17 + {...props} 18 + /> 19 + ) 20 + } 21 + 22 + export { Label }
+28
apps/web/src/components/ui/separator.tsx
··· 1 + "use client" 2 + 3 + import * as React from "react" 4 + import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 + 6 + import { cn } from "@/lib/utils" 7 + 8 + function Separator({ 9 + className, 10 + orientation = "horizontal", 11 + decorative = true, 12 + ...props 13 + }: React.ComponentProps<typeof SeparatorPrimitive.Root>) { 14 + return ( 15 + <SeparatorPrimitive.Root 16 + data-slot="separator" 17 + decorative={decorative} 18 + orientation={orientation} 19 + className={cn( 20 + "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", 21 + className 22 + )} 23 + {...props} 24 + /> 25 + ) 26 + } 27 + 28 + export { Separator }
+137
apps/web/src/components/ui/sheet.tsx
··· 1 + import * as React from "react" 2 + import * as SheetPrimitive from "@radix-ui/react-dialog" 3 + import { XIcon } from "lucide-react" 4 + 5 + import { cn } from "@/lib/utils" 6 + 7 + function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) { 8 + return <SheetPrimitive.Root data-slot="sheet" {...props} /> 9 + } 10 + 11 + function SheetTrigger({ 12 + ...props 13 + }: React.ComponentProps<typeof SheetPrimitive.Trigger>) { 14 + return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} /> 15 + } 16 + 17 + function SheetClose({ 18 + ...props 19 + }: React.ComponentProps<typeof SheetPrimitive.Close>) { 20 + return <SheetPrimitive.Close data-slot="sheet-close" {...props} /> 21 + } 22 + 23 + function SheetPortal({ 24 + ...props 25 + }: React.ComponentProps<typeof SheetPrimitive.Portal>) { 26 + return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} /> 27 + } 28 + 29 + function SheetOverlay({ 30 + className, 31 + ...props 32 + }: React.ComponentProps<typeof SheetPrimitive.Overlay>) { 33 + return ( 34 + <SheetPrimitive.Overlay 35 + data-slot="sheet-overlay" 36 + className={cn( 37 + "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50", 38 + className 39 + )} 40 + {...props} 41 + /> 42 + ) 43 + } 44 + 45 + function SheetContent({ 46 + className, 47 + children, 48 + side = "right", 49 + ...props 50 + }: React.ComponentProps<typeof SheetPrimitive.Content> & { 51 + side?: "top" | "right" | "bottom" | "left" 52 + }) { 53 + return ( 54 + <SheetPortal> 55 + <SheetOverlay /> 56 + <SheetPrimitive.Content 57 + data-slot="sheet-content" 58 + className={cn( 59 + "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500", 60 + side === "right" && 61 + "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm", 62 + side === "left" && 63 + "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm", 64 + side === "top" && 65 + "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b", 66 + side === "bottom" && 67 + "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t", 68 + className 69 + )} 70 + {...props} 71 + > 72 + {children} 73 + <SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"> 74 + <XIcon className="size-4" /> 75 + <span className="sr-only">Close</span> 76 + </SheetPrimitive.Close> 77 + </SheetPrimitive.Content> 78 + </SheetPortal> 79 + ) 80 + } 81 + 82 + function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { 83 + return ( 84 + <div 85 + data-slot="sheet-header" 86 + className={cn("flex flex-col gap-1.5 p-4", className)} 87 + {...props} 88 + /> 89 + ) 90 + } 91 + 92 + function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { 93 + return ( 94 + <div 95 + data-slot="sheet-footer" 96 + className={cn("mt-auto flex flex-col gap-2 p-4", className)} 97 + {...props} 98 + /> 99 + ) 100 + } 101 + 102 + function SheetTitle({ 103 + className, 104 + ...props 105 + }: React.ComponentProps<typeof SheetPrimitive.Title>) { 106 + return ( 107 + <SheetPrimitive.Title 108 + data-slot="sheet-title" 109 + className={cn("text-foreground font-semibold", className)} 110 + {...props} 111 + /> 112 + ) 113 + } 114 + 115 + function SheetDescription({ 116 + className, 117 + ...props 118 + }: React.ComponentProps<typeof SheetPrimitive.Description>) { 119 + return ( 120 + <SheetPrimitive.Description 121 + data-slot="sheet-description" 122 + className={cn("text-muted-foreground text-sm", className)} 123 + {...props} 124 + /> 125 + ) 126 + } 127 + 128 + export { 129 + Sheet, 130 + SheetTrigger, 131 + SheetClose, 132 + SheetContent, 133 + SheetHeader, 134 + SheetFooter, 135 + SheetTitle, 136 + SheetDescription, 137 + }
+724
apps/web/src/components/ui/sidebar.tsx
··· 1 + import * as React from "react" 2 + import { Slot } from "@radix-ui/react-slot" 3 + import { cva, type VariantProps } from "class-variance-authority" 4 + import { PanelLeftIcon } from "lucide-react" 5 + 6 + import { useIsMobile } from "@/hooks/use-mobile" 7 + import { cn } from "@/lib/utils" 8 + import { Button } from "@/components/ui/button" 9 + import { Input } from "@/components/ui/input" 10 + import { Separator } from "@/components/ui/separator" 11 + import { 12 + Sheet, 13 + SheetContent, 14 + SheetDescription, 15 + SheetHeader, 16 + SheetTitle, 17 + } from "@/components/ui/sheet" 18 + import { Skeleton } from "@/components/ui/skeleton" 19 + import { 20 + Tooltip, 21 + TooltipContent, 22 + TooltipProvider, 23 + TooltipTrigger, 24 + } from "@/components/ui/tooltip" 25 + 26 + const SIDEBAR_COOKIE_NAME = "sidebar_state" 27 + const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 28 + const SIDEBAR_WIDTH = "16rem" 29 + const SIDEBAR_WIDTH_MOBILE = "18rem" 30 + const SIDEBAR_WIDTH_ICON = "3rem" 31 + const SIDEBAR_KEYBOARD_SHORTCUT = "b" 32 + 33 + type SidebarContextProps = { 34 + state: "expanded" | "collapsed" 35 + open: boolean 36 + setOpen: (open: boolean) => void 37 + openMobile: boolean 38 + setOpenMobile: (open: boolean) => void 39 + isMobile: boolean 40 + toggleSidebar: () => void 41 + } 42 + 43 + const SidebarContext = React.createContext<SidebarContextProps | null>(null) 44 + 45 + function useSidebar() { 46 + const context = React.useContext(SidebarContext) 47 + if (!context) { 48 + throw new Error("useSidebar must be used within a SidebarProvider.") 49 + } 50 + 51 + return context 52 + } 53 + 54 + function SidebarProvider({ 55 + defaultOpen = true, 56 + open: openProp, 57 + onOpenChange: setOpenProp, 58 + className, 59 + style, 60 + children, 61 + ...props 62 + }: React.ComponentProps<"div"> & { 63 + defaultOpen?: boolean 64 + open?: boolean 65 + onOpenChange?: (open: boolean) => void 66 + }) { 67 + const isMobile = useIsMobile() 68 + const [openMobile, setOpenMobile] = React.useState(false) 69 + 70 + // This is the internal state of the sidebar. 71 + // We use openProp and setOpenProp for control from outside the component. 72 + const [_open, _setOpen] = React.useState(defaultOpen) 73 + const open = openProp ?? _open 74 + const setOpen = React.useCallback( 75 + (value: boolean | ((value: boolean) => boolean)) => { 76 + const openState = typeof value === "function" ? value(open) : value 77 + if (setOpenProp) { 78 + setOpenProp(openState) 79 + } else { 80 + _setOpen(openState) 81 + } 82 + 83 + // This sets the cookie to keep the sidebar state. 84 + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` 85 + }, 86 + [setOpenProp, open] 87 + ) 88 + 89 + // Helper to toggle the sidebar. 90 + const toggleSidebar = React.useCallback(() => { 91 + return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open) 92 + }, [isMobile, setOpen, setOpenMobile]) 93 + 94 + // Adds a keyboard shortcut to toggle the sidebar. 95 + React.useEffect(() => { 96 + const handleKeyDown = (event: KeyboardEvent) => { 97 + if ( 98 + event.key === SIDEBAR_KEYBOARD_SHORTCUT && 99 + (event.metaKey || event.ctrlKey) 100 + ) { 101 + event.preventDefault() 102 + toggleSidebar() 103 + } 104 + } 105 + 106 + window.addEventListener("keydown", handleKeyDown) 107 + return () => window.removeEventListener("keydown", handleKeyDown) 108 + }, [toggleSidebar]) 109 + 110 + // We add a state so that we can do data-state="expanded" or "collapsed". 111 + // This makes it easier to style the sidebar with Tailwind classes. 112 + const state = open ? "expanded" : "collapsed" 113 + 114 + const contextValue = React.useMemo<SidebarContextProps>( 115 + () => ({ 116 + state, 117 + open, 118 + setOpen, 119 + isMobile, 120 + openMobile, 121 + setOpenMobile, 122 + toggleSidebar, 123 + }), 124 + [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] 125 + ) 126 + 127 + return ( 128 + <SidebarContext.Provider value={contextValue}> 129 + <TooltipProvider delayDuration={0}> 130 + <div 131 + data-slot="sidebar-wrapper" 132 + style={ 133 + { 134 + "--sidebar-width": SIDEBAR_WIDTH, 135 + "--sidebar-width-icon": SIDEBAR_WIDTH_ICON, 136 + ...style, 137 + } as React.CSSProperties 138 + } 139 + className={cn( 140 + "group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full", 141 + className 142 + )} 143 + {...props} 144 + > 145 + {children} 146 + </div> 147 + </TooltipProvider> 148 + </SidebarContext.Provider> 149 + ) 150 + } 151 + 152 + function Sidebar({ 153 + side = "left", 154 + variant = "sidebar", 155 + collapsible = "offcanvas", 156 + className, 157 + children, 158 + ...props 159 + }: React.ComponentProps<"div"> & { 160 + side?: "left" | "right" 161 + variant?: "sidebar" | "floating" | "inset" 162 + collapsible?: "offcanvas" | "icon" | "none" 163 + }) { 164 + const { isMobile, state, openMobile, setOpenMobile } = useSidebar() 165 + 166 + if (collapsible === "none") { 167 + return ( 168 + <div 169 + data-slot="sidebar" 170 + className={cn( 171 + "bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col", 172 + className 173 + )} 174 + {...props} 175 + > 176 + {children} 177 + </div> 178 + ) 179 + } 180 + 181 + if (isMobile) { 182 + return ( 183 + <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}> 184 + <SheetContent 185 + data-sidebar="sidebar" 186 + data-slot="sidebar" 187 + data-mobile="true" 188 + className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden" 189 + style={ 190 + { 191 + "--sidebar-width": SIDEBAR_WIDTH_MOBILE, 192 + } as React.CSSProperties 193 + } 194 + side={side} 195 + > 196 + <SheetHeader className="sr-only"> 197 + <SheetTitle>Sidebar</SheetTitle> 198 + <SheetDescription>Displays the mobile sidebar.</SheetDescription> 199 + </SheetHeader> 200 + <div className="flex h-full w-full flex-col">{children}</div> 201 + </SheetContent> 202 + </Sheet> 203 + ) 204 + } 205 + 206 + return ( 207 + <div 208 + className="group peer text-sidebar-foreground hidden md:block" 209 + data-state={state} 210 + data-collapsible={state === "collapsed" ? collapsible : ""} 211 + data-variant={variant} 212 + data-side={side} 213 + data-slot="sidebar" 214 + > 215 + {/* This is what handles the sidebar gap on desktop */} 216 + <div 217 + data-slot="sidebar-gap" 218 + className={cn( 219 + "relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear", 220 + "group-data-[collapsible=offcanvas]:w-0", 221 + "group-data-[side=right]:rotate-180", 222 + variant === "floating" || variant === "inset" 223 + ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]" 224 + : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)" 225 + )} 226 + /> 227 + <div 228 + data-slot="sidebar-container" 229 + className={cn( 230 + "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex", 231 + side === "left" 232 + ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]" 233 + : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]", 234 + // Adjust the padding for floating and inset variants. 235 + variant === "floating" || variant === "inset" 236 + ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]" 237 + : "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l", 238 + className 239 + )} 240 + {...props} 241 + > 242 + <div 243 + data-sidebar="sidebar" 244 + data-slot="sidebar-inner" 245 + className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm" 246 + > 247 + {children} 248 + </div> 249 + </div> 250 + </div> 251 + ) 252 + } 253 + 254 + function SidebarTrigger({ 255 + className, 256 + onClick, 257 + ...props 258 + }: React.ComponentProps<typeof Button>) { 259 + const { toggleSidebar } = useSidebar() 260 + 261 + return ( 262 + <Button 263 + data-sidebar="trigger" 264 + data-slot="sidebar-trigger" 265 + variant="ghost" 266 + size="icon" 267 + className={cn("size-7", className)} 268 + onClick={(event) => { 269 + onClick?.(event) 270 + toggleSidebar() 271 + }} 272 + {...props} 273 + > 274 + <PanelLeftIcon /> 275 + <span className="sr-only">Toggle Sidebar</span> 276 + </Button> 277 + ) 278 + } 279 + 280 + function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { 281 + const { toggleSidebar } = useSidebar() 282 + 283 + return ( 284 + <button 285 + data-sidebar="rail" 286 + data-slot="sidebar-rail" 287 + aria-label="Toggle Sidebar" 288 + tabIndex={-1} 289 + onClick={toggleSidebar} 290 + title="Toggle Sidebar" 291 + className={cn( 292 + "hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex", 293 + "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize", 294 + "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize", 295 + "hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full", 296 + "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2", 297 + "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2", 298 + className 299 + )} 300 + {...props} 301 + /> 302 + ) 303 + } 304 + 305 + function SidebarInset({ className, ...props }: React.ComponentProps<"main">) { 306 + return ( 307 + <main 308 + data-slot="sidebar-inset" 309 + className={cn( 310 + "bg-background relative flex w-full flex-1 flex-col", 311 + "md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2", 312 + className 313 + )} 314 + {...props} 315 + /> 316 + ) 317 + } 318 + 319 + function SidebarInput({ 320 + className, 321 + ...props 322 + }: React.ComponentProps<typeof Input>) { 323 + return ( 324 + <Input 325 + data-slot="sidebar-input" 326 + data-sidebar="input" 327 + className={cn("bg-background h-8 w-full shadow-none", className)} 328 + {...props} 329 + /> 330 + ) 331 + } 332 + 333 + function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) { 334 + return ( 335 + <div 336 + data-slot="sidebar-header" 337 + data-sidebar="header" 338 + className={cn("flex flex-col gap-2 p-2", className)} 339 + {...props} 340 + /> 341 + ) 342 + } 343 + 344 + function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) { 345 + return ( 346 + <div 347 + data-slot="sidebar-footer" 348 + data-sidebar="footer" 349 + className={cn("flex flex-col gap-2 p-2", className)} 350 + {...props} 351 + /> 352 + ) 353 + } 354 + 355 + function SidebarSeparator({ 356 + className, 357 + ...props 358 + }: React.ComponentProps<typeof Separator>) { 359 + return ( 360 + <Separator 361 + data-slot="sidebar-separator" 362 + data-sidebar="separator" 363 + className={cn("bg-sidebar-border mx-2 w-auto", className)} 364 + {...props} 365 + /> 366 + ) 367 + } 368 + 369 + function SidebarContent({ className, ...props }: React.ComponentProps<"div">) { 370 + return ( 371 + <div 372 + data-slot="sidebar-content" 373 + data-sidebar="content" 374 + className={cn( 375 + "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden", 376 + className 377 + )} 378 + {...props} 379 + /> 380 + ) 381 + } 382 + 383 + function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) { 384 + return ( 385 + <div 386 + data-slot="sidebar-group" 387 + data-sidebar="group" 388 + className={cn("relative flex w-full min-w-0 flex-col p-2", className)} 389 + {...props} 390 + /> 391 + ) 392 + } 393 + 394 + function SidebarGroupLabel({ 395 + className, 396 + asChild = false, 397 + ...props 398 + }: React.ComponentProps<"div"> & { asChild?: boolean }) { 399 + const Comp = asChild ? Slot : "div" 400 + 401 + return ( 402 + <Comp 403 + data-slot="sidebar-group-label" 404 + data-sidebar="group-label" 405 + className={cn( 406 + "text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", 407 + "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", 408 + className 409 + )} 410 + {...props} 411 + /> 412 + ) 413 + } 414 + 415 + function SidebarGroupAction({ 416 + className, 417 + asChild = false, 418 + ...props 419 + }: React.ComponentProps<"button"> & { asChild?: boolean }) { 420 + const Comp = asChild ? Slot : "button" 421 + 422 + return ( 423 + <Comp 424 + data-slot="sidebar-group-action" 425 + data-sidebar="group-action" 426 + className={cn( 427 + "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", 428 + // Increases the hit area of the button on mobile. 429 + "after:absolute after:-inset-2 md:after:hidden", 430 + "group-data-[collapsible=icon]:hidden", 431 + className 432 + )} 433 + {...props} 434 + /> 435 + ) 436 + } 437 + 438 + function SidebarGroupContent({ 439 + className, 440 + ...props 441 + }: React.ComponentProps<"div">) { 442 + return ( 443 + <div 444 + data-slot="sidebar-group-content" 445 + data-sidebar="group-content" 446 + className={cn("w-full text-sm", className)} 447 + {...props} 448 + /> 449 + ) 450 + } 451 + 452 + function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) { 453 + return ( 454 + <ul 455 + data-slot="sidebar-menu" 456 + data-sidebar="menu" 457 + className={cn("flex w-full min-w-0 flex-col gap-1", className)} 458 + {...props} 459 + /> 460 + ) 461 + } 462 + 463 + function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) { 464 + return ( 465 + <li 466 + data-slot="sidebar-menu-item" 467 + data-sidebar="menu-item" 468 + className={cn("group/menu-item relative", className)} 469 + {...props} 470 + /> 471 + ) 472 + } 473 + 474 + const sidebarMenuButtonVariants = cva( 475 + "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", 476 + { 477 + variants: { 478 + variant: { 479 + default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", 480 + outline: 481 + "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]", 482 + }, 483 + size: { 484 + default: "h-8 text-sm", 485 + sm: "h-7 text-xs", 486 + lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!", 487 + }, 488 + }, 489 + defaultVariants: { 490 + variant: "default", 491 + size: "default", 492 + }, 493 + } 494 + ) 495 + 496 + function SidebarMenuButton({ 497 + asChild = false, 498 + isActive = false, 499 + variant = "default", 500 + size = "default", 501 + tooltip, 502 + className, 503 + ...props 504 + }: React.ComponentProps<"button"> & { 505 + asChild?: boolean 506 + isActive?: boolean 507 + tooltip?: string | React.ComponentProps<typeof TooltipContent> 508 + } & VariantProps<typeof sidebarMenuButtonVariants>) { 509 + const Comp = asChild ? Slot : "button" 510 + const { isMobile, state } = useSidebar() 511 + 512 + const button = ( 513 + <Comp 514 + data-slot="sidebar-menu-button" 515 + data-sidebar="menu-button" 516 + data-size={size} 517 + data-active={isActive} 518 + className={cn(sidebarMenuButtonVariants({ variant, size }), className)} 519 + {...props} 520 + /> 521 + ) 522 + 523 + if (!tooltip) { 524 + return button 525 + } 526 + 527 + if (typeof tooltip === "string") { 528 + tooltip = { 529 + children: tooltip, 530 + } 531 + } 532 + 533 + return ( 534 + <Tooltip> 535 + <TooltipTrigger asChild>{button}</TooltipTrigger> 536 + <TooltipContent 537 + side="right" 538 + align="center" 539 + hidden={state !== "collapsed" || isMobile} 540 + {...tooltip} 541 + /> 542 + </Tooltip> 543 + ) 544 + } 545 + 546 + function SidebarMenuAction({ 547 + className, 548 + asChild = false, 549 + showOnHover = false, 550 + ...props 551 + }: React.ComponentProps<"button"> & { 552 + asChild?: boolean 553 + showOnHover?: boolean 554 + }) { 555 + const Comp = asChild ? Slot : "button" 556 + 557 + return ( 558 + <Comp 559 + data-slot="sidebar-menu-action" 560 + data-sidebar="menu-action" 561 + className={cn( 562 + "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", 563 + // Increases the hit area of the button on mobile. 564 + "after:absolute after:-inset-2 md:after:hidden", 565 + "peer-data-[size=sm]/menu-button:top-1", 566 + "peer-data-[size=default]/menu-button:top-1.5", 567 + "peer-data-[size=lg]/menu-button:top-2.5", 568 + "group-data-[collapsible=icon]:hidden", 569 + showOnHover && 570 + "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0", 571 + className 572 + )} 573 + {...props} 574 + /> 575 + ) 576 + } 577 + 578 + function SidebarMenuBadge({ 579 + className, 580 + ...props 581 + }: React.ComponentProps<"div">) { 582 + return ( 583 + <div 584 + data-slot="sidebar-menu-badge" 585 + data-sidebar="menu-badge" 586 + className={cn( 587 + "text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none", 588 + "peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground", 589 + "peer-data-[size=sm]/menu-button:top-1", 590 + "peer-data-[size=default]/menu-button:top-1.5", 591 + "peer-data-[size=lg]/menu-button:top-2.5", 592 + "group-data-[collapsible=icon]:hidden", 593 + className 594 + )} 595 + {...props} 596 + /> 597 + ) 598 + } 599 + 600 + function SidebarMenuSkeleton({ 601 + className, 602 + showIcon = false, 603 + ...props 604 + }: React.ComponentProps<"div"> & { 605 + showIcon?: boolean 606 + }) { 607 + // Random width between 50 to 90%. 608 + const width = React.useMemo(() => { 609 + return `${Math.floor(Math.random() * 40) + 50}%` 610 + }, []) 611 + 612 + return ( 613 + <div 614 + data-slot="sidebar-menu-skeleton" 615 + data-sidebar="menu-skeleton" 616 + className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)} 617 + {...props} 618 + > 619 + {showIcon && ( 620 + <Skeleton 621 + className="size-4 rounded-md" 622 + data-sidebar="menu-skeleton-icon" 623 + /> 624 + )} 625 + <Skeleton 626 + className="h-4 max-w-(--skeleton-width) flex-1" 627 + data-sidebar="menu-skeleton-text" 628 + style={ 629 + { 630 + "--skeleton-width": width, 631 + } as React.CSSProperties 632 + } 633 + /> 634 + </div> 635 + ) 636 + } 637 + 638 + function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) { 639 + return ( 640 + <ul 641 + data-slot="sidebar-menu-sub" 642 + data-sidebar="menu-sub" 643 + className={cn( 644 + "border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5", 645 + "group-data-[collapsible=icon]:hidden", 646 + className 647 + )} 648 + {...props} 649 + /> 650 + ) 651 + } 652 + 653 + function SidebarMenuSubItem({ 654 + className, 655 + ...props 656 + }: React.ComponentProps<"li">) { 657 + return ( 658 + <li 659 + data-slot="sidebar-menu-sub-item" 660 + data-sidebar="menu-sub-item" 661 + className={cn("group/menu-sub-item relative", className)} 662 + {...props} 663 + /> 664 + ) 665 + } 666 + 667 + function SidebarMenuSubButton({ 668 + asChild = false, 669 + size = "md", 670 + isActive = false, 671 + className, 672 + ...props 673 + }: React.ComponentProps<"a"> & { 674 + asChild?: boolean 675 + size?: "sm" | "md" 676 + isActive?: boolean 677 + }) { 678 + const Comp = asChild ? Slot : "a" 679 + 680 + return ( 681 + <Comp 682 + data-slot="sidebar-menu-sub-button" 683 + data-sidebar="menu-sub-button" 684 + data-size={size} 685 + data-active={isActive} 686 + className={cn( 687 + "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", 688 + "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground", 689 + size === "sm" && "text-xs", 690 + size === "md" && "text-sm", 691 + "group-data-[collapsible=icon]:hidden", 692 + className 693 + )} 694 + {...props} 695 + /> 696 + ) 697 + } 698 + 699 + export { 700 + Sidebar, 701 + SidebarContent, 702 + SidebarFooter, 703 + SidebarGroup, 704 + SidebarGroupAction, 705 + SidebarGroupContent, 706 + SidebarGroupLabel, 707 + SidebarHeader, 708 + SidebarInput, 709 + SidebarInset, 710 + SidebarMenu, 711 + SidebarMenuAction, 712 + SidebarMenuBadge, 713 + SidebarMenuButton, 714 + SidebarMenuItem, 715 + SidebarMenuSkeleton, 716 + SidebarMenuSub, 717 + SidebarMenuSubButton, 718 + SidebarMenuSubItem, 719 + SidebarProvider, 720 + SidebarRail, 721 + SidebarSeparator, 722 + SidebarTrigger, 723 + useSidebar, 724 + }
+13
apps/web/src/components/ui/skeleton.tsx
··· 1 + import { cn } from "@/lib/utils" 2 + 3 + function Skeleton({ className, ...props }: React.ComponentProps<"div">) { 4 + return ( 5 + <div 6 + data-slot="skeleton" 7 + className={cn("bg-accent animate-pulse rounded-md", className)} 8 + {...props} 9 + /> 10 + ) 11 + } 12 + 13 + export { Skeleton }
+18
apps/web/src/components/ui/textarea.tsx
··· 1 + import * as React from "react" 2 + 3 + import { cn } from "@/lib/utils" 4 + 5 + function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 + return ( 7 + <textarea 8 + data-slot="textarea" 9 + className={cn( 10 + "border-input placeholder: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 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", 11 + className 12 + )} 13 + {...props} 14 + /> 15 + ) 16 + } 17 + 18 + export { Textarea }
+61
apps/web/src/components/ui/tooltip.tsx
··· 1 + "use client" 2 + 3 + import * as React from "react" 4 + import * as TooltipPrimitive from "@radix-ui/react-tooltip" 5 + 6 + import { cn } from "@/lib/utils" 7 + 8 + function TooltipProvider({ 9 + delayDuration = 0, 10 + ...props 11 + }: React.ComponentProps<typeof TooltipPrimitive.Provider>) { 12 + return ( 13 + <TooltipPrimitive.Provider 14 + data-slot="tooltip-provider" 15 + delayDuration={delayDuration} 16 + {...props} 17 + /> 18 + ) 19 + } 20 + 21 + function Tooltip({ 22 + ...props 23 + }: React.ComponentProps<typeof TooltipPrimitive.Root>) { 24 + return ( 25 + <TooltipProvider> 26 + <TooltipPrimitive.Root data-slot="tooltip" {...props} /> 27 + </TooltipProvider> 28 + ) 29 + } 30 + 31 + function TooltipTrigger({ 32 + ...props 33 + }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) { 34 + return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} /> 35 + } 36 + 37 + function TooltipContent({ 38 + className, 39 + sideOffset = 0, 40 + children, 41 + ...props 42 + }: React.ComponentProps<typeof TooltipPrimitive.Content>) { 43 + return ( 44 + <TooltipPrimitive.Portal> 45 + <TooltipPrimitive.Content 46 + data-slot="tooltip-content" 47 + sideOffset={sideOffset} 48 + className={cn( 49 + "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 origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance", 50 + className 51 + )} 52 + {...props} 53 + > 54 + {children} 55 + <TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" /> 56 + </TooltipPrimitive.Content> 57 + </TooltipPrimitive.Portal> 58 + ) 59 + } 60 + 61 + export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
+19
apps/web/src/hooks/use-mobile.ts
··· 1 + import * as React from "react" 2 + 3 + const MOBILE_BREAKPOINT = 768 4 + 5 + export function useIsMobile() { 6 + const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined) 7 + 8 + React.useEffect(() => { 9 + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) 10 + const onChange = () => { 11 + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 12 + } 13 + mql.addEventListener("change", onChange) 14 + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 15 + return () => mql.removeEventListener("change", onChange) 16 + }, []) 17 + 18 + return !!isMobile 19 + }
+119
apps/web/src/index.css
··· 1 1 @import "tailwindcss"; 2 + @import "tw-animate-css"; 3 + 4 + @custom-variant dark (&:is(.dark *)); 5 + 6 + @theme inline { 7 + --radius-sm: calc(var(--radius) - 4px); 8 + --radius-md: calc(var(--radius) - 2px); 9 + --radius-lg: var(--radius); 10 + --radius-xl: calc(var(--radius) + 4px); 11 + --color-background: var(--background); 12 + --color-foreground: var(--foreground); 13 + --color-card: var(--card); 14 + --color-card-foreground: var(--card-foreground); 15 + --color-popover: var(--popover); 16 + --color-popover-foreground: var(--popover-foreground); 17 + --color-primary: var(--primary); 18 + --color-primary-foreground: var(--primary-foreground); 19 + --color-secondary: var(--secondary); 20 + --color-secondary-foreground: var(--secondary-foreground); 21 + --color-muted: var(--muted); 22 + --color-muted-foreground: var(--muted-foreground); 23 + --color-accent: var(--accent); 24 + --color-accent-foreground: var(--accent-foreground); 25 + --color-destructive: var(--destructive); 26 + --color-border: var(--border); 27 + --color-input: var(--input); 28 + --color-ring: var(--ring); 29 + --color-chart-1: var(--chart-1); 30 + --color-chart-2: var(--chart-2); 31 + --color-chart-3: var(--chart-3); 32 + --color-chart-4: var(--chart-4); 33 + --color-chart-5: var(--chart-5); 34 + --color-sidebar: var(--sidebar); 35 + --color-sidebar-foreground: var(--sidebar-foreground); 36 + --color-sidebar-primary: var(--sidebar-primary); 37 + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 38 + --color-sidebar-accent: var(--sidebar-accent); 39 + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 40 + --color-sidebar-border: var(--sidebar-border); 41 + --color-sidebar-ring: var(--sidebar-ring); 42 + } 43 + 44 + :root { 45 + --radius: 0.625rem; 46 + --background: oklch(1 0 0); 47 + --foreground: oklch(0.141 0.005 285.823); 48 + --card: oklch(1 0 0); 49 + --card-foreground: oklch(0.141 0.005 285.823); 50 + --popover: oklch(1 0 0); 51 + --popover-foreground: oklch(0.141 0.005 285.823); 52 + --primary: oklch(0.21 0.006 285.885); 53 + --primary-foreground: oklch(0.985 0 0); 54 + --secondary: oklch(0.967 0.001 286.375); 55 + --secondary-foreground: oklch(0.21 0.006 285.885); 56 + --muted: oklch(0.967 0.001 286.375); 57 + --muted-foreground: oklch(0.552 0.016 285.938); 58 + --accent: oklch(0.967 0.001 286.375); 59 + --accent-foreground: oklch(0.21 0.006 285.885); 60 + --destructive: oklch(0.577 0.245 27.325); 61 + --border: oklch(0.92 0.004 286.32); 62 + --input: oklch(0.92 0.004 286.32); 63 + --ring: oklch(0.705 0.015 286.067); 64 + --chart-1: oklch(0.646 0.222 41.116); 65 + --chart-2: oklch(0.6 0.118 184.704); 66 + --chart-3: oklch(0.398 0.07 227.392); 67 + --chart-4: oklch(0.828 0.189 84.429); 68 + --chart-5: oklch(0.769 0.188 70.08); 69 + --sidebar: oklch(0.985 0 0); 70 + --sidebar-foreground: oklch(0.141 0.005 285.823); 71 + --sidebar-primary: oklch(0.21 0.006 285.885); 72 + --sidebar-primary-foreground: oklch(0.985 0 0); 73 + --sidebar-accent: oklch(0.967 0.001 286.375); 74 + --sidebar-accent-foreground: oklch(0.21 0.006 285.885); 75 + --sidebar-border: oklch(0.92 0.004 286.32); 76 + --sidebar-ring: oklch(0.705 0.015 286.067); 77 + } 78 + 79 + .dark { 80 + --background: oklch(0.141 0.005 285.823); 81 + --foreground: oklch(0.985 0 0); 82 + --card: oklch(0.21 0.006 285.885); 83 + --card-foreground: oklch(0.985 0 0); 84 + --popover: oklch(0.21 0.006 285.885); 85 + --popover-foreground: oklch(0.985 0 0); 86 + --primary: oklch(0.92 0.004 286.32); 87 + --primary-foreground: oklch(0.21 0.006 285.885); 88 + --secondary: oklch(0.274 0.006 286.033); 89 + --secondary-foreground: oklch(0.985 0 0); 90 + --muted: oklch(0.274 0.006 286.033); 91 + --muted-foreground: oklch(0.705 0.015 286.067); 92 + --accent: oklch(0.274 0.006 286.033); 93 + --accent-foreground: oklch(0.985 0 0); 94 + --destructive: oklch(0.704 0.191 22.216); 95 + --border: oklch(1 0 0 / 10%); 96 + --input: oklch(1 0 0 / 15%); 97 + --ring: oklch(0.552 0.016 285.938); 98 + --chart-1: oklch(0.488 0.243 264.376); 99 + --chart-2: oklch(0.696 0.17 162.48); 100 + --chart-3: oklch(0.769 0.188 70.08); 101 + --chart-4: oklch(0.627 0.265 303.9); 102 + --chart-5: oklch(0.645 0.246 16.439); 103 + --sidebar: oklch(0.21 0.006 285.885); 104 + --sidebar-foreground: oklch(0.985 0 0); 105 + --sidebar-primary: oklch(0.488 0.243 264.376); 106 + --sidebar-primary-foreground: oklch(0.985 0 0); 107 + --sidebar-accent: oklch(0.274 0.006 286.033); 108 + --sidebar-accent-foreground: oklch(0.985 0 0); 109 + --sidebar-border: oklch(1 0 0 / 10%); 110 + --sidebar-ring: oklch(0.552 0.016 285.938); 111 + } 112 + 113 + @layer base { 114 + * { 115 + @apply border-border outline-ring/50; 116 + } 117 + body { 118 + @apply bg-background text-foreground; 119 + } 120 + }
+1
apps/web/src/lib/consts.ts
··· 1 + export const IS_DEV = import.meta.env.DEV;
+6
apps/web/src/lib/utils.ts
··· 1 + import { clsx, type ClassValue } from "clsx" 2 + import { twMerge } from "tailwind-merge" 3 + 4 + export function cn(...inputs: ClassValue[]) { 5 + return twMerge(clsx(inputs)) 6 + }
apps/web/src/state/auth.tsx

This is a binary file and will not be displayed.

apps/web/src/state/client.tsx

This is a binary file and will not be displayed.

+11
apps/web/src/vite-env.d.ts
··· 1 + interface ViteTypeOptions {} 2 + 3 + interface ImportMetaEnv { 4 + readonly VITE_API_URL: string; 5 + readonly VITE_CLIENT_ID: string; 6 + readonly VITE_REDIRECT_URI: string; 7 + } 8 + 9 + interface ImportMeta { 10 + readonly env: ImportMetaEnv; 11 + }
-3
apps/web/vite.config.d.ts
··· 1 - declare const _default: import("vite").UserConfig; 2 - export default _default; 3 - //# sourceMappingURL=vite.config.d.ts.map
-1
apps/web/vite.config.d.ts.map
··· 1 - {"version":3,"file":"vite.config.d.ts","sourceRoot":"","sources":["vite.config.ts"],"names":[],"mappings":";AAMA,wBAeE"}
-20
apps/web/vite.config.js
··· 1 - import { defineConfig } from 'vite'; 2 - import react from '@vitejs/plugin-react'; 3 - import tailwindcss from '@tailwindcss/vite'; 4 - import { tanstackRouter } from '@tanstack/router-plugin/vite'; 5 - import path from 'node:path'; 6 - export default defineConfig({ 7 - plugins: [ 8 - tanstackRouter({ 9 - target: 'react', 10 - autoCodeSplitting: true, 11 - }), 12 - tailwindcss(), 13 - react() 14 - ], 15 - resolve: { 16 - alias: { 17 - "@": path.resolve(__dirname, "./src"), 18 - } 19 - } 20 - });
+13 -1
bun.lock
··· 109 109 "name": "web", 110 110 "version": "0.0.0", 111 111 "dependencies": { 112 + "@radix-ui/react-avatar": "^1.1.11", 113 + "@radix-ui/react-dialog": "^1.1.15", 114 + "@radix-ui/react-dropdown-menu": "^2.1.16", 115 + "@radix-ui/react-label": "^2.1.8", 116 + "@radix-ui/react-separator": "^1.1.8", 117 + "@radix-ui/react-slot": "^1.2.4", 118 + "@radix-ui/react-tooltip": "^1.2.8", 119 + "class-variance-authority": "^0.7.1", 120 + "clsx": "^2.1.1", 121 + "lucide-react": "^0.556.0", 112 122 "react": "^19.2.0", 113 123 "react-dom": "^19.2.0", 124 + "tailwind-merge": "^3.4.0", 114 125 "tailwindcss": "^4.1.17", 115 126 }, 116 127 "devDependencies": { 117 - "@cookware/tsconfig": "workspace:^", 128 + "@cookware/tsconfig": "workspace:*", 118 129 "@eslint/js": "^9.39.1", 119 130 "@tailwindcss/vite": "^4.1.17", 120 131 "@types/bun": "^1.3.4", ··· 125 136 "eslint-plugin-react-hooks": "^7.0.1", 126 137 "eslint-plugin-react-refresh": "^0.4.24", 127 138 "globals": "^16.5.0", 139 + "tw-animate-css": "^1.4.0", 128 140 "typescript": "~5.9.3", 129 141 "typescript-eslint": "^8.46.4", 130 142 "vite": "npm:rolldown-vite@7.2.5",