diff --git a/entrypoints/settings/tabs/voices/VoicesPanel.tsx b/entrypoints/settings/tabs/voices/VoicesPanel.tsx
index 2c8488ed5f..29e0264f38 100644
--- a/entrypoints/settings/tabs/voices/VoicesPanel.tsx
+++ b/entrypoints/settings/tabs/voices/VoicesPanel.tsx
@@ -21,6 +21,13 @@ export function VoicesPanel() {
Choose the voice that reads replies aloud on each site.
+ {/* Each pill wears its assistant's logo and brand color (#473) so
+ users orient at a glance — matching the platform cards on
+ saypi.ai/features/sites-supported. Logos are decorative (the pill
+ text is the accessible label), hence alt="" + aria-hidden; the
+ active state is also carried by aria-selected and font weight, so
+ color is never the only signal. Assets live in public/icons/logos/,
+ served from the extension root. */}
diff --git a/entrypoints/settings/tabs/voices/voices-controller.ts b/entrypoints/settings/tabs/voices/voices-controller.ts
index f38ee2825e..05adfc98be 100644
--- a/entrypoints/settings/tabs/voices/voices-controller.ts
+++ b/entrypoints/settings/tabs/voices/voices-controller.ts
@@ -73,7 +73,11 @@ export class VoicesController {
this.container
.querySelectorAll(".voice-host-pill")
.forEach((pill) => {
- pill.classList.toggle("active", pill.id === `voice-host-${host}`);
+ const isActive = pill.id === `voice-host-${host}`;
+ pill.classList.toggle("active", isActive);
+ // The pills are a tablist; state must be programmatic too, not just
+ // the brand-color accent (#473).
+ pill.setAttribute("aria-selected", String(isActive));
});
const [voices, current] = await Promise.all([
diff --git a/entrypoints/settings/tabs/voices/voices.css b/entrypoints/settings/tabs/voices/voices.css
index 863d4100d5..95a7a30c06 100644
--- a/entrypoints/settings/tabs/voices/voices.css
+++ b/entrypoints/settings/tabs/voices/voices.css
@@ -6,6 +6,9 @@
margin: 0.5rem 0 0.75rem;
}
.voice-host-pill {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
border: 1px solid #d1d5db;
border-radius: 9999px;
background: transparent;
@@ -13,10 +16,29 @@
font-size: 0.85rem;
cursor: pointer;
}
+.voice-host-logo {
+ width: 16px;
+ height: 16px;
+ border-radius: 4px;
+ object-fit: contain;
+}
+/* The active pill takes on its assistant's brand palette (#473): Pi's forest
+ green on cream, Claude's terracotta on ivory. Weight + border + aria-selected
+ carry the state too — color is an accent, never the only signal. */
.voice-host-pill.active {
background: #e5e7eb;
font-weight: 600;
}
+#voice-host-pi.active {
+ background: #f5efe1;
+ border-color: #12805c;
+ color: #0b5c41;
+}
+#voice-host-claude.active {
+ background: #f8f0e9;
+ border-color: #d97757;
+ color: #ae5630;
+}
.voice-shelf {
margin-bottom: 0.75rem;
}
diff --git a/public/icons/logos/claude.png b/public/icons/logos/claude.png
new file mode 100644
index 0000000000..5057173581
Binary files /dev/null and b/public/icons/logos/claude.png differ
diff --git a/public/icons/logos/pi.png b/public/icons/logos/pi.png
new file mode 100644
index 0000000000..921e9a7d7c
Binary files /dev/null and b/public/icons/logos/pi.png differ
diff --git a/test/settings/tabs/VoicesPanel.spec.tsx b/test/settings/tabs/VoicesPanel.spec.tsx
index 3da23934c3..cb1061c2b6 100644
--- a/test/settings/tabs/VoicesPanel.spec.tsx
+++ b/test/settings/tabs/VoicesPanel.spec.tsx
@@ -28,4 +28,23 @@ describe("VoicesPanel", () => {
]);
expect(container.querySelector("#voice-catalog")).toBeTruthy();
});
+
+ it("brands each pill with its assistant's logo while keeping the text label (#473)", () => {
+ const { container } = render();
+ const piLogo = container.querySelector(
+ "#voice-host-pi img.voice-host-logo"
+ );
+ const claudeLogo = container.querySelector(
+ "#voice-host-claude img.voice-host-logo"
+ );
+ expect(piLogo?.getAttribute("src")).toBe("/icons/logos/pi.png");
+ expect(claudeLogo?.getAttribute("src")).toBe("/icons/logos/claude.png");
+ // Decorative images: the pill text stays the accessible label.
+ expect(piLogo?.getAttribute("alt")).toBe("");
+ expect(claudeLogo?.getAttribute("aria-hidden")).toBe("true");
+ expect(container.querySelector("#voice-host-pi")?.textContent?.trim()).toBe("Pi");
+ expect(
+ container.querySelector("#voice-host-claude")?.textContent?.trim()
+ ).toBe("Claude");
+ });
});
diff --git a/test/settings/tabs/voices-controller.spec.tsx b/test/settings/tabs/voices-controller.spec.tsx
index b74e9255f6..8cfa905b50 100644
--- a/test/settings/tabs/voices-controller.spec.tsx
+++ b/test/settings/tabs/voices-controller.spec.tsx
@@ -60,6 +60,18 @@ describe("VoicesController", () => {
expect(rows.length).toBe(flipDayClaude.length);
});
+ it("mirrors the active pill into aria-selected, not just the color accent (#473)", async () => {
+ const { container } = await mount();
+ const pi = container.querySelector("#voice-host-pi")!;
+ const claude = container.querySelector("#voice-host-claude")!;
+ expect(pi.getAttribute("aria-selected")).toBe("true");
+ expect(claude.getAttribute("aria-selected")).toBe("false");
+ (claude as HTMLElement).click();
+ await flushAsync();
+ expect(pi.getAttribute("aria-selected")).toBe("false");
+ expect(claude.getAttribute("aria-selected")).toBe("true");
+ });
+
it("groups a mixed-tier catalog into HD and Everyday shelves", async () => {
const { container } = await mount();
(container.querySelector("#voice-host-claude") as HTMLElement).click();