mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-20 15:49:55 +00:00
wip
This commit is contained in:
34
client/ui/frontend/src/components/Avatar.tsx
Normal file
34
client/ui/frontend/src/components/Avatar.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
60
client/ui/frontend/src/components/BottomSheet.tsx
Normal file
60
client/ui/frontend/src/components/BottomSheet.tsx
Normal 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,
|
||||
);
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
76
client/ui/frontend/src/components/CardSelect.tsx
Normal file
76
client/ui/frontend/src/components/CardSelect.tsx
Normal 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 });
|
||||
@@ -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]",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user