diff --git a/DESIGN.md b/DESIGN.md
index d6e905f6b4..8bde7a684d 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -155,8 +155,8 @@ exist yet, add it in react-ui, not locally.
**Avatar identity** is the one deliberate color exception. A person without
an explicit image receives deterministic initials from the approved pastel
-palette. `@corbits/chat-ui` owns the finite Tailwind background/text class
-map and resolves a principal to one class pair; consumers never use CSS
+palette. `@corbits/chat-ui` owns the finite Tailwind color palette and
+resolves a principal to one class pair; consumers never use CSS
variables or inline color styles. Agents use `CorbitAvatar`: a circular
field with a dark lower visor and sensor glint. The two forms stay distinct
at a glance in every collaborative surface.
diff --git a/apps/web/src/pages/create-agent-panel.tsx b/apps/web/src/pages/create-agent-panel.tsx
index b058eee921..1cd27bb684 100644
--- a/apps/web/src/pages/create-agent-panel.tsx
+++ b/apps/web/src/pages/create-agent-panel.tsx
@@ -53,8 +53,7 @@ import {
import type { IntakeField } from "@corbits/react-ui";
import { useEffect, useState } from "react";
import {
- AVATAR_COLORS,
- avatarColorClass,
+ AVATAR_PALETTE,
CORBIT_DEFAULT_COLOR,
CorbitAvatar,
} from "@corbits/chat-ui";
@@ -434,13 +433,13 @@ export function CreateAgentPanel({
size="lg"
/>
- {AVATAR_COLORS.map((color) => (
+ {AVATAR_PALETTE.map(({ color, className }) => (
setAvatarColor(color)}
/>
diff --git a/packages/chat-ui/src/avatar.test.tsx b/packages/chat-ui/src/avatar.test.tsx
index bb6bed6931..5869236454 100644
--- a/packages/chat-ui/src/avatar.test.tsx
+++ b/packages/chat-ui/src/avatar.test.tsx
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
import { renderToStaticMarkup } from "react-dom/server";
import {
- AVATAR_COLORS,
+ AVATAR_PALETTE,
CORBIT_DEFAULT_COLOR,
CORBIT_GLINT_COLOR,
CORBIT_VISOR_COLOR,
@@ -29,7 +29,9 @@ describe("avatarColorForPrincipal", () => {
"prn_frank",
];
for (const p of principals) {
- expect(AVATAR_COLORS).toContain(avatarColorForPrincipal(p));
+ expect(AVATAR_PALETTE.map((entry) => entry.color)).toContain(
+ avatarColorForPrincipal(p),
+ );
}
});
@@ -55,6 +57,11 @@ describe("avatarClassForPrincipal", () => {
avatarClassForPrincipal("prn_super_secret_internal_id"),
).not.toContain("prn_super_secret_internal_id");
});
+
+ test("selects one of the palette's Tailwind class pairs", () => {
+ const paletteClassNames = AVATAR_PALETTE.map((entry) => entry.className);
+ expect(paletteClassNames).toContain(avatarClassForPrincipal("prn_alice"));
+ });
});
describe("resolveAvatarFill", () => {
diff --git a/packages/chat-ui/src/avatar.tsx b/packages/chat-ui/src/avatar.tsx
index 258b8ac045..6a97e9ba88 100644
--- a/packages/chat-ui/src/avatar.tsx
+++ b/packages/chat-ui/src/avatar.tsx
@@ -1,21 +1,14 @@
import type { CSSProperties } from "react";
export const CORBIT_DEFAULT_COLOR = "#C5D2DE";
-export const AVATAR_COLORS = [
- CORBIT_DEFAULT_COLOR,
- "#C1D1BE",
- "#F7EAD5",
- "#F2B277",
+export const AVATAR_PALETTE = [
+ { color: CORBIT_DEFAULT_COLOR, className: "bg-[#C5D2DE] text-black" },
+ { color: "#C1D1BE", className: "bg-[#C1D1BE] text-black" },
+ { color: "#F7EAD5", className: "bg-[#F7EAD5] text-black" },
+ { color: "#F2B277", className: "bg-[#F2B277] text-black" },
] as const;
-export type AvatarColor = (typeof AVATAR_COLORS)[number];
-
-export const avatarColorClass: Record = {
- "#C5D2DE": "bg-[#C5D2DE] text-black",
- "#C1D1BE": "bg-[#C1D1BE] text-black",
- "#F7EAD5": "bg-[#F7EAD5] text-black",
- "#F2B277": "bg-[#F2B277] text-black",
-};
+export type AvatarColor = (typeof AVATAR_PALETTE)[number]["color"];
export function hashPrincipal(principalId: string): number {
let hash = 0;
@@ -25,18 +18,22 @@ export function hashPrincipal(principalId: string): number {
return hash;
}
-export function avatarColorForPrincipal(principalId: string): AvatarColor {
+function avatarPaletteEntryForPrincipal(principalId: string) {
const hash = hashPrincipal(principalId);
- const index = hash % AVATAR_COLORS.length;
- const color = AVATAR_COLORS[index];
- if (color === undefined) {
+ const index = hash % AVATAR_PALETTE.length;
+ const entry = AVATAR_PALETTE[index];
+ if (entry === undefined) {
throw new Error("Avatar color palette is empty");
}
- return color;
+ return entry;
+}
+
+export function avatarColorForPrincipal(principalId: string): AvatarColor {
+ return avatarPaletteEntryForPrincipal(principalId).color;
}
export function avatarClassForPrincipal(principalId: string): string {
- return avatarColorClass[avatarColorForPrincipal(principalId)];
+ return avatarPaletteEntryForPrincipal(principalId).className;
}
export type AvatarFill =
diff --git a/packages/chat-ui/src/index.ts b/packages/chat-ui/src/index.ts
index 0215c5cec1..f573448d71 100644
--- a/packages/chat-ui/src/index.ts
+++ b/packages/chat-ui/src/index.ts
@@ -24,8 +24,7 @@ export {
CORBIT_DEFAULT_COLOR,
CORBIT_VISOR_COLOR,
CORBIT_GLINT_COLOR,
- AVATAR_COLORS,
- avatarColorClass,
+ AVATAR_PALETTE,
avatarColorForPrincipal,
avatarClassForPrincipal,
resolveAvatarFill,
diff --git a/packages/chat-ui/test/avatar-composition.test.tsx b/packages/chat-ui/test/avatar-composition.test.tsx
index 35579bfcae..2fdebf34aa 100644
--- a/packages/chat-ui/test/avatar-composition.test.tsx
+++ b/packages/chat-ui/test/avatar-composition.test.tsx
@@ -3,7 +3,7 @@ import { renderToStaticMarkup } from "react-dom/server";
import {
CorbitAvatar,
- AVATAR_COLORS,
+ AVATAR_PALETTE,
CORBIT_DEFAULT_COLOR,
avatarClassForPrincipal,
avatarColorForPrincipal,
@@ -26,8 +26,9 @@ describe("avatar composition and identity resolution", () => {
const aliceColor = avatarColorForPrincipal("usr_alice");
const bobColor = avatarColorForPrincipal("usr_bob");
- expect(AVATAR_COLORS).toContain(aliceColor);
- expect(AVATAR_COLORS).toContain(bobColor);
+ const avatarColors = AVATAR_PALETTE.map((entry) => entry.color);
+ expect(avatarColors).toContain(aliceColor);
+ expect(avatarColors).toContain(bobColor);
expect(avatarClassForPrincipal("usr_alice")).toContain(aliceColor);