deadhead-space-freight-tycoon/apps/web/src/ui/Sheet.tsx

72 lines
2.5 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`. It opens below the top
* bar (3.5rem, `--header-h` in Layout), so the credits there stay in view.
*/
export function Sheet({
titleId,
title,
eyebrow,
subtitle,
tone = "default",
layer = "z-20",
side = false,
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;
/** From `xl`, opens beside the wide panel and leaves it usable (decision 0.5.169). */
side?: boolean;
children: ReactNode;
}) {
return createPortal(
<div
className={`fixed inset-x-0 top-14 bottom-0 ${layer} flex items-end justify-center bg-space-950/70 sm:items-center sm:p-4 ${side ? "xl:pointer-events-none xl:top-20 xl:items-start xl:justify-end xl:bg-transparent xl:pr-[calc(45rem+2rem)]" : ""}`}
>
<div
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
className={`relative max-h-full w-full max-w-md animate-sheet ${side ? "xl:pointer-events-auto xl:max-w-[26rem]" : ""} overflow-y-auto rounded-t-3xl border bg-space-850 p-5 pb-[max(1.25rem,env(safe-area-inset-bottom))] shadow-panel sm:rounded-3xl ${BORDER_TONES[tone] ?? "border-glass-line"}`}
>
<div
aria-hidden="true"
className="mx-auto -mt-2 mb-3 h-1 w-10 rounded-full bg-white/25 sm:hidden"
/>
{eyebrow && <div className="text-xs font-semibold text-ink-400">{eyebrow}</div>}
<h2
id={titleId}
className={`font-title text-lg leading-snug ${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,
);
}