lumiui
blockdialog-02source: ok · hit
Dialog 02
Image gallery with nested auth dialogs. Inspired by Unsplash.
npx shadcn add "https://www.lumiui.dev/r/dialog-02.json"free
Where it goes
gallery
Interaction
modal
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lighticonslucide-react
Registry dependencies
@lumi-ui/dialog@lumi-ui/alert-dialog@lumi-ui/dropdown-menu@lumi-ui/button@lumi-ui/select@lumi-ui/toast@lumi-ui/separator
Files8 · source cached 10/15/2026, 7:52:22 AM
registry/blocks/dialog-02/page.tsx
"use client";
import type { DialogRootChangeEventDetails } from "@base-ui/react";
import { ChevronLeft, ChevronRight, XIcon } from "lucide-react";
import * as React from "react";
import { Button } from "@/registry/ui/button";
import {
createDialogHandle,
Dialog,
DialogBackdrop,
DialogClose,
DialogPopup,
DialogPortal,
DialogTrigger,
DialogViewport,
} from "@/registry/ui/dialog";
import { InfoMenu } from "./components/info-menu";
import { ProfileCard } from "./components/profile-card";
import { ShareMenu } from "./components/share-menu";
import { TooltipGroup } from "./components/tooltip-group";
const galleryHandle = createDialogHandle<IImage>();
export default function UnsplashDialog() {
const [open, setOpen] = React.useState(false);
const [triggerId, setTriggerId] = React.useState<string | null>(null);
const handleOpenChange = (
isOpen: boolean,
eventDetails: DialogRootChangeEventDetails,
) => {
setOpen(isOpen);
setTriggerId(eventDetails.trigger?.id ?? null);
};
const navigate = React.useCallback(
(direction: "next" | "prev") => {
if (!triggerId) return;
const currentIndex = parseInt(triggerId.replace("trigger-", ""), 10);
if (Number.isNaN(currentIndex)) return;
let nextIndex = 0;
if (direction === "next") {
nextIndex = (currentIndex + 1) % IMAGES.length;
} else {
nextIndex = (currentIndex - 1 + IMAGES.length) % IMAGES.length;
}
setTriggerId(`trigger-${nextIndex}`);
},
[triggerId],
);
return (
<>
<div className="grid grid-cols-3 gap-4 max-w-4xl">
{IMAGES.map((img, i) => (
<DialogTrigger
handle={galleryHandle}
id={`trigger-${i}`}
key={img.alt}
payload={img}
render={
<button
className="relative aspect-square overflow-hidden rounded-lg cursor-pointer"
type="button"
>
<img
alt={img.alt}
className="h-full w-full object-cover"
src={img.src}
/>
</button>
}
/>
))}
</div>
<Dialog
handle={galleryHandle}
onOpenChange={handleOpenChange}
open={open}
triggerId={triggerId}
>
{({ payload }) => (
<DialogPortal>
<DialogBackdrop />
<DialogViewport className="grid place-items-center overflow-y-auto">
<DialogPopup className="flex h-full w-full justify-center pointer-events-none bg-transparent outline-0 animate-fade pt-12 sm:px-12">
<div className="pointer-events-auto h-full w-full flex flex-col shadow-md bg-background sm:max-w-[calc(100vw-12rem)] rounded-2xl">
<DialogClose
className="fixed left-2 top-2 rounded-full"
render={
<Button
className="size-8 rounded-full text-white/70 hover:bg-white/20 hover:text-white"
size="icon"
variant="ghost"
>
<XIcon className="size-6" />
<span className="sr-only">Close</span>
</Button>
}
/>
<DialogClose
className="absolute -top-12 right-0 md:hidden"
render={
<Button
className="rounded-full text-white/70 hover:bg-white/20 hover:text-white"
size="icon"
variant="ghost"
>
<XIcon className="size-6" />
<span className="sr-only">Close</span>
</Button>
}
/>
<Button
className="fixed hidden md:flex left-4 top-1/2 -translate-y-1/2 rounded-full text-white/70 hover:bg-white/20 hover:text-white md:left-8 md:size-12"
onClick={() => navigate("prev")}
size="icon"
variant="ghost"
>
<ChevronLeft className="size-8" />
<span className="sr-only">Previous image</span>
</Button>
<Button
className="fixed hidden md:flex right-4 top-1/2 -translate-y-1/2 rounded-full text-white/70 hover:bg-white/20 hover:text-white md:right-8 md:size-12"
onClick={() => navigate("next")}
size="icon"
variant="ghost"
>
<ChevronRight className="size-8" />
<span className="sr-only">Next image</span>
</Button>
<div className="relative grid grid-rows-[auto_1fr_auto] h-[calc(100vh-3rem)]">
<div className="flex justify-between px-4 py-2">
<ProfileCard />
{payload && <TooltipGroup currentImage={payload} />}
</div>
{payload && (
<div className="flex flex-col max-w-4xl mx-auto items-center justify-center flex-1 w-full">
<img
alt={payload.alt}
className="h-full w-full h-[70vh] rounded-md object-contain"
key={payload.src}
src={payload.src}
/>
</div>
)}
{payload && (
<div className="flex gap-8 p-4 text-sm justify-between">
<div className="flex gap-12">
<div className="flex flex-col gap-2">
<div className="text-muted-foreground font-semibold">
Views
</div>
<div>{payload?.views}</div>
</div>
<div className="flex flex-col gap-2">
<div className="text-muted-foreground font-semibold">
Downloads
</div>
<div>{payload?.downloads}</div>
</div>
</div>
<div className="flex gap-2">
<ShareMenu />
<InfoMenu />
</div>
</div>
)}
</div>
<div className="max-w-7xl flex-1 w-full mx-auto space-y-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 min-h-screen pt-6">
{Array.from({ length: 12 }).map((_, i) => (
<div
className="flex h-64 w-full shrink-0 items-center justify-center rounded-md bg-muted"
key={String(i)}
/>
))}
</div>
</div>
</DialogPopup>
</DialogViewport>
</DialogPortal>
)}
</Dialog>
</>
);
}
const IMAGES: IImage[] = [
{
alt: "Brown trees beside river under blue sky during daytime",
downloads: 3578,
src: "https://images.unsplash.com/photo-1579353977828-2a4eab540b9a?q=80&w=1000&auto=format&fit=crop",
views: 5726,
},
{
alt: "People watching illuminated japanese lanterns",
downloads: 373,
src: "https://images.unsplash.com/photo-1765614244329-22ca4157345a?q=80&w=1000&auto=format&fit=crop",
views: 1253,
},
{
alt: "Abstract colorful waves",
downloads: 2354,
src: "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=1000&auto=format&fit=crop",
views: 7676,
},
];
export type IImage = {
alt: string;
src: string;
views: number;
downloads: number;
};
registry/blocks/dialog-02/components/info-menu.tsx
import { InfoIcon } from "lucide-react";
import { Button } from "@/registry/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/registry/ui/popover";
import { Tabs, TabsListContent, TabsPanel, TabsTab } from "@/registry/ui/tabs";
import { StatsChart } from "./stats-chart";
export const InfoMenu = () => {
return (
<Popover>
<PopoverTrigger render={<Button variant="outline" />}>
<InfoIcon /> Info
</PopoverTrigger>
<PopoverContent align="end" className="bg-secondary" side="top">
<Tabs className="w-72" defaultValue="views">
<TabsPanel value="downloads">
<StatsChart data="downloads" />
</TabsPanel>
<TabsPanel value="views">
<StatsChart data="views" />
</TabsPanel>
<TabsListContent>
<TabsTab value="downloads">Downloads</TabsTab>
<TabsTab value="views">Views</TabsTab>
</TabsListContent>
</Tabs>
</PopoverContent>
</Popover>
);
};
registry/blocks/dialog-02/components/stats-chart.tsx
"use client";
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/registry/ui/chart";
const chartConfig: ChartConfig = {
downloads: {
color: "var(--color-green-100)",
label: "Downloads",
},
views: {
color: "var(--color-green-100)",
label: "Views",
},
};
export const StatsChart = ({
data = "views",
}: {
data?: "views" | "downloads";
}) => {
return (
<ChartContainer
className="aspect-auto h-[120px] w-full"
config={chartConfig}
>
<BarChart
accessibilityLayer
data={chartData}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis dataKey="date" hide />
<ChartTooltip
content={
<ChartTooltipContent
className="bg-foreground text-background border-transparent shadow-none w-fit"
hideIndicator
labelFormatter={(value) => {
return new Date(value as string).toLocaleDateString("en-US", {
day: "numeric",
month: "short",
year: "numeric",
});
}}
/>
}
cursor={false}
/>
<Bar
className="hover:fill-green-600 transition-colors duration-50"
dataKey={data}
fill={`var(--color-${data})`}
radius={4}
/>
</BarChart>
</ChartContainer>
);
};
const chartData = [
{ date: "2025-12-24", downloads: 350, views: 6452 },
{ date: "2025-12-25", downloads: 120, views: 1945 },
{ date: "2025-12-26", downloads: 420, views: 6576 },
{ date: "2025-12-27", downloads: 180, views: 2371 },
{ date: "2025-12-28", downloads: 150, views: 2335 },
{ date: "2025-12-29", downloads: 380, views: 6237 },
{ date: "2025-12-30", downloads: 520, views: 7528 },
{ date: "2025-12-31", downloads: 110, views: 1944 },
{ date: "2026-01-01", downloads: 290, views: 4837 },
{ date: "2026-01-02", downloads: 160, views: 2358 },
{ date: "2026-01-03", downloads: 140, views: 2350 },
{ date: "2026-01-04", downloads: 410, views: 6734 },
{ date: "2026-01-05", downloads: 90, views: 1232 },
{ date: "2026-01-06", downloads: 230, views: 3677 },
{ date: "2026-01-07", downloads: 85, views: 1269 },
{ date: "2026-01-08", downloads: 115, views: 1658 },
{
date: "2026-01-09",
downloads: 145,
views: 2001,
},
{
date: "2026-01-10",
downloads: 395,
views: 6352,
},
{
date: "2026-01-11",
downloads: 190,
views: 2954,
},
{
date: "2026-01-12",
downloads: 310,
views: 4590,
},
{
date: "2026-01-13",
downloads: 110,
views: 1843,
},
];
registry/blocks/dialog-02/components/login-form.tsx
"use client";
import { Chrome, Eye, EyeOff, Github } from "lucide-react";
import * as React from "react";
import { Button } from "@/registry/ui/button";
import {
Field,
FieldControl,
FieldError,
FieldLabel,
} from "@/registry/ui/field";
import { Form } from "@/registry/ui/form";
import { Input } from "@/registry/ui/input";
import { Tabs, TabsListContent, TabsPanel, TabsTab } from "@/registry/ui/tabs";
export function LoginForm({ onSuccess }: { onSuccess: () => void }) {
const [isLoading, setIsLoading] = React.useState(false);
const [showPassword, setShowPassword] = React.useState(false);
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
onSuccess();
}, 2000);
};
return (
<div className="p-4 sm:p-8">
<div className="text-center space-y-2">
<h1 className="text-3xl font-bold tracking-tight text-foreground">
Welcome back
</h1>
<p className="text-sm text-muted-foreground">
Enter your credentials to access your account
</p>
</div>
<Tabs className="w-full mt-6" defaultValue="email">
<TabsListContent>
<TabsTab value="email">Email</TabsTab>
<TabsTab value="phone">Phone</TabsTab>
</TabsListContent>
<TabsPanel className="py-4" value="email">
<Form onSubmit={handleSubmit}>
<Field>
<FieldLabel>Email</FieldLabel>
<FieldControl
name="email"
placeholder="name@example.com"
required
type="email"
/>
<FieldError />
</Field>
<Field>
<div className="flex items-center justify-between">
<FieldLabel>Password</FieldLabel>
<Button
className="p-0 h-auto font-normal text-xs"
variant="link"
>
Forgot password?
</Button>
</div>
<div className="relative">
<Input
className="pr-10"
name="password"
placeholder="Enter your password"
required
type={showPassword ? "text" : "password"}
/>
<Button
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
onClick={() => setShowPassword(!showPassword)}
size="icon"
type="button"
variant="ghost"
>
{showPassword ? (
<EyeOff className="size-4 text-muted-foreground" />
) : (
<Eye className="size-4 text-muted-foreground" />
)}
<span className="sr-only">
{showPassword ? "Hide password" : "Show password"}
</span>
</Button>
</div>
<FieldError />
</Field>
<Button className="w-full" disabled={isLoading} type="submit">
{isLoading ? "Signing in..." : "Sign In"}
</Button>
</Form>
</TabsPanel>
<TabsPanel className="py-4" value="phone">
<Form onSubmit={handleSubmit}>
<Field>
<FieldLabel>Phone Number</FieldLabel>
<FieldControl
name="phone"
placeholder="+1 (555) 000-0000"
required
type="tel"
/>
<FieldError />
</Field>
<Field>
<div className="flex items-center justify-between">
<FieldLabel>Verification Code</FieldLabel>
<Button
className="p-0 h-auto text-xs"
type="button"
variant="link"
>
Send code
</Button>
</div>
<FieldControl
maxLength={6}
name="code"
placeholder="Enter 6-digit code"
required
type="text"
/>
<FieldError />
</Field>
<Button className="w-full" disabled={isLoading} type="submit">
{isLoading ? "Signing in..." : "Sign In"}
</Button>
</Form>
</TabsPanel>
</Tabs>
<div className="space-y-4">
<div className="relative">
<div className="relative flex justify-center text-xs uppercase">
<span className="px-2 text-muted-foreground">Or continue with</span>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<Button className="w-full" type="button" variant="outline">
<Github className="mr-2 size-4" />
GitHub
</Button>
<Button className="w-full" type="button" variant="outline">
<Chrome className="mr-2 size-4" />
Google
</Button>
</div>
</div>
</div>
);
}
registry/blocks/dialog-02/components/signup-form.tsx
"use client";
import * as React from "react";
import { Button } from "@/registry/ui/button";
import {
Field,
FieldControl,
FieldError,
FieldLabel,
} from "@/registry/ui/field";
import { Form } from "@/registry/ui/form";
import { Input } from "@/registry/ui/input";
export type IImage = {
alt: string;
src: string;
views: number;
downloads: number;
};
interface SignUpFormProps {
image: IImage;
onSuccess?: () => void;
}
export function SignUpForm({ image, onSuccess }: SignUpFormProps) {
const [isLoading, setIsLoading] = React.useState(false);
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
if (onSuccess) onSuccess();
}, 2000);
};
return (
<div className="flex max-h-full">
<div className="hidden w-1/3 lg:block relative">
<img
alt={image.alt}
className="h-full w-full object-cover rounded-l-md"
src={image.src}
/>
<div className="absolute bottom-4 left-4 text-white/80 text-xs bg-black/20 p-2 rounded backdrop-blur-sm">
Photo viewed {image.views.toLocaleString()} times
</div>
</div>
<div className="flex w-full flex-col justify-center p-4 sm:p-8 lg:w-2/3">
<div className="mx-auto w-full max-w-md space-y-8">
<div className="text-center">
<h1 className="text-4xl font-bold tracking-tight text-foreground">
Join Us
</h1>
<p className="mt-2 text-muted-foreground">
Already have an account? <Button variant={"link"}>Login</Button>
</p>
</div>
<Form className="space-y-6" onSubmit={handleSubmit}>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<Field>
<FieldLabel>First name</FieldLabel>
<FieldControl name="firstName" placeholder="" required />
<FieldError />
</Field>
<Field>
<FieldLabel>Last name</FieldLabel>
<FieldControl name="lastName" placeholder="" required />
<FieldError />
</Field>
</div>
<Field>
<FieldLabel>Email</FieldLabel>
<FieldControl name="email" required type="email" />
<FieldError />
</Field>
<Field>
<FieldLabel className="flex gap-1">
Username
<span className="font-normal text-muted-foreground">
(only letters, numbers and underscores)
</span>
</FieldLabel>
<FieldControl name="username" pattern="[a-zA-Z0-9_]+" required />
<FieldError />
</Field>
<Field>
<FieldLabel className="flex gap-1">
Password
<span className="font-normal text-muted-foreground">
(min. 8 char)
</span>
</FieldLabel>
<Input minLength={8} name="password" required type="password" />
<FieldError />
</Field>
<Button disabled={isLoading} type="submit">
{isLoading ? "Joining..." : "Join"}
</Button>
</Form>
<p className="text-center text-sm text-muted-foreground">
By joining, you agree to the{" "}
<Button className="px-0" variant={"link"}>
Terms
</Button>{" "}
and{" "}
<Button className="px-0" variant={"link"}>
Privacy Policy
</Button>
.
</p>
</div>
</div>
</div>
);
}
registry/blocks/dialog-02/components/share-menu.tsx
"use client";
import {
FacebookIcon,
InstagramIcon,
LinkIcon,
ShareIcon,
TwitterIcon,
} from "lucide-react";
import { Button } from "@/registry/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuGroupLabel,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/registry/ui/dropdown-menu";
import { toast } from "@/registry/ui/toast";
export const ShareMenu = () => {
const handleCopy = () => {
navigator.clipboard.writeText("https://lumiui.dev");
toast.success({
title: "Copied",
});
};
return (
<DropdownMenu>
<DropdownMenuTrigger
className="w-20"
render={
<Button variant="outline">
<ShareIcon className="size-4" /> Share
</Button>
}
/>
<DropdownMenuContent align="end" matchAnchorWidth={false} side="top">
<DropdownMenuGroup>
<DropdownMenuGroupLabel>Socials</DropdownMenuGroupLabel>
<DropdownMenuItem>
<FacebookIcon className="size-4" /> <span>Facebook</span>
</DropdownMenuItem>
<DropdownMenuItem>
<TwitterIcon className="size-4" /> <span>Twitter</span>
</DropdownMenuItem>
<DropdownMenuItem>
<InstagramIcon className="size-4" /> <span>Instagram</span>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleCopy}>
<LinkIcon className="size-4" /> Copy link
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};
registry/blocks/dialog-02/components/tooltip-group.tsx
import type { DialogRootChangeEventDetails } from "@base-ui/react";
import { BookmarkIcon, PlusIcon } from "lucide-react";
import React from "react";
import { cn } from "@/registry/lib/utils";
import { Button } from "@/registry/ui/button";
import {
createDialogHandle,
Dialog,
DialogBackdrop,
DialogPopup,
DialogPortal,
DialogTrigger,
} from "@/registry/ui/dialog";
import { toast } from "@/registry/ui/toast";
import {
createTooltipHandle,
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/registry/ui/tooltip";
import type { IImage } from "../page";
import { LoginForm } from "./login-form";
import { SignUpForm } from "./signup-form";
type UserAction = "bookmark" | "add-collection";
type FormType = "signin" | "signup";
type DialogPayload = {
formType: FormType;
action: UserAction;
title: string;
};
const dialogHandle = createDialogHandle<DialogPayload>();
const tooltipHandle = createTooltipHandle<{ label: string }>();
export const TooltipGroup = ({ currentImage }: { currentImage: IImage }) => {
const [isOpen, setIsOpen] = React.useState(false);
const [triggerId, setTriggerId] = React.useState<string | null>(null);
const handleOpenChange = (
open: boolean,
eventDetails: DialogRootChangeEventDetails,
) => {
setIsOpen(open);
if (eventDetails.trigger) {
setTriggerId(eventDetails.trigger.id);
}
};
const handleSuccess = (action: UserAction) => {
setIsOpen(false);
const messages: Record<UserAction, string> = {
"add-collection": `Added ${currentImage.alt} to collection`,
bookmark: `Bookmarked ${currentImage.alt}`,
};
toast.success({
description: messages[action] || "Action completed successfully",
title: "Success",
});
};
return (
<>
<TooltipProvider>
<div className="flex gap-2">
<TooltipTrigger
handle={tooltipHandle}
id="trigger-bookmark"
payload={{ label: "Bookmark this page" }}
render={
<DialogTrigger
handle={dialogHandle}
payload={{
action: "bookmark",
formType: "signin",
title: "Welcome Back",
}}
render={<Button size={"icon-sm"} variant={"outline"} />}
>
<BookmarkIcon aria-label="Bookmark" className="size-5" />
</DialogTrigger>
}
/>
<TooltipTrigger
handle={tooltipHandle}
id="trigger-collection"
payload={{ label: "Add to collection" }}
render={
<DialogTrigger
handle={dialogHandle}
payload={{
action: "add-collection",
formType: "signup",
title: "Create Account",
}}
render={<Button size={"icon-sm"} variant={"outline"} />}
>
<PlusIcon aria-label="Add to collection" className="size-5" />
</DialogTrigger>
}
/>
<Tooltip handle={tooltipHandle}>
{({ payload }) => (
<TooltipContent showArrow={false} side="bottom" sideOffset={5}>
<p>{payload?.label}</p>
</TooltipContent>
)}
</Tooltip>
</div>
</TooltipProvider>
<Dialog
handle={dialogHandle}
onOpenChange={handleOpenChange}
open={isOpen}
triggerId={triggerId}
>
{({ payload }) => {
return (
<DialogPortal>
<DialogBackdrop
className="backdrop-blur-none bg-black/40"
forceRender
/>
<DialogPopup
className={cn(
"fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-background rounded-md shadow-md dark:shadow-xs w-full",
payload?.formType === "signin" && "max-w-md",
payload?.formType === "signup" && "max-w-2xl",
)}
>
{payload?.formType === "signin" && (
<LoginForm onSuccess={() => handleSuccess(payload.action)} />
)}
{payload?.formType === "signup" && (
<SignUpForm
image={currentImage}
onSuccess={() => handleSuccess(payload.action)}
/>
)}
</DialogPopup>
</DialogPortal>
);
}}
</Dialog>
</>
);
};
registry/blocks/dialog-02/components/profile-card.tsx
import { Avatar, AvatarFallback, AvatarImage } from "@/registry/ui/avatar";
import { Button } from "@/registry/ui/button";
import {
PreviewCard,
PreviewCardContent,
PreviewCardTrigger,
} from "@/registry/ui/preview-card";
export function ProfileCard() {
return (
<PreviewCard>
<PreviewCardTrigger
delay={300}
render={
<Button className="cursor-pointer" variant="unstyled">
<Avatar>
<AvatarImage src="https://github.com/mui.png" />
<AvatarFallback>VC</AvatarFallback>
</Avatar>
</Button>
}
/>
<PreviewCardContent align="start" className="w-80">
<div className="flex justify-between gap-4">
<Avatar>
<AvatarImage src="https://github.com/mui.png" />
<AvatarFallback>VC</AvatarFallback>
</Avatar>
<div className="space-y-1">
<h4 className="text-sm font-semibold">@base-ui</h4>
<p className="text-sm">
Unstyled UI components for building accessible web apps and design
systems. From the creators of Radix, Floating UI, and Material UI.
</p>
<div className="text-muted-foreground text-xs">
Joined December 2021
</div>
</div>
</div>
</PreviewCardContent>
</PreviewCard>
);
}