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 (
+
+ );
+ }
+
+ 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 (
-
- );
- }
-
- 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 (
-
+