From b1023ba8ff8aca4a2b1bc0a6c3e1f01ed88150da Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Mon, 19 Oct 2026 05:35:42 +0100 Subject: [PATCH] Share one user avatar between the rail and the profile page UserAvatar (src/components/general/user-avatar.tsx) replaces the profile page's ProfileAvatar and the rail's initials chip, so the rail shows the user's photo too. Co-Authored-By: Claude Opus 5.5 --- src/components/general/user-avatar.tsx | 62 +++++++++++++++++++ src/layouts/bots/(partials)/rail-user-row.tsx | 15 +++-- .../profile/(partials)/profile-avatar.tsx | 32 ---------- .../(partials)/profile-photo-field.tsx | 9 ++- 4 files changed, 79 insertions(+), 39 deletions(-) create mode 100644 src/components/general/user-avatar.tsx delete mode 100644 src/pages/admin/settings/profile/(partials)/profile-avatar.tsx diff --git a/src/components/general/user-avatar.tsx b/src/components/general/user-avatar.tsx new file mode 100644 index 0000000..009de87 --- /dev/null +++ b/src/components/general/user-avatar.tsx @@ -0,0 +1,62 @@ +import Img from "@moduletrace/twui/layout/Img"; +import Span from "@moduletrace/twui/layout/Span"; +import { twMerge } from "tailwind-merge"; + +/** + * Round avatar sizes, each with the type scale for its initials. + */ +const UserAvatarSizeClasses = { + sm: "size-6 text-[11px]", + md: "size-8 text-xs", + lg: "size-16 text-lg", +} as const; + +type Props = { + src?: string; + /** + * Tried when `src` fails to load, e.g. a full image behind a thumbnail. + */ + img_err_src?: string; + initials: string; + alt?: string; + size?: keyof typeof UserAvatarSizeClasses; + className?: string; +}; + +/** + * A user's round photo, or their initials on sunken paper when there is none. + */ +export default function UserAvatar({ + src, + img_err_src, + initials, + alt = "", + size = "md", + className, +}: Props) { + const base_class = twMerge( + "shrink-0 rounded-full bg-sunken", + UserAvatarSizeClasses[size], + src + ? "object-cover" + : "flex items-center justify-center font-bold leading-none text-ink", + className, + ); + + if (src) { + return ( + {alt} + ); + } + + return ( + + {initials || "?"} + + ); +} diff --git a/src/layouts/bots/(partials)/rail-user-row.tsx b/src/layouts/bots/(partials)/rail-user-row.tsx index 69cb29c..d4c5c97 100644 --- a/src/layouts/bots/(partials)/rail-user-row.tsx +++ b/src/layouts/bots/(partials)/rail-user-row.tsx @@ -4,6 +4,7 @@ import Row from "@moduletrace/twui/layout/Row"; import Stack from "@moduletrace/twui/layout/Stack"; import Span from "@moduletrace/twui/layout/Span"; import { ChevronRight } from "lucide-react"; +import UserAvatar from "@/src/components/general/user-avatar"; import { AppContext } from "@/src/pages/__root"; import RailUserMenu from "./rail-user-menu"; import Tag from "@moduletrace/twui/elements/Tag"; @@ -35,20 +36,24 @@ export default function RailUserRow() { aria-label={`Account menu for ${name}`} className="w-full h-12 gap-3! flex-nowrap items-center rounded-default px-1 lg:px-0 cursor-pointer hover:bg-sunken transition-colors duration-[var(--m-fast)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ink focus-visible:ring-offset-2 focus-visible:ring-offset-paper" > - - {initials} - + {name} - + {user_types?.map((ut, indx) => { return ( {ut.user_type} diff --git a/src/pages/admin/settings/profile/(partials)/profile-avatar.tsx b/src/pages/admin/settings/profile/(partials)/profile-avatar.tsx deleted file mode 100644 index 001cf26..0000000 --- a/src/pages/admin/settings/profile/(partials)/profile-avatar.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import Img from "@moduletrace/twui/layout/Img"; -import Span from "@moduletrace/twui/layout/Span"; - -type Props = { - src?: string; - initials: string; -}; - -/** - * 64px round photo, or the initials on sunken paper when there is none. - * Same treatment as the account row in the rail, just larger. - */ -export default function ProfileAvatar({ src, initials }: Props) { - if (src) { - return ( - Your photo - ); - } - - return ( - - {initials || "?"} - - ); -} diff --git a/src/pages/admin/settings/profile/(partials)/profile-photo-field.tsx b/src/pages/admin/settings/profile/(partials)/profile-photo-field.tsx index ab55fe7..8f33991 100644 --- a/src/pages/admin/settings/profile/(partials)/profile-photo-field.tsx +++ b/src/pages/admin/settings/profile/(partials)/profile-photo-field.tsx @@ -6,7 +6,7 @@ import Button from "@moduletrace/twui/layout/Button"; import imageInputToBase64, { type ImageInputToBase64FunctionReturn, } from "@moduletrace/twui/utils/form/imageInputToBase64"; -import ProfileAvatar from "./profile-avatar"; +import UserAvatar from "@/src/components/general/user-avatar"; type Props = { saved_src?: string; @@ -32,7 +32,12 @@ export default function ProfilePhotoField({ return ( - +