From 620233a7acad4f951a3be1e3853cd72ed79d53e5 Mon Sep 17 00:00:00 2001 From: Eduard Gert Date: Wed, 20 May 2026 13:38:23 +0200 Subject: [PATCH] update dropdown ui padding, remove unused stuff --- client/ui/frontend/CLAUDE.md | 3 +- .../frontend/src/components/DropdownMenu.tsx | 4 +- .../src/components/ProfileDropdown.tsx | 27 +- .../src/components/ProfileSelector.tsx | 365 ------------------ .../ui/frontend/src/lib/MainModuleContext.tsx | 27 -- .../src/modules/settings/LanguagePicker.tsx | 4 +- 6 files changed, 15 insertions(+), 415 deletions(-) delete mode 100644 client/ui/frontend/src/components/ProfileSelector.tsx delete mode 100644 client/ui/frontend/src/lib/MainModuleContext.tsx diff --git a/client/ui/frontend/CLAUDE.md b/client/ui/frontend/CLAUDE.md index 63e1380f7..2097400c9 100644 --- a/client/ui/frontend/CLAUDE.md +++ b/client/ui/frontend/CLAUDE.md @@ -37,7 +37,7 @@ React 18 + TS 5.7 (`strict`, `noImplicitAny: false`) + Vite 6 + Tailwind 3 (`dar ## Directory layout (src/) -The split between `pages/`, `screens/`, and `modules/` is historical and not load-bearing. **Today:** `modules/` owns the polished AppLayout-shell-driven UI, `pages/` owns the few routes that live outside that shell, and `screens/` is the unsorted legacy bucket. Don't add new code under `screens/` — pick `pages/` (own route, no shell) or `modules//` (lives inside the shell). `lib/MainModuleContext.tsx` is exported but unused — candidate for deletion. +The split between `pages/`, `screens/`, and `modules/` is historical and not load-bearing. **Today:** `modules/` owns the polished AppLayout-shell-driven UI, `pages/` owns the few routes that live outside that shell, and `screens/` is the unsorted legacy bucket. Don't add new code under `screens/` — pick `pages/` (own route, no shell) or `modules//` (lives inside the shell). ## Wails event bus @@ -164,7 +164,6 @@ Defined in `tailwind.config.ts`. `nb-gray` is the neutral palette (background = - **`modules/authentication/SessionExpiredDialog.tsx`** and **`modules/authentication/SessionAboutToExpireDialog.tsx`** are the always-on-top auxiliary windows. Today they're only triggered via the DEV-only "Development" tab in Settings (`SettingsDevelopment.tsx`) — a daemon-status hook (status `SessionExpired`, plus a future "about-to-expire" signal) will drive them later. Sign-in / Stay-connected emit `EventTriggerLogin` so the main window's `startLogin()` orchestrator handles the SSO flow; Logout uses `Connection.Logout({profileName, username})`. - **`screens/QuickActions.tsx`** is wired to `/quick` in the route table but nothing on the Go side currently navigates there. - **`UpdateAvailableBanner`** is force-enabled via `FORCE_UPDATE_AVAILABLE = true` and additionally TODO-commented for the "only when management has auto updates enabled + force updates is disabled" case. -- **`lib/MainModuleContext.tsx`** is exported but unused. Candidate for deletion. ## Wails Go API reference diff --git a/client/ui/frontend/src/components/DropdownMenu.tsx b/client/ui/frontend/src/components/DropdownMenu.tsx index 53b712664..eb31f2852 100644 --- a/client/ui/frontend/src/components/DropdownMenu.tsx +++ b/client/ui/frontend/src/components/DropdownMenu.tsx @@ -75,7 +75,7 @@ const DropdownMenuContent = React.forwardRef< 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", + "z-50 min-w-[8rem] overflow-hidden rounded-lg 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", @@ -102,7 +102,7 @@ const DropdownMenuItem = React.forwardRef< { collisionPadding={12} onOpenAutoFocus={(e) => e.preventDefault()} className={cn( - "z-50 min-w-64 overflow-hidden rounded-xl border border-nb-gray-900 bg-nb-gray-935 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", "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", @@ -98,15 +98,11 @@ export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => { "data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", )} > - e.stopPropagation()} - > + e.stopPropagation()}> {sortedProfiles.length > 0 && ( <> - - + + {sortedProfiles.map((profile) => ( { -
+
)} -
+
{ onSelect={handleManage} disabled={!onManageProfiles} className={cn( - "flex items-center gap-2 px-2 py-1.5 mx-1.5 my-0.5", + "flex items-center gap-2 px-2 py-1.5 my-0.5", "rounded-md outline-none cursor-default text-sm", "data-[selected=true]:bg-nb-gray-900", "data-[disabled=true]:opacity-50 data-[disabled=true]:pointer-events-none", @@ -221,7 +217,7 @@ const ProfileRow = ({ profile, isActive, onSelect }: ProfileRowProps) => { value={profile.name} onSelect={() => onSelect(profile.name)} className={cn( - "flex gap-2 px-2 py-1.5 mx-1.5 my-0.5 w-auto", + "flex gap-2 px-2 py-2 pr-3 my-0.5 first:mt-0 last:mb-1 w-auto", "rounded-md outline-none cursor-default text-sm", "data-[selected=true]:bg-nb-gray-900", showEmail ? "items-start" : "items-center", @@ -233,10 +229,7 @@ const ProfileRow = ({ profile, isActive, onSelect }: ProfileRowProps) => { {showEmail && }
{isActive && ( - + )} ); diff --git a/client/ui/frontend/src/components/ProfileSelector.tsx b/client/ui/frontend/src/components/ProfileSelector.tsx deleted file mode 100644 index 6f93c6276..000000000 --- a/client/ui/frontend/src/components/ProfileSelector.tsx +++ /dev/null @@ -1,365 +0,0 @@ -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import * as Popover from "@radix-ui/react-popover"; -import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; -import * as ScrollArea from "@radix-ui/react-scroll-area"; -import { Command } from "cmdk"; -import { Dialogs } from "@wailsio/runtime"; -import { ChevronDown, MoreVertical, PlusCircle, Search, Trash2, UserMinus } from "lucide-react"; -import type { Profile } from "@bindings/services/models.js"; -import { cn } from "@/lib/cn"; -import { generateColorFromString } from "@/lib/color"; -import { NewProfileModal } from "@/components/NewProfileModal"; -import { useProfile } from "@/modules/profile/ProfileContext.tsx"; - -const DEFAULT_PROFILE = "default"; - -export const ProfileSelector = () => { - const { t } = useTranslation(); - const { - profiles, - activeProfile, - loaded, - switchProfile, - addProfile, - removeProfile, - logoutProfile, - } = useProfile(); - - const [open, setOpen] = useState(false); - const [newOpen, setNewOpen] = useState(false); - const [busy, setBusy] = useState(false); - - const selected = - profiles.find((p) => p.name === activeProfile) ?? - profiles.find((p) => p.isActive) ?? - profiles[0]; - - const sorted = [...profiles].sort((a, b) => a.name.localeCompare(b.name)); - - const guarded = async (title: string, fn: () => Promise) => { - 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 handleDeregister = async (name: string) => { - const cancelLabel = t("common.cancel"); - const confirmLabel = t("profile.deregister.confirm"); - const result = await Dialogs.Warning({ - Title: t("profile.deregister.title"), - Message: t("profile.deregister.message", { name }), - Buttons: [ - { Label: cancelLabel, IsCancel: true }, - { Label: confirmLabel, IsDefault: true }, - ], - }); - if (result !== confirmLabel) return; - void guarded(t("profile.error.deregisterTitle"), () => logoutProfile(name)); - }; - - const handleDelete = async (name: string) => { - if (name === DEFAULT_PROFILE) return; - const cancelLabel = t("common.cancel"); - const confirmLabel = t("common.delete"); - const result = await Dialogs.Warning({ - Title: t("profile.delete.title"), - Message: t("profile.delete.message", { name }), - Buttons: [ - { Label: cancelLabel, IsCancel: true }, - { Label: confirmLabel, IsDefault: true }, - ], - }); - if (result !== confirmLabel) return; - void guarded(t("profile.error.deleteTitle"), () => removeProfile(name)); - }; - - const handleNewProfile = () => { - setOpen(false); - setNewOpen(true); - }; - - const handleCreateProfile = (name: string) => { - void guarded(t("profile.error.createTitle"), () => addProfile(name)); - }; - - const displayName = - selected?.name ?? - (loaded ? t("profile.selector.noProfile") : t("profile.selector.loading")); - const initial = (selected?.name ?? "?").charAt(0).toUpperCase(); - const initialColor = generateColorFromString(selected?.name); - - return ( - <> - - - - - - - e.preventDefault()} - > - -
-
- - -
-
- - - - - -
-

- {t("profile.selector.emptyTitle")} -

-

- {t("profile.selector.emptyDescription")} -

-
-
- - {sorted.map((profile) => ( - handleSelect(profile.name)} - onDeregister={() => handleDeregister(profile.name)} - onDelete={() => handleDelete(profile.name)} - deletable={profile.name !== DEFAULT_PROFILE} - /> - ))} -
-
- - - -
- -
- - - - - - - - - ); -}; - -type ProfileRowProps = { - profile: Profile; - selected: boolean; - onSelect: () => void; - onDeregister: () => void; - onDelete: () => void; - deletable: boolean; -}; - -const ProfileRow = ({ - profile, - selected, - onSelect, - onDeregister, - onDelete, - deletable, -}: ProfileRowProps) => { - const { t } = useTranslation(); - const [menuOpen, setMenuOpen] = useState(false); - const initial = profile.name.charAt(0).toUpperCase(); - const initialColor = generateColorFromString(profile.name); - - return ( - onSelect()} - className={cn( - "group flex items-center gap-2 pl-2 pr-3 py-1.5 rounded-md cursor-default outline-none", - "data-[selected=true]:bg-nb-gray-910", - selected && "bg-nb-gray-910", - )} - > -
- {initial} -
- - {profile.name} - - - - - - - - e.stopPropagation()} - onPointerDown={(e) => e.stopPropagation()} - className={cn( - "w-44 rounded-md border border-nb-gray-850 bg-nb-gray-910 shadow-lg p-1 z-50", - )} - > - { - e.preventDefault(); - onDeregister(); - setMenuOpen(false); - }} - className={cn( - "flex items-center gap-2 px-2 py-1.5 rounded-md cursor-default outline-none font-medium", - "text-xs text-nb-gray-200 data-[highlighted]:bg-nb-gray-850", - )} - > - - {t("profile.selector.deregister")} - - { - e.preventDefault(); - if (!deletable) return; - onDelete(); - setMenuOpen(false); - }} - className={cn( - "flex items-center gap-2 px-2 py-1.5 rounded-md cursor-default outline-none font-medium", - "text-xs data-[highlighted]:bg-nb-gray-850", - deletable - ? "text-red-500" - : "text-nb-gray-500 cursor-not-allowed", - )} - > - - {t("profile.selector.delete")} - - - - -
- ); -}; diff --git a/client/ui/frontend/src/lib/MainModuleContext.tsx b/client/ui/frontend/src/lib/MainModuleContext.tsx deleted file mode 100644 index 40c83590a..000000000 --- a/client/ui/frontend/src/lib/MainModuleContext.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { createContext, ReactNode, useContext, useState } from "react"; - -export type MainModule = "peers" | "settings"; - -type Ctx = { - active: MainModule; - setActive: (m: MainModule) => void; -}; - -const MainModuleContext = createContext(null); - -export const MainModuleProvider = ({ children }: { children: ReactNode }) => { - const [active, setActive] = useState("peers"); - return ( - - {children} - - ); -}; - -export const useMainModule = () => { - const ctx = useContext(MainModuleContext); - if (!ctx) { - throw new Error("useMainModule must be used within MainModuleProvider"); - } - return ctx; -}; diff --git a/client/ui/frontend/src/modules/settings/LanguagePicker.tsx b/client/ui/frontend/src/modules/settings/LanguagePicker.tsx index fa9e0f33f..a72f60444 100644 --- a/client/ui/frontend/src/modules/settings/LanguagePicker.tsx +++ b/client/ui/frontend/src/modules/settings/LanguagePicker.tsx @@ -135,7 +135,7 @@ export function LanguagePicker() { onCloseAutoFocus={(e) => e.preventDefault()} className={cn( "w-[var(--radix-popover-trigger-width)]", - "rounded-md border border-nb-gray-850 bg-nb-gray-920 shadow-lg p-1 z-50", + "rounded-lg border border-nb-gray-850 bg-nb-gray-920 shadow-lg p-1 z-50", "origin-[var(--radix-popover-content-transform-origin)]", "data-[state=open]:animate-in data-[state=closed]:animate-out", "data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0", @@ -189,7 +189,7 @@ export function LanguagePicker() { className={cn( "flex items-center gap-2 px-2 py-2 rounded-md cursor-default outline-none my-0.5", "text-xs font-semibold text-nb-gray-200", - "data-[selected=true]:bg-nb-gray-900 data-[selected=true]:text-nb-gray-50", + "data-[selected=true]:bg-nb-gray-850 data-[selected=true]:text-nb-gray-50", )} >