36 changed files with 1771 additions and 706 deletions
@ -0,0 +1,22 @@ |
|||
{ |
|||
"$schema": "https://ui.shadcn.com/schema.json", |
|||
"style": "new-york", |
|||
"rsc": false, |
|||
"tsx": true, |
|||
"tailwind": { |
|||
"config": "", |
|||
"css": "src/index.css", |
|||
"baseColor": "slate", |
|||
"cssVariables": true, |
|||
"prefix": "" |
|||
}, |
|||
"iconLibrary": "lucide", |
|||
"aliases": { |
|||
"components": "@app/components", |
|||
"utils": "@app/lib/utils", |
|||
"ui": "@app/components/ui", |
|||
"lib": "@app/lib", |
|||
"hooks": "@app/hooks" |
|||
}, |
|||
"registries": {} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
import { useDevice } from "@app/core/stores"; |
|||
|
|||
export function ConnectionStatus() { |
|||
const { status } = useDevice(); |
|||
console.log(status); |
|||
|
|||
return <div>Connection Status Component</div>; |
|||
} |
|||
@ -1,101 +0,0 @@ |
|||
import { Monitor, Moon, Sun } from "lucide-react"; |
|||
import { useTranslation } from "react-i18next"; |
|||
import { useTheme } from "../core/hooks/useTheme.ts"; |
|||
import { useToggleVisibility } from "../core/hooks/useToggleVisiblility.ts"; |
|||
import { cn } from "../core/utils/cn.ts"; |
|||
import { Button } from "./UI/Button.tsx"; |
|||
import { Subtle } from "./UI/Typography/Subtle.tsx"; |
|||
|
|||
type ThemePreference = "light" | "dark" | "system"; |
|||
|
|||
interface ThemeSwitcherProps { |
|||
className?: string; |
|||
disableHover?: boolean; |
|||
} |
|||
|
|||
const TOOLTIP_TIMEOUT = 2000; // 2 seconds
|
|||
|
|||
export default function ThemeSwitcher({ |
|||
className: passedClassName = "", |
|||
disableHover = false, |
|||
}: ThemeSwitcherProps) { |
|||
const [showTooltip, toggleShowTooltip] = useToggleVisibility({ |
|||
timeout: TOOLTIP_TIMEOUT, |
|||
}); |
|||
|
|||
const { preference, setPreference } = useTheme(); |
|||
const { t } = useTranslation("ui"); |
|||
|
|||
const iconBaseClass = |
|||
"size-4 flex-shrink-0 text-gray-500 dark:text-gray-400 transition-colors duration-150"; |
|||
const iconHoverClass = !disableHover |
|||
? "group-hover:text-gray-700 dark:group-hover:text-gray-200" |
|||
: ""; |
|||
const combinedIconClass = cn(iconBaseClass, iconHoverClass); |
|||
|
|||
const themeIcons = { |
|||
light: <Sun className={combinedIconClass} />, |
|||
dark: <Moon className={combinedIconClass} />, |
|||
system: <Monitor className={combinedIconClass} />, |
|||
}; |
|||
|
|||
const toggleTheme = () => { |
|||
const preferences: ThemePreference[] = ["light", "dark", "system"]; |
|||
const currentIndex = preferences.indexOf(preference); |
|||
const nextPreference = |
|||
preferences[(currentIndex + 1) % preferences.length] ?? "system"; |
|||
setPreference(nextPreference); |
|||
toggleShowTooltip(); |
|||
}; |
|||
|
|||
const preferenceDisplayMap: Record<ThemePreference, string> = { |
|||
light: t("theme.light"), |
|||
dark: t("theme.dark"), |
|||
system: t("theme.system"), |
|||
}; |
|||
|
|||
const currentDisplayPreference = preferenceDisplayMap[preference]; |
|||
|
|||
return ( |
|||
<Button |
|||
variant="ghost" |
|||
onClick={toggleTheme} |
|||
aria-label={t("theme.changeTheme")} |
|||
className={cn( |
|||
"group relative flex justify-start", |
|||
"gap-2.5 p-1.5 rounded-md transition-colors duration-150", |
|||
"cursor-pointer", |
|||
!disableHover && "hover:bg-gray-100 dark:hover:bg-gray-700", |
|||
"focus:*:data-label:opacity-100", |
|||
passedClassName, |
|||
)} |
|||
> |
|||
<span |
|||
data-label="theme-preference-tooltip" |
|||
className={cn( |
|||
"transition-opacity duration-150 hidden", |
|||
"block absolute w-max max-w-xs", |
|||
"p-1 text-xs text-white dark:text-black bg-black dark:bg-white", |
|||
"rounded-md shadow-lg", |
|||
"left-1/2 -translate-x-1/2 -top-8", |
|||
showTooltip ? "visible" : "hidden opacity-0", |
|||
)} |
|||
> |
|||
{currentDisplayPreference} |
|||
</span> |
|||
|
|||
{themeIcons[preference]} |
|||
<Subtle |
|||
className={cn( |
|||
"text-sm", |
|||
"text-gray-600 dark:text-gray-300", |
|||
"transition-colors duration-150", |
|||
!disableHover && |
|||
"group-hover:text-gray-800 dark:group-hover:text-gray-100", |
|||
)} |
|||
> |
|||
{t("theme.changeTheme")} |
|||
</Subtle> |
|||
</Button> |
|||
); |
|||
} |
|||
@ -1,224 +1,20 @@ |
|||
import { useCopyToClipboard } from "@core/hooks/useCopyToClipboard.ts"; |
|||
import { usePasswordVisibilityToggle } from "@core/hooks/usePasswordVisibilityToggle.ts"; |
|||
import { cn } from "@core/utils/cn.ts"; |
|||
import { cva, type VariantProps } from "class-variance-authority"; |
|||
import { Check, Copy, Eye, EyeOff, type LucideIcon, X } from "lucide-react"; |
|||
import * as React from "react"; |
|||
import { useTranslation } from "react-i18next"; |
|||
|
|||
const cnInvalidBase = "border-2 border-red-500 dark:border-red-500"; |
|||
const cnDirtyBase = "border-2 border-sky-500 dark:border-sky-500"; |
|||
|
|||
const inputVariants = cva( |
|||
"flex h-10 w-full rounded-md border border-slate-300 bg-transparent py-2 px-3 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-1 focus:ring-slate-400 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-500 dark:bg-transparent dark:text-slate-100 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-600", |
|||
{ |
|||
variants: { |
|||
variant: { |
|||
default: "border-slate-300 dark:border-slate-500", |
|||
invalid: `${cnInvalidBase} focus:ring-red-500 dark:focus:ring-red-500`, |
|||
dirty: `${cnDirtyBase} focus:ring-sky-500 dark:focus:ring-sky-500`, |
|||
}, |
|||
}, |
|||
defaultVariants: { |
|||
variant: "default", |
|||
}, |
|||
}, |
|||
); |
|||
|
|||
type InputActionType = { |
|||
id: string; |
|||
icon: LucideIcon; |
|||
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void; |
|||
ariaLabel: string; |
|||
tooltip?: string; |
|||
condition?: boolean; |
|||
}; |
|||
|
|||
export interface InputProps |
|||
extends Omit< |
|||
React.InputHTMLAttributes<HTMLInputElement>, |
|||
"prefix" | "suffix" |
|||
>, |
|||
VariantProps<typeof inputVariants> { |
|||
prefix?: React.ReactNode; |
|||
suffix?: React.ReactNode; |
|||
showPasswordToggle?: boolean; |
|||
showCopyButton?: boolean; |
|||
showClearButton?: boolean; |
|||
containerClassName?: string; |
|||
import { cn } from "@app/lib/utils"; |
|||
import type * as React from "react"; |
|||
|
|||
function Input({ className, type, ...props }: React.ComponentProps<"input">) { |
|||
return ( |
|||
<input |
|||
type={type} |
|||
data-slot="input" |
|||
className={cn( |
|||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", |
|||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", |
|||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", |
|||
className, |
|||
)} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
const Input = React.forwardRef<HTMLInputElement, InputProps>( |
|||
( |
|||
{ |
|||
className, |
|||
containerClassName, |
|||
variant, |
|||
disabled, |
|||
type = "text", |
|||
prefix, |
|||
suffix, |
|||
showPasswordToggle, |
|||
showCopyButton, |
|||
showClearButton, |
|||
value, |
|||
onChange, |
|||
...props |
|||
}, |
|||
ref, |
|||
) => { |
|||
const { isVisible, toggleVisibility } = usePasswordVisibilityToggle(); |
|||
const { copy, isCopied } = useCopyToClipboard({ timeout: 1500 }); |
|||
const { t } = useTranslation("ui"); |
|||
|
|||
const potentialActions: InputActionType[] = [ |
|||
{ |
|||
id: "clear-input", |
|||
icon: X, |
|||
onClick: (e) => { |
|||
e.stopPropagation(); |
|||
if (onChange) { |
|||
const event = { |
|||
target: { value: "" }, |
|||
currentTarget: { value: "" }, |
|||
} as React.ChangeEvent<HTMLInputElement>; |
|||
onChange(event); |
|||
} |
|||
if (ref && typeof ref !== "function" && ref.current) { |
|||
ref.current.focus(); |
|||
} |
|||
}, |
|||
ariaLabel: t("clearInput.label"), |
|||
tooltip: t("clearInput.label"), |
|||
condition: !!showClearButton && !!value, |
|||
}, |
|||
{ |
|||
id: "toggle-visibility", |
|||
icon: isVisible ? EyeOff : Eye, |
|||
onClick: (e) => { |
|||
e.stopPropagation(); |
|||
toggleVisibility(); |
|||
}, |
|||
ariaLabel: isVisible |
|||
? t("notifications.hidePassword.label") |
|||
: t("notifications.showPassword.label"), |
|||
tooltip: isVisible |
|||
? t("notifications.hidePassword.label") |
|||
: t("notifications.showPassword.label"), |
|||
condition: !!showPasswordToggle && type === "password", |
|||
}, |
|||
{ |
|||
id: "copy-value", |
|||
icon: isCopied ? Check : Copy, |
|||
onClick: (e) => { |
|||
e.stopPropagation(); |
|||
if (value !== undefined && value !== null) { |
|||
copy(String(value)); |
|||
} |
|||
}, |
|||
ariaLabel: isCopied |
|||
? t("notifications.copied.label") |
|||
: t("notifications.copyToClipboard.label"), |
|||
tooltip: isCopied |
|||
? t("notifications.copied.label") |
|||
: t("notifications.copyToClipboard.label"), |
|||
condition: !!showCopyButton, |
|||
}, |
|||
]; |
|||
|
|||
const actions = potentialActions.filter((action) => action.condition); |
|||
|
|||
const inputType = showPasswordToggle |
|||
? isVisible |
|||
? "text" |
|||
: "password" |
|||
: type; |
|||
|
|||
const hasPrefix = !!prefix; |
|||
const hasSuffix = !!suffix; |
|||
const hasActions = actions.length > 0; |
|||
|
|||
const inputClassName = cn( |
|||
inputVariants({ variant }), |
|||
hasActions && !hasSuffix && "pr-10", |
|||
hasPrefix && "rounded-l-none", |
|||
className, |
|||
); |
|||
|
|||
const extrasClassName = cn([ |
|||
variant === "invalid" && `${cnInvalidBase} border-l-0`, |
|||
variant === "dirty" && `${cnDirtyBase} border-l-0`, |
|||
]); |
|||
|
|||
return ( |
|||
<div |
|||
className={cn("relative flex w-full items-stretch", containerClassName)} |
|||
> |
|||
{prefix && ( |
|||
<span className="inline-flex items-center rounded-l-md border border-r-0 border-slate-300 bg-slate-100/80 px-3 text-sm text-slate-600 dark:border-slate-700 dark:bg-slate-200 dark:text-slate-700"> |
|||
{prefix} |
|||
</span> |
|||
)} |
|||
|
|||
<input |
|||
type={inputType === "password" && isVisible ? "text" : inputType} |
|||
className={inputClassName} |
|||
ref={ref} |
|||
value={value} |
|||
onChange={onChange} |
|||
disabled={disabled} |
|||
{...props} |
|||
/> |
|||
|
|||
<div className="absolute right-0 top-0 flex h-full items-stretch"> |
|||
{suffix && ( |
|||
<span |
|||
className={cn( |
|||
"inline-flex items-center border border-l-0 border-slate-300 bg-slate-100/80 px-3 text-sm text-slate-600 dark:border-slate-700 dark:bg-slate-700 dark:text-slate-300", |
|||
extrasClassName, |
|||
!hasActions && "rounded-r-md", |
|||
)} |
|||
> |
|||
{suffix} |
|||
</span> |
|||
)} |
|||
|
|||
{hasActions && ( |
|||
<div |
|||
className={cn( |
|||
"flex items-center divide-x divide-slate-300 border border-l-0 border-slate-300 dark:divide-slate-700 dark:border-slate-500", |
|||
extrasClassName, |
|||
disabled && |
|||
"border-slate-200 dark:border-slate-700 divide-slate-200", |
|||
!hasSuffix && "rounded-r-md", |
|||
"bg-white dark:bg-slate-800", |
|||
)} |
|||
> |
|||
{actions.map((action) => ( |
|||
<button |
|||
key={action.id} |
|||
type="button" |
|||
className={cn( |
|||
"inline-flex h-full items-center justify-center px-2.5 text-slate-500 hover:bg-slate-100 hover:text-slate-700 focus:outline-none focus:ring-1 focus:ring-slate-400 focus:ring-offset-0 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-slate-200 dark:focus:ring-slate-500 last:hover:rounded-r-md last:dark:hover:rounded-r-md", |
|||
disabled && "text-slate-300 dark:text-slate-600", |
|||
action.id === "copy-value" && |
|||
isCopied && |
|||
"text-green-600 dark:text-green-500", |
|||
)} |
|||
onClick={action.onClick} |
|||
aria-label={action.ariaLabel} |
|||
title={action.tooltip || action.ariaLabel} |
|||
> |
|||
<action.icon size={18} aria-hidden="true" /> |
|||
</button> |
|||
))} |
|||
</div> |
|||
)} |
|||
</div> |
|||
</div> |
|||
); |
|||
}, |
|||
); |
|||
Input.displayName = "Input"; |
|||
|
|||
export { Input, inputVariants }; |
|||
export { Input }; |
|||
|
|||
@ -1,28 +1,25 @@ |
|||
import { cn } from "@core/utils/cn.ts"; |
|||
import { cn } from "@app/lib/utils"; |
|||
import * as SeparatorPrimitive from "@radix-ui/react-separator"; |
|||
import * as React from "react"; |
|||
import type * as React from "react"; |
|||
|
|||
const Separator = React.forwardRef< |
|||
React.ComponentRef<typeof SeparatorPrimitive.Root>, |
|||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> |
|||
>( |
|||
( |
|||
{ className, orientation = "horizontal", decorative = true, ...props }, |
|||
ref, |
|||
) => ( |
|||
function Separator({ |
|||
className, |
|||
orientation = "horizontal", |
|||
decorative = true, |
|||
...props |
|||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) { |
|||
return ( |
|||
<SeparatorPrimitive.Root |
|||
ref={ref} |
|||
data-slot="separator" |
|||
decorative={decorative} |
|||
orientation={orientation} |
|||
className={cn( |
|||
"bg-slate-200 dark:bg-slate-700", |
|||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", |
|||
"bg-border w-full shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", |
|||
className, |
|||
)} |
|||
{...props} |
|||
/> |
|||
), |
|||
); |
|||
Separator.displayName = SeparatorPrimitive.Root.displayName; |
|||
); |
|||
} |
|||
|
|||
export { Separator }; |
|||
|
|||
@ -1,38 +1,80 @@ |
|||
import { cn } from "@core/utils/cn.ts"; |
|||
import { cn } from "@app/lib/utils"; |
|||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"; |
|||
import * as React from "react"; |
|||
import type * as React from "react"; |
|||
|
|||
const TooltipProvider = TooltipPrimitive.Provider; |
|||
function TooltipProvider({ |
|||
delayDuration = 0, |
|||
...props |
|||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) { |
|||
return ( |
|||
<TooltipPrimitive.Provider |
|||
data-slot="tooltip-provider" |
|||
delayDuration={delayDuration} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
const Tooltip = ({ ...props }) => <TooltipPrimitive.Root {...props} />; |
|||
Tooltip.displayName = TooltipPrimitive.Tooltip.displayName; |
|||
function Tooltip({ |
|||
...props |
|||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) { |
|||
return ( |
|||
<TooltipProvider> |
|||
<TooltipPrimitive.Root data-slot="tooltip" {...props} /> |
|||
</TooltipProvider> |
|||
); |
|||
} |
|||
|
|||
const TooltipTrigger = TooltipPrimitive.Trigger; |
|||
const TooltipArrow = TooltipPrimitive.Arrow; |
|||
function TooltipTrigger({ |
|||
...props |
|||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) { |
|||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />; |
|||
} |
|||
|
|||
const TooltipContent = React.forwardRef< |
|||
React.ElementRef<typeof TooltipPrimitive.Content>, |
|||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> |
|||
>(({ className, sideOffset = 4, ...props }, ref) => ( |
|||
<TooltipPrimitive.Content |
|||
ref={ref} |
|||
sideOffset={sideOffset} |
|||
className={cn( |
|||
"z-50 overflow-hidden rounded-md border border-slate-100 bg-white px-3 py-1.5 text-sm text-slate-700 shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1 dark:border-slate-800 dark:bg-slate-800 dark:text-slate-200", |
|||
className, |
|||
)} |
|||
{...props} |
|||
/> |
|||
)); |
|||
TooltipContent.displayName = TooltipPrimitive.Content.displayName; |
|||
function TooltipContent({ |
|||
className, |
|||
sideOffset = 0, |
|||
children, |
|||
...props |
|||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) { |
|||
return ( |
|||
<TooltipPrimitive.Portal> |
|||
<TooltipPrimitive.Content |
|||
data-slot="tooltip-content" |
|||
sideOffset={sideOffset} |
|||
className={cn( |
|||
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance", |
|||
className, |
|||
)} |
|||
{...props} |
|||
> |
|||
{children} |
|||
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" /> |
|||
</TooltipPrimitive.Content> |
|||
</TooltipPrimitive.Portal> |
|||
); |
|||
} |
|||
|
|||
function TooltipArrow({ |
|||
className, |
|||
...props |
|||
}: React.ComponentProps<typeof TooltipPrimitive.Arrow>) { |
|||
return ( |
|||
<TooltipPrimitive.Arrow |
|||
data-slot="tooltip-arrow" |
|||
className={cn("fill-primary", className)} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
const TooltipPortal = TooltipPrimitive.Portal; |
|||
|
|||
export { |
|||
Tooltip, |
|||
TooltipArrow, |
|||
TooltipContent, |
|||
TooltipProvider, |
|||
TooltipTrigger, |
|||
TooltipPortal, |
|||
TooltipArrow, |
|||
}; |
|||
|
|||
@ -0,0 +1,254 @@ |
|||
import { cn } from "@app/lib/utils"; |
|||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; |
|||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; |
|||
import type * as React from "react"; |
|||
|
|||
function DropdownMenu({ |
|||
...props |
|||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) { |
|||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />; |
|||
} |
|||
|
|||
function DropdownMenuPortal({ |
|||
...props |
|||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) { |
|||
return ( |
|||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} /> |
|||
); |
|||
} |
|||
|
|||
function DropdownMenuTrigger({ |
|||
...props |
|||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) { |
|||
return ( |
|||
<DropdownMenuPrimitive.Trigger |
|||
data-slot="dropdown-menu-trigger" |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
function DropdownMenuContent({ |
|||
className, |
|||
sideOffset = 4, |
|||
...props |
|||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) { |
|||
return ( |
|||
<DropdownMenuPrimitive.Portal> |
|||
<DropdownMenuPrimitive.Content |
|||
data-slot="dropdown-menu-content" |
|||
sideOffset={sideOffset} |
|||
className={cn( |
|||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=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 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md", |
|||
className, |
|||
)} |
|||
{...props} |
|||
/> |
|||
</DropdownMenuPrimitive.Portal> |
|||
); |
|||
} |
|||
|
|||
function DropdownMenuGroup({ |
|||
...props |
|||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) { |
|||
return ( |
|||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} /> |
|||
); |
|||
} |
|||
|
|||
function DropdownMenuItem({ |
|||
className, |
|||
inset, |
|||
variant = "default", |
|||
...props |
|||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & { |
|||
inset?: boolean; |
|||
variant?: "default" | "destructive"; |
|||
}) { |
|||
return ( |
|||
<DropdownMenuPrimitive.Item |
|||
data-slot="dropdown-menu-item" |
|||
data-inset={inset} |
|||
data-variant={variant} |
|||
className={cn( |
|||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", |
|||
className, |
|||
)} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
function DropdownMenuCheckboxItem({ |
|||
className, |
|||
children, |
|||
checked, |
|||
...props |
|||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) { |
|||
return ( |
|||
<DropdownMenuPrimitive.CheckboxItem |
|||
data-slot="dropdown-menu-checkbox-item" |
|||
className={cn( |
|||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", |
|||
className, |
|||
)} |
|||
checked={checked} |
|||
{...props} |
|||
> |
|||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> |
|||
<DropdownMenuPrimitive.ItemIndicator> |
|||
<CheckIcon className="size-4" /> |
|||
</DropdownMenuPrimitive.ItemIndicator> |
|||
</span> |
|||
{children} |
|||
</DropdownMenuPrimitive.CheckboxItem> |
|||
); |
|||
} |
|||
|
|||
function DropdownMenuRadioGroup({ |
|||
...props |
|||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) { |
|||
return ( |
|||
<DropdownMenuPrimitive.RadioGroup |
|||
data-slot="dropdown-menu-radio-group" |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
function DropdownMenuRadioItem({ |
|||
className, |
|||
children, |
|||
...props |
|||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) { |
|||
return ( |
|||
<DropdownMenuPrimitive.RadioItem |
|||
data-slot="dropdown-menu-radio-item" |
|||
className={cn( |
|||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", |
|||
className, |
|||
)} |
|||
{...props} |
|||
> |
|||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> |
|||
<DropdownMenuPrimitive.ItemIndicator> |
|||
<CircleIcon className="size-2 fill-current" /> |
|||
</DropdownMenuPrimitive.ItemIndicator> |
|||
</span> |
|||
{children} |
|||
</DropdownMenuPrimitive.RadioItem> |
|||
); |
|||
} |
|||
|
|||
function DropdownMenuLabel({ |
|||
className, |
|||
inset, |
|||
...props |
|||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & { |
|||
inset?: boolean; |
|||
}) { |
|||
return ( |
|||
<DropdownMenuPrimitive.Label |
|||
data-slot="dropdown-menu-label" |
|||
data-inset={inset} |
|||
className={cn( |
|||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", |
|||
className, |
|||
)} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
function DropdownMenuSeparator({ |
|||
className, |
|||
...props |
|||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) { |
|||
return ( |
|||
<DropdownMenuPrimitive.Separator |
|||
data-slot="dropdown-menu-separator" |
|||
className={cn("bg-border -mx-1 my-1 h-px", className)} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
function DropdownMenuShortcut({ |
|||
className, |
|||
...props |
|||
}: React.ComponentProps<"span">) { |
|||
return ( |
|||
<span |
|||
data-slot="dropdown-menu-shortcut" |
|||
className={cn( |
|||
"text-muted-foreground ml-auto text-xs tracking-widest", |
|||
className, |
|||
)} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
function DropdownMenuSub({ |
|||
...props |
|||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) { |
|||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />; |
|||
} |
|||
|
|||
function DropdownMenuSubTrigger({ |
|||
className, |
|||
inset, |
|||
children, |
|||
...props |
|||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & { |
|||
inset?: boolean; |
|||
}) { |
|||
return ( |
|||
<DropdownMenuPrimitive.SubTrigger |
|||
data-slot="dropdown-menu-sub-trigger" |
|||
data-inset={inset} |
|||
className={cn( |
|||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8", |
|||
className, |
|||
)} |
|||
{...props} |
|||
> |
|||
{children} |
|||
<ChevronRightIcon className="ml-auto size-4" /> |
|||
</DropdownMenuPrimitive.SubTrigger> |
|||
); |
|||
} |
|||
|
|||
function DropdownMenuSubContent({ |
|||
className, |
|||
...props |
|||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) { |
|||
return ( |
|||
<DropdownMenuPrimitive.SubContent |
|||
data-slot="dropdown-menu-sub-content" |
|||
className={cn( |
|||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg", |
|||
className, |
|||
)} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
export { |
|||
DropdownMenu, |
|||
DropdownMenuPortal, |
|||
DropdownMenuTrigger, |
|||
DropdownMenuContent, |
|||
DropdownMenuGroup, |
|||
DropdownMenuLabel, |
|||
DropdownMenuItem, |
|||
DropdownMenuCheckboxItem, |
|||
DropdownMenuRadioGroup, |
|||
DropdownMenuRadioItem, |
|||
DropdownMenuSeparator, |
|||
DropdownMenuShortcut, |
|||
DropdownMenuSub, |
|||
DropdownMenuSubTrigger, |
|||
DropdownMenuSubContent, |
|||
}; |
|||
@ -0,0 +1,138 @@ |
|||
"use client"; |
|||
|
|||
import { cn } from "@app/lib/utils"; |
|||
import * as SheetPrimitive from "@radix-ui/react-dialog"; |
|||
import { XIcon } from "lucide-react"; |
|||
import type * as React from "react"; |
|||
|
|||
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", |
|||
...props |
|||
}: React.ComponentProps<typeof SheetPrimitive.Content> & { |
|||
side?: "top" | "right" | "bottom" | "left"; |
|||
}) { |
|||
return ( |
|||
<SheetPortal> |
|||
<SheetOverlay /> |
|||
<SheetPrimitive.Content |
|||
data-slot="sheet-content" |
|||
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 bottom-0 h-auto border-t", |
|||
className, |
|||
)} |
|||
{...props} |
|||
> |
|||
{children} |
|||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"> |
|||
<XIcon className="size-4" /> |
|||
<span className="sr-only">Close</span> |
|||
</SheetPrimitive.Close> |
|||
</SheetPrimitive.Content> |
|||
</SheetPortal> |
|||
); |
|||
} |
|||
|
|||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { |
|||
return ( |
|||
<div |
|||
data-slot="sheet-header" |
|||
className={cn("flex flex-col gap-1.5 p-4", 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, |
|||
}; |
|||
@ -0,0 +1,725 @@ |
|||
"use client"; |
|||
|
|||
import { |
|||
Tooltip, |
|||
TooltipContent, |
|||
TooltipProvider, |
|||
TooltipTrigger, |
|||
} from "@app/components/UI/tooltip"; |
|||
import { Button } from "@app/components/ui/button.tsx"; |
|||
import { Input } from "@app/components/ui/input.tsx"; |
|||
import { Separator } from "@app/components/ui/separator.tsx"; |
|||
import { |
|||
Sheet, |
|||
SheetContent, |
|||
SheetDescription, |
|||
SheetHeader, |
|||
SheetTitle, |
|||
} from "@app/components/ui/sheet.tsx"; |
|||
import { Skeleton } from "@app/components/ui/skeleton.tsx"; |
|||
import { useIsMobile } from "@app/hooks/use-mobile"; |
|||
import { cn } from "@app/lib/utils"; |
|||
import { Slot } from "@radix-ui/react-slot"; |
|||
import { cva, type VariantProps } from "class-variance-authority"; |
|||
import { PanelLeftIcon } from "lucide-react"; |
|||
import * as React from "react"; |
|||
|
|||
const SIDEBAR_COOKIE_NAME = "sidebar_state"; |
|||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; |
|||
const SIDEBAR_WIDTH = "16rem"; |
|||
const SIDEBAR_WIDTH_MOBILE = "18rem"; |
|||
const SIDEBAR_WIDTH_ICON = "3rem"; |
|||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"; |
|||
|
|||
type SidebarContextProps = { |
|||
state: "expanded" | "collapsed"; |
|||
open: boolean; |
|||
setOpen: (open: boolean) => void; |
|||
openMobile: boolean; |
|||
setOpenMobile: (open: boolean) => void; |
|||
isMobile: boolean; |
|||
toggleSidebar: () => void; |
|||
}; |
|||
|
|||
const SidebarContext = React.createContext<SidebarContextProps | null>(null); |
|||
|
|||
function useSidebar() { |
|||
const context = React.useContext(SidebarContext); |
|||
if (!context) { |
|||
throw new Error("useSidebar must be used within a SidebarProvider."); |
|||
} |
|||
|
|||
return context; |
|||
} |
|||
|
|||
function SidebarProvider({ |
|||
defaultOpen = true, |
|||
open: openProp, |
|||
onOpenChange: setOpenProp, |
|||
className, |
|||
style, |
|||
children, |
|||
...props |
|||
}: React.ComponentProps<"div"> & { |
|||
defaultOpen?: boolean; |
|||
open?: boolean; |
|||
onOpenChange?: (open: boolean) => void; |
|||
}) { |
|||
const isMobile = useIsMobile(); |
|||
const [openMobile, setOpenMobile] = React.useState(false); |
|||
|
|||
// This is the internal state of the sidebar.
|
|||
// We use openProp and setOpenProp for control from outside the component.
|
|||
const [_open, _setOpen] = React.useState(defaultOpen); |
|||
const open = openProp ?? _open; |
|||
const setOpen = React.useCallback( |
|||
(value: boolean | ((value: boolean) => boolean)) => { |
|||
const openState = typeof value === "function" ? value(open) : value; |
|||
if (setOpenProp) { |
|||
setOpenProp(openState); |
|||
} else { |
|||
_setOpen(openState); |
|||
} |
|||
|
|||
// This sets the cookie to keep the sidebar state.
|
|||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; |
|||
}, |
|||
[setOpenProp, open], |
|||
); |
|||
|
|||
// Helper to toggle the sidebar.
|
|||
const toggleSidebar = React.useCallback(() => { |
|||
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open); |
|||
}, [isMobile, setOpen, setOpenMobile]); |
|||
|
|||
// Adds a keyboard shortcut to toggle the sidebar.
|
|||
React.useEffect(() => { |
|||
const handleKeyDown = (event: KeyboardEvent) => { |
|||
if ( |
|||
event.key === SIDEBAR_KEYBOARD_SHORTCUT && |
|||
(event.metaKey || event.ctrlKey) |
|||
) { |
|||
event.preventDefault(); |
|||
toggleSidebar(); |
|||
} |
|||
}; |
|||
|
|||
window.addEventListener("keydown", handleKeyDown); |
|||
return () => window.removeEventListener("keydown", handleKeyDown); |
|||
}, [toggleSidebar]); |
|||
|
|||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
|||
// This makes it easier to style the sidebar with Tailwind classes.
|
|||
const state = open ? "expanded" : "collapsed"; |
|||
|
|||
const contextValue = React.useMemo<SidebarContextProps>( |
|||
() => ({ |
|||
state, |
|||
open, |
|||
setOpen, |
|||
isMobile, |
|||
openMobile, |
|||
setOpenMobile, |
|||
toggleSidebar, |
|||
}), |
|||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar], |
|||
); |
|||
|
|||
return ( |
|||
<SidebarContext.Provider value={contextValue}> |
|||
<TooltipProvider delayDuration={0}> |
|||
<div |
|||
data-slot="sidebar-wrapper" |
|||
style={ |
|||
{ |
|||
"--sidebar-width": SIDEBAR_WIDTH, |
|||
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON, |
|||
...style, |
|||
} as React.CSSProperties |
|||
} |
|||
className={cn( |
|||
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full", |
|||
className, |
|||
)} |
|||
{...props} |
|||
> |
|||
{children} |
|||
</div> |
|||
</TooltipProvider> |
|||
</SidebarContext.Provider> |
|||
); |
|||
} |
|||
|
|||
function Sidebar({ |
|||
side = "left", |
|||
variant = "sidebar", |
|||
collapsible = "offcanvas", |
|||
className, |
|||
children, |
|||
...props |
|||
}: React.ComponentProps<"div"> & { |
|||
side?: "left" | "right"; |
|||
variant?: "sidebar" | "floating" | "inset"; |
|||
collapsible?: "offcanvas" | "icon" | "none"; |
|||
}) { |
|||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); |
|||
|
|||
if (collapsible === "none") { |
|||
return ( |
|||
<div |
|||
data-slot="sidebar" |
|||
className={cn( |
|||
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col", |
|||
className, |
|||
)} |
|||
{...props} |
|||
> |
|||
{children} |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
if (isMobile) { |
|||
return ( |
|||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}> |
|||
<SheetContent |
|||
data-sidebar="sidebar" |
|||
data-slot="sidebar" |
|||
data-mobile="true" |
|||
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden" |
|||
style={ |
|||
{ |
|||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE, |
|||
} as React.CSSProperties |
|||
} |
|||
side={side} |
|||
> |
|||
<SheetHeader className="sr-only"> |
|||
<SheetTitle>Sidebar</SheetTitle> |
|||
<SheetDescription>Displays the mobile sidebar.</SheetDescription> |
|||
</SheetHeader> |
|||
<div className="flex h-full w-full flex-col">{children}</div> |
|||
</SheetContent> |
|||
</Sheet> |
|||
); |
|||
} |
|||
|
|||
return ( |
|||
<div |
|||
className="group peer text-sidebar-foreground hidden md:block" |
|||
data-state={state} |
|||
data-collapsible={state === "collapsed" ? collapsible : ""} |
|||
data-variant={variant} |
|||
data-side={side} |
|||
data-slot="sidebar" |
|||
> |
|||
{/* This is what handles the sidebar gap on desktop */} |
|||
<div |
|||
data-slot="sidebar-gap" |
|||
className={cn( |
|||
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear", |
|||
"group-data-[collapsible=offcanvas]:w-0", |
|||
"group-data-[side=right]:rotate-180", |
|||
variant === "floating" || variant === "inset" |
|||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]" |
|||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)", |
|||
)} |
|||
/> |
|||
<div |
|||
data-slot="sidebar-container" |
|||
className={cn( |
|||
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex", |
|||
side === "left" |
|||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]" |
|||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]", |
|||
// Adjust the padding for floating and inset variants.
|
|||
variant === "floating" || variant === "inset" |
|||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]" |
|||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l", |
|||
className, |
|||
)} |
|||
{...props} |
|||
> |
|||
<div |
|||
data-sidebar="sidebar" |
|||
data-slot="sidebar-inner" |
|||
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm" |
|||
> |
|||
{children} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
function SidebarTrigger({ |
|||
className, |
|||
onClick, |
|||
...props |
|||
}: React.ComponentProps<typeof Button>) { |
|||
const { toggleSidebar } = useSidebar(); |
|||
|
|||
return ( |
|||
<Button |
|||
data-sidebar="trigger" |
|||
data-slot="sidebar-trigger" |
|||
variant="ghost" |
|||
size="icon" |
|||
className={cn("size-7", className)} |
|||
onClick={(event) => { |
|||
onClick?.(event); |
|||
toggleSidebar(); |
|||
}} |
|||
{...props} |
|||
> |
|||
<PanelLeftIcon /> |
|||
<span className="sr-only">Toggle Sidebar</span> |
|||
</Button> |
|||
); |
|||
} |
|||
|
|||
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { |
|||
const { toggleSidebar } = useSidebar(); |
|||
|
|||
return ( |
|||
<button |
|||
data-sidebar="rail" |
|||
data-slot="sidebar-rail" |
|||
aria-label="Toggle Sidebar" |
|||
tabIndex={-1} |
|||
onClick={toggleSidebar} |
|||
title="Toggle Sidebar" |
|||
className={cn( |
|||
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex", |
|||
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize", |
|||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize", |
|||
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full", |
|||
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2", |
|||
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2", |
|||
className, |
|||
)} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) { |
|||
return ( |
|||
<main |
|||
data-slot="sidebar-inset" |
|||
className={cn( |
|||
"bg-background relative flex w-full flex-1 flex-col", |
|||
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2", |
|||
className, |
|||
)} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
function SidebarInput({ |
|||
className, |
|||
...props |
|||
}: React.ComponentProps<typeof Input>) { |
|||
return ( |
|||
<Input |
|||
data-slot="sidebar-input" |
|||
data-sidebar="input" |
|||
className={cn("bg-background h-8 w-full shadow-none", className)} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) { |
|||
return ( |
|||
<div |
|||
data-slot="sidebar-header" |
|||
data-sidebar="header" |
|||
className={cn("flex flex-col gap-2 p-2", className)} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) { |
|||
return ( |
|||
<div |
|||
data-slot="sidebar-footer" |
|||
data-sidebar="footer" |
|||
className={cn("flex flex-col gap-2 p-2", className)} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
function SidebarSeparator({ |
|||
className, |
|||
...props |
|||
}: React.ComponentProps<typeof Separator>) { |
|||
return ( |
|||
<Separator |
|||
data-slot="sidebar-separator" |
|||
data-sidebar="separator" |
|||
className={cn("bg-sidebar-border mx-2 w-auto", className)} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) { |
|||
return ( |
|||
<div |
|||
data-slot="sidebar-content" |
|||
data-sidebar="content" |
|||
className={cn( |
|||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden", |
|||
className, |
|||
)} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) { |
|||
return ( |
|||
<div |
|||
data-slot="sidebar-group" |
|||
data-sidebar="group" |
|||
className={cn("relative flex w-full min-w-0 flex-col p-2", className)} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
function SidebarGroupLabel({ |
|||
className, |
|||
asChild = false, |
|||
...props |
|||
}: React.ComponentProps<"div"> & { asChild?: boolean }) { |
|||
const Comp = asChild ? Slot : "div"; |
|||
|
|||
return ( |
|||
<Comp |
|||
data-slot="sidebar-group-label" |
|||
data-sidebar="group-label" |
|||
className={cn( |
|||
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", |
|||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", |
|||
className, |
|||
)} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
function SidebarGroupAction({ |
|||
className, |
|||
asChild = false, |
|||
...props |
|||
}: React.ComponentProps<"button"> & { asChild?: boolean }) { |
|||
const Comp = asChild ? Slot : "button"; |
|||
|
|||
return ( |
|||
<Comp |
|||
data-slot="sidebar-group-action" |
|||
data-sidebar="group-action" |
|||
className={cn( |
|||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", |
|||
// Increases the hit area of the button on mobile.
|
|||
"after:absolute after:-inset-2 md:after:hidden", |
|||
"group-data-[collapsible=icon]:hidden", |
|||
className, |
|||
)} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
function SidebarGroupContent({ |
|||
className, |
|||
...props |
|||
}: React.ComponentProps<"div">) { |
|||
return ( |
|||
<div |
|||
data-slot="sidebar-group-content" |
|||
data-sidebar="group-content" |
|||
className={cn("w-full text-sm", className)} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) { |
|||
return ( |
|||
<ul |
|||
data-slot="sidebar-menu" |
|||
data-sidebar="menu" |
|||
className={cn("flex w-full min-w-0 flex-col gap-1", className)} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) { |
|||
return ( |
|||
<li |
|||
data-slot="sidebar-menu-item" |
|||
data-sidebar="menu-item" |
|||
className={cn("group/menu-item relative", className)} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
const sidebarMenuButtonVariants = cva( |
|||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", |
|||
{ |
|||
variants: { |
|||
variant: { |
|||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", |
|||
outline: |
|||
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]", |
|||
}, |
|||
size: { |
|||
default: "h-8 text-sm", |
|||
sm: "h-7 text-xs", |
|||
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!", |
|||
}, |
|||
}, |
|||
defaultVariants: { |
|||
variant: "default", |
|||
size: "default", |
|||
}, |
|||
}, |
|||
); |
|||
|
|||
function SidebarMenuButton({ |
|||
asChild = false, |
|||
isActive = false, |
|||
variant = "default", |
|||
size = "default", |
|||
tooltip, |
|||
className, |
|||
...props |
|||
}: React.ComponentProps<"button"> & { |
|||
asChild?: boolean; |
|||
isActive?: boolean; |
|||
tooltip?: string | React.ComponentProps<typeof TooltipContent>; |
|||
} & VariantProps<typeof sidebarMenuButtonVariants>) { |
|||
const Comp = asChild ? Slot : "button"; |
|||
const { isMobile, state } = useSidebar(); |
|||
|
|||
const button = ( |
|||
<Comp |
|||
data-slot="sidebar-menu-button" |
|||
data-sidebar="menu-button" |
|||
data-size={size} |
|||
data-active={isActive} |
|||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)} |
|||
{...props} |
|||
/> |
|||
); |
|||
|
|||
if (!tooltip) { |
|||
return button; |
|||
} |
|||
|
|||
if (typeof tooltip === "string") { |
|||
tooltip = { |
|||
children: tooltip, |
|||
}; |
|||
} |
|||
|
|||
return ( |
|||
<Tooltip> |
|||
<TooltipTrigger asChild>{button}</TooltipTrigger> |
|||
<TooltipContent |
|||
side="right" |
|||
align="center" |
|||
hidden={state !== "collapsed" || isMobile} |
|||
{...tooltip} |
|||
/> |
|||
</Tooltip> |
|||
); |
|||
} |
|||
|
|||
function SidebarMenuAction({ |
|||
className, |
|||
asChild = false, |
|||
showOnHover = false, |
|||
...props |
|||
}: React.ComponentProps<"button"> & { |
|||
asChild?: boolean; |
|||
showOnHover?: boolean; |
|||
}) { |
|||
const Comp = asChild ? Slot : "button"; |
|||
|
|||
return ( |
|||
<Comp |
|||
data-slot="sidebar-menu-action" |
|||
data-sidebar="menu-action" |
|||
className={cn( |
|||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", |
|||
// Increases the hit area of the button on mobile.
|
|||
"after:absolute after:-inset-2 md:after:hidden", |
|||
"peer-data-[size=sm]/menu-button:top-1", |
|||
"peer-data-[size=default]/menu-button:top-1.5", |
|||
"peer-data-[size=lg]/menu-button:top-2.5", |
|||
"group-data-[collapsible=icon]:hidden", |
|||
showOnHover && |
|||
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0", |
|||
className, |
|||
)} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
function SidebarMenuBadge({ |
|||
className, |
|||
...props |
|||
}: React.ComponentProps<"div">) { |
|||
return ( |
|||
<div |
|||
data-slot="sidebar-menu-badge" |
|||
data-sidebar="menu-badge" |
|||
className={cn( |
|||
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none", |
|||
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground", |
|||
"peer-data-[size=sm]/menu-button:top-1", |
|||
"peer-data-[size=default]/menu-button:top-1.5", |
|||
"peer-data-[size=lg]/menu-button:top-2.5", |
|||
"group-data-[collapsible=icon]:hidden", |
|||
className, |
|||
)} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
function SidebarMenuSkeleton({ |
|||
className, |
|||
showIcon = false, |
|||
...props |
|||
}: React.ComponentProps<"div"> & { |
|||
showIcon?: boolean; |
|||
}) { |
|||
// Random width between 50 to 90%.
|
|||
const width = React.useMemo(() => { |
|||
return `${Math.floor(Math.random() * 40) + 50}%`; |
|||
}, []); |
|||
|
|||
return ( |
|||
<div |
|||
data-slot="sidebar-menu-skeleton" |
|||
data-sidebar="menu-skeleton" |
|||
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)} |
|||
{...props} |
|||
> |
|||
{showIcon && ( |
|||
<Skeleton |
|||
className="size-4 rounded-md" |
|||
data-sidebar="menu-skeleton-icon" |
|||
/> |
|||
)} |
|||
<Skeleton |
|||
className="h-4 max-w-(--skeleton-width) flex-1" |
|||
data-sidebar="menu-skeleton-text" |
|||
style={ |
|||
{ |
|||
"--skeleton-width": width, |
|||
} as React.CSSProperties |
|||
} |
|||
/> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) { |
|||
return ( |
|||
<ul |
|||
data-slot="sidebar-menu-sub" |
|||
data-sidebar="menu-sub" |
|||
className={cn( |
|||
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5", |
|||
"group-data-[collapsible=icon]:hidden", |
|||
className, |
|||
)} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
function SidebarMenuSubItem({ |
|||
className, |
|||
...props |
|||
}: React.ComponentProps<"li">) { |
|||
return ( |
|||
<li |
|||
data-slot="sidebar-menu-sub-item" |
|||
data-sidebar="menu-sub-item" |
|||
className={cn("group/menu-sub-item relative", className)} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
function SidebarMenuSubButton({ |
|||
asChild = false, |
|||
size = "md", |
|||
isActive = false, |
|||
className, |
|||
...props |
|||
}: React.ComponentProps<"a"> & { |
|||
asChild?: boolean; |
|||
size?: "sm" | "md"; |
|||
isActive?: boolean; |
|||
}) { |
|||
const Comp = asChild ? Slot : "a"; |
|||
|
|||
return ( |
|||
<Comp |
|||
data-slot="sidebar-menu-sub-button" |
|||
data-sidebar="menu-sub-button" |
|||
data-size={size} |
|||
data-active={isActive} |
|||
className={cn( |
|||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", |
|||
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground", |
|||
size === "sm" && "text-xs", |
|||
size === "md" && "text-sm", |
|||
"group-data-[collapsible=icon]:hidden", |
|||
className, |
|||
)} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
export { |
|||
Sidebar, |
|||
SidebarContent, |
|||
SidebarFooter, |
|||
SidebarGroup, |
|||
SidebarGroupAction, |
|||
SidebarGroupContent, |
|||
SidebarGroupLabel, |
|||
SidebarHeader, |
|||
SidebarInput, |
|||
SidebarInset, |
|||
SidebarMenu, |
|||
SidebarMenuAction, |
|||
SidebarMenuBadge, |
|||
SidebarMenuButton, |
|||
SidebarMenuItem, |
|||
SidebarMenuSkeleton, |
|||
SidebarMenuSub, |
|||
SidebarMenuSubButton, |
|||
SidebarMenuSubItem, |
|||
SidebarProvider, |
|||
SidebarRail, |
|||
SidebarSeparator, |
|||
SidebarTrigger, |
|||
useSidebar, |
|||
}; |
|||
@ -0,0 +1,13 @@ |
|||
import { cn } from "@app/lib/utils"; |
|||
|
|||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) { |
|||
return ( |
|||
<div |
|||
data-slot="skeleton" |
|||
className={cn("bg-accent animate-pulse rounded-md", className)} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
export { Skeleton }; |
|||
@ -0,0 +1,46 @@ |
|||
import { useTheme } from "@components/theme-provider.tsx"; |
|||
import { Button } from "@components/UI/button.tsx"; |
|||
import { |
|||
DropdownMenu, |
|||
DropdownMenuContent, |
|||
DropdownMenuItem, |
|||
DropdownMenuTrigger, |
|||
} from "@components/UI/dropdown-menu.tsx"; |
|||
import { Laptop, Moon, Sun } from "lucide-react"; |
|||
import type { JSX } from "react"; |
|||
|
|||
export function ThemeModeToggle() { |
|||
const { theme, setTheme } = useTheme(); |
|||
|
|||
// Map theme -> icon + label
|
|||
const themes: Record<string, { label: string; icon: JSX.Element }> = { |
|||
light: { label: "Light", icon: <Sun className="h-4 w-4" /> }, |
|||
dark: { label: "Dark", icon: <Moon className="h-4 w-4" /> }, |
|||
system: { label: "System", icon: <Laptop className="h-4 w-4" /> }, |
|||
}; |
|||
|
|||
const current = themes[theme ?? "system"]; |
|||
|
|||
return ( |
|||
<DropdownMenu> |
|||
<DropdownMenuTrigger asChild> |
|||
<Button variant="ghost" className="w-full justify-between"> |
|||
<span className="flex items-center gap-2"> |
|||
{current?.icon} |
|||
Current theme: {current?.label} |
|||
</span> |
|||
</Button> |
|||
</DropdownMenuTrigger> |
|||
<DropdownMenuContent align="end" className="w-full"> |
|||
{Object.entries(themes).map(([key, { label, icon }]) => ( |
|||
<DropdownMenuItem key={key} onClick={() => setTheme(key)}> |
|||
<span className="flex items-center gap-2 w-full"> |
|||
{icon} |
|||
{label} |
|||
</span> |
|||
</DropdownMenuItem> |
|||
))} |
|||
</DropdownMenuContent> |
|||
</DropdownMenu> |
|||
); |
|||
} |
|||
@ -0,0 +1,74 @@ |
|||
import { createContext, useContext, useEffect, useState } from "react"; |
|||
|
|||
type Theme = "dark" | "light" | "system"; |
|||
|
|||
type ThemeProviderProps = { |
|||
children: React.ReactNode; |
|||
defaultTheme?: Theme; |
|||
storageKey?: string; |
|||
}; |
|||
|
|||
type ThemeProviderState = { |
|||
theme: Theme; |
|||
setTheme: (theme: Theme) => void; |
|||
}; |
|||
|
|||
const initialState: ThemeProviderState = { |
|||
theme: "system", |
|||
setTheme: () => null, |
|||
}; |
|||
|
|||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState); |
|||
|
|||
export function ThemeProvider({ |
|||
children, |
|||
defaultTheme = "system", |
|||
storageKey = "web-client-theme", |
|||
...props |
|||
}: ThemeProviderProps) { |
|||
const [theme, setTheme] = useState<Theme>( |
|||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme, |
|||
); |
|||
|
|||
useEffect(() => { |
|||
const root = window.document.documentElement; |
|||
|
|||
root.classList.remove("light", "dark"); |
|||
|
|||
if (theme === "system") { |
|||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") |
|||
.matches |
|||
? "dark" |
|||
: "light"; |
|||
|
|||
root.classList.add(systemTheme); |
|||
return; |
|||
} |
|||
|
|||
root.classList.add(theme); |
|||
}, [theme]); |
|||
|
|||
const value = { |
|||
theme, |
|||
setTheme: (theme: Theme) => { |
|||
localStorage.setItem(storageKey, theme); |
|||
setTheme(theme); |
|||
}, |
|||
}; |
|||
|
|||
return ( |
|||
<ThemeProviderContext.Provider {...props} value={value}> |
|||
{children} |
|||
</ThemeProviderContext.Provider> |
|||
); |
|||
} |
|||
|
|||
export const useTheme = () => { |
|||
const context = useContext(ThemeProviderContext); |
|||
|
|||
if (context === undefined) { |
|||
throw new Error("useTheme must be used within a ThemeProvider"); |
|||
} |
|||
|
|||
return context; |
|||
}; |
|||
@ -1,4 +0,0 @@ |
|||
export type DeviceMetrics = { |
|||
batteryLevel?: number | null; |
|||
voltage?: number | null; |
|||
}; |
|||
@ -1,44 +0,0 @@ |
|||
import { useCallback, useEffect, useState } from "react"; |
|||
|
|||
type Theme = "light" | "dark" | "system"; |
|||
|
|||
export function useTheme() { |
|||
const getSystemTheme = () => |
|||
globalThis.matchMedia("(prefers-color-scheme: dark)").matches |
|||
? "dark" |
|||
: "light"; |
|||
|
|||
const getStoredPreference = useCallback( |
|||
(): Theme => (localStorage.getItem("theme") as Theme) || "system", |
|||
[], |
|||
); |
|||
|
|||
const [preference, setPreference] = useState<Theme>(() => |
|||
typeof window !== "undefined" ? getStoredPreference() : "light", |
|||
); |
|||
|
|||
const theme = preference === "system" ? getSystemTheme() : preference; |
|||
|
|||
useEffect(() => { |
|||
document.documentElement.setAttribute("data-theme", theme); |
|||
}, [theme]); |
|||
|
|||
useEffect(() => { |
|||
if (preference !== "system") { |
|||
return; |
|||
} |
|||
|
|||
const media = globalThis.matchMedia("(prefers-color-scheme: dark)"); |
|||
const updateTheme = () => setPreference(getStoredPreference()); |
|||
|
|||
media.addEventListener("change", updateTheme); |
|||
return () => media.removeEventListener("change", updateTheme); |
|||
}, [preference, getStoredPreference]); |
|||
|
|||
const setPreferenceValue = (newPreference: Theme) => { |
|||
localStorage.setItem("theme", newPreference); |
|||
setPreference(newPreference); |
|||
}; |
|||
|
|||
return { theme, preference, setPreference: setPreferenceValue }; |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
import * as React from "react"; |
|||
|
|||
const MOBILE_BREAKPOINT = 768; |
|||
|
|||
export function useIsMobile() { |
|||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>( |
|||
undefined, |
|||
); |
|||
|
|||
React.useEffect(() => { |
|||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); |
|||
const onChange = () => { |
|||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); |
|||
}; |
|||
mql.addEventListener("change", onChange); |
|||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); |
|||
return () => mql.removeEventListener("change", onChange); |
|||
}, []); |
|||
|
|||
return !!isMobile; |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
import { type ClassValue, clsx } from "clsx"; |
|||
import { twMerge } from "tailwind-merge"; |
|||
|
|||
export function cn(...inputs: ClassValue[]) { |
|||
return twMerge(clsx(inputs)); |
|||
} |
|||
@ -144,6 +144,9 @@ importers: |
|||
'@radix-ui/react-slider': |
|||
specifier: ^1.3.5 |
|||
version: 1.3.5(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected]) |
|||
'@radix-ui/react-slot': |
|||
specifier: ^1.2.3 |
|||
version: 1.2.3(@types/[email protected])([email protected]) |
|||
'@radix-ui/react-switch': |
|||
specifier: ^1.2.5 |
|||
version: 1.2.5(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected]) |
|||
@ -328,6 +331,9 @@ importers: |
|||
testing-library: |
|||
specifier: ^0.0.2 |
|||
version: 0.0.2(@angular/[email protected](@angular/[email protected]([email protected])([email protected]))([email protected]))(@angular/[email protected]([email protected])([email protected])) |
|||
tw-animate-css: |
|||
specifier: ^1.3.8 |
|||
version: 1.3.8 |
|||
typescript: |
|||
specifier: ^5.8.3 |
|||
version: 5.9.2 |
|||
@ -1398,8 +1404,8 @@ packages: |
|||
resolution: {integrity: sha512-IGJtuBbaGzOUgODdBRg66p8stnwj9iDXkgbYKoYcNiiQmaez5WVRfXm4b03MCDwmZyX93csbfHFWEJJYHnn5oA==} |
|||
hasBin: true |
|||
|
|||
'@napi-rs/[email protected].4': |
|||
resolution: {integrity: sha512-+ZEtJPp8EF8h4kN6rLQECRor00H7jtDgBVtttIUoxuDkXLiQMaSBqju3LV/IEsMvqVG5pviUvR4jYhIA1xNm8w==} |
|||
'@napi-rs/[email protected].5': |
|||
resolution: {integrity: sha512-TBr9Cf9onSAS2LQ2+QHx6XcC6h9+RIzJgbqG3++9TUZSH204AwEy5jg3BTQ0VATsyoGj4ee49tN/y6rvaOOtcg==} |
|||
|
|||
'@noble/[email protected]': |
|||
resolution: {integrity: sha512-GIKz/j99FRthB8icyJQA51E8Uk5hXmdyThjgQXRKiv9h0zeRlzSCLIzFw6K1LotZ3XuB7yzlf76qk7uBmTdFqA==} |
|||
@ -1421,12 +1427,8 @@ packages: |
|||
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} |
|||
engines: {node: '>= 8'} |
|||
|
|||
'@oxc-project/[email protected]': |
|||
resolution: {integrity: sha512-ky2Hqi2q/uGX36UfY79zxMbUqiNIl1RyKKVJfFenG70lbn+/fcaKBVTbhmUwn8a2wPyv2gNtDQxuDytbKX9giQ==} |
|||
engines: {node: '>=6.9.0'} |
|||
|
|||
'@oxc-project/[email protected]': |
|||
resolution: {integrity: sha512-ipZFWVGE9fADBVXXWJWY/cxpysc41Gt5upKDeb32F6WMgFyO7XETUMVq8UuREKCih+Km5E6p2VhEvf6Fuhey6g==} |
|||
'@oxc-project/[email protected]': |
|||
resolution: {integrity: sha512-yuo+ECPIW5Q9mSeNmCDC2im33bfKuwW18mwkaHMQh8KakHYDzj4ci/q7wxf2qS3dMlVVCIyrs3kFtH5LmnlYnw==} |
|||
|
|||
'@quansync/[email protected]': |
|||
resolution: {integrity: sha512-lNS9hL2aS2NZgNW7BBj+6EBl4rOf8l+tQ0eRY6JWCI8jI2kc53gSoqbjojU0OnAWhzoXiOjFyGsHcDGePB3lhA==} |
|||
@ -1943,85 +1945,85 @@ packages: |
|||
'@radix-ui/[email protected]': |
|||
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} |
|||
|
|||
'@rolldown/[email protected]7': |
|||
resolution: {integrity: sha512-Pdr3USGBdoYzcygfJTSATHd7x476vVF3rnQ6SuUAh4YjhgGoNaI/ZycQ0RsonptwwU5NmQRWxfWv+aUPL6JlJg==} |
|||
'@rolldown/[email protected]8': |
|||
resolution: {integrity: sha512-AE3HFQrjWCKLFZD1Vpiy+qsqTRwwoil1oM5WsKPSmfQ5fif/A+ZtOZetF32erZdsR7qyvns6qHEteEsF6g6rsQ==} |
|||
engines: {node: ^20.19.0 || >=22.12.0} |
|||
cpu: [arm64] |
|||
os: [android] |
|||
|
|||
'@rolldown/[email protected]7': |
|||
resolution: {integrity: sha512-iDdmatSgbWhTYOq51G2CkJXwFayiuQpv/ywG7Bv3wKqy31L7d0LltUhWqAdfCl7eBG3gybfUm/iEXiTldH3jYA==} |
|||
'@rolldown/[email protected]8': |
|||
resolution: {integrity: sha512-RaoWOKc0rrFsVmKOjQpebMY6c6/I7GR1FBc25v7L/R7NlM0166mUotwGEv7vxu7ruXH4SJcFeVrfADFUUXUmmQ==} |
|||
engines: {node: ^20.19.0 || >=22.12.0} |
|||
cpu: [arm64] |
|||
os: [darwin] |
|||
|
|||
'@rolldown/[email protected]7': |
|||
resolution: {integrity: sha512-LQPpi3YJDtIprj6mwMbVM1gLM4BV2m9oqe9h3Y1UwAd20xs+imnzWJqWFpm4Hw9SiFmefIf3q4EPx2k6Nj2K7A==} |
|||
'@rolldown/[email protected]8': |
|||
resolution: {integrity: sha512-Ymojqc2U35iUc8NFU2XX1WQPfBRRHN6xHcrxAf9WS8BFFBn8pDrH5QPvH1tYs3lDkw6UGGbanr1RGzARqdUp1g==} |
|||
engines: {node: ^20.19.0 || >=22.12.0} |
|||
cpu: [x64] |
|||
os: [darwin] |
|||
|
|||
'@rolldown/[email protected]7': |
|||
resolution: {integrity: sha512-9JnfSWfYd/YrZOu4Sj3rb2THBrCj70nJB/2FOSdg0O9ZoRrdTeB8b7Futo6N7HLWZM5uqqnJBX6VTpA0RZD+ow==} |
|||
'@rolldown/[email protected]8': |
|||
resolution: {integrity: sha512-0ermTQ//WzSI0nOL3z/LUWMNiE9xeM5cLGxjewPFEexqxV/0uM8/lNp9QageQ8jfc/VO1OURsGw34HYO5PaL8w==} |
|||
engines: {node: ^20.19.0 || >=22.12.0} |
|||
cpu: [x64] |
|||
os: [freebsd] |
|||
|
|||
'@rolldown/[email protected]7': |
|||
resolution: {integrity: sha512-eEmQTpvefEtHxc0vg5sOnWCqBcGQB/SIDlPkkzKR9ESKq9BsjQfHxssJWuNMyQ+rpr9CYaogddyQtZ9GHkp8vA==} |
|||
'@rolldown/[email protected]8': |
|||
resolution: {integrity: sha512-GADxzVUTCTp6EWI52831A29Tt7PukFe94nhg/SUsfkI33oTiNQtPxyLIT/3oRegizGuPSZSlrdBurkjDwxyEUQ==} |
|||
engines: {node: ^20.19.0 || >=22.12.0} |
|||
cpu: [arm] |
|||
os: [linux] |
|||
|
|||
'@rolldown/[email protected]7': |
|||
resolution: {integrity: sha512-Ekv4OjDzQUl0X9kHM7M23N9hVRiYCYr89neLBNITCp7P4IHs1f6SNZiCIvvBVy6NIFzO1w9LZJGEeJYK5cQBVQ==} |
|||
'@rolldown/[email protected]8': |
|||
resolution: {integrity: sha512-SKO7Exl5Yem/OSNoA5uLHzyrptUQ8Hg70kHDxuwEaH0+GUg+SQe9/7PWmc4hFKBMrJGdQtii8WZ0uIz9Dofg5Q==} |
|||
engines: {node: ^20.19.0 || >=22.12.0} |
|||
cpu: [arm64] |
|||
os: [linux] |
|||
|
|||
'@rolldown/[email protected]7': |
|||
resolution: {integrity: sha512-z8Aa5Kar5mhh0RVZEL+zKJwNz1cgcDISmwUMcTk0w986T8JZJOJCfJ/u9e8pqUTIJjxdM8SZq9/24nMgMlx5ng==} |
|||
'@rolldown/[email protected]8': |
|||
resolution: {integrity: sha512-SOo6+WqhXPBaShLxLT0eCgH17d3Yu1lMAe4mFP0M9Bvr/kfMSOPQXuLxBcbBU9IFM9w3N6qP9xWOHO+oUJvi8Q==} |
|||
engines: {node: ^20.19.0 || >=22.12.0} |
|||
cpu: [arm64] |
|||
os: [linux] |
|||
|
|||
'@rolldown/[email protected]7': |
|||
resolution: {integrity: sha512-e+fNseKhfE/socjOw6VrQcXrbNKfi2V/KZ+ssuLnmeaYNGuJWqPhvML56oYhGb3IgROEEc61lzr3Riy5BIqoMA==} |
|||
'@rolldown/[email protected]8': |
|||
resolution: {integrity: sha512-yvsQ3CyrodOX+lcoi+lejZGCOvJZa9xTsNB8OzpMDmHeZq3QzJfpYjXSAS6vie70fOkLVJb77UqYO193Cl8XBQ==} |
|||
engines: {node: ^20.19.0 || >=22.12.0} |
|||
cpu: [x64] |
|||
os: [linux] |
|||
|
|||
'@rolldown/[email protected]7': |
|||
resolution: {integrity: sha512-dPZfB396PMIasd19X0ikpdCvjK/7SaJFO8y5/TxnozJEy70vOf4GESe/oKcsJPav/MSTWBYsHjJSO6vX0oAW8g==} |
|||
'@rolldown/[email protected]8': |
|||
resolution: {integrity: sha512-84qzKMwUwikfYeOuJ4Kxm/3z15rt0nFGGQArHYIQQNSTiQdxGHxOkqXtzPFqrVfBJUdxBAf+jYzR1pttFJuWyg==} |
|||
engines: {node: ^20.19.0 || >=22.12.0} |
|||
cpu: [x64] |
|||
os: [linux] |
|||
|
|||
'@rolldown/[email protected]7': |
|||
resolution: {integrity: sha512-rFjLXoHpRqxJqkSBXHuyt6bhyiIFnvLD9X2iPmCYlfpEkdTbrY1AXg4ZbF8UMO5LM7DAAZm/7vPYPO1TKTA7Sg==} |
|||
'@rolldown/[email protected]8': |
|||
resolution: {integrity: sha512-QrNiWlce01DYH0rL8K3yUBu+lNzY+B0DyCbIc2Atan6/S6flxOL0ow5DLQvMamOI/oKhrJ4xG+9MkMb9dDHbLQ==} |
|||
engines: {node: ^20.19.0 || >=22.12.0} |
|||
cpu: [arm64] |
|||
os: [openharmony] |
|||
|
|||
'@rolldown/[email protected]7': |
|||
resolution: {integrity: sha512-oQAe3lMaBGX6q0GSic0l3Obmd6/rX8R6eHLnRC8kyy/CvPLiCMV82MPGT8fxpPTo/ULFGrupSu2nV1zmOFBt/w==} |
|||
'@rolldown/[email protected]8': |
|||
resolution: {integrity: sha512-fnLtHyjwEsG4/aNV3Uv3Qd1ZbdH+CopwJNoV0RgBqrcQB8V6/Qdikd5JKvnO23kb3QvIpP+dAMGZMv1c2PJMzw==} |
|||
engines: {node: '>=14.0.0'} |
|||
cpu: [wasm32] |
|||
|
|||
'@rolldown/[email protected]7': |
|||
resolution: {integrity: sha512-ucO6CiZhpkNRiVAk7ybvA9pZaMreCtfHej3BtJcBL5S3aYmp4h0g6TvaXLD5YRJx5sXobp/9A//xU4wPMul3Bg==} |
|||
'@rolldown/[email protected]8': |
|||
resolution: {integrity: sha512-19cTfnGedem+RY+znA9J6ARBOCEFD4YSjnx0p5jiTm9tR6pHafRfFIfKlTXhun+NL0WWM/M0eb2IfPPYUa8+wg==} |
|||
engines: {node: ^20.19.0 || >=22.12.0} |
|||
cpu: [arm64] |
|||
os: [win32] |
|||
|
|||
'@rolldown/[email protected]7': |
|||
resolution: {integrity: sha512-Ya9DBWJe1EGHwil7ielI8CdE0ELCg6KyDvDQqIFllnTJEYJ1Rb74DK6mvlZo273qz6Mw8WrMm26urfDeZhCc3Q==} |
|||
'@rolldown/[email protected]8': |
|||
resolution: {integrity: sha512-HcICm4YzFJZV+fI0O0bFLVVlsWvRNo/AB9EfUXvNYbtAxakCnQZ15oq22deFdz6sfi9Y4/SagH2kPU723dhCFA==} |
|||
engines: {node: ^20.19.0 || >=22.12.0} |
|||
cpu: [ia32] |
|||
os: [win32] |
|||
|
|||
'@rolldown/[email protected]7': |
|||
resolution: {integrity: sha512-r+RI+wMReoTIF/uXqQWJcD8xGWXzCzUyGdpLmQ8FC+MCyPHlkjEsFRv8OFIYI6HhiGAmbfWVYEGf+aeLJzkHGw==} |
|||
'@rolldown/[email protected]8': |
|||
resolution: {integrity: sha512-4Qx6cgEPXLb0XsCyLoQcUgYBpfL0sjugftob+zhUH0EOk/NVCAIT+h0NJhY+jn7pFpeKxhNMqhvTNx3AesxIAQ==} |
|||
engines: {node: ^20.19.0 || >=22.12.0} |
|||
cpu: [x64] |
|||
os: [win32] |
|||
@ -2029,8 +2031,8 @@ packages: |
|||
'@rolldown/[email protected]': |
|||
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} |
|||
|
|||
'@rolldown/[email protected]7': |
|||
resolution: {integrity: sha512-0taU1HpxFzrukvWIhLRI4YssJX2wOW5q1MxPXWztltsQ13TE51/larZIwhFdpyk7+K43TH7x6GJ8oEqAo+vDbA==} |
|||
'@rolldown/[email protected]8': |
|||
resolution: {integrity: sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==} |
|||
|
|||
'@rollup/[email protected]': |
|||
resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} |
|||
@ -4609,8 +4611,8 @@ packages: |
|||
vue-tsc: |
|||
optional: true |
|||
|
|||
[email protected]7: |
|||
resolution: {integrity: sha512-KiTU6z1kHGaLvqaYjgsrv2LshHqNBn74waRZivlK8WbfN1obZeScVkQPKYunB66E/mxZWv/zyZlCv3xF2t0WOQ==} |
|||
[email protected]8: |
|||
resolution: {integrity: sha512-58frPNX55Je1YsyrtPJv9rOSR3G5efUZpRqok94Efsj0EUa8dnqJV3BldShyI7A+bVPleucOtzXHwVpJRcR0kQ==} |
|||
engines: {node: ^20.19.0 || >=22.12.0} |
|||
hasBin: true |
|||
|
|||
@ -5011,6 +5013,9 @@ packages: |
|||
engines: {node: '>=18.0.0'} |
|||
hasBin: true |
|||
|
|||
[email protected]: |
|||
resolution: {integrity: sha512-Qrk3PZ7l7wUcGYhwZloqfkWCmaXZAoqjkdbIDvzfGshwGtexa/DAs9koXxIkrpEasyevandomzCBAV1Yyop5rw==} |
|||
|
|||
[email protected]: |
|||
resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} |
|||
engines: {node: '>=10'} |
|||
@ -6513,7 +6518,7 @@ snapshots: |
|||
rw: 1.3.3 |
|||
tinyqueue: 3.0.0 |
|||
|
|||
'@napi-rs/[email protected].4': |
|||
'@napi-rs/[email protected].5': |
|||
dependencies: |
|||
'@emnapi/core': 1.5.0 |
|||
'@emnapi/runtime': 1.5.0 |
|||
@ -6538,9 +6543,7 @@ snapshots: |
|||
'@nodelib/fs.scandir': 2.1.5 |
|||
fastq: 1.19.1 |
|||
|
|||
'@oxc-project/[email protected]': {} |
|||
|
|||
'@oxc-project/[email protected]': {} |
|||
'@oxc-project/[email protected]': {} |
|||
|
|||
'@quansync/[email protected]': |
|||
dependencies: |
|||
@ -7095,53 +7098,53 @@ snapshots: |
|||
|
|||
'@radix-ui/[email protected]': {} |
|||
|
|||
'@rolldown/[email protected]7': |
|||
'@rolldown/[email protected]8': |
|||
optional: true |
|||
|
|||
'@rolldown/[email protected]7': |
|||
'@rolldown/[email protected]8': |
|||
optional: true |
|||
|
|||
'@rolldown/[email protected]7': |
|||
'@rolldown/[email protected]8': |
|||
optional: true |
|||
|
|||
'@rolldown/[email protected]7': |
|||
'@rolldown/[email protected]8': |
|||
optional: true |
|||
|
|||
'@rolldown/[email protected]7': |
|||
'@rolldown/[email protected]8': |
|||
optional: true |
|||
|
|||
'@rolldown/[email protected]7': |
|||
'@rolldown/[email protected]8': |
|||
optional: true |
|||
|
|||
'@rolldown/[email protected]7': |
|||
'@rolldown/[email protected]8': |
|||
optional: true |
|||
|
|||
'@rolldown/[email protected]7': |
|||
'@rolldown/[email protected]8': |
|||
optional: true |
|||
|
|||
'@rolldown/[email protected]7': |
|||
'@rolldown/[email protected]8': |
|||
optional: true |
|||
|
|||
'@rolldown/[email protected]7': |
|||
'@rolldown/[email protected]8': |
|||
optional: true |
|||
|
|||
'@rolldown/[email protected]7': |
|||
'@rolldown/[email protected]8': |
|||
dependencies: |
|||
'@napi-rs/wasm-runtime': 1.0.4 |
|||
'@napi-rs/wasm-runtime': 1.0.5 |
|||
optional: true |
|||
|
|||
'@rolldown/[email protected]7': |
|||
'@rolldown/[email protected]8': |
|||
optional: true |
|||
|
|||
'@rolldown/[email protected]7': |
|||
'@rolldown/[email protected]8': |
|||
optional: true |
|||
|
|||
'@rolldown/[email protected]7': |
|||
'@rolldown/[email protected]8': |
|||
optional: true |
|||
|
|||
'@rolldown/[email protected]': {} |
|||
|
|||
'@rolldown/[email protected]7': {} |
|||
'@rolldown/[email protected]8': {} |
|||
|
|||
'@rollup/[email protected](@babel/[email protected])(@types/[email protected])([email protected])': |
|||
dependencies: |
|||
@ -10441,7 +10444,7 @@ snapshots: |
|||
|
|||
[email protected]: {} |
|||
|
|||
[email protected]([email protected]7)([email protected]): |
|||
[email protected]([email protected]8)([email protected]): |
|||
dependencies: |
|||
'@babel/generator': 7.28.3 |
|||
'@babel/parser': 7.28.4 |
|||
@ -10451,34 +10454,33 @@ snapshots: |
|||
debug: 4.4.1 |
|||
dts-resolver: 2.1.2 |
|||
get-tsconfig: 4.10.1 |
|||
rolldown: 1.0.0-beta.37 |
|||
rolldown: 1.0.0-beta.38 |
|||
optionalDependencies: |
|||
typescript: 5.9.2 |
|||
transitivePeerDependencies: |
|||
- oxc-resolver |
|||
- supports-color |
|||
|
|||
[email protected]7: |
|||
[email protected]8: |
|||
dependencies: |
|||
'@oxc-project/runtime': 0.87.0 |
|||
'@oxc-project/types': 0.87.0 |
|||
'@rolldown/pluginutils': 1.0.0-beta.37 |
|||
'@oxc-project/types': 0.89.0 |
|||
'@rolldown/pluginutils': 1.0.0-beta.38 |
|||
ansis: 4.1.0 |
|||
optionalDependencies: |
|||
'@rolldown/binding-android-arm64': 1.0.0-beta.37 |
|||
'@rolldown/binding-darwin-arm64': 1.0.0-beta.37 |
|||
'@rolldown/binding-darwin-x64': 1.0.0-beta.37 |
|||
'@rolldown/binding-freebsd-x64': 1.0.0-beta.37 |
|||
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.37 |
|||
'@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.37 |
|||
'@rolldown/binding-linux-arm64-musl': 1.0.0-beta.37 |
|||
'@rolldown/binding-linux-x64-gnu': 1.0.0-beta.37 |
|||
'@rolldown/binding-linux-x64-musl': 1.0.0-beta.37 |
|||
'@rolldown/binding-openharmony-arm64': 1.0.0-beta.37 |
|||
'@rolldown/binding-wasm32-wasi': 1.0.0-beta.37 |
|||
'@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.37 |
|||
'@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.37 |
|||
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.37 |
|||
'@rolldown/binding-android-arm64': 1.0.0-beta.38 |
|||
'@rolldown/binding-darwin-arm64': 1.0.0-beta.38 |
|||
'@rolldown/binding-darwin-x64': 1.0.0-beta.38 |
|||
'@rolldown/binding-freebsd-x64': 1.0.0-beta.38 |
|||
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.38 |
|||
'@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.38 |
|||
'@rolldown/binding-linux-arm64-musl': 1.0.0-beta.38 |
|||
'@rolldown/binding-linux-x64-gnu': 1.0.0-beta.38 |
|||
'@rolldown/binding-linux-x64-musl': 1.0.0-beta.38 |
|||
'@rolldown/binding-openharmony-arm64': 1.0.0-beta.38 |
|||
'@rolldown/binding-wasm32-wasi': 1.0.0-beta.38 |
|||
'@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.38 |
|||
'@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.38 |
|||
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.38 |
|||
|
|||
[email protected]: |
|||
optionalDependencies: |
|||
@ -10939,8 +10941,8 @@ snapshots: |
|||
diff: 8.0.2 |
|||
empathic: 2.0.0 |
|||
hookable: 5.5.3 |
|||
rolldown: 1.0.0-beta.37 |
|||
rolldown-plugin-dts: 0.16.3([email protected]7)([email protected]) |
|||
rolldown: 1.0.0-beta.38 |
|||
rolldown-plugin-dts: 0.16.3([email protected]8)([email protected]) |
|||
semver: 7.7.2 |
|||
tinyexec: 1.0.1 |
|||
tinyglobby: 0.2.15 |
|||
@ -10967,6 +10969,8 @@ snapshots: |
|||
optionalDependencies: |
|||
fsevents: 2.3.3 |
|||
|
|||
[email protected]: {} |
|||
|
|||
[email protected]: {} |
|||
|
|||
[email protected]: {} |
|||
|
|||
Loading…
Reference in new issue