feat(mobile): add safe area padding
All checks were successful
/ deploy-and-package (push) Successful in 1m16s

This commit is contained in:
Alois 2026-08-11 20:36:07 +02:00
commit e4c2d705e8
Signed by: alois
SSH key fingerprint: SHA256:GBzT2DXvAuGV9XIV5W3WrzVpjU54FThmxHXdbz95J24
22 changed files with 131 additions and 23 deletions

View file

@ -2,7 +2,10 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
/>
<meta name="theme-color" content="#111827" /> <meta name="theme-color" content="#111827" />
<title>Methanium UI Showcase</title> <title>Methanium UI Showcase</title>
</head> </head>

View file

@ -1,6 +1,6 @@
{ {
"name": "@methanium/ui", "name": "@methanium/ui",
"version": "0.0.28", "version": "0.0.29",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",

View file

@ -52,7 +52,7 @@ function AlertDialogContent({
data-slot="alert-dialog-content" data-slot="alert-dialog-content"
data-size={size} data-size={size}
className={cn( className={cn(
"group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", "group/alert-dialog-content fixed top-[calc((100dvh+max(1rem,var(--ui-safe-area-top))-max(1rem,var(--ui-safe-area-bottom)))/2)] left-[calc((100%+max(1rem,var(--ui-safe-area-left))-max(1rem,var(--ui-safe-area-right)))/2)] z-50 grid w-[calc(100%-max(1rem,var(--ui-safe-area-left))-max(1rem,var(--ui-safe-area-right)))] max-h-[calc(100dvh-max(1rem,var(--ui-safe-area-top))-max(1rem,var(--ui-safe-area-bottom)))] -translate-x-1/2 -translate-y-1/2 gap-4 overflow-y-auto rounded-xl bg-popover p-4 text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className, className,
)} )}
{...props} {...props}

View file

@ -2,6 +2,7 @@ import * as React from "react";
import { Combobox as ComboboxPrimitive } from "@base-ui/react"; import { Combobox as ComboboxPrimitive } from "@base-ui/react";
import { cn } from "../lib/utils"; import { cn } from "../lib/utils";
import { useSafeAreaCollisionPadding } from "../hooks/use-safe-area";
import { Button } from "./button"; import { Button } from "./button";
import { import {
InputGroup, InputGroup,
@ -89,12 +90,20 @@ function ComboboxContent({
align = "start", align = "start",
alignOffset = 0, alignOffset = 0,
anchor, anchor,
collisionPadding,
...props ...props
}: ComboboxPrimitive.Popup.Props & }: ComboboxPrimitive.Popup.Props &
Pick< Pick<
ComboboxPrimitive.Positioner.Props, ComboboxPrimitive.Positioner.Props,
"side" | "align" | "sideOffset" | "alignOffset" | "anchor" | "side"
| "align"
| "sideOffset"
| "alignOffset"
| "anchor"
| "collisionPadding"
>) { >) {
const safeAreaCollisionPadding = useSafeAreaCollisionPadding();
return ( return (
<ComboboxPrimitive.Portal> <ComboboxPrimitive.Portal>
<ComboboxPrimitive.Positioner <ComboboxPrimitive.Positioner
@ -103,6 +112,7 @@ function ComboboxContent({
align={align} align={align}
alignOffset={alignOffset} alignOffset={alignOffset}
anchor={anchor} anchor={anchor}
collisionPadding={collisionPadding ?? safeAreaCollisionPadding}
className="isolate z-50" className="isolate z-50"
> >
<ComboboxPrimitive.Popup <ComboboxPrimitive.Popup

View file

@ -52,7 +52,7 @@ function CommandDialog({
</DialogHeader> </DialogHeader>
<DialogContent <DialogContent
className={cn( className={cn(
"top-1/3 translate-y-0 overflow-hidden rounded-xl! p-0", "top-[calc(var(--ui-safe-area-top)+(100dvh-var(--ui-safe-area-top)-var(--ui-safe-area-bottom))/3)] max-h-[calc((100dvh-var(--ui-safe-area-top)-var(--ui-safe-area-bottom))*2/3-1rem)] translate-y-0 overflow-hidden rounded-xl! p-0",
className, className,
)} )}
showCloseButton={showCloseButton} showCloseButton={showCloseButton}

View file

@ -4,6 +4,7 @@ import * as React from "react";
import { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context-menu"; import { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context-menu";
import { cn } from "../lib/utils"; import { cn } from "../lib/utils";
import { useSafeAreaCollisionPadding } from "../hooks/use-safe-area";
import { ChevronRightIcon, CheckIcon } from "lucide-react"; import { ChevronRightIcon, CheckIcon } from "lucide-react";
function ContextMenu({ ...props }: ContextMenuPrimitive.Root.Props) { function ContextMenu({ ...props }: ContextMenuPrimitive.Root.Props) {
@ -35,12 +36,15 @@ function ContextMenuContent({
alignOffset = 4, alignOffset = 4,
side = "right", side = "right",
sideOffset = 0, sideOffset = 0,
collisionPadding,
...props ...props
}: ContextMenuPrimitive.Popup.Props & }: ContextMenuPrimitive.Popup.Props &
Pick< Pick<
ContextMenuPrimitive.Positioner.Props, ContextMenuPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset" "align" | "alignOffset" | "side" | "sideOffset" | "collisionPadding"
>) { >) {
const safeAreaCollisionPadding = useSafeAreaCollisionPadding();
return ( return (
<ContextMenuPrimitive.Portal> <ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Positioner <ContextMenuPrimitive.Positioner
@ -49,6 +53,7 @@ function ContextMenuContent({
alignOffset={alignOffset} alignOffset={alignOffset}
side={side} side={side}
sideOffset={sideOffset} sideOffset={sideOffset}
collisionPadding={collisionPadding ?? safeAreaCollisionPadding}
> >
<ContextMenuPrimitive.Popup <ContextMenuPrimitive.Popup
data-slot="context-menu-content" data-slot="context-menu-content"

View file

@ -53,7 +53,7 @@ function DialogContent({
<DialogPrimitive.Popup <DialogPrimitive.Popup
data-slot="dialog-content" data-slot="dialog-content"
className={cn( className={cn(
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground border duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", "fixed top-[calc((100dvh+max(1rem,var(--ui-safe-area-top))-max(1rem,var(--ui-safe-area-bottom)))/2)] left-[calc((100%+max(1rem,var(--ui-safe-area-left))-max(1rem,var(--ui-safe-area-right)))/2)] z-50 grid w-[calc(100%-max(1rem,var(--ui-safe-area-left))-max(1rem,var(--ui-safe-area-right)))] max-h-[calc(100dvh-max(1rem,var(--ui-safe-area-top))-max(1rem,var(--ui-safe-area-bottom)))] -translate-x-1/2 -translate-y-1/2 gap-4 overflow-y-auto rounded-xl bg-popover p-4 text-sm text-popover-foreground border duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className, className,
)} )}
{...props} {...props}

View file

@ -56,7 +56,7 @@ function DrawerContent({
<DrawerPrimitive.Content <DrawerPrimitive.Content
data-slot="drawer-content" data-slot="drawer-content"
className={cn( className={cn(
"group/drawer-content fixed z-50 flex h-auto flex-col bg-popover text-sm text-popover-foreground data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-xl data-[vaul-drawer-direction=bottom]:border-t data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:rounded-r-xl data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:rounded-l-xl data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-xl data-[vaul-drawer-direction=top]:border-b data-[vaul-drawer-direction=left]:sm:max-w-sm data-[vaul-drawer-direction=right]:sm:max-w-sm", "group/drawer-content fixed z-50 flex h-auto flex-col bg-popover text-sm text-popover-foreground data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80dvh] data-[vaul-drawer-direction=bottom]:rounded-t-xl data-[vaul-drawer-direction=bottom]:border-t data-[vaul-drawer-direction=bottom]:pr-[var(--ui-safe-area-right)] data-[vaul-drawer-direction=bottom]:pb-[var(--ui-safe-area-bottom)] data-[vaul-drawer-direction=bottom]:pl-[var(--ui-safe-area-left)] data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:rounded-r-xl data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:pt-[var(--ui-safe-area-top)] data-[vaul-drawer-direction=left]:pb-[var(--ui-safe-area-bottom)] data-[vaul-drawer-direction=left]:pl-[var(--ui-safe-area-left)] data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:rounded-l-xl data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:pt-[var(--ui-safe-area-top)] data-[vaul-drawer-direction=right]:pr-[var(--ui-safe-area-right)] data-[vaul-drawer-direction=right]:pb-[var(--ui-safe-area-bottom)] data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80dvh] data-[vaul-drawer-direction=top]:rounded-b-xl data-[vaul-drawer-direction=top]:border-b data-[vaul-drawer-direction=top]:pt-[var(--ui-safe-area-top)] data-[vaul-drawer-direction=top]:pr-[var(--ui-safe-area-right)] data-[vaul-drawer-direction=top]:pl-[var(--ui-safe-area-left)] data-[vaul-drawer-direction=left]:sm:max-w-sm data-[vaul-drawer-direction=right]:sm:max-w-sm",
className, className,
)} )}
{...props} {...props}

View file

@ -2,6 +2,7 @@ import * as React from "react";
import { Menu as MenuPrimitive } from "@base-ui/react/menu"; import { Menu as MenuPrimitive } from "@base-ui/react/menu";
import { cn } from "../lib/utils"; import { cn } from "../lib/utils";
import { useSafeAreaCollisionPadding } from "../hooks/use-safe-area";
import { ChevronRightIcon, CheckIcon } from "lucide-react"; import { ChevronRightIcon, CheckIcon } from "lucide-react";
function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) { function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
@ -21,13 +22,16 @@ function DropdownMenuContent({
alignOffset = 0, alignOffset = 0,
side = "bottom", side = "bottom",
sideOffset = 4, sideOffset = 4,
collisionPadding,
className, className,
...props ...props
}: MenuPrimitive.Popup.Props & }: MenuPrimitive.Popup.Props &
Pick< Pick<
MenuPrimitive.Positioner.Props, MenuPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset" "align" | "alignOffset" | "side" | "sideOffset" | "collisionPadding"
>) { >) {
const safeAreaCollisionPadding = useSafeAreaCollisionPadding();
return ( return (
<MenuPrimitive.Portal> <MenuPrimitive.Portal>
<MenuPrimitive.Positioner <MenuPrimitive.Positioner
@ -36,6 +40,7 @@ function DropdownMenuContent({
alignOffset={alignOffset} alignOffset={alignOffset}
side={side} side={side}
sideOffset={sideOffset} sideOffset={sideOffset}
collisionPadding={collisionPadding ?? safeAreaCollisionPadding}
> >
<MenuPrimitive.Popup <MenuPrimitive.Popup
data-slot="dropdown-menu-content" data-slot="dropdown-menu-content"

View file

@ -3,6 +3,7 @@
import { PreviewCard as PreviewCardPrimitive } from "@base-ui/react/preview-card"; import { PreviewCard as PreviewCardPrimitive } from "@base-ui/react/preview-card";
import { cn } from "../lib/utils"; import { cn } from "../lib/utils";
import { useSafeAreaCollisionPadding } from "../hooks/use-safe-area";
function HoverCard({ ...props }: PreviewCardPrimitive.Root.Props) { function HoverCard({ ...props }: PreviewCardPrimitive.Root.Props) {
return <PreviewCardPrimitive.Root data-slot="hover-card" {...props} />; return <PreviewCardPrimitive.Root data-slot="hover-card" {...props} />;
@ -20,12 +21,15 @@ function HoverCardContent({
sideOffset = 4, sideOffset = 4,
align = "center", align = "center",
alignOffset = 4, alignOffset = 4,
collisionPadding,
...props ...props
}: PreviewCardPrimitive.Popup.Props & }: PreviewCardPrimitive.Popup.Props &
Pick< Pick<
PreviewCardPrimitive.Positioner.Props, PreviewCardPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset" "align" | "alignOffset" | "side" | "sideOffset" | "collisionPadding"
>) { >) {
const safeAreaCollisionPadding = useSafeAreaCollisionPadding();
return ( return (
<PreviewCardPrimitive.Portal data-slot="hover-card-portal"> <PreviewCardPrimitive.Portal data-slot="hover-card-portal">
<PreviewCardPrimitive.Positioner <PreviewCardPrimitive.Positioner
@ -33,6 +37,7 @@ function HoverCardContent({
alignOffset={alignOffset} alignOffset={alignOffset}
side={side} side={side}
sideOffset={sideOffset} sideOffset={sideOffset}
collisionPadding={collisionPadding ?? safeAreaCollisionPadding}
className="isolate z-50" className="isolate z-50"
> >
<PreviewCardPrimitive.Popup <PreviewCardPrimitive.Popup

View file

@ -2,6 +2,7 @@ import { NavigationMenu as NavigationMenuPrimitive } from "@base-ui/react/naviga
import { cva } from "class-variance-authority"; import { cva } from "class-variance-authority";
import { cn } from "../lib/utils"; import { cn } from "../lib/utils";
import { useSafeAreaCollisionPadding } from "../hooks/use-safe-area";
import { ChevronDownIcon } from "lucide-react"; import { ChevronDownIcon } from "lucide-react";
function NavigationMenu({ function NavigationMenu({
@ -101,8 +102,11 @@ function NavigationMenuPositioner({
sideOffset = 8, sideOffset = 8,
align = "start", align = "start",
alignOffset = 0, alignOffset = 0,
collisionPadding,
...props ...props
}: NavigationMenuPrimitive.Positioner.Props) { }: NavigationMenuPrimitive.Positioner.Props) {
const safeAreaCollisionPadding = useSafeAreaCollisionPadding();
return ( return (
<NavigationMenuPrimitive.Portal> <NavigationMenuPrimitive.Portal>
<NavigationMenuPrimitive.Positioner <NavigationMenuPrimitive.Positioner
@ -110,6 +114,7 @@ function NavigationMenuPositioner({
sideOffset={sideOffset} sideOffset={sideOffset}
align={align} align={align}
alignOffset={alignOffset} alignOffset={alignOffset}
collisionPadding={collisionPadding ?? safeAreaCollisionPadding}
className={cn( className={cn(
"isolate z-50 h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] data-instant:transition-none data-[side=bottom]:before:top-[-10px] data-[side=bottom]:before:right-0 data-[side=bottom]:before:left-0", "isolate z-50 h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] data-instant:transition-none data-[side=bottom]:before:top-[-10px] data-[side=bottom]:before:right-0 data-[side=bottom]:before:left-0",
className, className,

View file

@ -123,7 +123,7 @@ function OnboardingFlow({
</ConfigureStepContext.Provider> </ConfigureStepContext.Provider>
</main> </main>
<footer className="shrink-0 px-6 pt-4 pb-[max(1.5rem,env(safe-area-inset-bottom))] md:px-10"> <footer className="shrink-0 px-6 pt-4 pb-6 md:px-10">
<div className="flex justify-center md:justify-end"> <div className="flex justify-center md:justify-end">
<Button <Button
size="lg" size="lg"

View file

@ -2,6 +2,7 @@ import * as React from "react";
import { Popover as PopoverPrimitive } from "@base-ui/react/popover"; import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
import { cn } from "../lib/utils"; import { cn } from "../lib/utils";
import { useSafeAreaCollisionPadding } from "../hooks/use-safe-area";
function Popover({ ...props }: PopoverPrimitive.Root.Props) { function Popover({ ...props }: PopoverPrimitive.Root.Props) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />; return <PopoverPrimitive.Root data-slot="popover" {...props} />;
@ -17,15 +18,18 @@ function PopoverContent({
alignOffset = 0, alignOffset = 0,
side = "bottom", side = "bottom",
sideOffset = 4, sideOffset = 4,
collisionPadding,
portalProps, portalProps,
...props ...props
}: PopoverPrimitive.Popup.Props & }: PopoverPrimitive.Popup.Props &
Pick< Pick<
PopoverPrimitive.Positioner.Props, PopoverPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset" "align" | "alignOffset" | "side" | "sideOffset" | "collisionPadding"
> & { > & {
portalProps?: PopoverPrimitive.Portal.Props; portalProps?: PopoverPrimitive.Portal.Props;
}) { }) {
const safeAreaCollisionPadding = useSafeAreaCollisionPadding();
return ( return (
<PopoverPrimitive.Portal {...portalProps}> <PopoverPrimitive.Portal {...portalProps}>
<PopoverPrimitive.Positioner <PopoverPrimitive.Positioner
@ -33,6 +37,7 @@ function PopoverContent({
alignOffset={alignOffset} alignOffset={alignOffset}
side={side} side={side}
sideOffset={sideOffset} sideOffset={sideOffset}
collisionPadding={collisionPadding ?? safeAreaCollisionPadding}
className="isolate z-50" className="isolate z-50"
> >
<PopoverPrimitive.Popup <PopoverPrimitive.Popup

View file

@ -4,6 +4,7 @@ import * as React from "react";
import { Select as SelectPrimitive } from "@base-ui/react/select"; import { Select as SelectPrimitive } from "@base-ui/react/select";
import { cn } from "../lib/utils"; import { cn } from "../lib/utils";
import { useSafeAreaCollisionPadding } from "../hooks/use-safe-area";
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"; import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react";
const Select = SelectPrimitive.Root; const Select = SelectPrimitive.Root;
@ -64,12 +65,20 @@ function SelectContent({
align = "center", align = "center",
alignOffset = 0, alignOffset = 0,
alignItemWithTrigger = true, alignItemWithTrigger = true,
collisionPadding,
...props ...props
}: SelectPrimitive.Popup.Props & }: SelectPrimitive.Popup.Props &
Pick< Pick<
SelectPrimitive.Positioner.Props, SelectPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger" | "align"
| "alignOffset"
| "side"
| "sideOffset"
| "alignItemWithTrigger"
| "collisionPadding"
>) { >) {
const safeAreaCollisionPadding = useSafeAreaCollisionPadding();
return ( return (
<SelectPrimitive.Portal> <SelectPrimitive.Portal>
<SelectPrimitive.Positioner <SelectPrimitive.Positioner
@ -78,6 +87,7 @@ function SelectContent({
align={align} align={align}
alignOffset={alignOffset} alignOffset={alignOffset}
alignItemWithTrigger={alignItemWithTrigger} alignItemWithTrigger={alignItemWithTrigger}
collisionPadding={collisionPadding ?? safeAreaCollisionPadding}
className="isolate z-50" className="isolate z-50"
> >
<SelectPrimitive.Popup <SelectPrimitive.Popup

View file

@ -53,7 +53,7 @@ function SheetContent({
data-slot="sheet-content" data-slot="sheet-content"
data-side={side} data-side={side}
className={cn( className={cn(
"fixed z-50 flex flex-col gap-4 bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg transition duration-200 ease-in-out data-ending-style:opacity-0 data-starting-style:opacity-0 data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=bottom]:data-ending-style:translate-y-[2.5rem] data-[side=bottom]:data-starting-style:translate-y-[2.5rem] data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=left]:data-ending-style:translate-x-[-2.5rem] data-[side=left]:data-starting-style:translate-x-[-2.5rem] data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=right]:data-ending-style:translate-x-[2.5rem] data-[side=right]:data-starting-style:translate-x-[2.5rem] data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=top]:data-ending-style:translate-y-[-2.5rem] data-[side=top]:data-starting-style:translate-y-[-2.5rem] data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm", "group/sheet-content fixed z-50 flex flex-col gap-4 bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg transition duration-200 ease-in-out data-ending-style:opacity-0 data-starting-style:opacity-0 data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=bottom]:pr-[var(--ui-safe-area-right)] data-[side=bottom]:pb-[var(--ui-safe-area-bottom)] data-[side=bottom]:pl-[var(--ui-safe-area-left)] data-[side=bottom]:data-ending-style:translate-y-[2.5rem] data-[side=bottom]:data-starting-style:translate-y-[2.5rem] data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=left]:pt-[var(--ui-safe-area-top)] data-[side=left]:pb-[var(--ui-safe-area-bottom)] data-[side=left]:pl-[var(--ui-safe-area-left)] data-[side=left]:data-ending-style:translate-x-[-2.5rem] data-[side=left]:data-starting-style:translate-x-[-2.5rem] data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=right]:pt-[var(--ui-safe-area-top)] data-[side=right]:pr-[var(--ui-safe-area-right)] data-[side=right]:pb-[var(--ui-safe-area-bottom)] data-[side=right]:data-ending-style:translate-x-[2.5rem] data-[side=right]:data-starting-style:translate-x-[2.5rem] data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=top]:pt-[var(--ui-safe-area-top)] data-[side=top]:pr-[var(--ui-safe-area-right)] data-[side=top]:pl-[var(--ui-safe-area-left)] data-[side=top]:data-ending-style:translate-y-[-2.5rem] data-[side=top]:data-starting-style:translate-y-[-2.5rem] data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm",
className, className,
)} )}
{...props} {...props}
@ -65,7 +65,7 @@ function SheetContent({
render={ render={
<Button <Button
variant="ghost" variant="ghost"
className="absolute top-3 right-3" className="absolute top-3 right-3 group-data-[side=bottom]/sheet-content:right-[max(0.75rem,var(--ui-safe-area-right))] group-data-[side=left]/sheet-content:top-[max(0.75rem,var(--ui-safe-area-top))] group-data-[side=right]/sheet-content:top-[max(0.75rem,var(--ui-safe-area-top))] group-data-[side=right]/sheet-content:right-[max(0.75rem,var(--ui-safe-area-right))] group-data-[side=top]/sheet-content:top-[max(0.75rem,var(--ui-safe-area-top))] group-data-[side=top]/sheet-content:right-[max(0.75rem,var(--ui-safe-area-right))]"
size="icon-sm" size="icon-sm"
/> />
} }

View file

@ -215,10 +215,10 @@ function Sidebar({
data-side={side} data-side={side}
style={{ width: SIDEBAR_WIDTH }} style={{ width: SIDEBAR_WIDTH }}
className={cn( className={cn(
"fixed inset-y-0 z-10 hidden h-svh transition-[left,right,width,transform] duration-200 ease-linear data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]:-translate-x-full data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]:translate-x-full md:flex", "fixed inset-y-0 z-10 hidden h-svh pt-[var(--ui-safe-area-top)] pb-[var(--ui-safe-area-bottom)] transition-[left,right,width,transform] duration-200 ease-linear data-[side=left]:left-0 data-[side=left]:pl-[var(--ui-safe-area-left)] data-[side=left]:group-data-[collapsible=offcanvas]:-translate-x-full data-[side=right]:right-0 data-[side=right]:pr-[var(--ui-safe-area-right)] data-[side=right]:group-data-[collapsible=offcanvas]:translate-x-full md:flex",
// Adjust the padding for floating and inset variants. // Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset" variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]" ? "pt-[max(0.5rem,var(--ui-safe-area-top))] pr-2 pb-[max(0.5rem,var(--ui-safe-area-bottom))] pl-2 data-[side=left]:pl-[max(0.5rem,var(--ui-safe-area-left))] data-[side=right]:pr-[max(0.5rem,var(--ui-safe-area-right))] group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)", : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
className, className,
)} )}

View file

@ -17,6 +17,18 @@ const Toaster = ({ ...props }: ToasterProps) => {
<Sonner <Sonner
theme={theme as ToasterProps["theme"]} theme={theme as ToasterProps["theme"]}
className="toaster group" className="toaster group"
offset={{
top: "max(1rem, var(--ui-safe-area-top))",
right: "max(1rem, var(--ui-safe-area-right))",
bottom: "max(1rem, var(--ui-safe-area-bottom))",
left: "max(1rem, var(--ui-safe-area-left))",
}}
mobileOffset={{
top: "max(1rem, var(--ui-safe-area-top))",
right: "max(1rem, var(--ui-safe-area-right), var(--ui-safe-area-left))",
bottom: "max(1rem, var(--ui-safe-area-bottom))",
left: "max(1rem, var(--ui-safe-area-right), var(--ui-safe-area-left))",
}}
icons={{ icons={{
success: <CircleCheckIcon className="size-4" />, success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />, info: <InfoIcon className="size-4" />,

View file

@ -2,6 +2,7 @@ import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
import type { ReactElement, ReactNode } from "react"; import type { ReactElement, ReactNode } from "react";
import { cn } from "../lib/utils"; import { cn } from "../lib/utils";
import { useSafeAreaCollisionPadding } from "../hooks/use-safe-area";
function TooltipProvider({ function TooltipProvider({
delay = 0, delay = 0,
@ -31,15 +32,18 @@ function TooltipContent({
align = "center", align = "center",
alignOffset = 0, alignOffset = 0,
children, children,
collisionPadding,
portalProps, portalProps,
...props ...props
}: TooltipPrimitive.Popup.Props & }: TooltipPrimitive.Popup.Props &
Pick< Pick<
TooltipPrimitive.Positioner.Props, TooltipPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset" "align" | "alignOffset" | "side" | "sideOffset" | "collisionPadding"
> & { > & {
portalProps?: TooltipPrimitive.Portal.Props; portalProps?: TooltipPrimitive.Portal.Props;
}) { }) {
const safeAreaCollisionPadding = useSafeAreaCollisionPadding();
return ( return (
<TooltipPrimitive.Portal {...portalProps}> <TooltipPrimitive.Portal {...portalProps}>
<TooltipPrimitive.Positioner <TooltipPrimitive.Positioner
@ -47,6 +51,7 @@ function TooltipContent({
alignOffset={alignOffset} alignOffset={alignOffset}
side={side} side={side}
sideOffset={sideOffset} sideOffset={sideOffset}
collisionPadding={collisionPadding ?? safeAreaCollisionPadding}
className="isolate z-50" className="isolate z-50"
> >
<TooltipPrimitive.Popup <TooltipPrimitive.Popup

View file

@ -0,0 +1,40 @@
import * as React from "react";
const properties = [
"--ui-safe-area-top",
"--ui-safe-area-right",
"--ui-safe-area-bottom",
"--ui-safe-area-left",
] as const;
function getSnapshot() {
const styles = getComputedStyle(document.documentElement);
return properties
.map((property) => styles.getPropertyValue(property))
.join(",");
}
function subscribe(onChange: () => void) {
window.addEventListener("resize", onChange);
window.addEventListener("orientationchange", onChange);
window.visualViewport?.addEventListener("resize", onChange);
return () => {
window.removeEventListener("resize", onChange);
window.removeEventListener("orientationchange", onChange);
window.visualViewport?.removeEventListener("resize", onChange);
};
}
export function useSafeAreaCollisionPadding() {
const snapshot = React.useSyncExternalStore(
subscribe,
getSnapshot,
() => "0,0,0,0",
);
const [top, right, bottom, left] = snapshot
.split(",")
.map((value) => Math.max(5, Number.parseFloat(value) || 0));
return { top, right, bottom, left };
}

View file

@ -88,6 +88,10 @@
} }
:root { :root {
--ui-safe-area-top: env(safe-area-inset-top, 0px);
--ui-safe-area-right: env(safe-area-inset-right, 0px);
--ui-safe-area-bottom: env(safe-area-inset-bottom, 0px);
--ui-safe-area-left: env(safe-area-inset-left, 0px);
--ui-density: 1; --ui-density: 1;
--ui-border-width: 1px; --ui-border-width: 1px;
--ui-shadow-strength: 0; --ui-shadow-strength: 0;

View file

@ -8,6 +8,7 @@ export { toast } from "sonner";
export * from "./theme"; export * from "./theme";
export * from "./screens/util"; export * from "./screens/util";
export * from "./hooks/use-mobile"; export * from "./hooks/use-mobile";
export * from "./hooks/use-safe-area";
export * from "./lib/utils"; export * from "./lib/utils";
export * from "./cmp/accordion"; export * from "./cmp/accordion";

View file

@ -1,6 +1,5 @@
import { cn } from "../lib/utils"; import { cn } from "../lib/utils";
import Controls from "../windowControls"; import Controls from "../windowControls";
import { isTauri } from "@tauri-apps/api/core";
export default function CreateScreen({ export default function CreateScreen({
children, children,
@ -12,13 +11,13 @@ export default function CreateScreen({
return ( return (
<div <div
className={cn( className={cn(
"bg-background w-screen h-screen flex flex-col justify-center items-center", "bg-background flex h-dvh w-screen flex-col items-center justify-center pt-[var(--ui-safe-area-top)] pr-[var(--ui-safe-area-right)] pb-[var(--ui-safe-area-bottom)] pl-[var(--ui-safe-area-left)]",
className, className,
)} )}
data-tauri-drag-region data-tauri-drag-region
> >
<> <>
<Controls className="fixed top-0 right-0 m-1.75 mr-[7.6px]! ring-foreground/15" /> <Controls className="fixed top-[max(0.4375rem,var(--ui-safe-area-top))] right-[max(0.475rem,var(--ui-safe-area-right))] ring-foreground/15" />
{children} {children}
</> </>
</div> </div>
@ -87,9 +86,8 @@ export function StorageDestoryer() {
return ( return (
<div <div
className={cn( className={cn(
"fixed p-5 bottom-0", "fixed bottom-0 p-5 pr-[max(1.25rem,var(--ui-safe-area-right))] pb-[max(1.25rem,var(--ui-safe-area-bottom))] pl-[max(1.25rem,var(--ui-safe-area-left))]",
isMobile ? "w-full" : "right-0", isMobile ? "w-full" : "right-0",
isTauri() ? "py-10" : "",
)} )}
> >
<ClearStorageButton /> <ClearStorageButton />