This is an automated email from the ASF dual-hosted git repository.

hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-gateway.git


The following commit(s) were added to refs/heads/master by this push:
     new 194a7be  style(header): improve language and account menus
194a7be is described below

commit 194a7bee9ad6cfa58b9cda7f44654f9766dc0d51
Author: Yang Luo <[email protected]>
AuthorDate: Fri Sep 4 13:08:08 2026 +0800

    style(header): improve language and account menus
---
 web/src/components/shared/app-header.tsx | 412 ++++++++++++++++---------------
 1 file changed, 210 insertions(+), 202 deletions(-)

diff --git a/web/src/components/shared/app-header.tsx 
b/web/src/components/shared/app-header.tsx
index f2af4e1..3cf1325 100644
--- a/web/src/components/shared/app-header.tsx
+++ b/web/src/components/shared/app-header.tsx
@@ -1,202 +1,210 @@
-// Copyright 2026 The casbin Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import * as React from "react";
-import {useNavigate} from "react-router-dom";
-import {Globe, LogOut, Moon, PanelLeft, PanelLeftClose, Sun, User} from 
"lucide-react";
-import i18next from "i18next";
-
-import * as Setting from "@/Setting";
-import type {Account, ThemeAlgorithm} from "@/types";
-import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar";
-import {Button} from "@/components/ui/button";
-import {
-  DropdownMenu,
-  DropdownMenuContent,
-  DropdownMenuItem,
-  DropdownMenuSeparator,
-  DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import {SimpleTooltip} from "@/components/ui/tooltip";
-import {BreadcrumbBar} from "@/components/shared/breadcrumb-bar";
-import {CommandPaletteTrigger} from "@/components/shared/command-palette";
-import {VersionPanel} from "@/components/shared/version-panel";
-
-function ThemeToggle({
-  themeAlgorithm,
-  onChange,
-}: {
-  themeAlgorithm: ThemeAlgorithm;
-  onChange: (next: ThemeAlgorithm) => void;
-}) {
-  const isDark = Setting.isDarkTheme(themeAlgorithm);
-  return (
-    <SimpleTooltip title={isDark ? i18next.t("general:Light") : 
i18next.t("general:Dark")}>
-      <Button
-        variant="ghost"
-        size="icon-sm"
-        onClick={() => onChange(isDark ? ["default"] : ["dark"])}
-        aria-label="Toggle theme"
-      >
-        {isDark ? <Sun className="size-4" /> : <Moon className="size-4" />}
-      </Button>
-    </SimpleTooltip>
-  );
-}
-
-function LanguageSelect() {
-  const [, forceRender] = React.useReducer((count: number) => count + 1, 0);
-  return (
-    <DropdownMenu>
-      <DropdownMenuTrigger asChild>
-        <Button variant="ghost" size="icon-sm" aria-label="Change language">
-          <Globe className="size-4" />
-        </Button>
-      </DropdownMenuTrigger>
-      <DropdownMenuContent align="end">
-        {Setting.Countries.map(country => (
-          <DropdownMenuItem
-            key={country.key}
-            onClick={() => {
-              Setting.setLanguage(country.key);
-              // The shell reads i18next.t directly, which does not re-render 
on
-              // a language change by itself. Forcing a render here is what 
makes
-              // the switch take effect immediately.
-              forceRender();
-            }}
-          >
-            <img
-              
src={`${Setting.StaticBaseUrl}/flag-icons/${country.country}.svg`}
-              alt={country.alt}
-              className="h-4 w-6 rounded-xs object-cover"
-            />
-            {country.label}
-          </DropdownMenuItem>
-        ))}
-      </DropdownMenuContent>
-    </DropdownMenu>
-  );
-}
-
-function AccountMenu({
-  account,
-  onSignout,
-}: {
-  account: Account | null | undefined;
-  onSignout: () => void;
-}) {
-  const navigate = useNavigate();
-
-  if (account === undefined) {
-    return null;
-  }
-  if (account === null) {
-    return (
-      <Button asChild size="sm">
-        <a href={Setting.getSigninUrl() || "/signin"}>{i18next.t("account:Sign 
In")}</a>
-      </Button>
-    );
-  }
-
-  const name = account.displayName || account.name || "";
-  const openProfile = () => {
-    const profileUrl = Setting.getMyProfileUrl(account);
-    if (profileUrl === "") {
-      navigate("/account");
-    } else {
-      Setting.openLink(profileUrl);
-    }
-  };
-
-  return (
-    <DropdownMenu>
-      <DropdownMenuTrigger asChild>
-        <button
-          type="button"
-          className="hover:bg-accent flex items-center gap-2 rounded-md px-1.5 
py-1 transition-colors"
-        >
-          <Avatar>
-            {account.avatar ? <AvatarImage src={account.avatar} alt={name} /> 
: null}
-            <AvatarFallback style={{backgroundColor: 
Setting.getAvatarColor(account.name), color: "#fff"}}>
-              {Setting.getShortName(account.name).slice(0, 2).toUpperCase()}
-            </AvatarFallback>
-          </Avatar>
-          <span className="hidden max-w-[120px] truncate text-sm font-medium 
sm:inline">{name}</span>
-        </button>
-      </DropdownMenuTrigger>
-      <DropdownMenuContent align="end" className="w-48">
-        <DropdownMenuItem onClick={openProfile}>
-          <User />
-          {i18next.t("account:My Account")}
-        </DropdownMenuItem>
-        <DropdownMenuSeparator />
-        <DropdownMenuItem variant="destructive" onClick={onSignout}>
-          <LogOut />
-          {i18next.t("account:Sign Out")}
-        </DropdownMenuItem>
-      </DropdownMenuContent>
-    </DropdownMenu>
-  );
-}
-
-/**
- * The top bar of the content column: where you are on the left, and everything
- * about the viewer themselves on the right. It sticks so the account menu 
stays
- * reachable on a long list page.
- */
-export function AppHeader({
-  collapsed,
-  onToggleSidebar,
-  uri,
-  account,
-  isAdmin,
-  themeAlgorithm,
-  onThemeChange,
-  onSignout,
-  onOpenPalette,
-}: {
-  collapsed: boolean;
-  onToggleSidebar: () => void;
-  uri: string;
-  account: Account | null | undefined;
-  isAdmin: boolean;
-  themeAlgorithm: ThemeAlgorithm;
-  onThemeChange: (next: ThemeAlgorithm) => void;
-  onSignout: () => void;
-  onOpenPalette: () => void;
-}) {
-  return (
-    <header className="bg-background/95 
supports-[backdrop-filter]:bg-background/80 sticky top-0 z-30 flex h-13 
shrink-0 items-center justify-between gap-2 border-b px-2 backdrop-blur">
-      <div className="flex min-w-0 items-center gap-1">
-        <Button
-          variant="ghost"
-          size="icon-sm"
-          onClick={onToggleSidebar}
-          aria-label={i18next.t("general:Toggle navigation")}
-        >
-          {collapsed ? <PanelLeft className="size-4" /> : <PanelLeftClose 
className="size-4" />}
-        </Button>
-        <BreadcrumbBar uri={uri} />
-      </div>
-
-      <div className="flex items-center gap-1.5 pr-1">
-        {account ? <CommandPaletteTrigger onOpen={onOpenPalette} /> : null}
-        <VersionPanel isAdmin={isAdmin} signedIn={account !== null && account 
!== undefined} />
-        <ThemeToggle themeAlgorithm={themeAlgorithm} onChange={onThemeChange} 
/>
-        <LanguageSelect />
-        <AccountMenu account={account} onSignout={onSignout} />
-      </div>
-    </header>
-  );
-}
+// Copyright 2026 The casbin Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as React from "react";
+import {useNavigate} from "react-router-dom";
+import {ChevronDown, Languages, LogOut, Moon, PanelLeft, PanelLeftClose, 
Settings, Sun} from "lucide-react";
+import i18next from "i18next";
+
+import * as Setting from "@/Setting";
+import type {Account, ThemeAlgorithm} from "@/types";
+import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar";
+import {Button} from "@/components/ui/button";
+import {
+  DropdownMenu,
+  DropdownMenuContent,
+  DropdownMenuItem,
+  DropdownMenuLabel,
+  DropdownMenuSeparator,
+  DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import {SimpleTooltip} from "@/components/ui/tooltip";
+import {cn} from "@/lib/utils";
+import {BreadcrumbBar} from "@/components/shared/breadcrumb-bar";
+import {CommandPaletteTrigger} from "@/components/shared/command-palette";
+import {VersionPanel} from "@/components/shared/version-panel";
+
+function ThemeToggle({
+  themeAlgorithm,
+  onChange,
+}: {
+  themeAlgorithm: ThemeAlgorithm;
+  onChange: (next: ThemeAlgorithm) => void;
+}) {
+  const isDark = Setting.isDarkTheme(themeAlgorithm);
+  return (
+    <SimpleTooltip title={isDark ? i18next.t("general:Light") : 
i18next.t("general:Dark")}>
+      <Button
+        variant="ghost"
+        size="icon-sm"
+        onClick={() => onChange(isDark ? ["default"] : ["dark"])}
+        aria-label="Toggle theme"
+      >
+        {isDark ? <Sun className="size-4" /> : <Moon className="size-4" />}
+      </Button>
+    </SimpleTooltip>
+  );
+}
+
+function LanguageSelect() {
+  const [, forceRender] = React.useReducer((count: number) => count + 1, 0);
+  const current = Setting.getLanguage();
+  return (
+    <DropdownMenu>
+      <DropdownMenuTrigger asChild>
+        <Button variant="ghost" size="icon-sm" aria-label="Language">
+          <Languages />
+        </Button>
+      </DropdownMenuTrigger>
+      <DropdownMenuContent align="end" className="max-h-80 overflow-y-auto">
+        {Setting.Countries.map(country => (
+          <DropdownMenuItem
+            key={country.key}
+            className={cn(current === country.key && "font-semibold")}
+            onSelect={() => {
+              Setting.setLanguage(country.key);
+              // The shell reads i18next.t directly, which does not re-render 
on
+              // a language change by itself. Forcing a render here is what 
makes
+              // the switch take effect immediately.
+              forceRender();
+            }}
+          >
+            <img
+              
src={`${Setting.StaticBaseUrl}/flag-icons/${country.country}.svg`}
+              alt={country.alt}
+              className="h-4 w-5 rounded-[2px] object-cover"
+            />
+            {country.label}
+          </DropdownMenuItem>
+        ))}
+      </DropdownMenuContent>
+    </DropdownMenu>
+  );
+}
+
+function AccountMenu({
+  account,
+  onSignout,
+}: {
+  account: Account | null | undefined;
+  onSignout: () => void;
+}) {
+  const navigate = useNavigate();
+
+  if (account === undefined) {
+    return null;
+  }
+  if (account === null) {
+    return (
+      <Button asChild size="sm">
+        <a href={Setting.getSigninUrl() || "/signin"}>{i18next.t("account:Sign 
In")}</a>
+      </Button>
+    );
+  }
+
+  const name = account.displayName || account.name || "";
+  const openProfile = () => {
+    const profileUrl = Setting.getMyProfileUrl(account);
+    if (profileUrl === "") {
+      navigate("/account");
+    } else {
+      Setting.openLink(profileUrl);
+    }
+  };
+
+  return (
+    <DropdownMenu>
+      <DropdownMenuTrigger asChild>
+        <button type="button" className="hover:bg-accent ml-1 flex 
items-center gap-2 rounded-md p-1 transition-colors">
+          <Avatar>
+            {account.avatar ? <AvatarImage src={account.avatar} alt={name} /> 
: null}
+            <AvatarFallback style={{backgroundColor: 
Setting.getAvatarColor(account.name), color: "#fff"}}>
+              {Setting.getShortName(account.name).slice(0, 2).toUpperCase()}
+            </AvatarFallback>
+          </Avatar>
+          <span className="hidden max-w-[160px] truncate text-sm 
md:inline">{name}</span>
+          <ChevronDown className="h-3.5 w-3.5 opacity-60" />
+        </button>
+      </DropdownMenuTrigger>
+      <DropdownMenuContent align="end" className="w-56">
+        <DropdownMenuLabel className="truncate font-normal">
+          <div className="text-sm font-medium">{name}</div>
+          <div className="text-muted-foreground truncate text-xs">
+            {account.owner}/{account.name}
+          </div>
+        </DropdownMenuLabel>
+        <DropdownMenuSeparator />
+        <DropdownMenuItem onSelect={openProfile}>
+          <Settings />
+          {i18next.t("account:My Account")}
+        </DropdownMenuItem>
+        <DropdownMenuItem onSelect={onSignout}>
+          <LogOut />
+          {i18next.t("account:Sign Out")}
+        </DropdownMenuItem>
+      </DropdownMenuContent>
+    </DropdownMenu>
+  );
+}
+
+/**
+ * The top bar of the content column: where you are on the left, and everything
+ * about the viewer themselves on the right. It sticks so the account menu 
stays
+ * reachable on a long list page.
+ */
+export function AppHeader({
+  collapsed,
+  onToggleSidebar,
+  uri,
+  account,
+  isAdmin,
+  themeAlgorithm,
+  onThemeChange,
+  onSignout,
+  onOpenPalette,
+}: {
+  collapsed: boolean;
+  onToggleSidebar: () => void;
+  uri: string;
+  account: Account | null | undefined;
+  isAdmin: boolean;
+  themeAlgorithm: ThemeAlgorithm;
+  onThemeChange: (next: ThemeAlgorithm) => void;
+  onSignout: () => void;
+  onOpenPalette: () => void;
+}) {
+  return (
+    <header className="bg-background/95 
supports-[backdrop-filter]:bg-background/80 sticky top-0 z-30 flex h-13 
shrink-0 items-center justify-between gap-2 border-b px-2 backdrop-blur">
+      <div className="flex min-w-0 items-center gap-1">
+        <Button
+          variant="ghost"
+          size="icon-sm"
+          onClick={onToggleSidebar}
+          aria-label={i18next.t("general:Toggle navigation")}
+        >
+          {collapsed ? <PanelLeft className="size-4" /> : <PanelLeftClose 
className="size-4" />}
+        </Button>
+        <BreadcrumbBar uri={uri} />
+      </div>
+
+      <div className="flex items-center gap-1.5 pr-1">
+        {account ? <CommandPaletteTrigger onOpen={onOpenPalette} /> : null}
+        <VersionPanel isAdmin={isAdmin} signedIn={account !== null && account 
!== undefined} />
+        <ThemeToggle themeAlgorithm={themeAlgorithm} onChange={onThemeChange} 
/>
+        <LanguageSelect />
+        <AccountMenu account={account} onSignout={onSignout} />
+      </div>
+    </header>
+  );
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to