Updated a lot of stuff

This commit is contained in:
Alois 2026-03-14 19:36:23 +01:00
commit c0102df54f
44 changed files with 1610 additions and 707 deletions

View file

@ -0,0 +1,107 @@
import * as React from "react"
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"
import { cn } from "../lib/utils"
function Avatar({
className,
size = "default",
...props
}: AvatarPrimitive.Root.Props & {
size?: "default" | "sm" | "lg"
}) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
data-size={size}
className={cn(
"group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten",
className
)}
{...props}
/>
)
}
function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn(
"aspect-square size-full rounded-full object-cover",
className
)}
{...props}
/>
)
}
function AvatarFallback({
className,
...props
}: AvatarPrimitive.Fallback.Props) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs",
className
)}
{...props}
/>
)
}
function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="avatar-badge"
className={cn(
"absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none",
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
className
)}
{...props}
/>
)
}
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="avatar-group"
className={cn(
"group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
className
)}
{...props}
/>
)
}
function AvatarGroupCount({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="avatar-group-count"
className={cn(
"relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-xs/relaxed text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
className
)}
{...props}
/>
)
}
export {
Avatar,
AvatarImage,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
AvatarBadge,
}

View file

@ -0,0 +1,56 @@
import { Button as ButtonPrimitive } from "@base-ui/react/button"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "../lib/utils"
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-xs/relaxed font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/80",
outline:
"border-border hover:bg-input/50 hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:bg-input/30",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default:
"h-7 gap-1 px-2 text-xs/relaxed has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
xs: "h-5 gap-1 rounded-sm px-2 text-[0.625rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-2.5",
sm: "h-6 gap-1 px-2 text-xs/relaxed has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
lg: "h-8 gap-1 px-2.5 text-xs/relaxed has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-4",
icon: "size-7 [&_svg:not([class*='size-'])]:size-3.5",
"icon-xs": "size-5 rounded-sm [&_svg:not([class*='size-'])]:size-2.5",
"icon-sm": "size-6 [&_svg:not([class*='size-'])]:size-3",
"icon-lg": "size-8 [&_svg:not([class*='size-'])]:size-4",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant = "default",
size = "default",
...props
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
return (
<ButtonPrimitive
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View file

@ -0,0 +1,100 @@
import * as React from "react"
import { cn } from "../lib/utils"
function Card({
className,
size = "default",
...props
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
return (
<div
data-slot="card"
data-size={size}
className={cn(
"group/card flex flex-col gap-4 overflow-hidden rounded-lg bg-card py-4 text-xs/relaxed text-card-foreground ring-1 ring-foreground/10 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-lg px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("text-sm font-medium", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-xs/relaxed text-muted-foreground", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn(
"flex items-center rounded-b-lg px-4 group-data-[size=sm]/card:px-3 [.border-t]:pt-4 group-data-[size=sm]/card:[.border-t]:pt-3",
className
)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View file

@ -0,0 +1,27 @@
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
import { cn } from "../lib/utils"
import { CheckIcon } from "lucide-react"
function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input transition-shadow outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
>
<CheckIcon
/>
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

View file

@ -0,0 +1,269 @@
import * as React from "react"
import { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context-menu"
import { cn } from "../lib/utils"
import { ChevronRightIcon, CheckIcon } from "lucide-react"
function ContextMenu({ ...props }: ContextMenuPrimitive.Root.Props) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
}
function ContextMenuPortal({ ...props }: ContextMenuPrimitive.Portal.Props) {
return (
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
)
}
function ContextMenuTrigger({
className,
...props
}: ContextMenuPrimitive.Trigger.Props) {
return (
<ContextMenuPrimitive.Trigger
data-slot="context-menu-trigger"
className={cn("select-none", className)}
{...props}
/>
)
}
function ContextMenuContent({
className,
align = "start",
alignOffset = 4,
side = "right",
sideOffset = 0,
...props
}: ContextMenuPrimitive.Popup.Props &
Pick<
ContextMenuPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset"
>) {
return (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Positioner
className="isolate z-50 outline-none"
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
>
<ContextMenuPrimitive.Popup
data-slot="context-menu-content"
className={cn("z-50 max-h-(--available-height) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-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 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 animate-none! relative bg-popover/70 before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:focus:bg-foreground/10! **:data-[variant=destructive]:text-accent-foreground! **:data-[variant=destructive]:**:text-accent-foreground!", className )}
{...props}
/>
</ContextMenuPrimitive.Positioner>
</ContextMenuPrimitive.Portal>
)
}
function ContextMenuGroup({ ...props }: ContextMenuPrimitive.Group.Props) {
return (
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
)
}
function ContextMenuLabel({
className,
inset,
...props
}: ContextMenuPrimitive.GroupLabel.Props & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.GroupLabel
data-slot="context-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-xs text-muted-foreground data-inset:pl-7.5",
className
)}
{...props}
/>
)
}
function ContextMenuItem({
className,
inset,
variant = "default",
...props
}: ContextMenuPrimitive.Item.Props & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<ContextMenuPrimitive.Item
data-slot="context-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"group/context-menu-item relative flex min-h-7 cursor-default items-center gap-2 rounded-md px-2 py-1 text-xs/relaxed outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7.5 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5 data-[variant=destructive]:*:[svg]:text-destructive",
className
)}
{...props}
/>
)
}
function ContextMenuSub({ ...props }: ContextMenuPrimitive.SubmenuRoot.Props) {
return (
<ContextMenuPrimitive.SubmenuRoot data-slot="context-menu-sub" {...props} />
)
}
function ContextMenuSubTrigger({
className,
inset,
children,
...props
}: ContextMenuPrimitive.SubmenuTrigger.Props & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.SubmenuTrigger
data-slot="context-menu-sub-trigger"
data-inset={inset}
className={cn(
"flex min-h-7 cursor-default items-center gap-2 rounded-md px-2 py-1 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7.5 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</ContextMenuPrimitive.SubmenuTrigger>
)
}
function ContextMenuSubContent({
...props
}: React.ComponentProps<typeof ContextMenuContent>) {
return (
<ContextMenuContent
data-slot="context-menu-sub-content"
className="shadow-lg animate-none! relative bg-popover/70 before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:focus:bg-foreground/10! **:data-[variant=destructive]:text-accent-foreground! **:data-[variant=destructive]:**:text-accent-foreground!"
side="right"
{...props}
/>
)
}
function ContextMenuCheckboxItem({
className,
children,
checked,
inset,
...props
}: ContextMenuPrimitive.CheckboxItem.Props & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item"
data-inset={inset}
className={cn(
"relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7.5 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute right-2 flex items-center justify-center">
<ContextMenuPrimitive.CheckboxItemIndicator>
<CheckIcon
/>
</ContextMenuPrimitive.CheckboxItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
)
}
function ContextMenuRadioGroup({
...props
}: ContextMenuPrimitive.RadioGroup.Props) {
return (
<ContextMenuPrimitive.RadioGroup
data-slot="context-menu-radio-group"
{...props}
/>
)
}
function ContextMenuRadioItem({
className,
children,
inset,
...props
}: ContextMenuPrimitive.RadioItem.Props & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item"
data-inset={inset}
className={cn(
"relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7.5 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
className
)}
{...props}
>
<span className="pointer-events-none absolute right-2 flex items-center justify-center">
<ContextMenuPrimitive.RadioItemIndicator>
<CheckIcon
/>
</ContextMenuPrimitive.RadioItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
)
}
function ContextMenuSeparator({
className,
...props
}: ContextMenuPrimitive.Separator.Props) {
return (
<ContextMenuPrimitive.Separator
data-slot="context-menu-separator"
className={cn("-mx-1 my-1 h-px bg-border/50", className)}
{...props}
/>
)
}
function ContextMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="context-menu-shortcut"
className={cn(
"ml-auto text-[0.625rem] tracking-widest text-muted-foreground group-focus/context-menu-item:text-accent-foreground",
className
)}
{...props}
/>
)
}
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}

View file

@ -0,0 +1,20 @@
import * as React from "react"
import { Input as InputPrimitive } from "@base-ui/react/input"
import { cn } from "../lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<InputPrimitive
type={type}
data-slot="input"
className={cn(
"h-7 w-full min-w-0 rounded-md border border-input bg-input/20 px-2 py-0.5 text-sm transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-xs/relaxed file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 md:text-xs/relaxed dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Input }

View file

@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "../lib/utils"
function Label({ className, ...props }: React.ComponentProps<"label">) {
return (
<label
data-slot="label"
className={cn(
"flex items-center gap-2 text-xs/relaxed leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

View file

@ -0,0 +1,47 @@
import { useTheme } from "next-themes"
import { Toaster as Sonner, type ToasterProps } from "sonner"
import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: (
<CircleCheckIcon className="size-4" />
),
info: (
<InfoIcon className="size-4" />
),
warning: (
<TriangleAlertIcon className="size-4" />
),
error: (
<OctagonXIcon className="size-4" />
),
loading: (
<Loader2Icon className="size-4 animate-spin" />
),
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
} as React.CSSProperties
}
toastOptions={{
classNames: {
toast: "cn-toast",
},
}}
{...props}
/>
)
}
export { Toaster }

129
packages/ui/src/index.css Normal file
View file

@ -0,0 +1,129 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@import "@fontsource-variable/inter";
@custom-variant dark (&:is(.dark *));
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.148 0.004 228.8);
--card: oklch(1 0 0);
--card-foreground: oklch(0.148 0.004 228.8);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.148 0.004 228.8);
--primary: oklch(0.511 0.096 186.391);
--primary-foreground: oklch(0.984 0.014 180.72);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.963 0.002 197.1);
--muted-foreground: oklch(0.56 0.021 213.5);
--accent: oklch(0.963 0.002 197.1);
--accent-foreground: oklch(0.218 0.008 223.9);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.925 0.005 214.3);
--input: oklch(0.925 0.005 214.3);
--ring: oklch(0.723 0.014 214.4);
--chart-1: oklch(0.855 0.138 181.071);
--chart-2: oklch(0.704 0.14 182.503);
--chart-3: oklch(0.6 0.118 184.704);
--chart-4: oklch(0.511 0.096 186.391);
--chart-5: oklch(0.437 0.078 188.216);
--radius: 0.625rem;
--sidebar: oklch(0.987 0.002 197.1);
--sidebar-foreground: oklch(0.148 0.004 228.8);
--sidebar-primary: oklch(0.6 0.118 184.704);
--sidebar-primary-foreground: oklch(0.984 0.014 180.72);
--sidebar-accent: oklch(0.963 0.002 197.1);
--sidebar-accent-foreground: oklch(0.218 0.008 223.9);
--sidebar-border: oklch(0.925 0.005 214.3);
--sidebar-ring: oklch(0.723 0.014 214.4);
}
.dark {
--background: oklch(0.148 0.004 228.8);
--foreground: oklch(0.987 0.002 197.1);
--card: oklch(0.218 0.008 223.9);
--card-foreground: oklch(0.987 0.002 197.1);
--popover: oklch(0.218 0.008 223.9);
--popover-foreground: oklch(0.987 0.002 197.1);
--primary: oklch(0.437 0.078 188.216);
--primary-foreground: oklch(0.984 0.014 180.72);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.275 0.011 216.9);
--muted-foreground: oklch(0.723 0.014 214.4);
--accent: oklch(0.275 0.011 216.9);
--accent-foreground: oklch(0.987 0.002 197.1);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.56 0.021 213.5);
--chart-1: oklch(0.855 0.138 181.071);
--chart-2: oklch(0.704 0.14 182.503);
--chart-3: oklch(0.6 0.118 184.704);
--chart-4: oklch(0.511 0.096 186.391);
--chart-5: oklch(0.437 0.078 188.216);
--sidebar: oklch(0.218 0.008 223.9);
--sidebar-foreground: oklch(0.987 0.002 197.1);
--sidebar-primary: oklch(0.704 0.14 182.503);
--sidebar-primary-foreground: oklch(0.277 0.046 192.524);
--sidebar-accent: oklch(0.275 0.011 216.9);
--sidebar-accent-foreground: oklch(0.987 0.002 197.1);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.56 0.021 213.5);
}
@theme inline {
--font-sans: 'Inter Variable', sans-serif;
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--color-foreground: var(--foreground);
--color-background: var(--background);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
html {
@apply font-sans;
}
}

View file

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View file

@ -1,5 +0,0 @@
import type { ClassValue } from "clsx";
import clsx from "clsx";
import { twMerge } from "tailwind-merge";
export const cn = (...classLists: ClassValue[]) => twMerge(clsx(classLists));

View file

@ -3,8 +3,8 @@ import Link from "../link";
export default function Screen(props: { error: string; description: string }) {
return (
<div className="bg-background w-full h-screen flex flex-col justify-center items-center">
<p className="text-3xl font-bold">{props.error}</p>
<p className="pt-4 text-lg w-1/2 text-center text-muted-foreground pb-8">
<p className="text-base font-semibold">{props.error}</p>
<p className="pt-4 text-sm w-1/2 text-center text-muted-foreground pb-8">
{props.description}
</p>
<Link label="status.tensamin.net" link="https://status.tensamin.net" />

View file

@ -2,7 +2,14 @@ import * as React from "react";
const DELAY = 250;
export default function Screen(props: { progress: number }) {
type ScreenProps = {
progress: number;
title?: string;
description?: string;
fullscreen?: boolean;
};
export default function Screen(props: ScreenProps) {
const [displayProgress, setDisplayProgress] = React.useState(0);
const displayProgressRef = React.useRef(0);
@ -41,8 +48,24 @@ export default function Screen(props: { progress: number }) {
};
}, [props.progress]);
const wrapperClass = props.fullscreen
? "fixed inset-0 z-50 bg-background"
: "bg-background w-full h-screen";
return (
<div className="bg-background w-full h-screen flex flex-col justify-center items-center">
<div
className={`${wrapperClass} flex flex-col justify-center items-center px-6`}
>
<h2 className="text-base font-semibold text-foreground">
{props.title ?? "Loading"}
</h2>
{props.description ? (
<p className="text-sm text-muted-foreground mt-1 mb-5">
{props.description}
</p>
) : (
<div className="mb-5" />
)}
<div className="w-64 h-1.5 bg-secondary rounded-full overflow-hidden relative">
<div
className="h-full bg-primary absolute left-0 top-0"

229
packages/ui/src/theme.tsx Normal file
View file

@ -0,0 +1,229 @@
import * as React from "react"
type Theme = "dark" | "light" | "system"
type ResolvedTheme = "dark" | "light"
type ThemeProviderProps = {
children: React.ReactNode
defaultTheme?: Theme
storageKey?: string
disableTransitionOnChange?: boolean
}
type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}
const COLOR_SCHEME_QUERY = "(prefers-color-scheme: dark)"
const THEME_VALUES: Theme[] = ["dark", "light", "system"]
const ThemeProviderContext = React.createContext<
ThemeProviderState | undefined
>(undefined)
function isTheme(value: string | null): value is Theme {
if (value === null) {
return false
}
return THEME_VALUES.includes(value as Theme)
}
function getSystemTheme(): ResolvedTheme {
if (window.matchMedia(COLOR_SCHEME_QUERY).matches) {
return "dark"
}
return "light"
}
function disableTransitionsTemporarily() {
const style = document.createElement("style")
style.appendChild(
document.createTextNode(
"*,*::before,*::after{-webkit-transition:none!important;transition:none!important}"
)
)
document.head.appendChild(style)
return () => {
window.getComputedStyle(document.body)
requestAnimationFrame(() => {
requestAnimationFrame(() => {
style.remove()
})
})
}
}
function isEditableTarget(target: EventTarget | null) {
if (!(target instanceof HTMLElement)) {
return false
}
if (target.isContentEditable) {
return true
}
const editableParent = target.closest(
"input, textarea, select, [contenteditable='true']"
)
if (editableParent) {
return true
}
return false
}
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "theme",
disableTransitionOnChange = true,
...props
}: ThemeProviderProps) {
const [theme, setThemeState] = React.useState<Theme>(() => {
const storedTheme = localStorage.getItem(storageKey)
if (isTheme(storedTheme)) {
return storedTheme
}
return defaultTheme
})
const setTheme = React.useCallback(
(nextTheme: Theme) => {
localStorage.setItem(storageKey, nextTheme)
setThemeState(nextTheme)
},
[storageKey]
)
const applyTheme = React.useCallback(
(nextTheme: Theme) => {
const root = document.documentElement
const resolvedTheme =
nextTheme === "system" ? getSystemTheme() : nextTheme
const restoreTransitions = disableTransitionOnChange
? disableTransitionsTemporarily()
: null
root.classList.remove("light", "dark")
root.classList.add(resolvedTheme)
if (restoreTransitions) {
restoreTransitions()
}
},
[disableTransitionOnChange]
)
React.useEffect(() => {
applyTheme(theme)
if (theme !== "system") {
return undefined
}
const mediaQuery = window.matchMedia(COLOR_SCHEME_QUERY)
const handleChange = () => {
applyTheme("system")
}
mediaQuery.addEventListener("change", handleChange)
return () => {
mediaQuery.removeEventListener("change", handleChange)
}
}, [theme, applyTheme])
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.repeat) {
return
}
if (event.metaKey || event.ctrlKey || event.altKey) {
return
}
if (isEditableTarget(event.target)) {
return
}
if (event.key.toLowerCase() !== "d") {
return
}
setThemeState((currentTheme) => {
const nextTheme =
currentTheme === "dark"
? "light"
: currentTheme === "light"
? "dark"
: getSystemTheme() === "dark"
? "light"
: "dark"
localStorage.setItem(storageKey, nextTheme)
return nextTheme
})
}
window.addEventListener("keydown", handleKeyDown)
return () => {
window.removeEventListener("keydown", handleKeyDown)
}
}, [storageKey])
React.useEffect(() => {
const handleStorageChange = (event: StorageEvent) => {
if (event.storageArea !== localStorage) {
return
}
if (event.key !== storageKey) {
return
}
if (isTheme(event.newValue)) {
setThemeState(event.newValue)
return
}
setThemeState(defaultTheme)
}
window.addEventListener("storage", handleStorageChange)
return () => {
window.removeEventListener("storage", handleStorageChange)
}
}, [defaultTheme, storageKey])
const value = React.useMemo(
() => ({
theme,
setTheme,
}),
[theme, setTheme]
)
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
)
}
export const useTheme = () => {
const context = React.useContext(ThemeProviderContext)
if (context === undefined) {
throw new Error("useTheme must be used within a ThemeProvider")
}
return context
}

View file

@ -1,20 +0,0 @@
import * as React from "react";
export default function Avatar(props: { img?: string; fallback: string }) {
const [imageFailed, setImageFailed] = React.useState(false);
return (
<div className="m-0 bg-card rounded-full border border-input/75 size-9 aspect-square flex items-center justify-center select-none overflow-hidden">
{props.img && !imageFailed ? (
<img
className="rounded-full w-full h-full object-cover"
src={props.img}
alt={props.fallback}
onError={() => setImageFailed(true)}
/>
) : (
<span>{props.fallback}</span>
)}
</div>
);
}

View file

@ -1,53 +0,0 @@
import { cn } from "../libs/cn";
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import * as React from "react";
export const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-[color,background-color,box-shadow] focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border bg-card shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
VariantProps<typeof buttonVariants> & {
className?: string;
};
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => {
return (
<button
ref={ref}
className={cn(buttonVariants({ variant, size }), className)}
{...props}
/>
);
},
);
Button.displayName = "Button";

View file

@ -1,35 +0,0 @@
import { cn } from "../libs/cn";
import * as React from "react";
export const Card = ({ className, ...props }: React.ComponentProps<"div">) => (
<div
className={cn("rounded-xl border bg-card text-card-foreground shadow", className)}
{...props}
/>
);
export const CardHeader = ({ className, ...props }: React.ComponentProps<"div">) => (
<div className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
);
export const CardTitle = ({ className, ...props }: React.ComponentProps<"h1">) => (
<h1
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
);
export const CardDescription = ({
className,
...props
}: React.ComponentProps<"h3">) => (
<h3 className={cn("text-sm text-muted-foreground", className)} {...props} />
);
export const CardContent = ({ className, ...props }: React.ComponentProps<"div">) => (
<div className={cn("p-6 pt-0", className)} {...props} />
);
export const CardFooter = ({ className, ...props }: React.ComponentProps<"div">) => (
<div className={cn("flex items-center p-6 pt-0", className)} {...props} />
);

View file

@ -1,104 +0,0 @@
import * as React from "react";
import { cn } from "../libs/cn";
type CheckboxContextValue = {
checked: boolean;
disabled?: boolean;
};
const CheckboxContext = React.createContext<CheckboxContextValue | null>(null);
export type CheckboxProps = {
checked: boolean;
onChange: (checked: boolean) => void;
disabled?: boolean;
className?: string;
children?: React.ReactNode;
};
export function Checkbox({
checked,
onChange,
disabled,
className,
children,
}: CheckboxProps) {
return (
<CheckboxContext.Provider value={{ checked, disabled }}>
<label className={cn("inline-flex items-center", className)}>
<input
type="checkbox"
className="sr-only peer"
checked={checked}
disabled={disabled}
onChange={(event) => onChange(event.currentTarget.checked)}
/>
{children}
</label>
</CheckboxContext.Provider>
);
}
export function CheckboxControl({
className,
}: {
className?: string;
}): React.ReactElement {
const context = React.useContext(CheckboxContext);
return (
<span
className={cn(
"h-4 w-4 shrink-0 rounded-sm border border-primary shadow transition-shadow peer-focus-visible:ring-[1.5px] peer-focus-visible:ring-ring peer-disabled:cursor-not-allowed",
context?.checked && "bg-primary text-primary-foreground",
context?.disabled && "opacity-50",
className,
)}
>
{context?.checked ? (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
className="h-4 w-4"
>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="m5 12l5 5L20 7"
/>
</svg>
) : null}
</span>
);
}
export function CheckboxLabel({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
className={cn("text-sm leading-none font-medium", className)}
{...props}
/>
);
}
export function CheckboxErrorMessage({
className,
...props
}: React.ComponentProps<"p">) {
return <p className={cn("text-sm text-destructive", className)} {...props} />;
}
export function CheckboxDescription({
className,
...props
}: React.ComponentProps<"p">) {
return (
<p className={cn("text-sm text-muted-foreground", className)} {...props} />
);
}

View file

@ -1,182 +0,0 @@
import * as React from "react";
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import { cn } from "../libs/cn";
export const ContextMenu = ContextMenuPrimitive.Root;
export const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
export const ContextMenuGroup = ContextMenuPrimitive.Group;
export const ContextMenuSub = ContextMenuPrimitive.Sub;
export const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
export const ContextMenuSubTrigger = React.forwardRef<
HTMLDivElement,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
>
{children}
</ContextMenuPrimitive.SubTrigger>
));
ContextMenuSubTrigger.displayName = "ContextMenuSubTrigger";
export const ContextMenuSubContent = React.forwardRef<
HTMLDivElement,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg",
className,
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
));
ContextMenuSubContent.displayName = "ContextMenuSubContent";
export const ContextMenuContent = React.forwardRef<
HTMLDivElement,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
className,
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
));
ContextMenuContent.displayName = "ContextMenuContent";
export const ContextMenuItem = React.forwardRef<
HTMLDivElement,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
/>
));
ContextMenuItem.displayName = "ContextMenuItem";
export const ContextMenuCheckboxItem = React.forwardRef<
HTMLDivElement,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground",
className,
)}
{...props}
>
{children}
</ContextMenuPrimitive.CheckboxItem>
));
ContextMenuCheckboxItem.displayName = "ContextMenuCheckboxItem";
export const ContextMenuRadioItem = React.forwardRef<
HTMLDivElement,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground",
className,
)}
{...props}
>
{children}
</ContextMenuPrimitive.RadioItem>
));
ContextMenuRadioItem.displayName = "ContextMenuRadioItem";
export const ContextMenuItemLabel = React.forwardRef<
HTMLDivElement,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold text-foreground",
inset && "pl-8",
className,
)}
{...props}
/>
));
ContextMenuItemLabel.displayName = "ContextMenuItemLabel";
export const ContextMenuGroupLabel = React.forwardRef<
HTMLDivElement,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold text-foreground",
inset && "pl-8",
className,
)}
{...props}
/>
));
ContextMenuGroupLabel.displayName = "ContextMenuGroupLabel";
export const ContextMenuSeparator = React.forwardRef<
HTMLDivElement,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
));
ContextMenuSeparator.displayName = "ContextMenuSeparator";
export const ContextMenuShortcut = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
{...props}
/>
);

View file

@ -1,30 +0,0 @@
import * as React from "react";
import { cn } from "../libs/cn";
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
type?: string;
}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
ref={ref}
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 text-foreground 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-input focus-visible:dark:bg-input/50 transition-all duration-200 ease-in-out",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className,
)}
{...props}
/>
);
},
);
Input.displayName = "Input";
export { Input };

View file

@ -1,23 +0,0 @@
import { cn } from "../libs/cn";
import * as React from "react";
const Label = React.forwardRef<
HTMLLabelElement,
React.LabelHTMLAttributes<HTMLLabelElement>
>(({ className, ...props }, ref) => {
return (
<label
ref={ref}
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 transition-all duration-200 ease-in-out",
className,
)}
{...props}
/>
);
});
Label.displayName = "Label";
export { Label };

View file

@ -1,5 +0,0 @@
import { Toaster as Sonner } from "sonner";
export const Toaster = (props: Parameters<typeof Sonner>[0]) => {
return <Sonner className="toaster group" {...props} />;
};