fix(ui): polish the light palette, center the stage bloom (#108)

* fix(ui): make the light theme a real light theme

The web shell hardcoded dark surfaces (panel hex, white/black alphas,
white text), so switching to light left the sidebar, chat, composer and
side panel dark with ink text on top. Shell surfaces now use the shared
tokens: light gets a warm paper frame with white panels, hairline borders
and short soft shadows; dark keeps its look via dark: variants.

- Light palette: visible borders (border was equal to muted), white
  primary-foreground, lighter user bubble, readable warning/success.
- Selected sidebar row lives in one place (roster-row-style).
- Markdown, inline code, code blocks, quotes and scrollbars follow tokens.
- cn() now knows the text-ui-* scale is a font size; tailwind-merge
  treated it as a color and dropped labels' color on resized buttons
  (sign-up submit rendered ink on ink).
- Stage bloom was offset left by a double translateX.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016or6oqCqP8wbNafvhnbLQT

* refactor(ui-web): leave the cn() fix to #115

#115 fixes the same tailwind-merge bug more completely (text, control,
icon and spacing scales, guarded against styles.css) and adds the same
two files, so this branch drops its narrower copy to avoid the overlap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016or6oqCqP8wbNafvhnbLQT

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
cynarAI 2026-10-04 04:37:24 +02:00 committed by GitHub
parent 4ba7d843b1
commit c4c5ce3c2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 58 additions and 55 deletions

View File

@ -67,6 +67,9 @@ test("account settings appearance control switches to light mode", async ({ page
await settings.getByRole("button", { name: "Close user settings" }).click(); await settings.getByRole("button", { name: "Close user settings" }).click();
await expect(settings).toBeHidden(); await expect(settings).toBeHidden();
await expect(page.locator("html")).toHaveAttribute("data-theme", "light"); await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
// Light surfaces stay light: no dark panel or white-on-paper text survives the switch.
await expect(page.locator("main").first()).toHaveCSS("background-color", "rgb(255, 255, 255)");
await expect(page.locator("aside").first()).toHaveCSS("background-color", "rgb(245, 245, 242)");
await captureScreenshot(page, testInfo, "ui-appearance-light-shell"); await captureScreenshot(page, testInfo, "ui-appearance-light-shell");
await captureSidebarSearchSelected(page, testInfo, "sidebar-search-selected-light"); await captureSidebarSearchSelected(page, testInfo, "sidebar-search-selected-light");

View File

@ -26,7 +26,7 @@
document.documentElement.dataset.theme = theme; document.documentElement.dataset.theme = theme;
document.documentElement.style.colorScheme = theme; document.documentElement.style.colorScheme = theme;
const meta = document.querySelector('meta[name="theme-color"]'); const meta = document.querySelector('meta[name="theme-color"]');
if (meta) meta.setAttribute("content", theme === "light" ? "#fafaf8" : "#07080a"); if (meta) meta.setAttribute("content", theme === "light" ? "#ffffff" : "#07080a");
})(); })();
</script> </script>
<script> <script>

View File

@ -134,7 +134,7 @@ export function OnboardingScanProgress({
> >
<div className="flex-shrink-0"> <div className="flex-shrink-0">
{isDone ? ( {isDone ? (
<span className="grid size-6 items-center justify-center rounded-full bg-emerald-500/20 text-emerald-500 text-sm"> <span className="grid size-6 items-center justify-center rounded-full bg-emerald-500/20 text-emerald-700 text-sm dark:text-emerald-500">
✓ ✓
</span> </span>
) : isActive ? ( ) : isActive ? (
@ -174,7 +174,7 @@ export function OnboardingScanProgress({
<div className="mt-8 text-center"> <div className="mt-8 text-center">
<button <button
type="button" type="button"
className="rounded-xl bg-primary px-6 py-3 text-sm font-medium text-white" className="rounded-xl bg-primary px-6 py-3 text-sm font-medium text-primary-foreground"
onClick={onComplete} onClick={onComplete}
> >
Continue to your workspace → Continue to your workspace →

View File

@ -55,22 +55,22 @@ export function StageHero({
) : null} ) : null}
<div <div
className={`relative flex ${orbSize} items-center justify-center rounded-full border border-border bg-muted/45 shadow-[0_16px_48px_rgba(0,0,0,0.45),inset_0_1px_0_rgba(255,255,255,0.12)] backdrop-blur-xl`} className={`relative flex ${orbSize} items-center justify-center rounded-full border border-border bg-muted/45 shadow-[0_12px_32px_-12px_rgba(24,24,22,0.18)] backdrop-blur-xl dark:shadow-[0_16px_48px_rgba(0,0,0,0.45),inset_0_1px_0_rgba(255,255,255,0.12)]`}
style={
reducedMotion
? undefined
: {
boxShadow: `0 16px 48px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,255,255,0.12), 0 0 ${Math.round(18 + energy * 32)}px ${glowRgb}`,
}
}
> >
{reducedMotion ? null : (
<div
aria-hidden
className="pointer-events-none absolute inset-0 rounded-full"
style={{ boxShadow: `0 0 ${Math.round(18 + energy * 32)}px ${glowRgb}` }}
/>
)}
<BotAvatar <BotAvatar
color={botColor} color={botColor}
identity={botId} identity={botId}
status={botStatus} status={botStatus}
pose={pose} pose={pose}
size={avatarSize} size={avatarSize}
className="drop-shadow-[0_6px_24px_rgba(0,0,0,0.4)]" className="drop-shadow-[0_6px_16px_rgba(0,0,0,0.12)] dark:drop-shadow-[0_6px_24px_rgba(0,0,0,0.4)]"
/> />
</div> </div>

View File

@ -41,7 +41,7 @@ export function TextStageCanvas({
style={{ style={{
background: `radial-gradient(circle, ${phaseTint} 0%, ${personaTint} 42%, transparent 72%)`, background: `radial-gradient(circle, ${phaseTint} 0%, ${personaTint} 42%, transparent 72%)`,
opacity: 0.55 + energy * 0.2, opacity: 0.55 + energy * 0.2,
transform: `translateX(-50%) scale(${bloomScale})`, transform: `scale(${bloomScale})`,
transition: reducedMotion transition: reducedMotion
? undefined ? undefined
: "transform 140ms ease-out, opacity 140ms ease-out", : "transform 140ms ease-out, opacity 140ms ease-out",

View File

@ -84,17 +84,17 @@ export function ChoiceCard({
aria-label={option.label} aria-label={option.label}
disabled={Boolean(block.answerId) || pending} disabled={Boolean(block.answerId) || pending}
onClick={() => void choose(option.id)} onClick={() => void choose(option.id)}
className={`tactile-press flex w-full items-start gap-3.5 rounded-[18px] border border-white/5 p-3.5 text-start text-foreground disabled:opacity-60 transition-all ${ className={`tactile-press flex w-full items-start gap-3.5 rounded-[18px] border border-foreground/5 p-3.5 text-start text-foreground disabled:opacity-60 transition-all ${
block.answerId === option.id block.answerId === option.id
? "bg-blue-500/20 border-blue-500/40 shadow-sm" ? "bg-blue-500/20 border-blue-500/40 shadow-sm"
: "bg-white/[0.04] hover:bg-white/[0.08] hover:border-white/10" : "bg-foreground/[0.04] hover:bg-foreground/[0.08] hover:border-foreground/10"
}`} }`}
> >
<span <span
className={`mt-0.5 grid h-6 w-6 shrink-0 place-items-center rounded-[8px] text-ui-body-sm font-semibold ${ className={`mt-0.5 grid h-6 w-6 shrink-0 place-items-center rounded-[8px] text-ui-body-sm font-semibold ${
block.answerId === option.id block.answerId === option.id
? "bg-blue-500 text-white" ? "bg-blue-500 text-white"
: "bg-white/10 text-foreground/80" : "bg-foreground/10 text-foreground/80"
}`} }`}
> >
{option.letter} {option.letter}
@ -105,7 +105,7 @@ export function ChoiceCard({
{option.label} {option.label}
</span> </span>
{block.answerId === option.id ? ( {block.answerId === option.id ? (
<span className="mt-0.5 text-blue-400 font-bold">✓</span> <span className="mt-0.5 text-blue-700 dark:text-blue-400 font-bold">✓</span>
) : null} ) : null}
</button> </button>
))} ))}

View File

@ -2,7 +2,7 @@
export function rosterRowClass(selected: boolean): string { export function rosterRowClass(selected: boolean): string {
return `flex w-full items-center gap-3 rounded-2xl border px-3 py-3 mb-[2px] text-start transition-all duration-150 cursor-pointer min-h-[52px] ${ return `flex w-full items-center gap-3 rounded-2xl border px-3 py-3 mb-[2px] text-start transition-all duration-150 cursor-pointer min-h-[52px] ${
selected selected
? "border-sidebar-border bg-sidebar-accent shadow-[inset_0_1px_0_rgba(255,255,255,0.14),0_4px_16px_rgba(0,0,0,0.4)]" ? "border-sidebar-border bg-sidebar-accent shadow-xs dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.14),0_4px_16px_rgba(0,0,0,0.4)]"
: "border-transparent hover:bg-sidebar-accent/50" : "border-transparent hover:bg-sidebar-accent/50"
}`; }`;
} }

View File

@ -82,38 +82,38 @@ export const darkTokens = {
} as const satisfies ColorTokens; } as const satisfies ColorTokens;
export const lightTokens = { export const lightTokens = {
background: "#FAFAF8", background: "#FFFFFF",
foreground: "#1A1A1A", foreground: "#1A1A1A",
card: "#FFFFFF", card: "#FFFFFF",
cardForeground: "#1A1A1A", cardForeground: "#1A1A1A",
popover: "#FFFFFF", popover: "#FFFFFF",
popoverForeground: "#1A1A1A", popoverForeground: "#1A1A1A",
primary: "#1A1A1A", primary: "#1A1A1A",
primaryForeground: "#F1F1EF", primaryForeground: "#FFFFFF",
secondary: "#F0F0ED", secondary: "#F4F4F1",
secondaryForeground: "#1A1A1A", secondaryForeground: "#1A1A1A",
chatUser: "#E2E2DC", chatUser: "#EFEFEB",
chatUserForeground: "#1A1A1A", chatUserForeground: "#1A1A1A",
muted: "#F0F0ED", muted: "#F4F4F1",
mutedForeground: "#6C6C70", mutedForeground: "#67676C",
accent: "#EAEAE6", accent: "#ECECE8",
accentForeground: "#1A1A1A", accentForeground: "#1A1A1A",
destructive: "#DC2626", destructive: "#DC2626",
destructiveForeground: "#FFFFFF", destructiveForeground: "#FFFFFF",
border: "#F0F0ED", border: "#E5E5E0",
input: "#EAEAE6", input: "#DADAD4",
ring: "#6C6C70", ring: "#9C9C97",
sidebar: "#ECECE9", sidebar: "#F5F5F2",
sidebarForeground: "#1A1A1A", sidebarForeground: "#1A1A1A",
sidebarBorder: "#E8E8E4", sidebarBorder: "#E8E8E3",
sidebarAccent: "#FFFFFF", sidebarAccent: "#FFFFFF",
sidebarAccentForeground: "#1A1A1A", sidebarAccentForeground: "#1A1A1A",
link: "#2563EB", link: "#2563EB",
success: "#228B3B", success: "#1E7F37",
warning: "#B7791F", warning: "#A16207",
overlay: "rgba(20, 20, 22, 0.45)", overlay: "rgba(24, 24, 22, 0.28)",
scrollbar: "#C8C8C4", scrollbar: "#D3D3CD",
scrollbarHover: "#A8A8A4", scrollbarHover: "#B3B3AC",
} as const satisfies ColorTokens; } as const satisfies ColorTokens;
/** Dark palette. Prefer `tokensForAppearance` when theme-aware. */ /** Dark palette. Prefer `tokensForAppearance` when theme-aware. */

View File

@ -65,38 +65,38 @@
[data-theme="light"] { [data-theme="light"] {
color-scheme: light; color-scheme: light;
--background: #fafaf8; --background: #ffffff;
--foreground: #1a1a1a; --foreground: #1a1a1a;
--card: #ffffff; --card: #ffffff;
--card-foreground: #1a1a1a; --card-foreground: #1a1a1a;
--popover: #ffffff; --popover: #ffffff;
--popover-foreground: #1a1a1a; --popover-foreground: #1a1a1a;
--primary: #1a1a1a; --primary: #1a1a1a;
--primary-foreground: #f1f1ef; --primary-foreground: #ffffff;
--secondary: #f0f0ed; --secondary: #f4f4f1;
--secondary-foreground: #1a1a1a; --secondary-foreground: #1a1a1a;
--chat-user: #e2e2dc; --chat-user: #efefeb;
--chat-user-foreground: #1a1a1a; --chat-user-foreground: #1a1a1a;
--muted: #f0f0ed; --muted: #f4f4f1;
--muted-foreground: #6c6c70; --muted-foreground: #67676c;
--accent: #eaeae6; --accent: #ecece8;
--accent-foreground: #1a1a1a; --accent-foreground: #1a1a1a;
--destructive: #dc2626; --destructive: #dc2626;
--destructive-foreground: #ffffff; --destructive-foreground: #ffffff;
--border: #f0f0ed; --border: #e5e5e0;
--input: #eaeae6; --input: #dadad4;
--ring: #6c6c70; --ring: #9c9c97;
--sidebar: #ecece9; --sidebar: #f5f5f2;
--sidebar-foreground: #1a1a1a; --sidebar-foreground: #1a1a1a;
--sidebar-border: #e8e8e4; --sidebar-border: #e8e8e3;
--sidebar-accent: #ffffff; --sidebar-accent: #ffffff;
--sidebar-accent-foreground: #1a1a1a; --sidebar-accent-foreground: #1a1a1a;
--link: #2563eb; --link: #2563eb;
--success: #228b3b; --success: #1e7f37;
--warning: #b7791f; --warning: #a16207;
--overlay: rgba(20, 20, 22, 0.45); --overlay: rgba(24, 24, 22, 0.28);
--scrollbar: #c8c8c4; --scrollbar: #d3d3cd;
--scrollbar-hover: #a8a8a4; --scrollbar-hover: #b3b3ac;
--radius: 0.75rem; --radius: 0.75rem;
--font-size-caption: 0.75rem; --font-size-caption: 0.75rem;
--font-size-label: 0.84375rem; --font-size-label: 0.84375rem;

View File

@ -459,8 +459,8 @@ export function GrokShapePreview({
className={cn( className={cn(
"relative flex size-11 items-center justify-center rounded-xl transition-transform hover:scale-105 active:scale-95 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-popover", "relative flex size-11 items-center justify-center rounded-xl transition-transform hover:scale-105 active:scale-95 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-popover",
selected selected
? "ring-2 ring-primary ring-offset-2 ring-offset-popover bg-white/10" ? "ring-2 ring-primary ring-offset-2 ring-offset-popover bg-foreground/10"
: "hover:bg-white/5", : "hover:bg-foreground/5",
)} )}
> >
<EvooMascot pose="idle" size={32} trackPointer="none" /> <EvooMascot pose="idle" size={32} trackPointer="none" />

View File

@ -295,7 +295,7 @@ export const EvooMascot = memo(function EvooMascot({
style={{ height: size * 0.86, paddingBottom: size * 0.23 }} style={{ height: size * 0.86, paddingBottom: size * 0.23 }}
> >
<span <span
className="font-['EvOo_Display',sans-serif] font-bold tracking-tight text-emerald-400 leading-none inline-flex items-baseline select-none" className="font-['EvOo_Display',sans-serif] font-bold tracking-tight text-emerald-500 leading-none dark:text-emerald-400 inline-flex items-baseline select-none"
style={{ fontSize: size * 0.65 }} style={{ fontSize: size * 0.65 }}
> >
<span className="tracking-[-0.03em]">e</span> <span className="tracking-[-0.03em]">e</span>