72 lines
2.2 KiB
TypeScript
72 lines
2.2 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import { createPortal } from "react-dom";
|
|
import type { Tone } from "./Panel";
|
|
|
|
const TITLE_TONES: Partial<Record<Tone, string>> = {
|
|
amber: "text-amber-300",
|
|
emerald: "text-emerald-300",
|
|
cyan: "text-cyan-200",
|
|
};
|
|
const BORDER_TONES: Partial<Record<Tone, string>> = {
|
|
amber: "border-amber-400/50",
|
|
emerald: "border-emerald-400/40",
|
|
cyan: "border-cyan-400/40",
|
|
};
|
|
|
|
/**
|
|
* A modal dialog: a bottom sheet on phones, a centred card from `sm` up.
|
|
* Labelled by its title, which it renders as an `h2` with `titleId`. Portalled
|
|
* to `body`: a transformed ancestor (the screens' rise-in) would otherwise
|
|
* become the containing block of `position: fixed`.
|
|
*/
|
|
export function Sheet({
|
|
titleId,
|
|
title,
|
|
eyebrow,
|
|
subtitle,
|
|
tone = "default",
|
|
layer = "z-20",
|
|
children,
|
|
}: {
|
|
titleId: string;
|
|
title: ReactNode;
|
|
eyebrow?: ReactNode;
|
|
subtitle?: ReactNode;
|
|
tone?: Tone;
|
|
/** Stacking layer, e.g. `z-30` for a sheet over other sheets. */
|
|
layer?: string;
|
|
children: ReactNode;
|
|
}) {
|
|
return createPortal(
|
|
<div
|
|
className={`fixed inset-0 ${layer} flex items-end justify-center bg-space-950/75 backdrop-blur-sm sm:items-center sm:p-4`}
|
|
>
|
|
<div
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby={titleId}
|
|
className={`relative max-h-[88dvh] w-full max-w-md animate-sheet overflow-y-auto rounded-t-2xl border bg-space-900/95 p-5 pb-[max(1.25rem,env(safe-area-inset-bottom))] shadow-panel sm:rounded-2xl ${BORDER_TONES[tone] ?? "border-space-600"}`}
|
|
>
|
|
<div
|
|
aria-hidden="true"
|
|
className="mx-auto -mt-2 mb-3 h-1 w-10 rounded-full bg-space-600 sm:hidden"
|
|
/>
|
|
{eyebrow && (
|
|
<div className="font-display text-[11px] font-semibold tracking-[0.2em] text-cyan-300/80 uppercase">
|
|
{eyebrow}
|
|
</div>
|
|
)}
|
|
<h2
|
|
id={titleId}
|
|
className={`font-display text-xl font-bold tracking-wide ${TITLE_TONES[tone] ?? "text-ink-50"}`}
|
|
>
|
|
{title}
|
|
</h2>
|
|
{subtitle && <div className="mt-1 text-sm text-ink-300">{subtitle}</div>}
|
|
{children}
|
|
</div>
|
|
</div>,
|
|
document.body,
|
|
);
|
|
}
|