this repo has no description
0
fork

Configure Feed

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

feat: design (kind of)

+1325 -3
+344
src/app/[id]/page.tsx
··· 1 + "use client"; 2 + 3 + import { 4 + Card, 5 + CardAction, 6 + CardContent, 7 + CardFooter, 8 + CardHeader, 9 + CardTitle, 10 + } from "@/components/ui/card"; 11 + import { Avatar, AvatarImage } from "@/components/ui/avatar"; 12 + import { Button } from "@/components/ui/button"; 13 + import { 14 + ArrowLeft01Icon, 15 + Copy01Icon, 16 + Delete01Icon, 17 + PlusSignIcon, 18 + Settings01Icon, 19 + Share08Icon, 20 + UserGroupIcon, 21 + } from "@hugeicons/core-free-icons"; 22 + import { HugeiconsIcon } from "@hugeicons/react"; 23 + import Link from "next/link"; 24 + import { 25 + Dialog, 26 + DialogContent, 27 + DialogHeader, 28 + DialogTitle, 29 + DialogTrigger, 30 + } from "@/components/ui/dialog"; 31 + import { 32 + Item, 33 + ItemActions, 34 + ItemContent, 35 + ItemDescription, 36 + ItemMedia, 37 + ItemTitle, 38 + } from "@/components/ui/item"; 39 + import { Progress } from "@/components/ui/progress"; 40 + import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"; 41 + import { Input } from "@/components/ui/input"; 42 + import { Slider } from "@/components/ui/slider"; 43 + import { 44 + InputGroup, 45 + InputGroupAddon, 46 + InputGroupButton, 47 + InputGroupInput, 48 + } from "@/components/ui/input-group"; 49 + 50 + export default function Page() { 51 + const me = 1; 52 + 53 + const data = { 54 + id: 1, 55 + emoji: "❤️", 56 + title: "Love & Stuff", 57 + host: 1, 58 + members: [ 59 + { 60 + id: 1, 61 + name: "Alice", 62 + avatar: "https://github.com/alice.png", 63 + }, 64 + { 65 + id: 2, 66 + name: "Bob", 67 + avatar: "https://github.com/bob.png", 68 + }, 69 + ], 70 + }; 71 + 72 + const vouchers = [ 73 + { 74 + id: 1, 75 + emoji: "🍕", 76 + name: "pizza Night", 77 + limit: 5, 78 + used: 2, 79 + }, 80 + ]; 81 + 82 + return ( 83 + <> 84 + <header className="flex justify-between"> 85 + <Button variant="secondary" size="icon" render={<Link href="/" />}> 86 + <HugeiconsIcon icon={ArrowLeft01Icon} /> 87 + </Button> 88 + <div> 89 + <Dialog> 90 + <DialogTrigger 91 + render={ 92 + <Button variant="secondary" size="icon"> 93 + <HugeiconsIcon icon={Share08Icon} /> 94 + </Button> 95 + } 96 + /> 97 + <DialogContent> 98 + <DialogHeader> 99 + <DialogTitle>Invite</DialogTitle> 100 + </DialogHeader> 101 + <img 102 + src="https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=LOVE123" 103 + alt="QR Code" 104 + className="mx-auto" 105 + /> 106 + <p className="text-sm text-muted-foreground"> 107 + Scan the QR code to join {data.title} 108 + </p> 109 + <InputGroup> 110 + <InputGroupInput placeholder="https://x.com/shadcn" readOnly /> 111 + <InputGroupAddon align="inline-end"> 112 + <InputGroupButton 113 + onClick={() => 114 + navigator.clipboard.writeText("https://x.com/shadcn") 115 + } 116 + > 117 + <HugeiconsIcon icon={Copy01Icon} /> 118 + </InputGroupButton> 119 + </InputGroupAddon> 120 + </InputGroup> 121 + </DialogContent> 122 + </Dialog> 123 + <Dialog> 124 + <DialogTrigger 125 + render={ 126 + <Button variant="secondary" size="icon"> 127 + <HugeiconsIcon icon={Settings01Icon} /> 128 + </Button> 129 + } 130 + /> 131 + <DialogContent> 132 + <DialogHeader> 133 + <DialogTitle>Group Settings</DialogTitle> 134 + </DialogHeader> 135 + <form> 136 + <FieldGroup> 137 + <div className="flex gap-4"> 138 + <Field> 139 + <FieldLabel htmlFor="form-emoji">Emoji</FieldLabel> 140 + <Input 141 + id="form-emoji" 142 + placeholder="Enter the emoji" 143 + required 144 + /> 145 + </Field> 146 + <Field> 147 + <FieldLabel htmlFor="form-name">Name</FieldLabel> 148 + <Input 149 + id="form-name" 150 + placeholder="Enter the name" 151 + required 152 + /> 153 + </Field> 154 + </div> 155 + <Field orientation="horizontal"> 156 + <Button variant="outline" size="icon"> 157 + <HugeiconsIcon icon={Delete01Icon} /> 158 + </Button> 159 + <Button type="submit" className="flex-1"> 160 + Create 161 + </Button> 162 + </Field> 163 + </FieldGroup> 164 + </form> 165 + </DialogContent> 166 + </Dialog> 167 + </div> 168 + </header> 169 + <Card className="ring-0"> 170 + <CardHeader> 171 + <CardTitle> 172 + {data.emoji} {data.title} 173 + </CardTitle> 174 + <CardAction> 175 + <Dialog> 176 + <DialogTrigger 177 + render={ 178 + <Button variant="outline"> 179 + <HugeiconsIcon icon={UserGroupIcon} /> 180 + {data.members.length} 181 + </Button> 182 + } 183 + /> 184 + <DialogContent> 185 + <DialogHeader> 186 + <DialogTitle>Members</DialogTitle> 187 + </DialogHeader> 188 + {data.members.map((member) => ( 189 + <Item key={member.id}> 190 + <ItemMedia> 191 + <Avatar> 192 + <AvatarImage src={member.avatar} /> 193 + </Avatar> 194 + </ItemMedia> 195 + <ItemContent> 196 + <ItemTitle>{member.name}</ItemTitle> 197 + <ItemDescription> 198 + {member.id === data.host ? "Host" : "Member"} 199 + </ItemDescription> 200 + </ItemContent> 201 + {data.host === me && ( 202 + <ItemActions> 203 + <Button variant="ghost" size="icon"> 204 + <HugeiconsIcon icon={Delete01Icon} /> 205 + </Button> 206 + </ItemActions> 207 + )} 208 + </Item> 209 + ))} 210 + </DialogContent> 211 + </Dialog> 212 + <Dialog> 213 + <DialogTrigger 214 + render={ 215 + <Button variant="outline"> 216 + <HugeiconsIcon icon={PlusSignIcon} /> 217 + </Button> 218 + } 219 + /> 220 + <DialogContent> 221 + <DialogHeader> 222 + <DialogTitle>New Voucher</DialogTitle> 223 + </DialogHeader> 224 + <form> 225 + <FieldGroup> 226 + <div className="flex gap-4"> 227 + <Field> 228 + <FieldLabel htmlFor="form-emoji">Emoji</FieldLabel> 229 + <Input 230 + id="form-emoji" 231 + placeholder="Enter the emoji" 232 + required 233 + /> 234 + </Field> 235 + <Field> 236 + <FieldLabel htmlFor="form-name">Name</FieldLabel> 237 + <Input 238 + id="form-name" 239 + placeholder="Enter the name" 240 + required 241 + /> 242 + </Field> 243 + </div> 244 + <Field> 245 + <FieldLabel htmlFor="form-limit">Limit</FieldLabel> 246 + <Slider 247 + id="form-limit" 248 + defaultValue={[1]} 249 + min={1} 250 + max={100} 251 + step={1} 252 + /> 253 + </Field> 254 + <Field orientation="horizontal"> 255 + <Button type="submit" className="flex-1"> 256 + Create 257 + </Button> 258 + </Field> 259 + </FieldGroup> 260 + </form> 261 + </DialogContent> 262 + </Dialog> 263 + </CardAction> 264 + </CardHeader> 265 + <CardContent className="grid gap-6"> 266 + {vouchers.map((voucher) => ( 267 + <Card key={voucher.id}> 268 + <CardHeader> 269 + <Button variant="secondary" size="icon"> 270 + {voucher.emoji} 271 + </Button> 272 + <CardTitle>{voucher.name}</CardTitle> 273 + <CardAction> 274 + <Button> 275 + {voucher.used}/{voucher.limit} 276 + </Button> 277 + </CardAction> 278 + </CardHeader> 279 + <CardContent> 280 + <Progress value={(voucher.used / voucher.limit) * 100} /> 281 + </CardContent> 282 + <CardFooter> 283 + {(me === data.host && ( 284 + <Dialog> 285 + <DialogTrigger 286 + render={<Button className="w-full">Edit</Button>} 287 + /> 288 + <DialogContent> 289 + <DialogHeader> 290 + <DialogTitle>Edit Voucher</DialogTitle> 291 + </DialogHeader> 292 + <form> 293 + <FieldGroup> 294 + <div className="flex gap-4"> 295 + <Field> 296 + <FieldLabel htmlFor="form-emoji"> 297 + Emoji 298 + </FieldLabel> 299 + <Input 300 + id="form-emoji" 301 + placeholder="Enter the emoji" 302 + required 303 + /> 304 + </Field> 305 + <Field> 306 + <FieldLabel htmlFor="form-name">Name</FieldLabel> 307 + <Input 308 + id="form-name" 309 + placeholder="Enter the name" 310 + required 311 + /> 312 + </Field> 313 + </div> 314 + <Field> 315 + <FieldLabel htmlFor="form-limit">Limit</FieldLabel> 316 + <Slider 317 + id="form-limit" 318 + defaultValue={[voucher.limit]} 319 + min={1} 320 + max={100} 321 + step={1} 322 + /> 323 + </Field> 324 + <Field orientation="horizontal"> 325 + <Button variant="outline" size="icon"> 326 + <HugeiconsIcon icon={Delete01Icon} /> 327 + </Button> 328 + <Button type="submit" className="flex-1"> 329 + Create 330 + </Button> 331 + </Field> 332 + </FieldGroup> 333 + </form> 334 + </DialogContent> 335 + </Dialog> 336 + )) || <Button className="w-full">Redeem</Button>} 337 + </CardFooter> 338 + </Card> 339 + ))} 340 + </CardContent> 341 + </Card> 342 + </> 343 + ); 344 + }
+40
src/app/auth/page.tsx
··· 1 + "use client"; 2 + 3 + import { 4 + Card, 5 + CardContent, 6 + CardDescription, 7 + CardHeader, 8 + CardTitle, 9 + } from "@/components/ui/card"; 10 + import { Button } from "@/components/ui/button"; 11 + import { HugeiconsIcon } from "@hugeicons/react"; 12 + import { Field, FieldLabel } from "@/components/ui/field"; 13 + import { Input } from "@/components/ui/input"; 14 + import { TicketStarIcon } from "@hugeicons/core-free-icons"; 15 + 16 + export default function Page() { 17 + return ( 18 + <Card className="ring-0"> 19 + <CardHeader> 20 + <Button variant="secondary" size="icon"> 21 + <HugeiconsIcon icon={TicketStarIcon} /> 22 + </Button> 23 + <CardTitle>Welcome back</CardTitle> 24 + <CardDescription>Sign in to continue to Vouch.</CardDescription> 25 + </CardHeader> 26 + <CardContent className="space-y-6"> 27 + <Field> 28 + <FieldLabel>Email</FieldLabel> 29 + <Input placeholder="you@example.com" /> 30 + </Field> 31 + <Field> 32 + <FieldLabel>Password</FieldLabel> 33 + <Input type="password" placeholder="********" /> 34 + </Field> 35 + <Button className="w-full">Sign In</Button> 36 + <Button variant="link">No account? Sign Up</Button> 37 + </CardContent> 38 + </Card> 39 + ); 40 + }
+1 -1
src/app/layout.tsx
··· 15 15 children: React.ReactNode; 16 16 }>) { 17 17 return ( 18 - <html lang="en" className={figtree.variable}> 18 + <html lang="en" className={`${figtree.variable} max-w-md mx-auto`}> 19 19 <body>{children}</body> 20 20 </html> 21 21 );
+136 -2
src/app/page.tsx
··· 1 - import { ComponentExample } from "@/components/component-example"; 1 + "use client"; 2 + 3 + import { 4 + Card, 5 + CardAction, 6 + CardContent, 7 + CardDescription, 8 + CardHeader, 9 + CardTitle, 10 + } from "@/components/ui/card"; 11 + 12 + import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 13 + import { Button } from "@/components/ui/button"; 14 + import { 15 + Empty, 16 + EmptyHeader, 17 + EmptyMedia, 18 + EmptyTitle, 19 + } from "@/components/ui/empty"; 20 + import { PlusSignIcon } from "@hugeicons/core-free-icons"; 21 + import { HugeiconsIcon } from "@hugeicons/react"; 22 + import Link from "next/link"; 23 + import { 24 + Dialog, 25 + DialogContent, 26 + DialogHeader, 27 + DialogTitle, 28 + DialogTrigger, 29 + } from "@/components/ui/dialog"; 30 + import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"; 31 + import { Input } from "@/components/ui/input"; 2 32 3 33 export default function Page() { 4 - return <ComponentExample />; 34 + const groups = [ 35 + { 36 + id: 1, 37 + emoji: "❤️", 38 + title: "Love & Stuff", 39 + stats: { 40 + total: 10, 41 + members: 5, 42 + }, 43 + host: true, 44 + }, 45 + { 46 + id: 2, 47 + emoji: "👩‍💻", 48 + title: "Work Buddies", 49 + stats: { 50 + total: 20, 51 + members: 10, 52 + }, 53 + host: false, 54 + }, 55 + ]; 56 + 57 + return ( 58 + <Card className="ring-0"> 59 + <CardHeader> 60 + <CardTitle>Vouch</CardTitle> 61 + <CardDescription>Your shared promises</CardDescription> 62 + <CardAction> 63 + <Link href="/settings"> 64 + <Avatar> 65 + <AvatarImage src="https://github.com/shadcn.png" /> 66 + <AvatarFallback>CN</AvatarFallback> 67 + </Avatar> 68 + </Link> 69 + </CardAction> 70 + </CardHeader> 71 + <CardContent className="grid gap-6"> 72 + {groups.map((group) => ( 73 + <Link key={group.id} href={`/${group.id}`}> 74 + <Card> 75 + <CardHeader> 76 + <Button variant="secondary" size="icon"> 77 + {group.emoji} 78 + </Button> 79 + <CardTitle>{group.title}</CardTitle> 80 + <CardDescription> 81 + {group.stats.total} vouches, {group.stats.members} members 82 + </CardDescription> 83 + {group.host && ( 84 + <CardAction> 85 + <Button>Host</Button> 86 + </CardAction> 87 + )} 88 + </CardHeader> 89 + </Card> 90 + </Link> 91 + ))} 92 + <Dialog> 93 + <DialogTrigger 94 + render={ 95 + <Empty className="border border-dashed"> 96 + <EmptyHeader> 97 + <EmptyMedia variant="icon"> 98 + <HugeiconsIcon icon={PlusSignIcon} /> 99 + </EmptyMedia> 100 + <EmptyTitle>New Group</EmptyTitle> 101 + </EmptyHeader> 102 + </Empty> 103 + } 104 + /> 105 + <DialogContent> 106 + <DialogHeader> 107 + <DialogTitle>Create a new group</DialogTitle> 108 + </DialogHeader> 109 + <form> 110 + <FieldGroup> 111 + <div className="flex gap-4"> 112 + <Field> 113 + <FieldLabel htmlFor="form-emoji">Emoji</FieldLabel> 114 + <Input 115 + id="form-emoji" 116 + placeholder="Enter the emoji" 117 + required 118 + /> 119 + </Field> 120 + <Field> 121 + <FieldLabel htmlFor="form-name">Name</FieldLabel> 122 + <Input 123 + id="form-name" 124 + placeholder="Enter the name" 125 + required 126 + /> 127 + </Field> 128 + </div> 129 + <Field className="flex flex-col"> 130 + <Button type="submit">Create</Button> 131 + </Field> 132 + </FieldGroup> 133 + </form> 134 + </DialogContent> 135 + </Dialog> 136 + </CardContent> 137 + </Card> 138 + ); 5 139 }
+62
src/app/settings/page.tsx
··· 1 + "use client"; 2 + 3 + import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 4 + import { Avatar, AvatarImage } from "@/components/ui/avatar"; 5 + import { Button } from "@/components/ui/button"; 6 + import { ArrowLeft01Icon, Logout01Icon } from "@hugeicons/core-free-icons"; 7 + import { HugeiconsIcon } from "@hugeicons/react"; 8 + import Link from "next/link"; 9 + import { Field, FieldLabel } from "@/components/ui/field"; 10 + import { Input } from "@/components/ui/input"; 11 + import { Switch } from "@/components/ui/switch"; 12 + 13 + export default function Page() { 14 + const me = { 15 + id: 1, 16 + name: "Alice", 17 + avatar: "https://github.com/alice.png", 18 + email: "alice@example.com", 19 + notifications: true, 20 + }; 21 + 22 + return ( 23 + <> 24 + <header> 25 + <Button variant="secondary" size="icon" render={<Link href="/" />}> 26 + <HugeiconsIcon icon={ArrowLeft01Icon} /> 27 + </Button> 28 + </header> 29 + <Card className="ring-0"> 30 + <CardHeader> 31 + <CardTitle>Settings</CardTitle> 32 + </CardHeader> 33 + <CardContent className="space-y-6"> 34 + <Avatar className="w-24 h-24"> 35 + <AvatarImage src={me.avatar} alt={me.name} /> 36 + </Avatar> 37 + <Field> 38 + <FieldLabel>Name</FieldLabel> 39 + <Input defaultValue={me.name} /> 40 + </Field> 41 + <Field> 42 + <FieldLabel>Email</FieldLabel> 43 + <Input defaultValue={me.email} /> 44 + </Field> 45 + <Field> 46 + <FieldLabel>Notifications</FieldLabel> 47 + <Switch id="notifications" defaultChecked={me.notifications} /> 48 + </Field> 49 + <Button className="w-full">Save Changes</Button> 50 + <Button 51 + variant="destructive" 52 + className="w-full" 53 + render={<Link href="/auth" />} 54 + > 55 + <HugeiconsIcon icon={Logout01Icon} className="me-2" /> 56 + Logout 57 + </Button> 58 + </CardContent> 59 + </Card> 60 + </> 61 + ); 62 + }
+109
src/components/ui/avatar.tsx
··· 1 + "use client"; 2 + 3 + import * as React from "react"; 4 + import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"; 5 + 6 + import { cn } from "@/lib/utils"; 7 + 8 + function Avatar({ 9 + className, 10 + size = "default", 11 + ...props 12 + }: AvatarPrimitive.Root.Props & { 13 + size?: "default" | "sm" | "lg"; 14 + }) { 15 + return ( 16 + <AvatarPrimitive.Root 17 + data-slot="avatar" 18 + data-size={size} 19 + className={cn( 20 + "size-8 rounded-full after:rounded-full data-[size=lg]:size-10 data-[size=sm]:size-6 after:border-border group/avatar relative flex shrink-0 select-none after:absolute after:inset-0 after:border after:mix-blend-darken dark:after:mix-blend-lighten", 21 + className, 22 + )} 23 + {...props} 24 + /> 25 + ); 26 + } 27 + 28 + function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) { 29 + return ( 30 + <AvatarPrimitive.Image 31 + data-slot="avatar-image" 32 + className={cn( 33 + "rounded-full aspect-square size-full object-cover", 34 + className, 35 + )} 36 + {...props} 37 + /> 38 + ); 39 + } 40 + 41 + function AvatarFallback({ 42 + className, 43 + ...props 44 + }: AvatarPrimitive.Fallback.Props) { 45 + return ( 46 + <AvatarPrimitive.Fallback 47 + data-slot="avatar-fallback" 48 + className={cn( 49 + "bg-muted text-muted-foreground rounded-full flex size-full items-center justify-center text-sm group-data-[size=sm]/avatar:text-xs", 50 + className, 51 + )} 52 + {...props} 53 + /> 54 + ); 55 + } 56 + 57 + function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) { 58 + return ( 59 + <span 60 + data-slot="avatar-badge" 61 + className={cn( 62 + "bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-blend-color ring-2 select-none", 63 + "group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden", 64 + "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", 65 + "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", 66 + className, 67 + )} 68 + {...props} 69 + /> 70 + ); 71 + } 72 + 73 + function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) { 74 + return ( 75 + <div 76 + data-slot="avatar-group" 77 + className={cn( 78 + "*:data-[slot=avatar]:ring-background group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2", 79 + className, 80 + )} 81 + {...props} 82 + /> 83 + ); 84 + } 85 + 86 + function AvatarGroupCount({ 87 + className, 88 + ...props 89 + }: React.ComponentProps<"div">) { 90 + return ( 91 + <div 92 + data-slot="avatar-group-count" 93 + className={cn( 94 + "bg-muted text-muted-foreground size-8 rounded-full text-sm group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3 ring-background relative flex shrink-0 items-center justify-center ring-2", 95 + className, 96 + )} 97 + {...props} 98 + /> 99 + ); 100 + } 101 + 102 + export { 103 + Avatar, 104 + AvatarImage, 105 + AvatarFallback, 106 + AvatarGroup, 107 + AvatarGroupCount, 108 + AvatarBadge, 109 + };
+157
src/components/ui/dialog.tsx
··· 1 + "use client"; 2 + 3 + import * as React from "react"; 4 + import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"; 5 + 6 + import { cn } from "@/lib/utils"; 7 + import { Button } from "@/components/ui/button"; 8 + import { HugeiconsIcon } from "@hugeicons/react"; 9 + import { Cancel01Icon } from "@hugeicons/core-free-icons"; 10 + 11 + function Dialog({ ...props }: DialogPrimitive.Root.Props) { 12 + return <DialogPrimitive.Root data-slot="dialog" {...props} />; 13 + } 14 + 15 + function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) { 16 + return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />; 17 + } 18 + 19 + function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) { 20 + return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />; 21 + } 22 + 23 + function DialogClose({ ...props }: DialogPrimitive.Close.Props) { 24 + return <DialogPrimitive.Close data-slot="dialog-close" {...props} />; 25 + } 26 + 27 + function DialogOverlay({ 28 + className, 29 + ...props 30 + }: DialogPrimitive.Backdrop.Props) { 31 + return ( 32 + <DialogPrimitive.Backdrop 33 + data-slot="dialog-overlay" 34 + className={cn( 35 + "data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/80 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50", 36 + className, 37 + )} 38 + {...props} 39 + /> 40 + ); 41 + } 42 + 43 + function DialogContent({ 44 + className, 45 + children, 46 + showCloseButton = true, 47 + ...props 48 + }: DialogPrimitive.Popup.Props & { 49 + showCloseButton?: boolean; 50 + }) { 51 + return ( 52 + <DialogPortal> 53 + <DialogOverlay /> 54 + <DialogPrimitive.Popup 55 + data-slot="dialog-content" 56 + className={cn( 57 + "bg-background data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/5 grid max-w-[calc(100%-2rem)] gap-6 rounded-4xl p-6 text-sm ring-1 duration-100 sm:max-w-md fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none", 58 + className, 59 + )} 60 + {...props} 61 + > 62 + {children} 63 + {showCloseButton && ( 64 + <DialogPrimitive.Close 65 + data-slot="dialog-close" 66 + render={ 67 + <Button 68 + variant="ghost" 69 + className="absolute top-4 right-4" 70 + size="icon-sm" 71 + /> 72 + } 73 + > 74 + <HugeiconsIcon icon={Cancel01Icon} strokeWidth={2} /> 75 + <span className="sr-only">Close</span> 76 + </DialogPrimitive.Close> 77 + )} 78 + </DialogPrimitive.Popup> 79 + </DialogPortal> 80 + ); 81 + } 82 + 83 + function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { 84 + return ( 85 + <div 86 + data-slot="dialog-header" 87 + className={cn("gap-2 flex flex-col", className)} 88 + {...props} 89 + /> 90 + ); 91 + } 92 + 93 + function DialogFooter({ 94 + className, 95 + showCloseButton = false, 96 + children, 97 + ...props 98 + }: React.ComponentProps<"div"> & { 99 + showCloseButton?: boolean; 100 + }) { 101 + return ( 102 + <div 103 + data-slot="dialog-footer" 104 + className={cn( 105 + "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", 106 + className, 107 + )} 108 + {...props} 109 + > 110 + {children} 111 + {showCloseButton && ( 112 + <DialogPrimitive.Close render={<Button variant="outline" />}> 113 + Close 114 + </DialogPrimitive.Close> 115 + )} 116 + </div> 117 + ); 118 + } 119 + 120 + function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) { 121 + return ( 122 + <DialogPrimitive.Title 123 + data-slot="dialog-title" 124 + className={cn("text-base leading-none font-medium", className)} 125 + {...props} 126 + /> 127 + ); 128 + } 129 + 130 + function DialogDescription({ 131 + className, 132 + ...props 133 + }: DialogPrimitive.Description.Props) { 134 + return ( 135 + <DialogPrimitive.Description 136 + data-slot="dialog-description" 137 + className={cn( 138 + "text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3", 139 + className, 140 + )} 141 + {...props} 142 + /> 143 + ); 144 + } 145 + 146 + export { 147 + Dialog, 148 + DialogClose, 149 + DialogContent, 150 + DialogDescription, 151 + DialogFooter, 152 + DialogHeader, 153 + DialogOverlay, 154 + DialogPortal, 155 + DialogTitle, 156 + DialogTrigger, 157 + };
+101
src/components/ui/empty.tsx
··· 1 + import { cva, type VariantProps } from "class-variance-authority"; 2 + 3 + import { cn } from "@/lib/utils"; 4 + 5 + function Empty({ className, ...props }: React.ComponentProps<"div">) { 6 + return ( 7 + <div 8 + data-slot="empty" 9 + className={cn( 10 + "gap-4 rounded-lg border-dashed p-12 flex w-full min-w-0 flex-1 flex-col items-center justify-center text-center text-balance", 11 + className, 12 + )} 13 + {...props} 14 + /> 15 + ); 16 + } 17 + 18 + function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) { 19 + return ( 20 + <div 21 + data-slot="empty-header" 22 + className={cn("gap-2 flex max-w-sm flex-col items-center", className)} 23 + {...props} 24 + /> 25 + ); 26 + } 27 + 28 + const emptyMediaVariants = cva( 29 + "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0", 30 + { 31 + variants: { 32 + variant: { 33 + default: "bg-transparent", 34 + icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6", 35 + }, 36 + }, 37 + defaultVariants: { 38 + variant: "default", 39 + }, 40 + }, 41 + ); 42 + 43 + function EmptyMedia({ 44 + className, 45 + variant = "default", 46 + ...props 47 + }: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) { 48 + return ( 49 + <div 50 + data-slot="empty-icon" 51 + data-variant={variant} 52 + className={cn(emptyMediaVariants({ variant, className }))} 53 + {...props} 54 + /> 55 + ); 56 + } 57 + 58 + function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) { 59 + return ( 60 + <div 61 + data-slot="empty-title" 62 + className={cn("text-lg font-medium tracking-tight", className)} 63 + {...props} 64 + /> 65 + ); 66 + } 67 + 68 + function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) { 69 + return ( 70 + <div 71 + data-slot="empty-description" 72 + className={cn( 73 + "text-sm/relaxed text-muted-foreground [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", 74 + className, 75 + )} 76 + {...props} 77 + /> 78 + ); 79 + } 80 + 81 + function EmptyContent({ className, ...props }: React.ComponentProps<"div">) { 82 + return ( 83 + <div 84 + data-slot="empty-content" 85 + className={cn( 86 + "gap-4 text-sm flex w-full max-w-sm min-w-0 flex-col items-center text-balance", 87 + className, 88 + )} 89 + {...props} 90 + /> 91 + ); 92 + } 93 + 94 + export { 95 + Empty, 96 + EmptyHeader, 97 + EmptyTitle, 98 + EmptyDescription, 99 + EmptyContent, 100 + EmptyMedia, 101 + };
+201
src/components/ui/item.tsx
··· 1 + import * as React from "react"; 2 + import { mergeProps } from "@base-ui/react/merge-props"; 3 + import { useRender } from "@base-ui/react/use-render"; 4 + import { cva, type VariantProps } from "class-variance-authority"; 5 + 6 + import { cn } from "@/lib/utils"; 7 + import { Separator } from "@/components/ui/separator"; 8 + 9 + function ItemGroup({ className, ...props }: React.ComponentProps<"div">) { 10 + return ( 11 + <div 12 + role="list" 13 + data-slot="item-group" 14 + className={cn( 15 + "gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2 group/item-group flex w-full flex-col", 16 + className, 17 + )} 18 + {...props} 19 + /> 20 + ); 21 + } 22 + 23 + function ItemSeparator({ 24 + className, 25 + ...props 26 + }: React.ComponentProps<typeof Separator>) { 27 + return ( 28 + <Separator 29 + data-slot="item-separator" 30 + orientation="horizontal" 31 + className={cn("my-2", className)} 32 + {...props} 33 + /> 34 + ); 35 + } 36 + 37 + const itemVariants = cva( 38 + "[a]:hover:bg-muted rounded-2xl border text-sm w-full group/item focus-visible:border-ring focus-visible:ring-ring/50 flex items-center flex-wrap outline-none transition-colors duration-100 focus-visible:ring-[3px] [a]:transition-colors", 39 + { 40 + variants: { 41 + variant: { 42 + default: "border-transparent", 43 + outline: "border-border", 44 + muted: "bg-muted/50 border-transparent", 45 + }, 46 + size: { 47 + default: "gap-3.5 px-4 py-3.5", 48 + sm: "gap-3.5 px-3.5 py-3", 49 + xs: "gap-2.5 px-3 py-2.5 in-data-[slot=dropdown-menu-content]:p-0", 50 + }, 51 + }, 52 + defaultVariants: { 53 + variant: "default", 54 + size: "default", 55 + }, 56 + }, 57 + ); 58 + 59 + function Item({ 60 + className, 61 + variant = "default", 62 + size = "default", 63 + render, 64 + ...props 65 + }: useRender.ComponentProps<"div"> & VariantProps<typeof itemVariants>) { 66 + return useRender({ 67 + defaultTagName: "div", 68 + props: mergeProps<"div">( 69 + { 70 + className: cn(itemVariants({ variant, size, className })), 71 + }, 72 + props, 73 + ), 74 + render, 75 + state: { 76 + slot: "item", 77 + variant, 78 + size, 79 + }, 80 + }); 81 + } 82 + 83 + const itemMediaVariants = cva( 84 + "gap-2 group-has-data-[slot=item-description]/item:translate-y-0.5 group-has-data-[slot=item-description]/item:self-start flex shrink-0 items-center justify-center [&_svg]:pointer-events-none", 85 + { 86 + variants: { 87 + variant: { 88 + default: "bg-transparent", 89 + icon: "[&_svg:not([class*='size-'])]:size-4", 90 + image: 91 + "size-10 overflow-hidden rounded-lg group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 group-data-[size=xs]/item:rounded-md [&_img]:size-full [&_img]:object-cover", 92 + }, 93 + }, 94 + defaultVariants: { 95 + variant: "default", 96 + }, 97 + }, 98 + ); 99 + 100 + function ItemMedia({ 101 + className, 102 + variant = "default", 103 + ...props 104 + }: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) { 105 + return ( 106 + <div 107 + data-slot="item-media" 108 + data-variant={variant} 109 + className={cn(itemMediaVariants({ variant, className }))} 110 + {...props} 111 + /> 112 + ); 113 + } 114 + 115 + function ItemContent({ className, ...props }: React.ComponentProps<"div">) { 116 + return ( 117 + <div 118 + data-slot="item-content" 119 + className={cn( 120 + "gap-1 group-data-[size=xs]/item:gap-0.5 flex flex-1 flex-col [&+[data-slot=item-content]]:flex-none", 121 + className, 122 + )} 123 + {...props} 124 + /> 125 + ); 126 + } 127 + 128 + function ItemTitle({ className, ...props }: React.ComponentProps<"div">) { 129 + return ( 130 + <div 131 + data-slot="item-title" 132 + className={cn( 133 + "gap-2 text-sm leading-snug font-medium underline-offset-4 line-clamp-1 flex w-fit items-center", 134 + className, 135 + )} 136 + {...props} 137 + /> 138 + ); 139 + } 140 + 141 + function ItemDescription({ className, ...props }: React.ComponentProps<"p">) { 142 + return ( 143 + <p 144 + data-slot="item-description" 145 + className={cn( 146 + "text-muted-foreground text-left text-sm [&>a:hover]:text-primary line-clamp-2 font-normal [&>a]:underline [&>a]:underline-offset-4", 147 + className, 148 + )} 149 + {...props} 150 + /> 151 + ); 152 + } 153 + 154 + function ItemActions({ className, ...props }: React.ComponentProps<"div">) { 155 + return ( 156 + <div 157 + data-slot="item-actions" 158 + className={cn("gap-2 flex items-center", className)} 159 + {...props} 160 + /> 161 + ); 162 + } 163 + 164 + function ItemHeader({ className, ...props }: React.ComponentProps<"div">) { 165 + return ( 166 + <div 167 + data-slot="item-header" 168 + className={cn( 169 + "gap-2 flex basis-full items-center justify-between", 170 + className, 171 + )} 172 + {...props} 173 + /> 174 + ); 175 + } 176 + 177 + function ItemFooter({ className, ...props }: React.ComponentProps<"div">) { 178 + return ( 179 + <div 180 + data-slot="item-footer" 181 + className={cn( 182 + "gap-2 flex basis-full items-center justify-between", 183 + className, 184 + )} 185 + {...props} 186 + /> 187 + ); 188 + } 189 + 190 + export { 191 + Item, 192 + ItemMedia, 193 + ItemContent, 194 + ItemActions, 195 + ItemGroup, 196 + ItemSeparator, 197 + ItemTitle, 198 + ItemDescription, 199 + ItemHeader, 200 + ItemFooter, 201 + };
+83
src/components/ui/progress.tsx
··· 1 + "use client"; 2 + 3 + import { Progress as ProgressPrimitive } from "@base-ui/react/progress"; 4 + 5 + import { cn } from "@/lib/utils"; 6 + 7 + function Progress({ 8 + className, 9 + children, 10 + value, 11 + ...props 12 + }: ProgressPrimitive.Root.Props) { 13 + return ( 14 + <ProgressPrimitive.Root 15 + value={value} 16 + data-slot="progress" 17 + className={cn("flex flex-wrap gap-3", className)} 18 + {...props} 19 + > 20 + {children} 21 + <ProgressTrack> 22 + <ProgressIndicator /> 23 + </ProgressTrack> 24 + </ProgressPrimitive.Root> 25 + ); 26 + } 27 + 28 + function ProgressTrack({ className, ...props }: ProgressPrimitive.Track.Props) { 29 + return ( 30 + <ProgressPrimitive.Track 31 + className={cn( 32 + "bg-muted h-3 rounded-4xl relative flex w-full items-center overflow-x-hidden", 33 + className, 34 + )} 35 + data-slot="progress-track" 36 + {...props} 37 + /> 38 + ); 39 + } 40 + 41 + function ProgressIndicator({ 42 + className, 43 + ...props 44 + }: ProgressPrimitive.Indicator.Props) { 45 + return ( 46 + <ProgressPrimitive.Indicator 47 + data-slot="progress-indicator" 48 + className={cn("bg-primary h-full transition-all", className)} 49 + {...props} 50 + /> 51 + ); 52 + } 53 + 54 + function ProgressLabel({ className, ...props }: ProgressPrimitive.Label.Props) { 55 + return ( 56 + <ProgressPrimitive.Label 57 + className={cn("text-sm font-medium", className)} 58 + data-slot="progress-label" 59 + {...props} 60 + /> 61 + ); 62 + } 63 + 64 + function ProgressValue({ className, ...props }: ProgressPrimitive.Value.Props) { 65 + return ( 66 + <ProgressPrimitive.Value 67 + className={cn( 68 + "text-muted-foreground ml-auto text-sm tabular-nums", 69 + className, 70 + )} 71 + data-slot="progress-value" 72 + {...props} 73 + /> 74 + ); 75 + } 76 + 77 + export { 78 + Progress, 79 + ProgressTrack, 80 + ProgressIndicator, 81 + ProgressLabel, 82 + ProgressValue, 83 + };
+59
src/components/ui/slider.tsx
··· 1 + "use client"; 2 + 3 + import * as React from "react"; 4 + import { Slider as SliderPrimitive } from "@base-ui/react/slider"; 5 + 6 + import { cn } from "@/lib/utils"; 7 + 8 + function Slider({ 9 + className, 10 + defaultValue, 11 + value, 12 + min = 0, 13 + max = 100, 14 + ...props 15 + }: SliderPrimitive.Root.Props) { 16 + const _values = React.useMemo( 17 + () => 18 + Array.isArray(value) 19 + ? value 20 + : Array.isArray(defaultValue) 21 + ? defaultValue 22 + : [min, max], 23 + [value, defaultValue, min, max], 24 + ); 25 + 26 + return ( 27 + <SliderPrimitive.Root 28 + className={cn("data-horizontal:w-full data-vertical:h-full", className)} 29 + data-slot="slider" 30 + defaultValue={defaultValue} 31 + value={value} 32 + min={min} 33 + max={max} 34 + thumbAlignment="edge" 35 + {...props} 36 + > 37 + <SliderPrimitive.Control className="data-vertical:min-h-40 relative flex w-full touch-none items-center select-none data-disabled:opacity-50 data-vertical:h-full data-vertical:w-auto data-vertical:flex-col"> 38 + <SliderPrimitive.Track 39 + data-slot="slider-track" 40 + className="bg-muted rounded-4xl data-horizontal:h-3 data-horizontal:w-full data-vertical:h-full data-vertical:w-3 relative grow overflow-hidden select-none" 41 + > 42 + <SliderPrimitive.Indicator 43 + data-slot="slider-range" 44 + className="bg-primary select-none data-horizontal:h-full data-vertical:w-full" 45 + /> 46 + </SliderPrimitive.Track> 47 + {Array.from({ length: _values.length }, (_, index) => ( 48 + <SliderPrimitive.Thumb 49 + data-slot="slider-thumb" 50 + key={index} 51 + className="border-primary ring-ring/50 size-4 rounded-4xl border bg-white shadow-sm transition-colors hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden block shrink-0 select-none disabled:pointer-events-none disabled:opacity-50" 52 + /> 53 + ))} 54 + </SliderPrimitive.Control> 55 + </SliderPrimitive.Root> 56 + ); 57 + } 58 + 59 + export { Slider };
+32
src/components/ui/switch.tsx
··· 1 + "use client"; 2 + 3 + import { Switch as SwitchPrimitive } from "@base-ui/react/switch"; 4 + 5 + import { cn } from "@/lib/utils"; 6 + 7 + function Switch({ 8 + className, 9 + size = "default", 10 + ...props 11 + }: SwitchPrimitive.Root.Props & { 12 + size?: "sm" | "default"; 13 + }) { 14 + return ( 15 + <SwitchPrimitive.Root 16 + data-slot="switch" 17 + data-size={size} 18 + className={cn( 19 + "data-checked:bg-primary data-unchecked:bg-input 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:aria-invalid:border-destructive/50 dark:data-unchecked:bg-input/80 shrink-0 rounded-full border border-transparent focus-visible:ring-[3px] aria-invalid:ring-[3px] data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] peer group/switch relative inline-flex items-center transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 data-disabled:cursor-not-allowed data-disabled:opacity-50", 20 + className, 21 + )} 22 + {...props} 23 + > 24 + <SwitchPrimitive.Thumb 25 + data-slot="switch-thumb" 26 + className="bg-background dark:data-unchecked:bg-foreground dark:data-checked:bg-primary-foreground rounded-full group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 pointer-events-none block ring-0 transition-transform" 27 + /> 28 + </SwitchPrimitive.Root> 29 + ); 30 + } 31 + 32 + export { Switch };