Skip to content

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-dialog
Open ↗

Source

"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

PropTypeDefaultDescription
triggerReactNode—Opener (default Share button).
title / linkstring—Texts.
people{ name, email, role }[]—Initial access list.
onInvite(email, role) => void—Invite handler.
dastring"glass"DA scope for the portal.

Other modal variants in Glass

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.

BrutalistModal

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.

BrutalistModal

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.

BrutalistModal

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.

BrutalistModal

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.

MinimalModal

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.

MinimalModal

Popover Card

Anchored popover (Radix Popover) with arrow, optional title + close button and any content — quick forms (snooze), definitions or details. Collision-aware placement.

MinimalModal

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.

MinimalModal

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.

Mono CleanModal

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.

Mono CleanModal

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.

Mono CleanModal

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.

Mono CleanModal

Confirm Dialog

Destructive confirmation (Radix AlertDialog): red warning icon, title, consequence text, optional “type VOID to confirm” guard, cancel + red confirm.

Neo CorporateModal

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.

Neo CorporateModal

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.

Neo CorporateModal

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.

Neo CorporateModal

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.

Organic SoftModal

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.

Organic SoftModal

Modal

Soft dialog (Radix): large-radius cream card that rises gently with a slight overshoot, serif title, round close button and pill actions.

Organic SoftModal

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.

Organic SoftModal

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.

Soft FlatModal

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.

Soft FlatModal

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.

Soft FlatModal

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.

Soft FlatModal