Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 29 additions & 17 deletions src/components/ui/alert-dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
"use client";

import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog";
import * as React from "react";

import { cn } from "@/lib/utils";
import { Button } from "./button";
import { useThemeOptional, defaultCssVariables } from "@/providers";
import { useThemeOptional } from "@/providers";

function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
Expand All @@ -15,9 +17,14 @@ function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) {
);
}

function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) {
function AlertDialogPortal({
children,
...props
}: AlertDialogPrimitive.Portal.Props) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props}>
{children}
</AlertDialogPrimitive.Portal>
);
}

Expand All @@ -40,25 +47,30 @@ function AlertDialogOverlay({
function AlertDialogContent({
className,
size = "default",
children,
...props
}: AlertDialogPrimitive.Popup.Props & {
size?: "default" | "sm";
}) {
const theme = useThemeOptional();
const mode = theme?.mode ?? 'light';
const cssVariables = theme?.cssVariables ?? defaultCssVariables;
const { resolvedMode, cssVariables, className: themeClassName } = useThemeOptional();

return (
<AlertDialogPortal className={ cn('pui-root', mode, theme?.className) } style={cssVariables}>
<AlertDialogOverlay />
<AlertDialogPrimitive.Popup
data-slot="alert-dialog-content"
data-size={size}
className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-background ring-foreground/10 gap-6 rounded-xl p-6 ring-1 duration-100 data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-lg group/alert-dialog-content fixed top-1/2 start-1/2 z-50 grid w-full -translate-x-1/2 rtl:translate-x-1/2 -translate-y-1/2 outline-none",
className
)}
{...props}
/>
<AlertDialogPortal>
<div className={ cn('pui-root', resolvedMode, themeClassName) } style={cssVariables}>

<AlertDialogOverlay />
<AlertDialogPrimitive.Popup
data-slot="alert-dialog-content"
data-size={size}
className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-background ring-foreground/10 gap-6 rounded-xl p-6 ring-1 duration-100 data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-lg group/alert-dialog-content fixed top-1/2 start-1/2 z-50 grid w-full -translate-x-1/2 rtl:translate-x-1/2 -translate-y-1/2 outline-none",
className
)}
{...props}
>
{children}
</AlertDialogPrimitive.Popup>
</div>
</AlertDialogPortal>
);
}
Expand Down
61 changes: 37 additions & 24 deletions src/components/ui/combobox.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"use client";

import { Combobox as ComboboxPrimitive } from "@base-ui/react";
import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react";
import * as React from "react";

import { cn } from "@/lib/utils";
import { Button } from "./button";
import { Input } from "./input";
import { useThemeOptional, defaultCssVariables } from "@/providers";
import { useThemeOptional } from "@/providers";

const Combobox = ComboboxPrimitive.Root;

Expand Down Expand Up @@ -103,6 +105,17 @@ const ComboboxInput = React.forwardRef<
);
ComboboxInput.displayName = "ComboboxInput";

function ComboboxPortal({
children,
...props
}: ComboboxPrimitive.Portal.Props) {
return (
<ComboboxPrimitive.Portal data-slot="combobox-portal" {...props}>
{children}
</ComboboxPrimitive.Portal>
);
}

function ComboboxContent({
className,
side = "bottom",
Expand All @@ -116,30 +129,30 @@ function ComboboxContent({
ComboboxPrimitive.Positioner.Props,
"side" | "align" | "sideOffset" | "alignOffset" | "anchor"
>) {
const theme = useThemeOptional();
const mode = theme?.mode ?? 'light';
const cssVariables = theme?.cssVariables ?? defaultCssVariables;
const { resolvedMode, cssVariables, className: themeClassName } = useThemeOptional();
return (
<ComboboxPrimitive.Portal className={ cn('pui-root', mode, theme?.className) } style={cssVariables}>
<ComboboxPrimitive.Positioner
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
anchor={anchor}
className="isolate z-50"
>
<ComboboxPrimitive.Popup
data-slot="combobox-content"
data-chips={!!anchor}
className={cn(
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 max-h-72 min-w-36 overflow-hidden rounded-md shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-end-2 data-[side=inline-end]:slide-in-from-start-2 group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) data-[chips=true]:min-w-(--anchor-width)",
className
)}
{...props}
/>
</ComboboxPrimitive.Positioner>
</ComboboxPrimitive.Portal>
<ComboboxPortal>
<div className={ cn('pui-root', resolvedMode, themeClassName) } style={cssVariables}>
<ComboboxPrimitive.Positioner
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
anchor={anchor}
className="isolate z-50"
>
<ComboboxPrimitive.Popup
data-slot="combobox-content"
data-chips={!!anchor}
className={cn(
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 max-h-72 min-w-36 overflow-hidden rounded-md shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-end-2 data-[side=inline-end]:slide-in-from-start-2 group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) data-[chips=true]:min-w-(--anchor-width)",
className
)}
{...props}
/>
</ComboboxPrimitive.Positioner>
</div>
</ComboboxPortal>
);
}

Expand Down
73 changes: 40 additions & 33 deletions src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { XIcon } from "lucide-react"
import { useThemeOptional, defaultCssVariables } from "@/providers"
import { useThemeOptional } from "@/providers"

function Dialog({ ...props }: DialogPrimitive.Root.Props) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
Expand All @@ -20,8 +20,15 @@ function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}

function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
function DialogPortal({
children,
...props
}: DialogPrimitive.Portal.Props) {
return (
<DialogPrimitive.Portal data-slot="dialog-portal" {...props}>
{children}
</DialogPrimitive.Portal>
)
}

function DialogOverlay({
Expand All @@ -48,37 +55,37 @@ function DialogContent({
}: DialogPrimitive.Popup.Props & {
showCloseButton?: boolean
}) {
const theme = useThemeOptional()
const mode = theme?.mode ?? "light"
const cssVariables = theme?.cssVariables ?? defaultCssVariables
const { resolvedMode, cssVariables, className: themeClassName } = useThemeOptional()
return (
<DialogPortal className={cn("pui-root", mode, theme?.className)} style={cssVariables}>
<DialogOverlay />
<DialogPrimitive.Popup
data-slot="dialog-content"
className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-background ring-foreground/10 gap-4 rounded-lg border border-border p-6 shadow-lg duration-150 fixed top-1/2 start-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 rtl:translate-x-1/2 -translate-y-1/2 outline-none",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
render={
<Button
variant="ghost"
className="absolute top-4 end-4"
size="icon-sm"
/>
}
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Popup>
<DialogPortal>
<div className={cn("pui-root", resolvedMode, themeClassName)} style={cssVariables}>
<DialogOverlay />
<DialogPrimitive.Popup
data-slot="dialog-content"
className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-background ring-foreground/10 gap-4 rounded-lg border border-border p-6 shadow-lg duration-150 fixed top-1/2 start-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 rtl:translate-x-1/2 -translate-y-1/2 outline-none",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
render={
<Button
variant="ghost"
className="absolute top-4 end-4"
size="icon-sm"
/>
}
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Popup>
</div>
</DialogPortal>
)
}
Expand Down
57 changes: 33 additions & 24 deletions src/components/ui/dropdown-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
"use client";

import { Menu as MenuPrimitive } from "@base-ui/react/menu";
import { CheckIcon, ChevronRightIcon } from "lucide-react";
import * as React from "react";

import { cn } from "@/lib/utils";
import { useThemeOptional, defaultCssVariables } from "@/providers";
import { useThemeOptional } from "@/providers";

function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}

function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {
return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
function DropdownMenuPortal({
children,
...props
}: MenuPrimitive.Portal.Props) {
return (
<MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props}>
{children}
</MenuPrimitive.Portal>
);
}

function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {
Expand All @@ -29,28 +38,28 @@ function DropdownMenuContent({
MenuPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset"
>) {
const theme = useThemeOptional();
const mode = theme?.mode ?? 'light';
const cssVariables = theme?.cssVariables ?? defaultCssVariables;
const { resolvedMode, cssVariables, className: themeClassName } = useThemeOptional();
return (
<MenuPrimitive.Portal className={ cn('pui-root', mode, theme?.className) } style={cssVariables}>
<MenuPrimitive.Positioner
className="isolate z-50 outline-none"
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
>
<MenuPrimitive.Popup
data-slot="dropdown-menu-content"
className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-md p-1 shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-end-2 data-[side=inline-end]:slide-in-from-start-2 z-50 max-h-(--available-height) w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto outline-none data-closed:overflow-hidden",
className
)}
{...props}
/>
</MenuPrimitive.Positioner>
</MenuPrimitive.Portal>
<DropdownMenuPortal>
<div className={ cn('pui-root', resolvedMode, themeClassName) } style={cssVariables}>
<MenuPrimitive.Positioner
className="isolate z-50 outline-none"
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
>
<MenuPrimitive.Popup
data-slot="dropdown-menu-content"
className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-md p-1 shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-end-2 data-[side=inline-end]:slide-in-from-start-2 z-50 max-h-(--available-height) w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto outline-none data-closed:overflow-hidden",
className
)}
{...props}
/>
</MenuPrimitive.Positioner>
</div>
</DropdownMenuPortal>
);
}

Expand Down
14 changes: 6 additions & 8 deletions src/components/ui/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from "react";
import { createPortal } from "react-dom";
import { cn } from "@/lib/utils";
import { useThemeOptional, defaultCssVariables } from "@/providers";
import { useThemeOptional } from "@/providers";

/* ============================================
Modal Overlay
Expand Down Expand Up @@ -296,9 +296,7 @@ export function Modal({
className,
size = "default",
}: ModalProps) {
const theme = useThemeOptional();
const mode = theme?.mode ?? 'light';
const cssVariables = theme?.cssVariables ?? defaultCssVariables;
const { resolvedMode, cssVariables, className: themeClassName } = useThemeOptional();
const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(
null,
);
Expand All @@ -314,7 +312,7 @@ export function Modal({

const container = document.createElement("div");
container.setAttribute("data-pui-modal-root", "true");
container.className = cn("pui-root", mode, theme?.className);
container.className = cn("pui-root", resolvedMode, themeClassName);
Object.assign(container.style, cssVariables);

document.body.appendChild(container);
Expand All @@ -331,15 +329,15 @@ export function Modal({
previousActiveElement.current.focus();
}
};
}, [open, mode, cssVariables, theme?.className]);
}, [open, resolvedMode, cssVariables, themeClassName]);

// Keep portal container class and CSS variables in sync with theme
useEffect(() => {
if (portalContainer) {
portalContainer.className = cn("pui-root", mode, theme?.className);
portalContainer.className = cn("pui-root", resolvedMode, themeClassName);
Object.assign(portalContainer.style, cssVariables);
}
}, [mode, cssVariables, portalContainer, theme?.className]);
}, [resolvedMode, cssVariables, portalContainer, themeClassName]);

// Handle escape key
useEffect(() => {
Expand Down
Loading
Loading