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-clean/ui/command-dialogSource
"use client";
import { useId, useMemo, useState, type KeyboardEvent, type ReactNode } from "react";
import { Dialog } from "radix-ui";
import { cn } from "@/lib/utils";
export interface CommandItem {
label: string;
group: string;
shortcut?: string;
}
export interface CommandDialogProps {
trigger?: ReactNode;
defaultOpen?: boolean;
items?: CommandItem[];
onSelect?: (item: CommandItem) => void;
da?: string;
}
const DEFAULT_ITEMS: CommandItem[] = [
{ label: "New project", group: "Create", shortcut: "N" },
{ label: "New page", group: "Create", shortcut: "P" },
{ label: "Upload images", group: "Create", shortcut: "U" },
{ label: "Go to Projects", group: "Navigate" },
{ label: "Go to Settings", group: "Navigate" },
{ label: "Switch template", group: "Site" },
{ label: "Publish site", group: "Site", shortcut: "⇧P" },
];
/** 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. */
export function CommandDialog({ trigger, defaultOpen, items = DEFAULT_ITEMS, onSelect, da = "mono-clean" }: CommandDialogProps) {
const id = useId();
const [open, setOpen] = useState(defaultOpen ?? false);
const [q, setQ] = useState("");
const [active, setActive] = useState(0);
const list = useMemo(() => items.filter((i) => i.label.toLowerCase().includes(q.trim().toLowerCase())), [items, q]);
const groups = [...new Set(list.map((i) => i.group))];
const choose = (it: CommandItem | undefined) => {
if (!it) return;
onSelect?.(it);
setOpen(false);
};
const key = (e: KeyboardEvent) => {
if (e.key === "ArrowDown") {
e.preventDefault();
setActive((a) => Math.min(list.length - 1, a + 1));
} else if (e.key === "ArrowUp") {
e.preventDefault();
setActive((a) => Math.max(0, a - 1));
} else if (e.key === "Enter") choose(list[active]);
};
return (
<Dialog.Root
open={open}
onOpenChange={(o) => {
setOpen(o);
setQ("");
setActive(0);
}}
>
{trigger && <Dialog.Trigger asChild>{trigger}</Dialog.Trigger>}
<Dialog.Portal>
<Dialog.Overlay
data-da={da}
className="fixed inset-0 z-50 bg-da-overlay transition-opacity duration-(--da-duration) starting:opacity-0 motion-reduce:transition-none"
/>
<Dialog.Content
data-da={da}
className="fixed top-[12vh] left-1/2 z-50 w-[calc(100%-2rem)] max-w-xl -translate-x-1/2 bg-da-surface text-da-surface-fg shadow-da-lg outline-none transition-[opacity,translate] duration-(--da-duration) starting:translate-y-2 starting:opacity-0 motion-reduce:transition-none"
>
<Dialog.Title className="sr-only">Command palette</Dialog.Title>
<Dialog.Description className="sr-only">Type to filter commands, use arrow keys and Enter to run.</Dialog.Description>
<input
autoFocus
value={q}
onChange={(e) => {
setQ(e.target.value);
setActive(0);
}}
onKeyDown={key}
role="combobox"
aria-expanded
aria-controls={`${id}-l`}
aria-activedescendant={list[active] ? `${id}-${active}` : undefined}
aria-label="Search commands"
placeholder="Type a command…"
className="h-16 w-full border-b border-da-border-strong bg-transparent px-5 text-xl outline-none placeholder:text-da-muted-fg"
/>
<ul id={`${id}-l`} role="listbox" aria-label="Commands" className="max-h-80 overflow-y-auto py-2">
{groups.map((g) => (
<li key={g} role="presentation">
<p className="px-5 pt-3 pb-1 font-da-mono text-[10px] tracking-da-label text-da-muted-fg uppercase">{g}</p>
<ul role="group" aria-label={g}>
{list
.filter((i) => i.group === g)
.map((it) => {
const idx = list.indexOf(it);
return (
<li
key={it.label}
id={`${id}-${idx}`}
role="option"
aria-selected={idx === active}
onMouseEnter={() => setActive(idx)}
onClick={() => choose(it)}
className={cn("flex cursor-pointer items-center justify-between px-5 py-2.5", idx === active && "bg-da-fg text-da-bg")}
>
{it.label}
{it.shortcut && <kbd className="font-da-mono text-[11px] opacity-70">{it.shortcut}</kbd>}
</li>
);
})}
</ul>
</li>
))}
{!list.length && <li className="px-5 py-6 text-da-muted-fg">No command matches “{q}”.</li>}
</ul>
<p className="flex gap-4 border-t border-da-border px-5 py-2 font-da-mono text-[10px] tracking-da-label text-da-muted-fg uppercase">
<span>↑↓ Navigate</span>
<span>↵ Run</span>
<span>Esc Close</span>
</p>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
export default CommandDialog;
modules/mono-clean/ui/command-dialog/index.tsx
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| trigger | ReactNode | — | Trigger. |
| defaultOpen | boolean | — | Default Open. |
| items | CommandItem[] | — | Content items (see the exported types). |
| onSelect | (item: CommandItem) => void | — | Callback. |
| da | string | — | DA scope applied to portalled content. |
Other modal variants in Mono Clean
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.
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.
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.
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.
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.
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.