add default and advanced resize

This commit is contained in:
Eduard Gert
2026-05-22 09:53:08 +02:00
parent 8d4f35352f
commit 580cfa0dc5
10 changed files with 105 additions and 120 deletions

View File

@@ -2,13 +2,7 @@ import { forwardRef, type ComponentType, type HTMLAttributes } from "react";
import type { LucideProps } from "lucide-react";
import { cn } from "@/lib/cn";
export type BadgeVariant =
| "info"
| "neutral"
| "brand"
| "success"
| "warning"
| "danger";
export type BadgeVariant = "info" | "neutral" | "brand" | "success" | "warning" | "danger";
type Props = HTMLAttributes<HTMLSpanElement> & {
/** Visual color scheme. Defaults to `info` (sky), used as the
@@ -34,22 +28,15 @@ const VARIANT_CLASSES: Record<BadgeVariant, string> = {
// lets the small text sit flush in the pill without the line-height padding
// inflating it.
export const Badge = forwardRef<HTMLSpanElement, Props>(function Badge(
{
variant = "info",
icon: Icon,
iconSize = 10,
className,
children,
...rest
},
{ variant = "info", icon: Icon, iconSize = 10, className, children, ...rest },
ref,
) {
return (
<span
ref={ref}
className={cn(
"relative top-px inline-flex items-center gap-1 rounded-full px-2 py-[0.2rem]",
"text-[0.65rem] leading-none font-semibold shrink-0",
"relative top-px inline-flex items-center gap-1 rounded-full px-1.5 py-[0.15rem]",
"text-[0.64rem] leading-none font-semibold shrink-0",
VARIANT_CLASSES[variant],
className,
)}

View File

@@ -19,7 +19,7 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButt
className={cn(
"h-10 w-10 flex items-center justify-center rounded-lg cursor-default outline-none",
"text-nb-gray-400 hover:text-nb-gray-300 hover:bg-nb-gray-900",
"transition-colors duration-150",
"transition-colors duration-150 wails-no-draggable",
className,
)}
{...props}

View File

@@ -81,7 +81,7 @@ export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => {
return (
<>
<Popover.Root open={open} onOpenChange={setOpen}>
<Popover.Trigger asChild>
<Popover.Trigger asChild className={"wails-no-draggable"}>
<ProfileTriggerButton name={displayName} />
</Popover.Trigger>
<Popover.Portal>
@@ -91,7 +91,7 @@ export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => {
collisionPadding={12}
onOpenAutoFocus={(e) => e.preventDefault()}
className={cn(
"z-50 min-w-64 overflow-hidden rounded-lg border border-nb-gray-900 bg-nb-gray-935 p-1 text-nb-gray-200 shadow-lg",
"z-50 min-w-64 overflow-hidden rounded-lg border border-nb-gray-900 bg-nb-gray-935 p-1 text-nb-gray-200 shadow-lg wails-no-draggable",
"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",
@@ -186,19 +186,19 @@ const ProfileTriggerButton = forwardRef<HTMLButtonElement, ProfileTriggerButtonP
ref={ref}
type="button"
className={cn(
"h-10 flex items-center gap-2 px-3 rounded-lg outline-none cursor-default",
"h-10 flex items-center gap-2 px-3 rounded-lg outline-none cursor-default wails-no-draggable",
"text-nb-gray-200 hover:bg-nb-gray-900",
"data-[state=open]:bg-nb-gray-900",
"transition-colors duration-150",
"transition-colors duration-150 wails-no-draggable",
className,
)}
{...props}
>
<Icon size={16} className={"text-nb-gray-200 shrink-0"} />
<span className={"text-sm font-medium truncate max-w-[140px]"}>
<Icon size={16} className={"text-nb-gray-200 shrink-0 wails-no-draggable"} />
<span className={"text-sm font-medium truncate max-w-[140px] wails-no-draggable"}>
{name}
</span>
<ChevronDown size={14} className={"text-nb-gray-200 shrink-0"} />
<ChevronDown size={14} className={"text-nb-gray-200 shrink-0 wails-no-draggable"} />
</button>
);
},

View File

@@ -8,72 +8,67 @@ import { cn } from "@/lib/cn";
type SwitchVariants = VariantProps<typeof switchVariants>;
const switchVariants = cva("", {
variants: {
size: {
default: "h-[24px] w-[44px]",
small: "h-[18px] w-[36px]",
large: "h-[36px] w-[66px]",
variants: {
size: {
default: "h-[24px] w-[44px]",
small: "h-[18px] w-[36px]",
large: "h-[36px] w-[66px]",
},
variant: {
default: [
"dark:data-[state=checked]:bg-netbird dark:data-[state=unchecked]:bg-nb-gray-700",
"dark:data-[state=checked]:hover:bg-netbird-500 dark:data-[state=unchecked]:hover:bg-nb-gray-600",
"data-[state=checked]:bg-neutral-900 data-[state=unchecked]:bg-neutral-200",
"data-[state=checked]:hover:bg-neutral-800 data-[state=unchecked]:hover:bg-neutral-300",
],
"red-green": [
"dark:data-[state=checked]:bg-red-600 dark:data-[state=unchecked]:bg-nb-gray-700",
"dark:data-[state=checked]:hover:bg-red-500 dark:data-[state=unchecked]:hover:bg-nb-gray-600",
"data-[state=checked]:bg-red-500 data-[state=unchecked]:bg-red-200",
"data-[state=checked]:hover:bg-red-400 data-[state=unchecked]:hover:bg-red-300",
],
red: [
"dark:data-[state=checked]:bg-red-600 dark:data-[state=unchecked]:bg-nb-gray-700",
"dark:data-[state=checked]:hover:bg-red-500 dark:data-[state=unchecked]:hover:bg-nb-gray-600",
"data-[state=checked]:bg-red-500 data-[state=unchecked]:bg-red-200",
"data-[state=checked]:hover:bg-red-400 data-[state=unchecked]:hover:bg-red-300",
],
},
"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-[34px]",
},
},
variant: {
default: [
"dark:data-[state=checked]:bg-netbird dark:data-[state=unchecked]:bg-nb-gray-700",
"dark:data-[state=checked]:hover:bg-netbird-500 dark:data-[state=unchecked]:hover:bg-nb-gray-600",
"data-[state=checked]:bg-neutral-900 data-[state=unchecked]:bg-neutral-200",
"data-[state=checked]:hover:bg-neutral-800 data-[state=unchecked]:hover:bg-neutral-300",
],
"red-green": [
"dark:data-[state=checked]:bg-red-600 dark:data-[state=unchecked]:bg-nb-gray-700",
"dark:data-[state=checked]:hover:bg-red-500 dark:data-[state=unchecked]:hover:bg-nb-gray-600",
"data-[state=checked]:bg-red-500 data-[state=unchecked]:bg-red-200",
"data-[state=checked]:hover:bg-red-400 data-[state=unchecked]:hover:bg-red-300",
],
red: [
"dark:data-[state=checked]:bg-red-600 dark:data-[state=unchecked]:bg-nb-gray-700",
"dark:data-[state=checked]:hover:bg-red-500 dark:data-[state=unchecked]:hover:bg-nb-gray-600",
"data-[state=checked]:bg-red-500 data-[state=unchecked]:bg-red-200",
"data-[state=checked]:hover:bg-red-400 data-[state=unchecked]:hover:bg-red-300",
],
},
"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-[34px]",
},
},
});
const ToggleSwitch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> &
SwitchVariants & { dataCy?: string }
>(
(
{ className, size = "default", variant = "default", dataCy, ...props },
ref,
) => (
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> &
SwitchVariants & { dataCy?: string }
>(({ className, size = "default", variant = "default", dataCy, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex shrink-0 cursor-default items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-50 dark:focus-visible:ring-neutral-300 dark:focus-visible:ring-offset-neutral-950",
className,
switchVariants({ size, variant }),
)}
{...props}
data-cy={dataCy}
onClick={(e) => {
e.stopPropagation();
props.onClick?.(e);
}}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
switchVariants({ "thumb-size": size }),
"pointer-events-none block rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=unchecked]:translate-x-0 dark:bg-white",
"wails-no-draggable peer inline-flex shrink-0 cursor-default items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-50 dark:focus-visible:ring-neutral-300 dark:focus-visible:ring-offset-neutral-950",
className,
switchVariants({ size, variant }),
)}
/>
{...props}
data-cy={dataCy}
onClick={(e) => {
e.stopPropagation();
props.onClick?.(e);
}}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
switchVariants({ "thumb-size": size }),
"pointer-events-none block rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=unchecked]:translate-x-0 dark:bg-white",
)}
/>
</SwitchPrimitives.Root>
),
);
));
ToggleSwitch.displayName = SwitchPrimitives.Root.displayName;
export { ToggleSwitch };

View File

@@ -33,11 +33,7 @@ const STATUS_KEY: Record<ConnectionState, string> = {
const EVENT_BROWSER_LOGIN_CANCEL = "browser-login:cancel";
const EVENT_TRIGGER_LOGIN = "trigger-login";
const NEEDS_LOGIN_STATES = new Set([
"NeedsLogin",
"SessionExpired",
"LoginFailed",
]);
const NEEDS_LOGIN_STATES = new Set(["NeedsLogin", "SessionExpired", "LoginFailed"]);
const errorMessage = formatErrorMessage;
@@ -164,10 +160,7 @@ export const ConnectionStatusSwitch = () => {
if (action === "disconnect" && daemonState === "Connected") {
return ConnectionState.Disconnecting;
}
if (
(action === "connect" || action === "logging-in") &&
daemonState !== "Connected"
) {
if ((action === "connect" || action === "logging-in") && daemonState !== "Connected") {
return ConnectionState.Connecting;
}
switch (daemonState) {
@@ -271,11 +264,7 @@ export const ConnectionStatusSwitch = () => {
return;
}
if (action === "disconnect") {
if (
daemonState === "Idle" ||
daemonState === "Disconnected" ||
unreachable
) {
if (daemonState === "Idle" || daemonState === "Disconnected" || unreachable) {
setAction(null);
}
}
@@ -307,11 +296,9 @@ export const ConnectionStatusSwitch = () => {
};
const isTransitioning =
connState === ConnectionState.Connecting ||
connState === ConnectionState.Disconnecting;
connState === ConnectionState.Connecting || connState === ConnectionState.Disconnecting;
const isOn =
connState === ConnectionState.Connected ||
connState === ConnectionState.Connecting;
connState === ConnectionState.Connected || connState === ConnectionState.Connecting;
const showLocal = connState === ConnectionState.Connected;
const fqdn = status?.local.fqdn || "";
const ip = status?.local.ip || "";
@@ -321,7 +308,7 @@ export const ConnectionStatusSwitch = () => {
<img
src={netbirdFullLogo}
alt={"NetBird"}
className={"h-7 w-auto select-none mb-4"}
className={"h-7 w-auto select-none mb-4 wails-no-draggable"}
draggable={false}
/>
@@ -330,23 +317,20 @@ export const ConnectionStatusSwitch = () => {
checked={isOn}
onCheckedChange={handleSwitch}
disabled={isTransitioning || unreachable}
className={cn(
unreachable && "opacity-80",
isTransitioning && "animate-pulse",
)}
className={cn(unreachable && "opacity-80", isTransitioning && "animate-pulse")}
/>
<div className={"flex flex-col items-center"}>
<h1
className={
"text-sm font-medium text-nb-gray-200 tracking-wide transition-colors duration-300"
"text-sm font-medium text-nb-gray-200 tracking-wide transition-colors duration-300 wails-no-draggable"
}
>
{t(STATUS_KEY[connState])}
</h1>
<p
className={cn(
"font-mono text-xs leading-tight min-h-[1em] text-nb-gray-300 mt-2 transition-opacity duration-300",
"font-mono text-xs leading-tight min-h-[1em] text-nb-gray-300 mt-2 transition-opacity duration-300 wails-no-draggable",
showLocal && fqdn ? "opacity-100" : "opacity-0",
)}
>
@@ -354,7 +338,7 @@ export const ConnectionStatusSwitch = () => {
</p>
<p
className={cn(
"font-mono text-xs leading-tight min-h-[1em] text-nb-gray-300 mt-0.5 transition-opacity duration-300",
"font-mono text-xs leading-tight min-h-[1em] text-nb-gray-300 mt-0.5 transition-opacity duration-300 wails-no-draggable",
showLocal && ip ? "opacity-100" : "opacity-0",
)}
>

View File

@@ -8,6 +8,7 @@ import {
Settings,
type LucideIcon,
} from "lucide-react";
import { Window } from "@wailsio/runtime";
import { WindowManager } from "@bindings/services";
import {
DropdownMenu,
@@ -22,6 +23,14 @@ import { cn } from "@/lib/cn";
type ViewMode = "default" | "advanced";
// Window dimensions per view. Height matches the Settings window (640) so the
// chrome height is identical across surfaces; width grows from the compact
// 380 default to 900 in advanced.
const VIEW_SIZE: Record<ViewMode, { width: number; height: number }> = {
default: { width: 380, height: 640 },
advanced: { width: 900, height: 640 },
};
export const Header = () => {
const { t } = useTranslation();
const [menuOpen, setMenuOpen] = useState(false);
@@ -38,7 +47,10 @@ export const Header = () => {
const selectMode = (mode: ViewMode) => {
setMenuOpen(false);
if (mode === viewMode) return;
setViewMode(mode);
const { width, height } = VIEW_SIZE[mode];
void Window.SetSize(width, height).catch(() => {});
};
return (
@@ -54,12 +66,19 @@ export const Header = () => {
<div className={"flex justify-center ml-4"}>
<ProfileDropdown onManageProfiles={openManageProfiles} />
</div>
<div className={"flex justify-end"}>
<div className={"flex justify-end wails-no-draggable"}>
<DropdownMenu modal={false} open={menuOpen} onOpenChange={setMenuOpen}>
<DropdownMenuTrigger asChild>
<IconButton icon={MoreVertical} iconClassName={"text-nb-gray-200"} />
<DropdownMenuTrigger asChild className={"wails-no-draggable"}>
<IconButton
icon={MoreVertical}
iconClassName={"text-nb-gray-200 wails-no-draggable"}
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={8} className="min-w-52">
<DropdownMenuContent
align="end"
sideOffset={8}
className="min-w-52 data-[state=closed]:!animate-none data-[state=closed]:!duration-0"
>
<DropdownMenuItem onClick={openSettings}>
<div className="flex items-center gap-2">
<Settings size={14} />