Files
personal-admin-2026/apps/frontend/src/components/ui/responsive-dialog.tsx
Jose Selesan 5b6ccf0fa6 chore: add Biome for lint and fix all lint errors
- Install @biomejs/biome as devDependency at root
- Configure biome.json with 2-space indent, double quotes, Tailwind CSS support
- Add lint/lint:fix/format/format:fix scripts to root and app package.json
- Fix noNonNullAssertion: env vars extracted to variables with suppression, <div role=button> replaced with <button>
- Fix noUnusedVariables: remove unused destructured vars
- Fix useIterableCallbackReturn: arrow functions with block body
- Fix noExplicitAny: recharts Tooltip formatters
- Fix noLabelWithoutControl: add htmlFor+id or use <span> for non-input labels
- Fix noStaticElementInteractions/useKeyWithClickEvents: role+keyboard events for overlays
- Fix noArrayIndexKey: use error string as key
- Fix CSS parse: enable tailwindDirectives parser
- Normalize formatting across 80 files with biome check --write
2026-06-04 10:48:07 -03:00

243 lines
6.6 KiB
TypeScript

"use client";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
const MEDIA_QUERY = "(min-width: 640px)";
function useMediaQuery(query: string) {
const [matches, setMatches] = React.useState(() => {
if (typeof window === "undefined") return true;
return window.matchMedia(query).matches;
});
React.useEffect(() => {
const mql = window.matchMedia(query);
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
mql.addEventListener("change", handler);
return () => mql.removeEventListener("change", handler);
}, [query]);
return matches;
}
interface ResponsiveDialogContextValue {
open: boolean;
onOpenChange: (open: boolean) => void;
isDesktop: boolean;
}
const ResponsiveDialogContext =
React.createContext<ResponsiveDialogContextValue | null>(null);
function useResponsiveDialog() {
const ctx = React.useContext(ResponsiveDialogContext);
if (!ctx)
throw new Error(
"ResponsiveDialog components must be used within ResponsiveDialog",
);
return ctx;
}
function ResponsiveDialog({
open,
onOpenChange,
children,
}: {
open?: boolean;
onOpenChange?: (open: boolean) => void;
children: React.ReactNode;
}) {
const [internalOpen, setInternalOpen] = React.useState(false);
const isDesktop = useMediaQuery(MEDIA_QUERY);
const controlled = open !== undefined;
const currentOpen = controlled ? open : internalOpen;
// biome-ignore lint/style/noNonNullAssertion: controlled ensures it's defined
const setCurrentOpen = controlled ? onOpenChange! : setInternalOpen;
return (
<ResponsiveDialogContext
value={{ open: currentOpen, onOpenChange: setCurrentOpen, isDesktop }}
>
{isDesktop ? (
<DialogPrimitive.Root open={currentOpen} onOpenChange={setCurrentOpen}>
{children}
</DialogPrimitive.Root>
) : (
<DialogPrimitive.Root open={currentOpen} onOpenChange={setCurrentOpen}>
{children}
</DialogPrimitive.Root>
)}
</ResponsiveDialogContext>
);
}
function ResponsiveDialogTrigger({
children,
asChild,
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return (
<DialogPrimitive.Trigger data-slot="responsive-dialog-trigger" {...props}>
{children}
</DialogPrimitive.Trigger>
);
}
function ResponsiveDialogContent({
className,
children,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
const { isDesktop, open, onOpenChange } = useResponsiveDialog();
if (isDesktop) {
return (
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay
data-slot="responsive-dialog-overlay"
className="fixed inset-0 z-50 bg-black/10 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0"
/>
<DialogPrimitive.Content
data-slot="responsive-dialog-content"
className={cn(
"fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2",
"flex flex-col gap-4 rounded-xl border bg-popover p-6 text-popover-foreground shadow-lg",
"data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-open:slide-in-from-left-1/2 data-open:slide-in-from-top-[48%]",
"data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-closed:slide-out-to-left-1/2 data-closed:slide-out-to-top-[48%]",
"duration-200",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close asChild>
<Button
variant="ghost"
className="absolute top-3 right-3"
size="icon-sm"
>
<XIcon />
<span className="sr-only">Close</span>
</Button>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
);
}
if (!open) return null;
return (
<div
className="fixed inset-0 z-50 flex items-end sm:hidden"
data-slot="responsive-dialog-mobile"
>
<button
type="button"
className="fixed inset-0 z-40 bg-black/10 supports-backdrop-filter:backdrop-blur-xs cursor-default"
onClick={() => onOpenChange(false)}
aria-label="Cerrar"
/>
<div
className={cn(
"relative z-50 flex w-full flex-col gap-4 rounded-t-xl border bg-popover p-6 text-popover-foreground shadow-lg",
"animate-in slide-in-from-bottom-10 fade-in-0 duration-200",
className,
)}
>
<div className="mx-auto -mt-2 mb-1 h-1.5 w-10 rounded-full bg-muted" />
<DialogPrimitive.Close asChild>
<Button
variant="ghost"
size="icon-sm"
className="absolute top-3 right-3"
>
<XIcon />
<span className="sr-only">Close</span>
</Button>
</DialogPrimitive.Close>
{children}
</div>
</div>
);
}
function ResponsiveDialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="responsive-dialog-header"
className={cn("flex flex-col gap-0.5", className)}
{...props}
/>
);
}
function ResponsiveDialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="responsive-dialog-footer"
className={cn(
"mt-2 flex flex-col-reverse sm:flex-row sm:justify-end gap-2",
className,
)}
{...props}
/>
);
}
function ResponsiveDialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="responsive-dialog-title"
className={cn("text-base font-medium text-foreground", className)}
{...props}
/>
);
}
function ResponsiveDialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="responsive-dialog-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
);
}
function ResponsiveDialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return (
<DialogPrimitive.Close data-slot="responsive-dialog-close" {...props} />
);
}
export {
ResponsiveDialog,
ResponsiveDialogClose,
ResponsiveDialogContent,
ResponsiveDialogDescription,
ResponsiveDialogFooter,
ResponsiveDialogHeader,
ResponsiveDialogTitle,
ResponsiveDialogTrigger,
};