*]:w-full sm:[&>*]:w-auto",
"px-8 pt-6",
className,
)}
diff --git a/client/ui/frontend/src/components/NewProfileDialog.tsx b/client/ui/frontend/src/components/NewProfileModal.tsx
similarity index 72%
rename from client/ui/frontend/src/components/NewProfileDialog.tsx
rename to client/ui/frontend/src/components/NewProfileModal.tsx
index 6fc7d0e9f..72aa0314c 100644
--- a/client/ui/frontend/src/components/NewProfileDialog.tsx
+++ b/client/ui/frontend/src/components/NewProfileModal.tsx
@@ -10,7 +10,7 @@ type Props = {
onCreate: (name: string) => void;
};
-export const NewProfileDialog = ({ open, onOpenChange, onCreate }: Props) => {
+export const NewProfileModal = ({ open, onOpenChange, onCreate }: Props) => {
const { t } = useTranslation();
const [name, setName] = useState("");
@@ -30,14 +30,11 @@ export const NewProfileDialog = ({ open, onOpenChange, onCreate }: Props) => {
return (
- e.preventDefault()}
- >
+ e.preventDefault()}>
diff --git a/client/ui/frontend/src/components/ProfileAvatar.tsx b/client/ui/frontend/src/components/ProfileAvatar.tsx
index a4baf4e93..47531973f 100644
--- a/client/ui/frontend/src/components/ProfileAvatar.tsx
+++ b/client/ui/frontend/src/components/ProfileAvatar.tsx
@@ -1,11 +1,12 @@
import { ButtonHTMLAttributes, forwardRef } from "react";
import {
+ Beaker,
Briefcase,
- Building2,
+ Building,
Gamepad2,
GraduationCap,
House,
- Server,
+ Cloud,
ServerCog,
SquareCode,
TestTube,
@@ -17,19 +18,21 @@ import {
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],
+ [/(default|personal)/i, UserCircle],
+ [/(work|business|office|company|corp|corporate)/i, Briefcase],
+ [/(home|house|private)/i, House],
+ [/(dev|development|developer|code|coding|engineering)/i, SquareCode],
+ [/(local|localhost|loopback)/i, SquareCode],
+ [/(stage|staging)/i, Beaker],
+ [/(test|testing|qa)/i, TestTube],
+ [/(prod|production)/i, Cloud],
+ [/(live)/i, Cloud],
+ [/(selfhosted|self-hosted|on-prem|onprem)/i, ServerCog],
+ [/(school|university|edu|study|student)/i, GraduationCap],
+ [/(client|customer)/i, Building],
+ [/(family)/i, Users],
+ [/(gaming|game)/i, Gamepad2],
+ [/(guest)/i, UserPlus],
];
export const pickProfileIcon = (name: string | undefined): LucideIcon | null => {
diff --git a/client/ui/frontend/src/components/ProfileDropdown.tsx b/client/ui/frontend/src/components/ProfileDropdown.tsx
index decfd1a7c..fcf2b2268 100644
--- a/client/ui/frontend/src/components/ProfileDropdown.tsx
+++ b/client/ui/frontend/src/components/ProfileDropdown.tsx
@@ -1,17 +1,14 @@
-import { forwardRef, useState } from "react";
+import { forwardRef, useLayoutEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Dialogs } from "@wailsio/runtime";
+import * as Popover from "@radix-ui/react-popover";
import * as ScrollArea from "@radix-ui/react-scroll-area";
+import { Command } from "cmdk";
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 type { Profile } from "@bindings/services/models.js";
+import { NewProfileModal } from "@/components/NewProfileModal";
+import { Tooltip } from "@/components/Tooltip";
import { useProfile } from "@/modules/profile/ProfileContext";
import { cn } from "@/lib/cn";
@@ -19,6 +16,9 @@ type ProfileDropdownProps = {
onManageProfiles?: () => void;
};
+const ADD_VALUE = "__add_profile__";
+const MANAGE_VALUE = "__manage_profiles__";
+
export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => {
const { t } = useTranslation();
const { activeProfile, profiles, addProfile, switchProfile } = useProfile();
@@ -26,9 +26,11 @@ export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => {
const [newProfileOpen, setNewProfileOpen] = useState(false);
const [busy, setBusy] = useState(false);
- const sortedProfiles = [...profiles].sort((a, b) =>
- a.name.localeCompare(b.name),
- );
+ const sortedProfiles = [...profiles].sort((a, b) => {
+ if (a.name === activeProfile) return -1;
+ if (b.name === activeProfile) return 1;
+ return a.name.localeCompare(b.name);
+ });
const guarded = async (title: string, fn: () => Promise) => {
if (busy) return;
@@ -76,71 +78,96 @@ export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => {
return (
<>
-
-
+
+
-
-
- {sortedProfiles.length > 0 && (
- <>
-
-
- {sortedProfiles.map((profile) => {
- const isActive = profile.name === activeProfile;
- const Icon = pickProfileIcon(profile.name) ?? UserCircle;
- return (
- handleSelect(profile.name)}
- >
-
-
-
- {profile.name}
-
- {isActive && (
-
- )}
-
-
- );
- })}
-
-
+
+ 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",
+ "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",
+ )}
+ >
+ e.stopPropagation()}
+ >
+ {sortedProfiles.length > 0 && (
+ <>
+
+
+
+ {sortedProfiles.map((profile) => (
+
+ ))}
+
+
+
+
+
+
+
+ >
+ )}
+
+
+
-
-
-
-
- >
- )}
-
-
-
-
- {t("profile.dropdown.addProfile")}
-
-
-
-
-
- {t("profile.dropdown.manageProfiles")}
-
-
-
-
-
+
+ {t("profile.dropdown.addProfile")}
+
+
+
+
+
+ {t("profile.dropdown.manageProfiles")}
+
+
+
+
+
+
+
+ void;
+};
+
+const ProfileRow = ({ profile, isActive, onSelect }: ProfileRowProps) => {
+ const showEmail = !!profile.email;
+ const Icon = pickProfileIcon(profile.name) ?? UserCircle;
+ return (
+ onSelect(profile.name)}
+ className={cn(
+ "flex gap-2 px-2 py-1.5 mx-1.5 my-0.5 w-auto",
+ "rounded-md outline-none cursor-default text-sm",
+ "data-[selected=true]:bg-nb-gray-900",
+ showEmail ? "items-start" : "items-center",
+ )}
+ >
+
+
+ {profile.name}
+ {showEmail && }
+
+ {isActive && (
+
+ )}
+
+ );
+};
+
+const TruncatedEmail = ({ email }: { email: string }) => {
+ const ref = useRef(null);
+ const [overflowing, setOverflowing] = useState(false);
+
+ useLayoutEffect(() => {
+ const el = ref.current;
+ if (!el) return;
+ setOverflowing(el.scrollWidth > el.clientWidth);
+ }, [email]);
+
+ const span = (
+
+ {email}
+
+ );
+ if (!overflowing) return span;
+ return {span};
+};
diff --git a/client/ui/frontend/src/components/ProfileSelector.tsx b/client/ui/frontend/src/components/ProfileSelector.tsx
index 7a5af60bf..6f93c6276 100644
--- a/client/ui/frontend/src/components/ProfileSelector.tsx
+++ b/client/ui/frontend/src/components/ProfileSelector.tsx
@@ -9,7 +9,7 @@ import { ChevronDown, MoreVertical, PlusCircle, Search, Trash2, UserMinus } from
import type { Profile } from "@bindings/services/models.js";
import { cn } from "@/lib/cn";
import { generateColorFromString } from "@/lib/color";
-import { NewProfileDialog } from "@/components/NewProfileDialog";
+import { NewProfileModal } from "@/components/NewProfileModal";
import { useProfile } from "@/modules/profile/ProfileContext.tsx";
const DEFAULT_PROFILE = "default";
@@ -233,7 +233,7 @@ export const ProfileSelector = () => {
-