Skip to content

Commit 2e825fd

Browse files
committed
fix(webapp): theme the agent's mono logo so it survives the light theme
The dot-matrix logo is drawn on canvas with a white-based ramp, so on the light theme it was white ink on a white surface: the chat spinner, the panel's hero logo and the Ask AI button's glyph all disappeared. Adds a useThemeMode hook and an AgentMonoLogo wrapper that picks the ink from the active theme, and routes every mono call site through it.
1 parent 81eefe4 commit 2e825fd

7 files changed

Lines changed: 77 additions & 39 deletions

File tree

apps/webapp/app/components/dashboard-agent/DashboardAgentHero.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { AgentPageContext, SuggestedPrompt } from "@internal/dashboard-agent-contracts";
22
import { BetaBadge } from "~/components/FeatureBadges";
3-
import { AgentDotMatrix } from "~/components/primitives/AgentDotMatrix";
3+
import { AgentMonoLogo } from "~/components/primitives/AgentDotMatrix";
44
import { Header1 } from "~/components/primitives/Headers";
55
import { Paragraph } from "~/components/primitives/Paragraph";
66
import { DashboardAgentSuggestedPrompts } from "./DashboardAgentSuggestedPrompts";
@@ -39,7 +39,7 @@ export function DashboardAgentHero({
3939
<div className="flex w-full max-w-2xl flex-col items-center gap-5">
4040
<div className="flex flex-col items-center gap-1.5 text-center">
4141
<Header1 className="flex items-center gap-2">
42-
<AgentDotMatrix size={22} palette="mono" restColor="#ffffff" decorative />
42+
<AgentMonoLogo size={22} decorative />
4343
Ask AI
4444
<BetaBadge />
4545
</Header1>

apps/webapp/app/components/primitives/AgentDotMatrix.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { type CSSProperties, useEffect, useRef } from "react";
2+
import { useThemeMode } from "~/hooks/useThemeMode";
23

34
// Our own 5x5 dot-matrix system, reverse-engineered from dotmatrix
45
// (github.com/zzzzshawn/matrix) but written from scratch on canvas.
@@ -117,6 +118,8 @@ export type DotMatrixPalette = {
117118

118119
export const DOT_MATRIX_PALETTES = {
119120
mono: { stops: ["#e2e8f0", "#ffffff", "#94a3b8"], glow: "#ffffff" },
121+
/** `mono` inverted, for light surfaces — the white ramp vanishes on white. */
122+
monoLight: { stops: ["#0d0e12", "#1a1b1f", "#3b3e45"], glow: "#1a1b1f" },
120123
trigger: { stops: ["#41ff54", "#a4ff53", "#e7ff52"], glow: "#86ff53" },
121124
aurora: { stops: ["#ff3cac", "#784ba0", "#2b86c5"], glow: "#9c64bf" },
122125
ocean: { stops: ["#00c6ff", "#0072ff", "#4facfe"], glow: "#2f8fff" },
@@ -631,3 +634,25 @@ export function AgentDotMatrix({
631634
/>
632635
);
633636
}
637+
638+
/**
639+
* The agent's monochrome logo — the glyph that stands for the agent wherever it
640+
* appears (the ask-ai button, the panel's hero, the chat spinner).
641+
*
642+
* Use this instead of setting `palette="mono"` by hand: the mono ramp is
643+
* white-based, so on the light theme it would be white ink on a white panel.
644+
* The matrix draws on canvas and can't read a CSS variable, so the ink is
645+
* picked from the active theme here.
646+
*/
647+
export function AgentMonoLogo(props: Omit<AgentDotMatrixProps, "palette" | "restColor" | "mode">) {
648+
const mode = useThemeMode();
649+
const light = mode === "light";
650+
return (
651+
<AgentDotMatrix
652+
{...props}
653+
mode={mode}
654+
palette={light ? "monoLight" : "mono"}
655+
restColor={light ? DOT_MATRIX_PALETTES.monoLight.glow : DOT_MATRIX_PALETTES.mono.glow}
656+
/>
657+
);
658+
}

apps/webapp/app/components/primitives/Buttons.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import React, {
99
} from "react";
1010
import { type ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys";
1111
import { cn } from "~/utils/cn";
12-
import { AgentDotMatrix } from "./AgentDotMatrix";
12+
import { AgentMonoLogo } from "./AgentDotMatrix";
1313
import { ShortcutKey } from "./ShortcutKey";
1414
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./Tooltip";
1515
import { Icon, type RenderIcon } from "./Icon";
@@ -144,16 +144,16 @@ function createVariant(sizeName: Size, themeName: Theme) {
144144
// The ask-ai button always leads with the square agent logo, so it supplies its
145145
// own leading icon and its padding is tuned around it: small = 16px logo, 4px
146146
// left / 6px right; medium 32/16 -> 8px; large 40/20 -> 10px. Pass an explicit
147-
// `LeadingIcon` (e.g. an <AgentDotMatrix active />) to animate it.
147+
// `LeadingIcon` (e.g. an <AgentMonoLogo active />) to animate it.
148148
function createAskAiVariant(sizeName: Size, opticalPadding: string, logoSize: number) {
149149
const base = createVariant(sizeName, "ask-ai");
150150
return {
151151
...base,
152152
button: cn(base.button, opticalPadding),
153153
iconSpacing: "gap-x-1.5",
154-
defaultLeadingIcon: (
155-
<AgentDotMatrix size={logoSize} palette="mono" restColor="#ffffff" decorative />
156-
),
154+
// The button is `bg-secondary` — charcoal on dark, white on light — so the
155+
// logo has to follow the theme or it disappears on one of them.
156+
defaultLeadingIcon: <AgentMonoLogo size={logoSize} decorative />,
157157
};
158158
}
159159

apps/webapp/app/components/primitives/Spinner.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AgentDotMatrix } from "~/components/primitives/AgentDotMatrix";
1+
import { AgentMonoLogo } from "~/components/primitives/AgentDotMatrix";
22
import { cn } from "~/utils/cn";
33

44
type CustomColor = {
@@ -86,14 +86,12 @@ export function SpinnerWhite({ className }: { className?: string }) {
8686
*/
8787
export function AgentSpinner({ size = 16 }: { size?: number }) {
8888
return (
89-
<AgentDotMatrix
89+
<AgentMonoLogo
9090
size={size}
9191
active
9292
// A spinner is born spinning: resting on the playlist's first shape means
9393
// the initial frame is already mid-cycle — no logo-head flash on mount.
9494
restShape="square"
95-
palette="mono"
96-
restColor="#ffffff"
9795
decorative
9896
/>
9997
);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { useEffect, useState } from "react";
2+
3+
/** Whether the active theme paints on dark or light surfaces. */
4+
export type ThemeMode = "dark" | "light";
5+
6+
/**
7+
* The active theme's mode, for the rare color that can't come from a CSS
8+
* variable — a canvas paints with concrete values, so it has to ask.
9+
*
10+
* Prefer a theme-aware token (`text-*`, `bg-*`, `var(--color-*)`) whenever the
11+
* color goes through CSS; this is the escape hatch for canvas and for props
12+
* that take a color string.
13+
*
14+
* `light` is the only mode that isn't dark, so anything else — `dark`,
15+
* `classic`, an unset attribute during SSR — resolves to `dark`. Resolution
16+
* happens in an effect so server and hydration renders agree (the pre-paint
17+
* script in `root.tsx` can flip `data-theme` before hydration), and a
18+
* `MutationObserver` keeps long-lived components correct across theme
19+
* switches. Same shape as `useThemeColor`.
20+
*/
21+
export function useThemeMode(): ThemeMode {
22+
const [mode, setMode] = useState<ThemeMode>("dark");
23+
useEffect(() => {
24+
const resolve = () => {
25+
setMode(document.documentElement.getAttribute("data-theme") === "light" ? "light" : "dark");
26+
};
27+
resolve();
28+
const observer = new MutationObserver(resolve);
29+
observer.observe(document.documentElement, {
30+
attributes: true,
31+
attributeFilter: ["data-theme"],
32+
});
33+
return () => observer.disconnect();
34+
}, []);
35+
return mode;
36+
}

apps/webapp/app/routes/storybook.agent-ui/route.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1365,7 +1365,8 @@ const WIDE_SECTIONS = new Set(["diagnosis-badge-matrix", "hero-fullscreen"]);
13651365
function ThemeToggle() {
13661366
return (
13671367
<div className="flex items-center gap-1.5">
1368-
{(["dark", "light"] as const).map((theme) => (
1368+
{/* classic is still the default theme for most users, so it's part of the pack */}
1369+
{(["classic", "dark", "light"] as const).map((theme) => (
13691370
<button
13701371
key={theme}
13711372
type="button"

apps/webapp/app/routes/storybook.ai-agent/route.tsx

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Header1, Header2 } from "~/components/primitives/Headers";
1111
import { Paragraph } from "~/components/primitives/Paragraph";
1212
import {
1313
AgentDotMatrix,
14+
AgentMonoLogo,
1415
DOT_MATRIX_PALETTES,
1516
DOT_SHAPES,
1617
EXTRA_FACE_SHAPES,
@@ -59,12 +60,6 @@ export default function Story() {
5960

6061
// --- Dot matrix (5x5) ---------------------------------------------------------
6162

62-
// Dark-ink mono ramp for light surfaces (the built-in mono palette is white-based).
63-
const LIGHT_MONO = {
64-
stops: ["#0d0e12", "#1a1b1f", "#3b3e45"] as [string, string, string],
65-
glow: "#1a1b1f",
66-
};
67-
6863
function DotMatrixTab() {
6964
return (
7065
<div className="flex flex-col gap-6 py-6">
@@ -133,10 +128,10 @@ function DotMatrixTab() {
133128
/>
134129
</div>
135130
<div className="flex items-center gap-8 rounded-md border border-grid-bright bg-charcoal-100 px-6 py-5">
136-
<AgentDotMatrix size={40} mode="light" palette={LIGHT_MONO} restColor="#1a1b1f" />
131+
<AgentDotMatrix size={40} mode="light" palette="monoLight" restColor="#1a1b1f" />
137132
<ToggleableMatrix
138133
size={40}
139-
matrix={{ mode: "light", palette: LIGHT_MONO, restColor: "#1a1b1f" }}
134+
matrix={{ mode: "light", palette: "monoLight", restColor: "#1a1b1f" }}
140135
/>
141136
</div>
142137
</div>
@@ -202,15 +197,7 @@ function AskAiButton({ variant, matrixSize }: { variant: ButtonVariant; matrixSi
202197
<Button
203198
variant={variant}
204199
onClick={trigger}
205-
LeadingIcon={
206-
<AgentDotMatrix
207-
size={matrixSize}
208-
active={active}
209-
palette="mono"
210-
restColor="#ffffff"
211-
decorative
212-
/>
213-
}
200+
LeadingIcon={<AgentMonoLogo size={matrixSize} active={active} decorative />}
214201
>
215202
Ask AI
216203
</Button>
@@ -233,16 +220,7 @@ function FaceButton({ name }: { name: DotShapeName }) {
233220
<Button
234221
variant="ask-ai/small"
235222
onClick={trigger}
236-
LeadingIcon={
237-
<AgentDotMatrix
238-
size={16}
239-
active={active}
240-
restShape={name}
241-
palette="mono"
242-
restColor="#ffffff"
243-
decorative
244-
/>
245-
}
223+
LeadingIcon={<AgentMonoLogo size={16} active={active} restShape={name} decorative />}
246224
>
247225
{name}
248226
</Button>

0 commit comments

Comments
 (0)