This commit is contained in:
Eduard Gert
2026-05-13 10:11:38 +02:00
parent c8e18585c6
commit 1c8a6e3798
24 changed files with 959 additions and 96 deletions

View File

@@ -0,0 +1,34 @@
import { ButtonHTMLAttributes, forwardRef } from "react";
import { generateColorFromString } from "@/lib/color";
import { cn } from "@/lib/cn";
type Props = ButtonHTMLAttributes<HTMLButtonElement> & {
name?: string;
size?: number;
};
export const Avatar = forwardRef<HTMLButtonElement, Props>(function Avatar(
{ name = "", size = 28, className, type = "button", ...props },
ref,
) {
const initial = (name.trim().charAt(0) || "?").toUpperCase();
const color = generateColorFromString(name);
return (
<button
ref={ref}
type={type}
className={cn(
"flex items-center justify-center rounded-full bg-nb-gray-900",
"text-xs font-semibold cursor-default outline-none",
"transition-colors duration-150 hover:bg-nb-gray-850",
"data-[state=open]:bg-nb-gray-850",
className,
)}
style={{ width: size, height: size, color }}
{...props}
>
{initial}
</button>
);
});

View File

@@ -0,0 +1,60 @@
import { ReactNode, useEffect } from "react";
import { createPortal } from "react-dom";
import { AnimatePresence, motion } from "framer-motion";
import { cn } from "@/lib/cn";
type Props = {
open: boolean;
onOpenChange: (open: boolean) => void;
children?: ReactNode;
className?: string;
};
export const BottomSheet = ({ open, onOpenChange, children, className }: Props) => {
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onOpenChange(false);
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open, onOpenChange]);
return createPortal(
<AnimatePresence>
{open && (
<div className={"fixed inset-0 z-50"}>
<motion.div
className={"absolute inset-0 bg-black/40 backdrop-blur-sm"}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.18, ease: "easeOut" }}
onClick={() => onOpenChange(false)}
/>
<motion.div
role={"dialog"}
aria-modal={"true"}
className={cn(
"absolute left-0 right-0 bottom-0",
"bg-nb-gray-925 border-t border-nb-gray-850 rounded-t-2xl",
"shadow-2xl outline-none",
"max-h-[85vh] overflow-hidden",
className,
)}
initial={{ y: "100%" }}
animate={{ y: 0 }}
exit={{ y: "100%" }}
transition={{ type: "spring", stiffness: 360, damping: 34 }}
>
<div className={"flex justify-center pt-2"}>
<div className={"h-1 w-10 rounded-full bg-nb-gray-700"} />
</div>
<div className={"px-5 pt-4 pb-6"}>{children}</div>
</motion.div>
</div>
)}
</AnimatePresence>,
document.body,
);
};

View File

@@ -91,7 +91,7 @@ export const buttonVariants = cva(
],
},
size: {
xs: "text-xs py-2 px-3.5",
xs: "text-xs py-2.5 px-3.5",
xs2: "text-[0.78rem] py-2 px-4",
sm: "text-sm py-[9px] px-4",
md: "text-md py-[9px] px-4",

View File

@@ -0,0 +1,76 @@
import * as RadioGroup from "@radix-ui/react-radio-group";
import { CheckIcon } from "lucide-react";
import { ReactNode } from "react";
import { cn } from "@/lib/cn";
type RootProps = {
value: string;
onChange: (value: string) => void;
children: ReactNode;
className?: string;
};
const Root = ({ value, onChange, children, className }: RootProps) => {
return (
<RadioGroup.Root
value={value}
onValueChange={onChange}
className={cn("grid grid-cols-2 gap-3", className)}
>
{children}
</RadioGroup.Root>
);
};
type OptionProps = {
value: string;
title: string;
description?: string;
preview?: ReactNode;
className?: string;
};
const Option = ({ value, title, description, preview, className }: OptionProps) => {
return (
<RadioGroup.Item
value={value}
className={cn(
"group relative flex flex-col items-stretch text-left rounded-lg",
"border border-nb-gray-850 bg-nb-gray-925 p-3 cursor-default outline-none",
"transition-colors duration-150",
"hover:border-nb-gray-800",
"data-[state=checked]:border-netbird data-[state=checked]:ring-1 data-[state=checked]:ring-netbird",
className,
)}
>
<span
className={cn(
"absolute top-2.5 right-2.5 flex h-4 w-4 items-center justify-center rounded-[4px]",
"border border-nb-gray-700 bg-nb-gray-900",
"group-data-[state=checked]:border-netbird group-data-[state=checked]:bg-netbird",
)}
>
<RadioGroup.Indicator className={"flex items-center justify-center"}>
<CheckIcon size={11} className={"text-white"} strokeWidth={3} />
</RadioGroup.Indicator>
</span>
<div
className={cn(
"h-48 -mx-3 -mt-3 mb-3 overflow-hidden",
"bg-gradient-to-b from-nb-gray-800/15 to-nb-gray",
"rounded-t-lg flex items-center justify-center",
)}
>
{preview}
</div>
<h3 className={"text-sm font-semibold text-nb-gray-100"}>{title}</h3>
{description && (
<p className={"text-[0.72rem] leading-snug text-nb-gray-400 mt-0.5"}>
{description}
</p>
)}
</RadioGroup.Item>
);
};
export const CardSelect = Object.assign(Root, { Option });

View File

@@ -12,6 +12,7 @@ const switchVariants = cva("", {
size: {
default: "h-[24px] w-[44px]",
small: "h-[18px] w-[36px]",
large: "h-[36px] w-[66px]",
},
variant: {
default: [
@@ -36,6 +37,7 @@ const switchVariants = cva("", {
"thumb-size": {
default: "h-5 w-5 data-[state=checked]:translate-x-5",
small: "h-[14px] w-[14px] data-[state=checked]:translate-x-[17px]",
large: "h-[28px] w-[28px] data-[state=checked]:translate-x-[30px]",
},
},
});