update profile ui

This commit is contained in:
Eduard Gert
2026-05-19 14:21:14 +02:00
parent 3f8cd29006
commit 1c5254cb31
10 changed files with 541 additions and 127 deletions

View File

@@ -19,13 +19,19 @@ export const Avatar = forwardRef<HTMLButtonElement, Props>(function Avatar(
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",
"inline-grid place-items-center rounded-full bg-nb-gray-850 p-0 text-center",
"text-[0.9rem] 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 }}
style={{
width: size,
height: size,
color,
lineHeight: 0,
letterSpacing: 0,
}}
{...props}
>
{initial}

View File

@@ -0,0 +1,233 @@
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { cva } from "class-variance-authority";
import { Check, ChevronRight, Circle } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/cn";
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const menuItemVariants = cva("", {
variants: {
variant: {
default:
"text-nb-gray-200 focus:bg-nb-gray-900 focus:text-nb-gray-50 data-[state=open]:bg-nb-gray-900 data-[state=open]:text-nb-gray-50",
danger: "text-red-500 focus:bg-red-900/20 focus:text-red-500",
},
},
defaultVariants: { variant: "default" },
});
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
variant?: "default" | "danger";
}
>(({ className, inset, children, variant, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"relative flex select-none items-center rounded-md pl-3 pr-2 py-1.5 text-sm outline-none cursor-default",
"transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
menuItemVariants({ variant }),
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-nb-gray-900 bg-nb-gray-930 p-1 text-nb-gray-200 shadow-lg",
"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",
"data-[side=bottom]:slide-in-from-top-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",
className,
)}
{...props}
/>
));
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-nb-gray-900 bg-nb-gray-935 p-1 text-nb-gray-200 shadow-lg",
"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",
"data-[side=bottom]:slide-in-from-top-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",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
variant?: "default" | "danger";
href?: string;
target?: string;
rel?: string;
}
>(({ className, inset, variant, onClick, href, target, rel, children, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex select-none items-center rounded-md pl-3 pr-2 py-1.5 text-sm outline-none cursor-default",
"transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
menuItemVariants({ variant }),
className,
)}
onClick={(e) => {
if (href) return;
e.preventDefault();
e.stopPropagation();
onClick?.(e);
}}
{...props}
>
{href ? (
<a href={href} target={target} rel={rel} className="flex w-full items-center gap-3">
{children}
</a>
) : (
children
)}
</DropdownMenuPrimitive.Item>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.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",
"transition-colors text-nb-gray-200 focus:bg-nb-gray-900 focus:text-nb-gray-50",
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.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",
"transition-colors text-nb-gray-200 focus:bg-nb-gray-900 focus:text-nb-gray-50",
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold text-nb-gray-200",
inset && "pl-8",
className,
)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-nb-gray-910", className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => (
<span
className={cn("ml-auto text-xs tracking-widest text-nb-gray-400 opacity-60", className)}
{...props}
/>
);
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
};

View File

@@ -9,33 +9,24 @@ type Props = HTMLMotionProps<"button"> & {
iconClassName?: string;
};
export const IconButton = forwardRef<HTMLButtonElement, Props>(
function IconButton(
{
icon: Icon,
iconSize = 18,
iconClassName,
className,
type = "button",
...props
},
ref,
) {
return (
<motion.button
ref={ref}
type={type}
whileTap={{ scale: 0.95 }}
className={cn(
"h-11 w-11 flex items-center justify-center rounded-md cursor-default outline-none",
"text-nb-gray-400 hover:text-nb-gray-300 hover:bg-nb-gray-930",
"transition-colors duration-150",
className,
)}
{...props}
>
<Icon size={iconSize} className={iconClassName} />
</motion.button>
);
},
);
export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButton(
{ icon: Icon, iconSize = 17, iconClassName, className, type = "button", ...props },
ref,
) {
return (
<motion.button
ref={ref}
type={type}
whileTap={{ scale: 0.95 }}
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",
className,
)}
{...props}
>
<Icon size={iconSize} className={iconClassName} />
</motion.button>
);
});

View File

@@ -0,0 +1,70 @@
import { ButtonHTMLAttributes, forwardRef } from "react";
import {
Briefcase,
Building2,
Gamepad2,
GraduationCap,
House,
Server,
ServerCog,
SquareCode,
TestTube,
UserCircle,
UserPlus,
Users,
type LucideIcon,
} from "lucide-react";
import { cn } from "@/lib/cn";
const ICON_MAP: ReadonlyArray<[RegExp, LucideIcon]> = [
[/\b(default|user|me|personal)\b/i, UserCircle],
[/\b(work|business|office|company|corp|corporate)\b/i, Briefcase],
[/\b(home|house|private)\b/i, House],
[/\b(dev|development|developer|code|coding|engineering)\b/i, SquareCode],
[/\b(local|localhost|loopback)\b/i, SquareCode],
[/\b(test|testing|staging|qa|stage)\b/i, TestTube],
[/\b(prod|production|live)\b/i, Server],
[/\b(selfhosted|self-hosted|on-prem|onprem)\b/i, ServerCog],
[/\b(school|university|edu|study|student)\b/i, GraduationCap],
[/\b(client|customer)\b/i, Building2],
[/\b(family)\b/i, Users],
[/\b(gaming|game)\b/i, Gamepad2],
[/\b(guest)\b/i, UserPlus],
];
export const pickProfileIcon = (name: string | undefined): LucideIcon | null => {
if (!name) return null;
for (const [pattern, Icon] of ICON_MAP) {
if (pattern.test(name)) return Icon;
}
return null;
};
type Props = ButtonHTMLAttributes<HTMLButtonElement> & {
name?: string;
size?: number;
};
export const ProfileAvatar = forwardRef<HTMLButtonElement, Props>(function ProfileAvatar(
{ name = "", size = 28, className, type = "button", ...props },
ref,
) {
const Icon = pickProfileIcon(name) ?? UserCircle;
return (
<button
ref={ref}
type={type}
className={cn(
"inline-grid place-items-center rounded-full bg-nb-gray-900 p-0 text-center",
"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 }}
{...props}
>
<Icon size={Math.round(size * 0.4)} className={"text-nb-gray-200"} />
</button>
);
});

View File

@@ -0,0 +1,180 @@
import { forwardRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Dialogs } from "@wailsio/runtime";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { Check, ChevronDown, PlusCircle, Settings2, UserCircle } from "lucide-react";
import { pickProfileIcon } from "@/components/ProfileAvatar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/DropdownMenu";
import { NewProfileDialog } from "@/components/NewProfileDialog";
import { useProfile } from "@/modules/profile/ProfileContext";
import { cn } from "@/lib/cn";
type ProfileDropdownProps = {
onManageProfiles?: () => void;
};
export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => {
const { t } = useTranslation();
const { activeProfile, profiles, addProfile, switchProfile } = useProfile();
const [open, setOpen] = useState(false);
const [newProfileOpen, setNewProfileOpen] = useState(false);
const [busy, setBusy] = useState(false);
const sortedProfiles = [...profiles].sort((a, b) =>
a.name.localeCompare(b.name),
);
const guarded = async (title: string, fn: () => Promise<void>) => {
if (busy) return;
setBusy(true);
try {
await fn();
} catch (e) {
await Dialogs.Error({
Title: title,
Message: e instanceof Error ? e.message : String(e),
});
} finally {
setBusy(false);
}
};
const handleSelect = (name: string) => {
setOpen(false);
if (name === activeProfile) return;
void guarded(t("profile.error.switchTitle"), () => switchProfile(name));
};
const handleAdd = () => {
setOpen(false);
setNewProfileOpen(true);
};
const handleManage = () => {
setOpen(false);
onManageProfiles?.();
};
const handleCreateProfile = async (name: string) => {
try {
await addProfile(name);
} catch (e) {
await Dialogs.Error({
Title: t("profile.error.createTitle"),
Message: e instanceof Error ? e.message : String(e),
});
}
};
const displayName = activeProfile || t("profile.selector.loading");
return (
<>
<DropdownMenu modal={false} open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<ProfileTriggerButton name={displayName} />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-64" align="start">
{sortedProfiles.length > 0 && (
<>
<ScrollArea.Root type="auto" className="overflow-hidden -mx-1">
<ScrollArea.Viewport className="max-h-56 px-1">
{sortedProfiles.map((profile) => {
const isActive = profile.name === activeProfile;
const Icon = pickProfileIcon(profile.name) ?? UserCircle;
return (
<DropdownMenuItem
key={profile.name}
onClick={() => handleSelect(profile.name)}
>
<div className="flex items-center gap-3 w-full min-w-0">
<Icon size={14} className="shrink-0" />
<span className="capitalize truncate flex-1">
{profile.name}
</span>
{isActive && (
<Check
size={14}
className="shrink-0 text-nb-gray-200"
/>
)}
</div>
</DropdownMenuItem>
);
})}
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
orientation="vertical"
className={cn(
"flex select-none touch-none transition-colors",
"w-1.5 bg-transparent py-1",
)}
>
<ScrollArea.Thumb className="flex-1 rounded-full bg-nb-gray-800 hover:bg-nb-gray-700 relative" />
</ScrollArea.Scrollbar>
</ScrollArea.Root>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem onClick={handleAdd}>
<div className="flex items-center gap-3">
<PlusCircle size={14} />
{t("profile.dropdown.addProfile")}
</div>
</DropdownMenuItem>
<DropdownMenuItem
onClick={handleManage}
disabled={!onManageProfiles}
>
<div className="flex items-center gap-3">
<Settings2 size={14} />
{t("profile.dropdown.manageProfiles")}
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<NewProfileDialog
open={newProfileOpen}
onOpenChange={setNewProfileOpen}
onCreate={handleCreateProfile}
/>
</>
);
};
type ProfileTriggerButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
name: string;
};
const ProfileTriggerButton = forwardRef<HTMLButtonElement, ProfileTriggerButtonProps>(
function ProfileTriggerButton({ name, className, ...props }, ref) {
const Icon = pickProfileIcon(name) ?? UserCircle;
return (
<button
ref={ref}
type="button"
className={cn(
"h-10 flex items-center gap-2 px-3 rounded-lg outline-none cursor-default",
"text-nb-gray-200 hover:bg-nb-gray-900",
"data-[state=open]:bg-nb-gray-900",
"transition-colors duration-150",
className,
)}
{...props}
>
<Icon size={16} className={"text-nb-gray-200 shrink-0"} />
<span className={"text-sm font-medium capitalize truncate max-w-[140px]"}>
{name}
</span>
<ChevronDown size={14} className={"text-nb-gray-200 shrink-0"} />
</button>
);
},
);

View File

@@ -91,6 +91,11 @@
"profile.error.createTitle": "Create Profile Failed",
"profile.error.loadTitle": "Load Profiles Failed",
"profile.dropdown.activeProfile": "Active profile",
"profile.dropdown.addProfile": "Add Profile",
"profile.dropdown.manageProfiles": "Manage Profiles",
"profile.dropdown.settings": "Settings",
"settings.error.loadTitle": "Load Settings Failed",
"settings.error.saveTitle": "Save Settings Failed",
"settings.error.debugBundleTitle": "Debug Bundle Failed",

View File

@@ -1,4 +1,3 @@
import { useState } from "react";
import { Outlet } from "react-router-dom";
import { Header } from "@/layouts/Header.tsx";
import { ClientVersionProvider } from "@/modules/auto-update/ClientVersionContext.tsx";
@@ -6,22 +5,15 @@ import { StatusProvider } from "@/modules/daemon-status/StatusContext.tsx";
import { DebugBundleProvider } from "@/modules/debug-bundle/DebugBundleContext.tsx";
import { ProfileProvider } from "@/modules/profile/ProfileContext.tsx";
// The wide-panel toggle lives in plain React state here so every launch
// starts in the small layout — no localStorage, no cross-machine drift.
// Header drives the toggle; Main reads it via Outlet context to decide
// whether to mount the right-side panel.
export type MainOutletContext = { expanded: boolean };
export const AppLayout = () => {
const [expanded, setExpanded] = useState(false);
return (
<div className={"relative flex h-full flex-col"}>
<StatusProvider>
<ProfileProvider>
<DebugBundleProvider>
<ClientVersionProvider>
<Header expanded={expanded} setExpanded={setExpanded} />
<Outlet context={{ expanded } satisfies MainOutletContext} />
<Header />
<Outlet />
</ClientVersionProvider>
</DebugBundleProvider>
</ProfileProvider>

View File

@@ -1,54 +1,10 @@
import { useEffect, useRef } from "react";
import { PanelRightCloseIcon, PanelRightOpenIcon, SettingsIcon } from "lucide-react";
import { Window } from "@wailsio/runtime";
import { SettingsIcon } from "lucide-react";
import { WindowManager } from "@bindings/services";
import { ProfileSelector } from "@/components/ProfileSelector.tsx";
import { IconButton } from "@/components/IconButton.tsx";
import { IconButton } from "@/components/IconButton";
import { ProfileDropdown } from "@/components/ProfileDropdown";
import { cn } from "@/lib/cn";
const WINDOW_SMALL_WIDTH = 380;
const WINDOW_BIG_WIDTH = 925;
const WINDOW_HEIGHT = 615;
const EXPANDED_THRESHOLD = 500;
type HeaderProps = {
expanded: boolean;
setExpanded: (next: boolean) => void;
};
export const Header = ({ expanded, setExpanded }: HeaderProps) => {
const didInitialResize = useRef(false);
useEffect(() => {
if (didInitialResize.current) return;
didInitialResize.current = true;
const w = expanded ? WINDOW_BIG_WIDTH : WINDOW_SMALL_WIDTH;
void Window.SetSize(w, WINDOW_HEIGHT).catch(() => {});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (!didInitialResize.current) return;
const w = expanded ? WINDOW_BIG_WIDTH : WINDOW_SMALL_WIDTH;
void Window.SetSize(w, WINDOW_HEIGHT).catch(() => {});
}, [expanded]);
useEffect(() => {
const onResize = () => {
const isWide = window.innerWidth >= EXPANDED_THRESHOLD;
if (isWide !== expanded) setExpanded(isWide);
};
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, [expanded, setExpanded]);
const togglePanel = () => {
const next = !expanded;
setExpanded(next);
const w = next ? WINDOW_BIG_WIDTH : WINDOW_SMALL_WIDTH;
void Window.SetSize(w, WINDOW_HEIGHT).catch(() => {});
};
export const Header = () => {
const openSettings = () => {
void WindowManager.OpenSettings().catch(() => {});
};
@@ -56,19 +12,23 @@ export const Header = ({ expanded, setExpanded }: HeaderProps) => {
return (
<div
className={cn(
"shrink-0 cursor-default wails-draggable flex items-center justify-end px-4 gap-3 bg-gradient-to-b from-nb-gray-800/15",
"pt-4",
"shrink-0 cursor-default wails-draggable grid grid-cols-3 items-center",
//"bg-gradient-to-b from-nb-gray-850/30",
//"bg-nb-gray-935 border border-b border-nb-gray-850",
"py-3 px-3",
)}
>
<div className={"ml-20"}>
<ProfileSelector />
<div />
<div className={"flex justify-center ml-3"}>
<ProfileDropdown />
</div>
<div className={"flex justify-end"}>
<IconButton
icon={SettingsIcon}
iconClassName={"text-nb-gray-200"}
onClick={openSettings}
/>
</div>
<IconButton
icon={expanded ? PanelRightOpenIcon : PanelRightCloseIcon}
onClick={togglePanel}
/>
<IconButton icon={SettingsIcon} onClick={openSettings} />
</div>
);
};

View File

@@ -1,29 +1,11 @@
import { useOutletContext } from "react-router-dom";
import { ConnectionStatusSwitch } from "@/layouts/ConnectionStatusSwitch.tsx";
import { MainRightSide } from "@/layouts/MainRightSide.tsx";
import { Navigation } from "@/layouts/Navigation.tsx";
import { Peers } from "@/modules/peers/Peers.tsx";
import type { MainOutletContext } from "@/layouts/AppLayout.tsx";
import { cn } from "@/lib/cn";
export const Main = () => {
const { expanded } = useOutletContext<MainOutletContext>();
return (
<div className={"wails-draggable flex flex-1 min-h-0 p-4 gap-4"}>
<div
className={cn(
"flex flex-col w-full shrink-0 items-center",
expanded && "max-w-xs",
)}
>
<div className={"flex flex-col w-full shrink-0 items-center"}>
<ConnectionStatusSwitch />
<Navigation peersActive />
</div>
{expanded && (
<MainRightSide>
<Peers />
</MainRightSide>
)}
</div>
);
};

View File

@@ -167,15 +167,10 @@ func main() {
app.RegisterService(application.NewService(services.NewI18n(bundle)))
app.RegisterService(application.NewService(services.NewPreferences(prefStore)))
// Initial size matches AppearanceContext's default `expanded: false`
// (small / simple view). When the user has previously expanded the
// window, Header.tsx's mount effect resizes back up to 925 via
// Window.SetSize — that's a one-shot grow rather than a shrink, which
// reads better than starting wide and snapping narrow on every launch.
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "NetBird",
Width: 380,
Height: 615,
Width: 310,
Height: 420,
Hidden: true,
BackgroundColour: application.NewRGB(24, 26, 29),
URL: "/",