mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
Unlayered, the safe-area, keyboard and sheet-clearance rules outranked every utility on the same element, so a later bg-* on the shell or a bottom-* on a sheet would have been ignored without a trace. They now sit under the utilities. The bottom sheet drops its bottom-0, which the layered keyboard rule replaces with the same zero until the keyboard opens; the keyboard rules that must win keep their !important. The toast lift stays unlayered to outrank the toaster's own rules.
173 lines
6.1 KiB
TypeScript
173 lines
6.1 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`).
|
|
// A bottom sheet's `bottom` comes from that rule (0 until the
|
|
// keyboard opens), so its branch below carries no `bottom-*`: a
|
|
// utility would outrank the layered rule and pin it to the edge.
|
|
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 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,
|
|
};
|