mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
Safe areas. The installed app draws under the status bar and beside the notch, and each 4rem header band padded the inset inside its own border-box height: on a phone with a 59 px inset the top bar kept a 5 px content box and the logo and account menu straddled its bottom border. The authenticated shell now takes the top and side insets once on its outer wrapper (`shell-safe-area`, the strip painted in the chrome colour), so the top bar, the sidebar band and any docked panel start below the status bar together and the seam between them holds. The two Coach full-height pages subtract the inset at every width, the full- height day sheet keeps its handle clear of the Dynamic Island, side sheets keep clear of the status bar and the notch, the bottom bar and the Coach button take the side insets of a phone held sideways, a footer-less responsive sheet keeps its last row above the home indicator, and the sign-in and onboarding shells pad themselves. The sign-in shell grows with its content (`min-h-dvh`), so a phone held sideways can scroll to the top of the card. Keyboard. iOS Safari and Chrome on Android shrink only the visual viewport when the keyboard opens, so a bottom sheet kept Save and the Coach composer behind it. `KeyboardInsetBridge` publishes the covered height (`--keyboard-inset`, `data-keyboard` on <html>); bottom sheets stand on the keyboard and give up its height to their scrolling body, and the bottom bar and the Coach button step aside while it is open. Pinch-zoom and toolbar-sized changes do not count as a keyboard. The sheet primitive names its side in `data-sheet-side`, which consumers do not override the way they replace `data-slot`. pwa-mobile-shell-guard.test.ts pins the structure no headless browser can show (no browser in CI reports a safe-area inset).
170 lines
5.9 KiB
TypeScript
170 lines
5.9 KiB
TypeScript
"use client";
|
|
|
|
import * as React from "react";
|
|
import { XIcon } from "lucide-react";
|
|
import { Dialog as SheetPrimitive } from "radix-ui";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
import { useTranslations } from "@/lib/i18n/context";
|
|
|
|
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
|
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
|
|
}
|
|
|
|
function SheetTrigger({
|
|
...props
|
|
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
|
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
|
|
}
|
|
|
|
function SheetClose({
|
|
...props
|
|
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
|
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
|
|
}
|
|
|
|
function SheetPortal({
|
|
...props
|
|
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
|
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
|
|
}
|
|
|
|
function SheetOverlay({
|
|
className,
|
|
...props
|
|
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
|
return (
|
|
<SheetPrimitive.Overlay
|
|
data-slot="sheet-overlay"
|
|
className={cn(
|
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function SheetContent({
|
|
className,
|
|
children,
|
|
side = "right",
|
|
showCloseButton = true,
|
|
...props
|
|
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
|
side?: "top" | "right" | "bottom" | "left";
|
|
showCloseButton?: boolean;
|
|
}) {
|
|
const { t } = useTranslations();
|
|
return (
|
|
<SheetPortal>
|
|
<SheetOverlay />
|
|
<SheetPrimitive.Content
|
|
data-slot="sheet-content"
|
|
// Read by the safe-area and on-screen-keyboard rules in
|
|
// `globals.css`: a bottom sheet stands on the keyboard, the
|
|
// full-height sides keep clear of the status bar and the notch.
|
|
// Its own attribute rather than `data-slot`, which consumers
|
|
// replace with theirs (`responsive-sheet-content`, `day-panel`).
|
|
data-sheet-side={side}
|
|
className={cn(
|
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
|
side === "right" &&
|
|
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
|
side === "left" &&
|
|
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
|
side === "top" &&
|
|
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
|
side === "bottom" &&
|
|
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
{showCloseButton && (
|
|
// v1.4.27 R4 RC3 — widen the icon-only close-X tap target to
|
|
// a 36 px floor, matching the Dialog close-X treatment from
|
|
// MB2 (Decision I). 36 px (not 44 px) is the intentional
|
|
// exception for fixed icon-only close affordances per the
|
|
// dialog primitive's rationale: 44 px would crowd the sheet
|
|
// header out of proportion on the bottom-sheet branch.
|
|
<SheetPrimitive.Close
|
|
data-slot="sheet-close-button"
|
|
className="ring-offset-background focus-visible:border-ring focus-visible:ring-ring/50 text-muted-foreground hover:bg-accent hover:text-foreground absolute top-3 right-4 inline-flex min-h-11 min-w-11 items-center justify-center rounded-md transition-colors focus-visible:ring-[3px] focus-visible:outline-none disabled:pointer-events-none sm:min-h-9 sm:min-w-9 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
|
>
|
|
<XIcon />
|
|
{/* v1.4.34 IW-G — read `common.close` so screen readers
|
|
hear the localised label instead of literal English. */}
|
|
<span className="sr-only">{t("common.close")}</span>
|
|
</SheetPrimitive.Close>
|
|
)}
|
|
</SheetPrimitive.Content>
|
|
</SheetPortal>
|
|
);
|
|
}
|
|
|
|
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
return (
|
|
<div
|
|
data-slot="sheet-header"
|
|
// Reserve the trailing gutter for the absolute close-X the way
|
|
// `DialogHeader` (pr-9) does, sized to the sheet's own close button
|
|
// (`top-3 right-4`, min-w-9…11) so a long raw-`<Sheet>` title never
|
|
// slides under the X — the Documents close-X overlap class, foreclosed
|
|
// at the primitive. Consumers that render `showCloseButton={false}`
|
|
// (or their own close affordance) drop it back with `pr-4` via
|
|
// `className` — tailwind-merge keeps the later value.
|
|
className={cn("flex flex-col gap-1.5 p-4 pr-12", className)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
return (
|
|
<div
|
|
data-slot="sheet-footer"
|
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function SheetTitle({
|
|
className,
|
|
...props
|
|
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
|
return (
|
|
<SheetPrimitive.Title
|
|
data-slot="sheet-title"
|
|
className={cn("text-foreground font-semibold", className)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function SheetDescription({
|
|
className,
|
|
...props
|
|
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
|
return (
|
|
<SheetPrimitive.Description
|
|
data-slot="sheet-description"
|
|
className={cn("text-muted-foreground text-sm", className)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export {
|
|
Sheet,
|
|
SheetTrigger,
|
|
SheetClose,
|
|
SheetContent,
|
|
SheetHeader,
|
|
SheetFooter,
|
|
SheetTitle,
|
|
SheetDescription,
|
|
};
|