Files
flvx/vite-frontend/src/shadcn-bridge/heroui/modal.tsx
T
sagit 7668e24f4b feat(ui): redesign with transparent liquid glass (#414)
* docs: add design spec for iOS 26 Liquid Glass UI redesign

* docs: add implementation plan for iOS 26 Liquid Glass UI redesign

* feat(ui): add liquid glass CSS variables and update root layouts

* feat(ui): update Card and Modal components to iOS 26 Liquid Glass style

* feat(ui): redesign dashboard page to Liquid Glass style

* feat(ui): redesign node management page cards and charts

* feat(ui): redesign monitor page to Liquid Glass style

* feat(ui): redesign group and panel-sharing cards to Liquid Glass

* feat(ui): redesign settings and user components to Liquid Glass

* feat(ui): redesign profile and auth pages to Liquid Glass

* fix(ui): revert to LineChart to fix compilation issues

* refactor(ui): extract dashboard user header and cleanup admin layout

* feat(ui): refine monitor page hero metrics and fix build

* fix(ui): real data for monitor hero metrics and card padding adjustments

* fix(ui): apply global card padding and fix index layouts

* feat(ui): implement liquid-glass-react for all Card components

* fix(ui): resolve liquid-glass-react rendering issues causing invisible text and broken layout

* fix(ui): native CSS liquid-glass effect replacing NPM package

* fix(ui): cleanup duplicate closing tags and complete liquid glass tables

* fix(ui): apply perfect rounded corners to list views and table headers in non-compact mode

* fix(ui): apply liquid glass styling to grouped rules view in forward page

* fix(ui): re-implement liquid-glass natively to prevent text distortion and remove package dependency

* fix(ui): remove mouse distortion and preserve proper liquid glass texture

* fix(ui): remove svg filter causing text distortion and replace with pure css liquid glass

* fix(ui): apply properly scoped border radius to table headers

* fix(ui): remove hardcoded bg-default-50/60 from TableHeader

* fix(ui): apply global liquid glass styling and transparent overlays to all modals and dialogs

* fix(ui): remove absolute/relative class conflicts hiding Dialog modales and implement pure transparent layout

* feat(ui): apply global liquid glass styling to cards, buttons, and diagnosis modals
2026-04-21 09:19:02 +08:00

219 lines
4.7 KiB
TypeScript

import * as React from "react";
import {
Dialog,
DialogContent as BaseDialogContent,
} from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
interface DisclosureOptions {
isOpen?: boolean;
}
type OpenChangeHandler = (open?: boolean) => void;
export function useDisclosure(options: DisclosureOptions = {}) {
const [isOpen, setIsOpen] = React.useState(Boolean(options.isOpen));
const onOpen = React.useCallback(() => {
setIsOpen(true);
}, []);
const onClose = React.useCallback(() => {
setIsOpen(false);
}, []);
const onOpenChange = React.useCallback<OpenChangeHandler>((open) => {
setIsOpen((prev) => (typeof open === "boolean" ? open : !prev));
}, []);
return {
isOpen,
onClose,
onOpen,
onOpenChange,
};
}
interface ModalContextValue {
classNames?: Record<string, string>;
onClose: () => void;
scrollBehavior?: "inside" | "outside";
size?: ModalSize;
}
const ModalContext = React.createContext<ModalContextValue | null>(null);
function useModalContext() {
return React.useContext(ModalContext);
}
type ModalSize = "sm" | "md" | "lg" | "xl" | "2xl" | "4xl" | "full";
function mapSize(size: ModalSize | undefined) {
if (size === "sm") {
return "max-w-md";
}
if (size === "lg") {
return "max-w-2xl";
}
if (size === "xl") {
return "max-w-3xl";
}
if (size === "2xl") {
return "max-w-5xl";
}
if (size === "4xl") {
return "max-w-6xl";
}
if (size === "full") {
return "w-[95vw] max-w-[95vw] h-[95vh] max-h-[95vh]";
}
return "max-w-lg";
}
export interface ModalProps {
backdrop?: "blur" | "opaque" | "transparent";
children: React.ReactNode;
className?: string;
classNames?: Record<string, string>;
isDismissable?: boolean;
isOpen?: boolean;
onClose?: () => void;
onOpenChange?: (open: boolean) => void;
placement?: "center" | "top" | "bottom";
scrollBehavior?: "inside" | "outside";
size?: ModalSize;
}
export function Modal({
children,
classNames,
isOpen = false,
onClose,
onOpenChange,
scrollBehavior,
size,
}: ModalProps) {
const handleOpenChange = (open: boolean) => {
onOpenChange?.(open);
if (!open) {
onClose?.();
}
};
const contextValue = {
classNames,
onClose: () => {
handleOpenChange(false);
},
scrollBehavior,
size,
};
return (
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<ModalContext.Provider value={contextValue}>
{children}
</ModalContext.Provider>
</Dialog>
);
}
interface ModalContentProps
extends Omit<React.ComponentProps<"div">, "children"> {
children: React.ReactNode | ((onClose: () => void) => React.ReactNode);
scrollBehavior?: "inside" | "outside";
size?: ModalSize;
}
export function ModalContent({
children,
className,
scrollBehavior,
size,
...props
}: ModalContentProps) {
const context = useModalContext();
const resolvedScrollBehavior = scrollBehavior ?? context?.scrollBehavior;
const resolvedSize = size ?? context?.size;
const renderedChildren =
typeof children === "function"
? children(() => context?.onClose())
: children;
return (
<BaseDialogContent
className={cn(
mapSize(resolvedSize),
context?.classNames?.base,
resolvedScrollBehavior === "outside"
? "max-h-[90vh] overflow-y-auto scrollbar-hide"
: "",
resolvedScrollBehavior === "inside"
? "max-h-[90vh] flex flex-col overflow-hidden [&_[data-slot=modal-body]]:min-h-0 [&_[data-slot=modal-body]]:flex-1 [&_[data-slot=modal-body]]:overflow-y-auto [&_[data-slot=modal-body]]:scrollbar-hide"
: "",
className,
)}
showCloseButton={false}
{...props}
>
{renderedChildren}
</BaseDialogContent>
);
}
export function ModalHeader({
className,
...props
}: React.ComponentProps<"div">) {
const context = useModalContext();
return (
<div
className={cn(
"text-lg font-semibold",
context?.classNames?.header,
className,
)}
data-slot="modal-header"
{...props}
/>
);
}
export function ModalBody({
className,
...props
}: React.ComponentProps<"div">) {
const context = useModalContext();
return (
<div
className={cn("space-y-4", context?.classNames?.body, className)}
data-slot="modal-body"
{...props}
/>
);
}
export function ModalFooter({
className,
...props
}: React.ComponentProps<"div">) {
const context = useModalContext();
return (
<div
className={cn(
"mt-4 flex flex-wrap justify-end gap-2",
context?.classNames?.footer,
className,
)}
data-slot="modal-footer"
{...props}
/>
);
}