Files
selfphoneng/apps/web/src/components/Dialog.tsx
T
iamdoubz 3d0f091bba
ci / check (push) Successful in 2m5s
ci / images (push) Successful in 3m7s
web: icons, grouped admin sidebar, Home cards, phone-style softphone
lucide-react (1.56.0) is the one icon set; it hides icons from screen
readers by default and only the icons used are bundled (+6 kB gzipped).
It replaces the ✕ ☀ ☾ ↑ ↓ ▶ ✓ ← characters and the hand-drawn menu and
phone icons.

The admin sidebar's 21 pages are grouped under Call routing, People,
Features, History and System, each with an icon; the current page gets a
left bar. The My Phone tabs have icons and a blue active state.

Home is a greeting and cards (new shared Panel): My status, Missed calls
with a call-back button, New voicemails, with friendly empty states.

Softphone: a status pill (dot and word), call controls as icon-over-label
buttons that are toggles with aria-pressed (Mute, Hold, Keypad, Transfer,
red Hang up), and a round keypad with the letters under the digits.
2026-10-28 10:00:29 -05:00

101 lines
2.5 KiB
TypeScript

// SPDX-License-Identifier: AGPL-3.0-or-later
// Native <dialog> (focus trap + Esc for free) as a centered modal or a right-hand drawer.
import { X } from 'lucide-react';
import { useEffect, useRef, type ReactNode } from 'react';
import { Button } from './ui.tsx';
export function Dialog({
open,
onClose,
title,
children,
footer,
drawer = false,
}: {
open: boolean;
onClose: () => void;
title: string;
children: ReactNode;
footer?: ReactNode;
drawer?: boolean;
}) {
const ref = useRef<HTMLDialogElement>(null);
useEffect(() => {
const d = ref.current;
if (!d) return;
if (open && !d.open) d.showModal();
if (!open && d.open) d.close();
}, [open]);
const shape = drawer
? 'ml-auto mr-0 h-dvh max-h-dvh w-full max-w-md'
: 'm-auto w-[calc(100%-2rem)] max-w-md rounded-xl';
return (
<dialog
ref={ref}
onClose={onClose}
onClick={(e) => e.target === ref.current && onClose()}
aria-labelledby="dialog-title"
className={`${shape} bg-surface p-0 text-fg shadow-xl backdrop:bg-black/40`}
>
<div className="flex h-full flex-col">
<header className="flex items-center justify-between border-b border-line px-4 py-3">
<h2 id="dialog-title" className="text-base font-semibold">
{title}
</h2>
<Button variant="ghost" onClick={onClose} aria-label="Close" className="px-2 py-1">
<X className="size-4" />
</Button>
</header>
<div className="flex-1 overflow-y-auto p-4">{children}</div>
{footer && (
<footer className="flex justify-end gap-2 border-t border-line px-4 py-3">
{footer}
</footer>
)}
</div>
</dialog>
);
}
export function ConfirmDialog({
open,
title,
message,
confirmLabel,
onConfirm,
onCancel,
busy,
error,
}: {
open: boolean;
title: string;
message: ReactNode;
confirmLabel: string;
onConfirm: () => void;
onCancel: () => void;
busy?: boolean;
error?: string;
}) {
return (
<Dialog
open={open}
onClose={onCancel}
title={title}
footer={
<>
<Button onClick={onCancel}>Cancel</Button>
<Button variant="danger" onClick={onConfirm} disabled={busy}>
{confirmLabel}
</Button>
</>
}
>
<div className="flex flex-col gap-3 text-sm">
{message}
{error && <p className="text-red-600 dark:text-red-400">{error}</p>}
</div>
</Dialog>
);
}