Share Dialog
Glass share dialog: invite-by-email field with role select, people-with-access list, a general access switch (invited only / anyone with link) and a copy-link button with confirmation.
glass/ui/share-dialogSource
"use client";
import { useId, useState, type ReactNode } from "react";
import { Dialog } from "radix-ui";
import { Check, Globe, Link2, Lock, X } from "lucide-react";
import { cn } from "@/lib/utils";
export interface ShareDialogPerson {
name: string;
email: string;
role: "Owner" | "Can edit" | "Can view";
}
export interface ShareDialogProps {
trigger?: ReactNode;
title?: string;
link?: string;
people?: ShareDialogPerson[];
onInvite?: (email: string, role: ShareDialogPerson["role"]) => void;
da?: string;
}
const TONES = ["from-da-primary to-da-accent", "from-da-accent to-da-success", "from-da-danger to-da-primary", "from-da-warning to-da-danger"];
/** Glass share dialog (Radix Dialog): invite field with role select, people list with roles, general access toggle and copy-link button. */
export function ShareDialog({
trigger,
title = "Share “Q4 roadmap sync”",
link = "https://halo.app/r/q4-roadmap-8f2k",
people = [
{ name: "Amara Diallo", email: "amara@lumen.io", role: "Owner" },
{ name: "Leo Park", email: "leo@lumen.io", role: "Can edit" },
{ name: "Sofia Rossi", email: "sofia@lumen.io", role: "Can view" },
],
onInvite,
da = "glass",
}: ShareDialogProps) {
const id = useId();
const [list, setList] = useState(people);
const [email, setEmail] = useState("");
const [role, setRole] = useState<ShareDialogPerson["role"]>("Can view");
const [publicLink, setPublicLink] = useState(false);
const [copied, setCopied] = useState(false);
const invite = () => {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return;
setList((l) => [...l, { name: email.split("@")[0]!, email, role }]);
onInvite?.(email, role);
setEmail("");
};
return (
<Dialog.Root>
<Dialog.Trigger asChild>
{trigger ?? <button className="da-focus rounded-da-pill bg-da-fg px-5 py-2.5 text-sm font-semibold text-da-bg">Share</button>}
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay
data-da={da}
className="fixed inset-0 z-50 bg-da-overlay bg-none backdrop-blur-sm transition-opacity duration-(--da-duration) starting:opacity-0"
/>
<Dialog.Content
data-da={da}
className="da-stroke fixed top-1/2 left-1/2 z-50 w-[calc(100%-2rem)] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-da-lg bg-da-surface bg-none p-6 text-da-surface-fg shadow-da-lg outline-none backdrop-blur-[40px] backdrop-saturate-150 transition-[opacity,scale] duration-(--da-duration-slow) ease-da-emphasized starting:scale-95 starting:opacity-0 motion-reduce:transition-none"
>
<Dialog.Close
aria-label="Close"
className="da-focus absolute top-4 right-4 grid size-8 place-items-center rounded-full text-da-muted-fg hover:bg-da-fg/10"
>
<X aria-hidden className="size-4" />
</Dialog.Close>
<Dialog.Title className="pr-8 font-da-display text-lg font-semibold tracking-da-display">{title}</Dialog.Title>
<Dialog.Description className="mt-1 text-sm text-da-muted-fg">People with access see the recap, transcript and video.</Dialog.Description>
<form
className="mt-5 flex gap-2"
onSubmit={(e) => {
e.preventDefault();
invite();
}}
>
<label htmlFor={`${id}-email`} className="sr-only">
Email to invite
</label>
<div className="da-stroke flex min-w-0 flex-1 items-center rounded-da-pill bg-da-input pr-1 has-focus-visible:ring-(length:--da-ring-width) has-focus-visible:ring-da-ring">
<input
id={`${id}-email`}
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Add people by email"
className="h-11 min-w-0 flex-1 bg-transparent pl-4 text-sm outline-none placeholder:text-da-muted-fg"
/>
<label htmlFor={`${id}-role`} className="sr-only">
Role
</label>
<select
id={`${id}-role`}
value={role}
onChange={(e) => setRole(e.target.value as ShareDialogPerson["role"])}
className="da-focus h-9 rounded-da-pill bg-transparent px-2 text-xs font-medium"
>
<option>Can view</option>
<option>Can edit</option>
</select>
</div>
<button
type="submit"
className="da-focus h-11 rounded-da-pill bg-gradient-to-r from-da-primary to-da-accent px-5 text-sm font-semibold text-da-primary-fg shadow-da-sm"
>
Invite
</button>
</form>
<ul className="mt-5 space-y-3" aria-label="People with access">
{list.map((p, i) => (
<li key={p.email} className="flex items-center gap-3">
<span
aria-hidden
className={cn(
"grid size-9 place-items-center rounded-full bg-gradient-to-br text-xs font-semibold text-da-primary-fg",
TONES[i % TONES.length],
)}
>
{p.name[0]?.toUpperCase()}
</span>
<span className="min-w-0 flex-1 text-sm leading-tight">
<span className="block truncate font-medium">{p.name}</span>
<span className="block truncate text-xs text-da-muted-fg">{p.email}</span>
</span>
<span className="text-xs text-da-muted-fg">{p.role}</span>
</li>
))}
</ul>
<div className="mt-5 flex items-center gap-3 rounded-da-md bg-da-surface-2 p-3">
<span className="grid size-9 shrink-0 place-items-center rounded-full bg-da-surface">
{publicLink ? <Globe aria-hidden className="size-4 text-da-success" /> : <Lock aria-hidden className="size-4" />}
</span>
<span className="flex-1 text-sm">
<span className="block font-medium">{publicLink ? "Anyone with the link" : "Only people invited"}</span>
<span className="block text-xs text-da-muted-fg">{publicLink ? "can view the recap" : "can open this recap"}</span>
</span>
<button
type="button"
role="switch"
aria-checked={publicLink}
aria-label="Anyone with the link can view"
onClick={() => setPublicLink((v) => !v)}
className={cn(
"da-focus da-transition relative h-6 w-11 rounded-full",
publicLink ? "bg-gradient-to-r from-da-primary to-da-accent" : "bg-da-border-strong",
)}
>
<span
className={cn("da-transition absolute top-0.5 left-0.5 size-5 rounded-full bg-da-primary-fg shadow-da-sm", publicLink && "translate-x-5")}
/>
</button>
</div>
<button
type="button"
onClick={async () => {
try {
await navigator.clipboard.writeText(link);
setCopied(true);
window.setTimeout(() => setCopied(false), 1600);
} catch {
/* clipboard unavailable */
}
}}
className="da-focus da-transition da-stroke mt-5 flex h-11 w-full items-center justify-center gap-2 rounded-da-pill bg-da-surface text-sm font-semibold hover:bg-da-secondary"
>
{copied ? <Check aria-hidden className="size-4 text-da-success" /> : <Link2 aria-hidden className="size-4" />}
{copied ? "Link copied" : "Copy link"}
</button>
<span role="status" className="sr-only">
{copied ? "Link copied to clipboard" : ""}
</span>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
export default ShareDialog;
modules/glass/ui/share-dialog/index.tsx
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| trigger | ReactNode | — | Opener (default Share button). |
| title / link | string | — | Texts. |
| people | { name, email, role }[] | — | Initial access list. |
| onInvite | (email, role) => void | — | Invite handler. |
| da | string | "glass" | DA scope for the portal. |
Other modal variants in Glass
Bottom Sheet
Mobile-style bottom sheet (Radix Dialog): heavily frosted panel sliding up with a grab handle, title and description; becomes a floating centered card on wider screens.
Dropdown Menu
Frosted dropdown menu (Radix DropdownMenu) built from an items array: labels, icons, shortcuts, separators, checkbox items, nested submenus and a danger item.
Modal
Frosted glass sheet dialog (Radix): 40px backdrop blur, optional icon bubble, rise-and-pop entrance with overshoot, blurred overlay, round close button and footer actions.
Modal in other art directions
Confirm Dialog
Destructive confirmation (Radix AlertDialog) over a hazard-striped overlay: red 'Danger zone' band, consequences list, and a type-the-name field that arms the delete button; async pending state.
Drawer
Side sheet (Radix Dialog) sliding in from the right or left in stepped motion: yellow title band with close square, scrollable body for forms, ruled footer actions.
Modal
Accessible dialog on Radix Dialog: colored title band (primary or danger), close button, scrollable body, footer action bar. Re-applies the DA scope inside the portal. Stepped drop-in entrance.
Onboarding Modal
Multi-step onboarding dialog: a colored visual panel (giant step number by default) next to title, description, segmented progress bar and Back/Next buttons; resets on open.
Command Palette
⌘K command palette (Radix Dialog): search input, results filtered as you type and grouped (actions, incidents, schedules), icons and shortcuts, arrow-key highlight, Enter to run, footer hints.
Modal
Radix-based dialog with fade + subtle scale entrance, optional icon, title/description, scrollable body and a tinted footer action bar. Re-applies the DA scope inside the portal.
Popover Card
Anchored popover (Radix Popover) with arrow, optional title + close button and any content — quick forms (snooze), definitions or details. Collision-aware placement.
Sheet
Side sheet (Radix Dialog) sliding in from the right or left over a dimmed overlay: title and description header with close button, scrollable body and footer actions.
Command Dialog
Command palette (Radix Dialog + listbox): a square panel near the top with a large borderless search, grouped results under mono headings, arrow-key navigation with an inverted active row and shortcut hints.
Drawer
Right side panel (Radix Dialog) sliding in over a dimmed page: mono top bar with label and “Close”, large title, scrollable body with ruled rows and a sticky footer.
Lightbox
Full-screen image viewer (Radix Dialog) on ink (stays dark in dark mode): the image frame centered, mono “03 / 04” counter and caption, Prev/Next text buttons and arrow-key navigation, thumbnails strip.
Modal
Square dialog (Radix): a mono label + text “Close” in the top bar, a full-ink rule, a large title, body and right-aligned actions; opens with a quick fade and 8px rise.
Confirm Dialog
Destructive confirmation (Radix AlertDialog): red warning icon, title, consequence text, optional “type VOID to confirm” guard, cancel + red confirm.
Modal
Corporate dialog (Radix): white card with header (title, description, close), divided scrollable body and a tinted footer bar for actions; quick fade + slight scale.
Payment Modal
Pay-invoice dialog: amount summary, card/bank method radio cards, method-specific fields, secure note and a pay button that switches to a success state.
Side Sheet
Detail drawer (Radix Dialog) sliding in from the right: full-height bordered panel with sticky header, scrollable body and footer actions — for record details.
Bottom Sheet
Bottom sheet (Radix Dialog): slides up from the bottom with a grab handle and very rounded top corners; becomes a centered card from sm up.
Confirm Dialog
Gentle confirmation (Radix AlertDialog): a wilting-leaf illustration in a clay circle, serif question, kind consequence text, a sage “keep” action and a quiet clay confirm.
Modal
Soft dialog (Radix): large-radius cream card that rises gently with a slight overshoot, serif title, round close button and pill actions.
Welcome Modal
Onboarding dialog: an illustrated tinted header (sprout growing with each step), serif title and copy, leaf-shaped step dots, Back/Next pills and “Let’s begin” on the last step.
Celebrate Modal
“Week complete!” celebration dialog: pastel confetti shapes burst from the center, trophy emoji, friendly copy, three pastel stat tiles and one pill button.
Confirm Sheet
Destructive confirmation shown as a bottom sheet on mobile and a centered card on desktop: big emoji in a red-tint circle, gentle copy, red confirm with pending state and a soft cancel.
Modal
Soft modal (Radix Dialog): big rounded white card popping in with a small bounce, pastel emoji circle, round close button, body and pill footer actions.
Popover Menu
Rounded dropdown menu (Radix DropdownMenu) with emoji items, keyboard hints, soft highlight, separators and a red danger item; pops in with a bounce.