[management, reverse proxy] Add reverse proxy feature (#5291)

* implement reverse proxy


---------

Co-authored-by: Alisdair MacLeod <git@alisdairmacleod.co.uk>
Co-authored-by: mlsmaycon <mlsmaycon@gmail.com>
Co-authored-by: Eduard Gert <kontakt@eduardgert.de>
Co-authored-by: Viktor Liu <viktor@netbird.io>
Co-authored-by: Diego Noguês <diego.sure@gmail.com>
Co-authored-by: Diego Noguês <49420+diegocn@users.noreply.github.com>
Co-authored-by: Bethuel Mmbaga <bethuelmbaga12@gmail.com>
Co-authored-by: Zoltan Papp <zoltan.pmail@gmail.com>
Co-authored-by: Ashley Mensah <ashleyamo982@gmail.com>
This commit is contained in:
Pascal Fischer
2026-02-13 19:37:43 +01:00
committed by GitHub
parent edce11b34d
commit f53155562f
225 changed files with 35513 additions and 235 deletions

View File

@@ -0,0 +1,156 @@
import { cn } from "@/utils/helpers";
import { forwardRef } from "react";
type Variant =
| "default"
| "primary"
| "secondary"
| "secondaryLighter"
| "input"
| "dropdown"
| "dotted"
| "tertiary"
| "white"
| "outline"
| "danger-outline"
| "danger-text"
| "default-outline"
| "danger";
type Size = "xs" | "xs2" | "sm" | "md" | "lg";
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: Variant;
size?: Size;
rounded?: boolean;
border?: 0 | 1 | 2;
disabled?: boolean;
stopPropagation?: boolean;
}
const baseStyles = [
"relative cursor-pointer",
"text-sm focus:z-10 focus:ring-2 font-medium focus:outline-none whitespace-nowrap shadow-sm",
"inline-flex gap-2 items-center justify-center transition-colors focus:ring-offset-1",
"disabled:opacity-40 disabled:cursor-not-allowed disabled:text-nb-gray-300 ring-offset-neutral-950/50",
];
const variantStyles: Record<Variant, string[]> = {
default: [
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900",
"dark:focus:ring-zinc-800/50 dark:bg-nb-gray dark:text-gray-400 dark:border-gray-700/30 dark:hover:text-white dark:hover:bg-zinc-800/50",
],
primary: [
"dark:focus:ring-netbird-600/50 dark:ring-offset-neutral-950/50 enabled:dark:bg-netbird disabled:dark:bg-nb-gray-910 dark:text-gray-100 enabled:dark:hover:text-white enabled:dark:hover:bg-netbird-500/80",
"enabled:bg-netbird enabled:text-white enabled:focus:ring-netbird-400/50 enabled:hover:bg-netbird-500",
],
secondary: [
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900",
"dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20",
"dark:bg-nb-gray-920 dark:text-gray-400 dark:border-gray-700/40 dark:hover:text-white dark:hover:bg-nb-gray-910",
],
secondaryLighter: [
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900",
"dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20",
"dark:bg-nb-gray-900/70 dark:text-gray-400 dark:border-gray-700/70 dark:hover:text-white dark:hover:bg-nb-gray-800/60",
],
input: [
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-neutral-200 text-gray-900",
"dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20",
"dark:bg-nb-gray-900 dark:text-gray-400 dark:border-nb-gray-700 dark:hover:bg-nb-gray-900/80",
],
dropdown: [
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-neutral-200 text-gray-900",
"dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20",
"dark:bg-nb-gray-900/40 dark:text-gray-400 dark:border-nb-gray-900 dark:hover:bg-nb-gray-900/50",
],
dotted: [
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900 border-dashed",
"dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20",
"dark:bg-nb-gray-900/30 dark:text-gray-400 dark:border-gray-500/40 dark:hover:text-white dark:hover:bg-zinc-800/50",
],
tertiary: [
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900",
"dark:focus:ring-zinc-800/50 dark:bg-white dark:text-gray-800 dark:border-gray-700/40 dark:hover:bg-neutral-200 disabled:dark:bg-nb-gray-920 disabled:dark:text-nb-gray-300",
],
white: [
"focus:ring-white/50 bg-white text-gray-800 border-white outline-none hover:bg-neutral-200 disabled:dark:bg-nb-gray-920 disabled:dark:text-nb-gray-300",
"disabled:dark:bg-nb-gray-900 disabled:dark:text-nb-gray-300 disabled:dark:border-nb-gray-900",
],
outline: [
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900",
"dark:focus:ring-zinc-800/50 dark:bg-transparent dark:text-netbird dark:border-netbird dark:hover:bg-nb-gray-900/30",
],
"danger-outline": [
"enabled:dark:focus:ring-red-800/20 enabled:dark:focus:bg-red-950/40 enabled:hover:dark:bg-red-950/50 enabled:dark:hover:border-red-800/50 dark:bg-transparent dark:text-red-500",
],
"danger-text": [
"dark:bg-transparent dark:text-red-500 dark:hover:text-red-600 dark:border-transparent !px-0 !shadow-none !py-0 focus:ring-red-500/30 dark:ring-offset-neutral-950/50",
],
"default-outline": [
"dark:ring-offset-nb-gray-950/50 dark:focus:ring-nb-gray-500/20",
"dark:bg-transparent dark:text-nb-gray-400 dark:border-transparent dark:hover:text-white dark:hover:bg-nb-gray-900/30 dark:hover:border-nb-gray-800/50",
"data-[state=open]:dark:text-white data-[state=open]:dark:bg-nb-gray-900/30 data-[state=open]:dark:border-nb-gray-800/50",
],
danger: [
"dark:focus:ring-red-700/20 dark:focus:bg-red-700 hover:dark:bg-red-700 dark:hover:border-red-800/50 dark:bg-red-600 dark:text-red-100",
],
};
const sizeStyles: Record<Size, string> = {
xs: "text-xs py-2 px-4",
xs2: "text-[0.78rem] py-2 px-4",
sm: "text-sm py-2.5 px-4",
md: "text-sm py-2.5 px-4",
lg: "text-base py-2.5 px-4",
};
const borderStyles: Record<0 | 1 | 2, string> = {
0: "border",
1: "border border-transparent",
2: "border border-t-0 border-b-0",
};
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
variant = "default",
rounded = true,
border = 1,
size = "md",
stopPropagation = true,
className,
onClick,
children,
...props
},
ref
) => {
return (
<button
type="button"
{...props}
ref={ref}
className={cn(
baseStyles,
variantStyles[variant],
sizeStyles[size],
borderStyles[border ? 1 : 0],
rounded && "rounded-md",
className
)}
onClick={(e) => {
if (stopPropagation) e.stopPropagation();
onClick?.(e);
}}
>
{children}
</button>
);
}
);
Button.displayName = "Button";
export default Button;

View File

@@ -0,0 +1,23 @@
import { cn } from "@/utils/helpers";
import { GradientFadedBackground } from "@/components/GradientFadedBackground";
export const Card = ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => {
return (
<div
className={cn(
"px-6 sm:px-10 py-10 pt-8",
"bg-nb-gray-940 border border-nb-gray-910 rounded-lg relative",
className
)}
>
<GradientFadedBackground />
{children}
</div>
);
};

View File

@@ -0,0 +1,26 @@
import { X } from "lucide-react";
interface ConnectionLineProps {
success?: boolean;
}
export function ConnectionLine({ success = true }: Readonly<ConnectionLineProps>) {
if (success) {
return (
<div className="flex-1 flex items-center justify-center h-12 w-full px-5">
<div className="w-full border-t-2 border-dashed border-green-500" />
</div>
);
}
return (
<div className="flex-1 flex items-center justify-center h-12 min-w-10 px-5 relative">
<div className="w-full border-t-2 border-dashed border-nb-gray-900" />
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-8 h-8 rounded-full flex items-center justify-center">
<X size={18} className="text-netbird" />
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,14 @@
import { cn } from "@/utils/helpers";
type Props = {
children: React.ReactNode;
className?: string;
};
export function Description({ children, className }: Readonly<Props>) {
return (
<div className={cn("text-sm text-nb-gray-300 font-light mt-2 block text-center z-10 relative", className)}>
{children}
</div>
);
}

View File

@@ -0,0 +1,7 @@
export const ErrorMessage = ({ error }: { error?: string }) => {
return (
<div className="text-red-400 bg-red-800/20 border border-red-800/50 rounded-lg px-4 py-3 whitespace-break-spaces text-sm">
{error}
</div>
);
};

View File

@@ -0,0 +1,22 @@
import { cn } from "@/utils/helpers";
type Props = {
className?: string;
};
export const GradientFadedBackground = ({ className }: Props) => {
return (
<div
className={cn(
"h-full w-full absolute left-0 top-0 rounded-md overflow-hidden z-0 pointer-events-none",
className
)}
>
<div
className={
"bg-linear-to-b from-nb-gray-900/10 via-transparent to-transparent w-full h-full rounded-md"
}
></div>
</div>
);
};

View File

@@ -0,0 +1,19 @@
import { cn } from "@/utils/helpers";
interface HelpTextProps {
children?: React.ReactNode;
className?: string;
}
export default function HelpText({ children, className }: Readonly<HelpTextProps>) {
return (
<span
className={cn(
"text-[.8rem] text-nb-gray-300 block font-light tracking-wide",
className
)}
>
{children}
</span>
);
}

View File

@@ -0,0 +1,137 @@
import { cn } from "@/utils/helpers";
import { Eye, EyeOff } from "lucide-react";
import * as React from "react";
import { useState } from "react";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
customPrefix?: React.ReactNode;
customSuffix?: React.ReactNode;
maxWidthClass?: string;
icon?: React.ReactNode;
error?: string;
prefixClassName?: string;
showPasswordToggle?: boolean;
variant?: "default" | "darker";
}
const variantStyles = {
default: [
"bg-nb-gray-900 placeholder:text-neutral-400/70 border-nb-gray-700",
"ring-offset-neutral-950/50 focus-visible:ring-neutral-500/20",
],
darker: [
"bg-nb-gray-920 placeholder:text-neutral-400/70 border-nb-gray-800",
"ring-offset-neutral-950/50 focus-visible:ring-neutral-500/20",
],
error: [
"bg-nb-gray-900 placeholder:text-neutral-400/70 border-red-500 text-red-500",
"ring-offset-red-500/10 focus-visible:ring-red-500/10",
],
};
const prefixSuffixStyles = {
default: "bg-nb-gray-900 border-nb-gray-700 text-nb-gray-300",
error: "bg-nb-gray-900 border-red-500 text-nb-gray-300 text-red-500",
};
const Input = React.forwardRef<HTMLInputElement, InputProps>(
(
{
className,
type,
customSuffix,
customPrefix,
icon,
maxWidthClass = "",
error,
variant = "default",
prefixClassName,
showPasswordToggle = false,
...props
},
ref
) => {
const [showPassword, setShowPassword] = useState(false);
const isPasswordType = type === "password";
const inputType = isPasswordType && showPassword ? "text" : type;
const passwordToggle =
isPasswordType && showPasswordToggle ? (
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="hover:text-white transition-all"
aria-label="Toggle password visibility"
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
) : null;
const suffix = passwordToggle || customSuffix;
const activeVariant = error ? "error" : variant;
return (
<>
<div className={cn("flex relative h-[42px]", maxWidthClass)}>
{customPrefix && (
<div
className={cn(
prefixSuffixStyles[error ? "error" : "default"],
"flex h-[42px] w-auto rounded-l-md px-3 py-2 text-sm",
"border items-center whitespace-nowrap",
props.disabled && "opacity-40",
prefixClassName
)}
>
{customPrefix}
</div>
)}
<div
className={cn(
"absolute left-0 top-0 h-full flex items-center text-xs text-nb-gray-300 pl-3 leading-[0]",
props.disabled && "opacity-40"
)}
>
{icon}
</div>
<input
type={inputType}
ref={ref}
{...props}
className={cn(
variantStyles[activeVariant],
"flex h-[42px] w-full rounded-md px-3 py-2 text-sm",
"file:bg-transparent file:text-sm file:font-medium file:border-0",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
"disabled:cursor-not-allowed disabled:opacity-40",
"border",
customPrefix && "!border-l-0 !rounded-l-none",
suffix && "!pr-16",
icon && "!pl-10",
className
)}
/>
<div
className={cn(
"absolute right-0 top-0 h-full flex items-center text-xs text-nb-gray-300 pr-4 leading-[0] select-none",
props.disabled && "opacity-30"
)}
>
{suffix}
</div>
</div>
{error && (
<p className="text-xs text-red-500 mt-2">{error}</p>
)}
</>
);
}
);
Input.displayName = "Input";
export { Input };

View File

@@ -0,0 +1,19 @@
import { cn } from "@/utils/helpers";
type LabelProps = React.LabelHTMLAttributes<HTMLLabelElement>;
export function Label({ className, htmlFor, ...props }: Readonly<LabelProps>) {
return (
<label
htmlFor={htmlFor}
className={cn(
"text-sm font-medium tracking-wider leading-none",
"peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
"mb-2.5 inline-block text-nb-gray-200",
"flex items-center gap-2 select-none",
className
)}
{...props}
/>
);
}

View File

@@ -0,0 +1,46 @@
import { cn } from "@/utils/helpers";
import netbirdFull from "@/assets/netbird-full.svg";
import netbirdMark from "@/assets/netbird.svg";
type Props = {
size?: "small" | "default" | "large";
mobile?: boolean;
};
const sizes = {
small: {
desktop: 14,
mobile: 20,
},
default: {
desktop: 22,
mobile: 30,
},
large: {
desktop: 24,
mobile: 40,
},
};
export const NetBirdLogo = ({ size = "default", mobile = true }: Props) => {
return (
<>
<img
src={netbirdFull}
height={sizes[size].desktop}
style={{ height: sizes[size].desktop }}
alt="NetBird Logo"
className={cn(mobile && "hidden md:block", "group-hover:opacity-80 transition-all")}
/>
{mobile && (
<img
src={netbirdMark}
width={sizes[size].mobile}
style={{ width: sizes[size].mobile }}
alt="NetBird Logo"
className={cn(mobile && "md:hidden ml-4")}
/>
)}
</>
);
};

View File

@@ -0,0 +1,109 @@
import { cn } from "@/utils/helpers";
import React, {
useRef,
type KeyboardEvent,
type ClipboardEvent,
forwardRef,
useImperativeHandle,
} from "react";
export interface PinCodeInputRef {
focus: () => void;
}
interface Props {
value: string;
onChange: (value: string) => void;
length?: number;
disabled?: boolean;
className?: string;
autoFocus?: boolean;
}
const PinCodeInput = forwardRef<PinCodeInputRef, Readonly<Props>>(function PinCodeInput(
{ value, onChange, length = 6, disabled = false, className, autoFocus = false },
ref,
) {
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
useImperativeHandle(ref, () => ({
focus: () => {
inputRefs.current[0]?.focus();
},
}));
const digits = value.split("").concat(new Array(length).fill("")).slice(0, length);
const slotIds = Array.from({ length }, (_, i) => `pin-${i}`);
const handleChange = (index: number, digit: string) => {
if (!/^\d*$/.test(digit)) return;
const newDigits = [...digits];
newDigits[index] = digit.slice(-1);
const newValue = newDigits.join("").replaceAll(/\s/g, "");
onChange(newValue);
if (digit && index < length - 1) {
inputRefs.current[index + 1]?.focus();
}
};
const handleKeyDown = (index: number, e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Backspace" && !digits[index] && index > 0) {
inputRefs.current[index - 1]?.focus();
}
if (e.key === "ArrowLeft" && index > 0) {
inputRefs.current[index - 1]?.focus();
}
if (e.key === "ArrowRight" && index < length - 1) {
inputRefs.current[index + 1]?.focus();
}
};
const handlePaste = (e: ClipboardEvent<HTMLInputElement>) => {
e.preventDefault();
const pastedData = e.clipboardData.getData("text").replaceAll(/\D/g, "").slice(0, length);
onChange(pastedData);
const nextIndex = Math.min(pastedData.length, length - 1);
inputRefs.current[nextIndex]?.focus();
};
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
e.target.select();
};
return (
<div className={cn("flex gap-2 w-full min-w-0", className)}>
{digits.map((digit, index) => (
<input
key={slotIds[index]}
id={slotIds[index]}
ref={(el) => {
inputRefs.current[index] = el;
}}
type="text"
inputMode="numeric"
maxLength={1}
value={digit}
onChange={(e) => handleChange(index, e.target.value)}
onKeyDown={(e) => handleKeyDown(index, e)}
onPaste={handlePaste}
onFocus={handleFocus}
disabled={disabled}
autoFocus={autoFocus && index === 0}
className={cn(
"flex-1 min-w-0 h-[42px] text-center text-sm rounded-md",
"dark:bg-nb-gray-900 border dark:border-nb-gray-700",
"dark:placeholder:text-neutral-400/70",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
"ring-offset-neutral-200/20 dark:ring-offset-neutral-950/50 dark:focus-visible:ring-neutral-500/20",
"disabled:cursor-not-allowed disabled:opacity-40"
)}
/>
))}
</div>
);
});
export default PinCodeInput;

View File

@@ -0,0 +1,17 @@
import { NetBirdLogo } from "./NetBirdLogo";
export function PoweredByNetBird() {
return (
<a
href="https://netbird.io?utm_source=netbird-proxy&utm_medium=web&utm_campaign=powered_by"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center mt-8 gap-2 group cursor-pointer"
>
<span className="text-sm text-nb-gray-400 font-light text-center group-hover:opacity-80 transition-all">
Powered by
</span>
<NetBirdLogo size="small" mobile={false} />
</a>
);
}

View File

@@ -0,0 +1,145 @@
import { cn } from "@/utils/helpers";
import { useState, useMemo, useCallback } from "react";
import { TabContext, useTabContext } from "./TabContext";
type TabsProps = {
value?: string;
defaultValue?: string;
onChange?: (value: string) => void;
children:
| React.ReactNode
| ((context: { value: string; onChange: (value: string) => void }) => React.ReactNode);
};
function SegmentedTabs({ value, defaultValue, onChange, children }: Readonly<TabsProps>) {
const [internalValue, setInternalValue] = useState(defaultValue ?? "");
const currentValue = value ?? internalValue;
const handleChange = useCallback((newValue: string) => {
if (value === undefined) {
setInternalValue(newValue);
}
onChange?.(newValue);
}, [value, onChange]);
const contextValue = useMemo(
() => ({ value: currentValue, onChange: handleChange }),
[currentValue, handleChange],
);
return (
<TabContext.Provider value={contextValue}>
<div>
{typeof children === "function"
? children({ value: currentValue, onChange: handleChange })
: children}
</div>
</TabContext.Provider>
);
}
function List({
children,
className,
}: Readonly<{
children: React.ReactNode;
className?: string;
}>) {
return (
<div
role="tablist"
className={cn(
"bg-nb-gray-930/70 p-1.5 flex justify-center gap-1 border-nb-gray-900",
className
)}
>
{children}
</div>
);
}
function Trigger({
children,
value,
disabled = false,
className,
selected,
onClick,
}: Readonly<{
children: React.ReactNode;
value: string;
disabled?: boolean;
className?: string;
selected?: boolean;
onClick?: () => void;
}>) {
const context = useTabContext();
const isSelected = selected ?? value === context.value;
let stateClassName = "";
if (isSelected) {
stateClassName = "bg-nb-gray-900 text-white";
} else if (!disabled) {
stateClassName = "text-nb-gray-400 hover:bg-nb-gray-900/50";
}
const handleClick = () => {
context.onChange(value);
onClick?.();
};
return (
<button
role="tab"
type="button"
disabled={disabled}
aria-selected={isSelected}
onClick={handleClick}
className={cn(
"px-4 py-2 text-sm rounded-md w-full transition-all cursor-pointer",
disabled && "opacity-30 cursor-not-allowed",
stateClassName,
className
)}
>
<div className="flex items-center w-full justify-center gap-2">
{children}
</div>
</button>
);
}
function Content({
children,
value,
className,
visible,
}: Readonly<{
children: React.ReactNode;
value: string;
className?: string;
visible?: boolean;
}>) {
const context = useTabContext();
const isVisible = visible ?? value === context.value;
if (!isVisible) return null;
return (
<div
role="tabpanel"
className={cn(
"bg-nb-gray-930/70 px-4 pt-4 pb-5 rounded-b-md border border-t-0 border-nb-gray-900",
className
)}
>
{children}
</div>
);
}
SegmentedTabs.List = List;
SegmentedTabs.Trigger = Trigger;
SegmentedTabs.Content = Content;
export { SegmentedTabs };

View File

@@ -0,0 +1,10 @@
export const Separator = () => {
return (
<div className="flex items-center justify-center relative my-4">
<span className="bg-nb-gray-940 relative z-10 px-4 text-xs text-nb-gray-400 font-medium">
OR
</span>
<span className="h-px bg-nb-gray-900 w-full absolute z-0" />
</div>
);
};

View File

@@ -0,0 +1,38 @@
import type { LucideIcon } from "lucide-react";
import { ConnectionLine } from "./ConnectionLine";
interface StatusCardProps {
icon: LucideIcon;
label: string;
detail?: string;
success?: boolean;
line?: boolean;
}
export function StatusCard({
icon: Icon,
label,
detail,
success = true,
line = true,
}: Readonly<StatusCardProps>) {
return (
<>
{line && <ConnectionLine success={success} />}
<div className="flex flex-col items-center gap-2">
<div className="w-14 h-14 rounded-md flex items-center justify-center from-nb-gray-940 to-nb-gray-930/70 bg-gradient-to-br border border-nb-gray-910">
<Icon size={20} className="text-nb-gray-200" />
</div>
<span className="text-sm text-nb-gray-200 font-normal mt-1">{label}</span>
<span className={`text-xs font-medium uppercase ${success ? "text-green-500" : "text-netbird"}`}>
{success ? "Connected" : "Unreachable"}
</span>
{detail && (
<span className="text-xs text-nb-gray-400 truncate text-center">
{detail}
</span>
)}
</div>
</>
);
}

View File

@@ -0,0 +1,13 @@
import { createContext, useContext } from "react";
type TabContextValue = {
value: string;
onChange: (value: string) => void;
};
export const TabContext = createContext<TabContextValue>({
value: "",
onChange: () => {},
});
export const useTabContext = () => useContext(TabContext);

View File

@@ -0,0 +1,14 @@
import { cn } from "@/utils/helpers";
type Props = {
children: React.ReactNode;
className?: string;
};
export function Title({ children, className }: Readonly<Props>) {
return (
<h1 className={cn("text-xl! text-center z-10 relative", className)}>
{children}
</h1>
);
}