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
This commit is contained in:
Claude 2026-10-03 13:00:00 +00:00
parent 110984b7bc
commit b757523a77
No known key found for this signature in database
17 changed files with 253 additions and 139 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 expect(settings).toBeHidden();
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 captureSidebarSearchSelected(page, testInfo, "sidebar-search-selected-light");

View File

@ -26,7 +26,7 @@
document.documentElement.dataset.theme = theme;
document.documentElement.style.colorScheme = theme;
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>

View File

@ -134,7 +134,7 @@ export function OnboardingScanProgress({
>
<div className="flex-shrink-0">
{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>
) : isActive ? (
@ -174,7 +174,7 @@ export function OnboardingScanProgress({
<div className="mt-8 text-center">
<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}
>
Continue to your workspace →

View File

@ -175,7 +175,7 @@ export function OoStage({
{(hasMessages || phase !== "idle") && (
<div className={`flex w-full justify-center ${hasMessages ? "mb-1" : "mb-3"}`}>
<div
className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-2.5 py-0.5 text-ui-label font-semibold uppercase tracking-wider text-muted-foreground backdrop-blur-sm"
className="inline-flex items-center gap-2 rounded-full border border-foreground/10 bg-foreground/[0.05] px-2.5 py-0.5 text-ui-label font-semibold uppercase tracking-wider text-muted-foreground backdrop-blur-sm"
style={
reducedMotion
? undefined
@ -280,7 +280,7 @@ function StageEnsemble({
return (
<li key={member.botId} className="flex flex-col items-center gap-1" title={member.name}>
<div
className={`relative flex size-14 items-center justify-center rounded-full border border-white/12 bg-slate-950/40 backdrop-blur-md ${
className={`relative flex size-14 items-center justify-center rounded-full border border-border bg-card backdrop-blur-md dark:border-foreground/12 dark:bg-slate-950/40 ${
isWorking && !reducedMotion ? "animate-pulse" : ""
}`}
>
@ -484,7 +484,7 @@ export function OoGroupStage({
className={`relative z-10 mx-auto flex w-full max-w-3xl flex-col items-center px-4 md:px-8 ${hasMessages ? "py-2 md:py-3" : "py-5 md:py-6"}`}
>
<div className="mb-3 w-full min-w-0 text-center">
<div className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-2.5 py-0.5 text-ui-label font-semibold uppercase tracking-wider text-muted-foreground backdrop-blur-sm">
<div className="inline-flex items-center gap-2 rounded-full border border-foreground/10 bg-foreground/[0.05] px-2.5 py-0.5 text-ui-label font-semibold uppercase tracking-wider text-muted-foreground backdrop-blur-sm">
<span
className={`size-1.5 rounded-full ${
running ? "bg-violet-400" : "bg-emerald-400"

View File

@ -9,12 +9,12 @@ export type StagePresencePhase =
| "error";
const PHASE_BAR_COLOR: Record<StagePresencePhase, string> = {
idle: "bg-emerald-400/60",
working: "bg-violet-400/70",
listening: "bg-cyan-400/90",
thinking: "bg-violet-400/70",
speaking: "bg-emerald-400/90",
error: "bg-rose-400/80",
idle: "bg-emerald-500/70 dark:bg-emerald-400/60",
working: "bg-violet-500/75 dark:bg-violet-400/70",
listening: "bg-cyan-500/90 dark:bg-cyan-400/90",
thinking: "bg-violet-500/75 dark:bg-violet-400/70",
speaking: "bg-emerald-500/90 dark:bg-emerald-400/90",
error: "bg-rose-500/80 dark:bg-rose-400/80",
};
export function StageHero({
@ -55,22 +55,22 @@ export function StageHero({
) : null}
<div
className={`relative flex ${orbSize} items-center justify-center rounded-full border border-white/14 bg-slate-950/45 shadow-[0_16px_48px_rgba(0,0,0,0.45),inset_0_1px_0_rgba(255,255,255,0.12)] backdrop-blur-xl`}
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}`,
}
}
className={`relative flex ${orbSize} items-center justify-center rounded-full border border-border bg-card shadow-[0_12px_32px_-12px_rgba(24,24,22,0.18)] backdrop-blur-xl dark:border-foreground/14 dark:bg-slate-950/45 dark:shadow-[0_16px_48px_rgba(0,0,0,0.45),inset_0_1px_0_rgba(255,255,255,0.12)]`}
>
{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
color={botColor}
identity={botId}
status={botStatus}
pose={pose}
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>

View File

@ -34,14 +34,17 @@ export function TextStageCanvas({
const personaTint = ACCENT_TINT[personaAccent];
return (
<div aria-hidden className="pointer-events-none absolute inset-0 overflow-hidden">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_100%_70%_at_50%_0%,rgba(12,16,24,0.55)_0%,transparent_68%)]" />
<div
aria-hidden
className="pointer-events-none absolute inset-0 overflow-hidden opacity-60 dark:opacity-100"
>
<div className="absolute inset-0 dark:bg-[radial-gradient(ellipse_100%_70%_at_50%_0%,rgba(12,16,24,0.55)_0%,transparent_68%)]" />
<div
className={`absolute left-1/2 top-0 h-[min(48vh,320px)] w-[min(72vw,480px)] -translate-x-1/2 rounded-full blur-[72px] ${reducedMotion ? "" : "voice-bloom-emerald"}`}
style={{
background: `radial-gradient(circle, ${phaseTint} 0%, ${personaTint} 42%, transparent 72%)`,
opacity: 0.55 + energy * 0.2,
transform: `translateX(-50%) scale(${bloomScale})`,
transform: `scale(${bloomScale})`,
transition: reducedMotion
? undefined
: "transform 140ms ease-out, opacity 140ms ease-out",

View File

@ -28,15 +28,15 @@ export function VoiceAwarenessStrip({
<div
role="status"
data-testid="voice-awareness"
className="mx-auto flex max-w-2xl items-start gap-3 rounded-[20px] border border-white/10 bg-white/[0.05] px-4 py-3 backdrop-blur-md"
className="mx-auto flex max-w-2xl items-start gap-3 rounded-[20px] border border-foreground/10 bg-foreground/[0.05] px-4 py-3 backdrop-blur-md"
>
<BotAvatar color={botColor} identity={botId} size={32} className="shrink-0" />
<div className="min-w-0 flex-1 text-start">
<div className="flex items-center gap-2 text-ui-label font-semibold uppercase tracking-wide text-zinc-300">
<div className="flex items-center gap-2 text-ui-label font-semibold uppercase tracking-wide text-foreground/75">
<Icon strokeWidth={1.8} className="size-icon-sm shrink-0 opacity-80" />
<span>{awarenessKindLabel(event.kind)}</span>
</div>
<p dir="auto" className="mt-1 text-ui-body leading-relaxed text-zinc-100">
<p dir="auto" className="mt-1 text-ui-body leading-relaxed text-foreground/90">
{label}
</p>
</div>

View File

@ -290,6 +290,7 @@ import {
rosterRowClass,
rosterTimeClass,
rosterUnreadDotClass,
selectedSidebarRowClass,
} from "./shell/roster-row-style";
import { WindowChrome } from "./WindowChrome";
@ -3354,7 +3355,7 @@ export function ShellPage() {
<div
data-testid="shell-root"
data-ready={shellReady}
className="relative flex h-full min-w-0 overflow-hidden bg-black bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-zinc-900 via-black to-black text-foreground/90 p-0 md:p-3 gap-0 md:gap-3"
className="relative flex h-full min-w-0 overflow-hidden bg-sidebar dark:bg-black dark:bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] dark:from-zinc-900 dark:via-black dark:to-black text-foreground/90 p-0 md:p-3 gap-0 md:gap-3"
onTouchStartCapture={(event) => {
if (
mobileSidebarOpen ||
@ -3428,7 +3429,7 @@ export function ShellPage() {
width: mobileSidebarOpen ? undefined : botsSidebarCollapsed ? 0 : leftSidebarWidth,
maxWidth: mobileSidebarOpen ? 316 : undefined,
}}
className={`absolute inset-y-0 start-0 z-40 flex w-[calc(100%-48px)] shrink-0 flex-col md:rounded-2xl md:border md:border-white/10 md:bg-[#0A0A0C] md:shadow-2xl bg-sidebar md:static md:z-auto md:translate-x-0 ${
className={`absolute inset-y-0 start-0 z-40 flex w-[calc(100%-48px)] shrink-0 flex-col md:rounded-2xl md:border md:border-transparent md:shadow-none dark:md:border-foreground/10 dark:md:shadow-2xl bg-sidebar md:static md:z-auto md:translate-x-0 ${
mobileSidebarOpen ? "translate-x-0" : "-translate-x-full rtl:translate-x-full"
} ${
botsSidebarCollapsed
@ -3567,11 +3568,11 @@ export function ShellPage() {
}}
className={`flex w-full items-center gap-3 rounded-2xl px-3 py-3 text-start transition-all duration-150 cursor-pointer min-h-[52px] ${
activeNavPillar === "heute"
? "bg-white/[0.09] shadow-[inset_0_1px_0_rgba(255,255,255,0.14),0_4px_16px_rgba(0,0,0,0.4)] border border-emerald-500/30"
: "hover:bg-white/[0.04]"
? selectedSidebarRowClass
: "hover:bg-foreground/[0.04]"
}`}
>
<div className="relative grid size-control-lg shrink-0 place-items-center rounded-full bg-emerald-500/10 text-emerald-400">
<div className="relative grid size-control-lg shrink-0 place-items-center rounded-full bg-emerald-500/10 text-emerald-700 dark:text-emerald-400">
<Calendar strokeWidth={2.2} className="size-icon-md" />
{heuteActionCount > 0 ? (
<span
@ -3825,12 +3826,12 @@ export function ShellPage() {
status={item.chat.status}
/>
<span
className={`absolute -bottom-0.5 -right-0.5 size-2.5 rounded-full border-2 border-[#0b0c10] ${
className={`absolute -bottom-0.5 -right-0.5 size-2.5 rounded-full border-2 border-sidebar ${
rosterStatusDot(item.chat.status) === "needs-you"
? "bg-warning animate-pulse"
: rosterStatusDot(item.chat.status) === "working"
? "bg-amber-400 animate-pulse"
: "bg-emerald-400"
? "bg-amber-500 animate-pulse dark:bg-amber-400"
: "bg-emerald-500 dark:bg-emerald-400"
}`}
/>
</div>
@ -3851,7 +3852,7 @@ export function ShellPage() {
{needsYouChipLabel(item.chat.status)}
</span>
) : item.chat.title ? (
<span className="min-w-0 truncate rounded-full border border-emerald-500/25 bg-emerald-500/10 px-2 py-0.5 text-ui-label font-medium text-emerald-400">
<span className="min-w-0 truncate rounded-full border border-emerald-500/25 bg-emerald-500/10 px-2 py-0.5 text-ui-label font-medium text-emerald-700 dark:text-emerald-400">
{item.chat.title}
</span>
) : null}
@ -3905,7 +3906,7 @@ export function ShellPage() {
})}
<div className="sidebar-projekte">
{/* PROJEKTE SECTION */}
<div className="flex items-center justify-between pt-4 pb-1 mt-3 border-t border-white/5 px-2.5">
<div className="flex items-center justify-between pt-4 pb-1 mt-3 border-t border-foreground/5 px-2.5">
<div>
<div className="rk-shell-section-label">{t`Projekte`}</div>
<p className="mt-0.5 px-0.5 text-ui-caption text-muted-foreground/70">
@ -4094,12 +4095,12 @@ export function ShellPage() {
}}
className={`flex w-full items-center gap-3 rounded-2xl px-3 py-3 mb-[2px] text-start transition-all duration-150 cursor-pointer min-h-[52px] ${
item.kind === "bot" && !inGroup && active?.id === item.chat.id
? "bg-white/[0.09] shadow-[inset_0_1px_0_rgba(255,255,255,0.14),0_4px_16px_rgba(0,0,0,0.4)] border border-emerald-500/30"
? selectedSidebarRowClass
: item.kind === "group" &&
inGroup &&
activeGroup?.id === item.chat.id
? "bg-white/[0.09] shadow-[inset_0_1px_0_rgba(255,255,255,0.14),0_4px_16px_rgba(0,0,0,0.4)] border border-emerald-500/30"
: "hover:bg-white/[0.05] border border-transparent"
? selectedSidebarRowClass
: "hover:bg-foreground/[0.05] border border-transparent"
}`}
>
{item.kind === "bot" ? (
@ -4119,15 +4120,15 @@ export function ShellPage() {
dir="auto"
className={`shrink-0 max-w-[170px] truncate text-ui-body tracking-tight ${
item.chat.unread
? "font-bold text-white"
: "font-semibold text-white/95"
? "font-bold text-foreground"
: "font-semibold text-foreground/95"
}`}
>
{item.chat.name}
</span>
</div>
<div className="flex shrink-0 items-center gap-1.5">
<span className="text-ui-body-sm text-zinc-400 tabular-nums font-medium">
<span className="text-ui-body-sm text-muted-foreground tabular-nums font-medium">
{formatRosterTime(item.chat.updatedAt)}
</span>
{item.chat.unread ? (
@ -4142,8 +4143,8 @@ export function ShellPage() {
dir="auto"
className={`mt-0.5 truncate text-ui-body leading-snug ${
item.chat.unread
? "font-medium text-white"
: "text-zinc-300"
? "font-medium text-foreground"
: "text-foreground/75"
}`}
>
{cleanRosterPreview(item.chat.preview)}
@ -4356,7 +4357,7 @@ export function ShellPage() {
<main
aria-hidden={mobileSidebarOpen || undefined}
inert={mobileSidebarOpen}
className="flex min-w-0 flex-1 flex-col bg-background md:rounded-2xl md:border md:border-white/10 md:bg-[#0A0A0C] md:shadow-2xl overflow-hidden transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)]"
className="flex min-w-0 flex-1 flex-col bg-background md:rounded-2xl md:border md:border-border md:shadow-sm dark:md:border-foreground/10 dark:md:shadow-2xl overflow-hidden transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)]"
>
{activeNavPillar === "heute" ? (
<div className="flex h-full flex-1 flex-col overflow-hidden">
@ -4718,7 +4719,7 @@ export function ShellPage() {
? rightSidebarWidth
: 0,
}}
className={`absolute inset-y-0 end-0 z-20 flex min-h-0 shrink-0 flex-col overflow-hidden bg-background md:rounded-2xl md:border md:border-white/10 md:bg-[#0A0A0C] md:shadow-2xl md:relative ${
className={`absolute inset-y-0 end-0 z-20 flex min-h-0 shrink-0 flex-col overflow-hidden bg-background md:rounded-2xl md:border md:border-border md:shadow-sm dark:md:border-foreground/10 dark:md:shadow-2xl md:relative ${
panel &&
(
active ||
@ -4822,7 +4823,7 @@ export function ShellPage() {
/>
{isDesktop ? (
devServerRunning ? (
<span className="inline-flex items-center gap-1 rounded-full bg-emerald-500/10 px-2 py-0.5 text-ui-label font-medium text-emerald-400">
<span className="inline-flex items-center gap-1 rounded-full bg-emerald-500/10 px-2 py-0.5 text-ui-label font-medium text-emerald-700 dark:text-emerald-400">
<span className="size-1.5 rounded-full bg-emerald-400" />
<Trans>Dev</Trans>
</span>
@ -4998,11 +4999,11 @@ export function ShellPage() {
<div className="fixed inset-0 z-50 flex items-center justify-center">
<button
type="button"
className="absolute inset-0 bg-black/60"
className="absolute inset-0 bg-overlay"
aria-label={t`Close`}
onClick={() => setShowCreateBotModal(false)}
/>
<div className="relative z-10 max-h-[90vh] w-[480px] overflow-y-auto rounded-2xl border border-white/10 bg-[#0A0A0C] p-6 shadow-2xl">
<div className="relative z-10 max-h-[90vh] w-[480px] overflow-y-auto rounded-2xl border border-border bg-popover p-6 shadow-2xl">
<div className="mb-4 flex items-center justify-between">
<h2 className="text-lg font-semibold text-foreground">
<Trans>Create new Bot</Trans>
@ -6107,10 +6108,11 @@ const Transcript = memo(function Transcript({
<div className="relative mb-5 flex items-center justify-center p-3">
<EvooMascot size={76} pose="greeting" interactive trackPointer="hover" />
</div>
<h2 className="text-3xl font-bold tracking-tight text-white mb-2">
{t`Guten Tag.`} <span className="text-emerald-400">evOo</span> {t`ist bereit.`}
<h2 className="text-3xl font-bold tracking-tight text-foreground mb-2">
{t`Guten Tag.`} <span className="text-emerald-700 dark:text-emerald-400">evOo</span>{" "}
{t`ist bereit.`}
</h2>
<p className="max-w-md text-ui-body leading-relaxed text-zinc-300 mb-8 font-normal">
<p className="max-w-md text-ui-body leading-relaxed text-foreground/75 mb-8 font-normal">
{activeName
? t`Frag ${activeName} nach Marktchancen, autonomen Tasks oder Architektur-Reviews.`
: t`Wähle einen autonomen Agenten aus deinem Team oder starte direkt einen Prompt.`}
@ -6124,10 +6126,10 @@ const Transcript = memo(function Transcript({
className="apple-glass-card group flex flex-col items-start p-4 rounded-2xl text-start transition-all hover:scale-[1.02] hover:border-emerald-500/40 cursor-pointer"
>
<span className="text-xl mb-2">⚡</span>
<span className="text-[14.5px] font-semibold text-white group-hover:text-emerald-400 transition-colors">
<span className="text-[14.5px] font-semibold text-foreground group-hover:text-emerald-700 dark:group-hover:text-emerald-400 transition-colors">
{t`Portfolio-Check`}
</span>
<span className="text-ui-body-sm text-zinc-400 mt-1 leading-snug">
<span className="text-ui-body-sm text-muted-foreground mt-1 leading-snug">
{t`Echtzeit-Risiko & Drawdown-Analyse`}
</span>
</button>
@ -6139,10 +6141,10 @@ const Transcript = memo(function Transcript({
className="apple-glass-card group flex flex-col items-start p-4 rounded-2xl text-start transition-all hover:scale-[1.02] hover:border-sky-500/40 cursor-pointer"
>
<span className="text-xl mb-2">📈</span>
<span className="text-[14.5px] font-semibold text-white group-hover:text-sky-400 transition-colors">
<span className="text-[14.5px] font-semibold text-foreground group-hover:text-sky-700 dark:group-hover:text-sky-400 transition-colors">
{t`Alpha-Signale`}
</span>
<span className="text-ui-body-sm text-zinc-400 mt-1 leading-snug">
<span className="text-ui-body-sm text-muted-foreground mt-1 leading-snug">
{t`Volatilität & Orderbuch-Liquidität`}
</span>
</button>
@ -6154,10 +6156,10 @@ const Transcript = memo(function Transcript({
className="apple-glass-card group flex flex-col items-start p-4 rounded-2xl text-start transition-all hover:scale-[1.02] hover:border-purple-500/40 cursor-pointer"
>
<span className="text-xl mb-2">💎</span>
<span className="text-[14.5px] font-semibold text-white group-hover:text-purple-400 transition-colors">
<span className="text-[14.5px] font-semibold text-foreground group-hover:text-purple-700 dark:group-hover:text-purple-400 transition-colors">
{t`Steve Jobs Audit`}
</span>
<span className="text-ui-body-sm text-zinc-400 mt-1 leading-snug">
<span className="text-ui-body-sm text-muted-foreground mt-1 leading-snug">
{t`Schlichtheit, Eleganz & Detailtiefe`}
</span>
</button>
@ -6733,8 +6735,8 @@ const Composer = memo(function Composer({
{/* Quick Action Prompt Chips */}
{showComposerPlaceholder ? (
<div className="mb-3 flex items-center gap-2 overflow-x-auto no-scrollbar py-1">
<span className="flex items-center gap-1.5 text-ui-label font-semibold uppercase tracking-wider text-emerald-400 shrink-0 px-2.5 py-1 rounded-full bg-emerald-500/10 border border-emerald-500/20 me-1 shadow-xs">
<Sparkles className="text-amber-400 size-icon-sm" />
<span className="flex items-center gap-1.5 text-ui-label font-semibold uppercase tracking-wider text-emerald-700 dark:text-emerald-400 shrink-0 px-2.5 py-1 rounded-full bg-emerald-500/10 border border-emerald-500/20 me-1 shadow-xs">
<Sparkles className="text-amber-700 dark:text-amber-400 size-icon-sm" />
Quick
</span>
{quickPromptsForBot(activeName).map((promptText) => (
@ -6745,7 +6747,7 @@ const Composer = memo(function Composer({
updateDraft(promptText);
focusComposer();
}}
className="shrink-0 rounded-full border border-white/[0.1] bg-white/[0.05] px-3.5 py-1.5 text-ui-body-sm font-medium text-zinc-200 hover:border-emerald-500/40 hover:bg-white/[0.1] hover:text-white transition-all shadow-sm active:scale-95 cursor-pointer"
className="shrink-0 rounded-full border border-foreground/[0.1] bg-foreground/[0.05] px-3.5 py-1.5 text-ui-body-sm font-medium text-foreground/85 hover:border-foreground/20 dark:hover:border-emerald-500/40 hover:bg-foreground/[0.1] hover:text-foreground transition-all shadow-sm active:scale-95 cursor-pointer"
>
{promptText}
</button>
@ -6928,7 +6930,7 @@ const Composer = memo(function Composer({
) : null}
<div
data-testid="composer-bar"
className="apple-island relative flex items-center gap-3.5 rounded-[30px] py-3 pe-3.5 ps-4 transition-all duration-200 focus-within:border-emerald-500/40 focus-within:shadow-[0_20px_50px_-10px_rgba(0,0,0,0.85),inset_0_1px_0_rgba(255,255,255,0.22),0_0_0_1px_rgba(16,185,129,0.35)] shadow-[0_16px_40px_-8px_rgba(0,0,0,0.7),inset_0_1px_0_rgba(255,255,255,0.14)]"
className="apple-island relative flex items-center gap-3.5 rounded-[30px] py-3 pe-3.5 ps-4 transition-all duration-200 focus-within:border-foreground/25 dark:focus-within:border-emerald-500/40 dark:focus-within:shadow-[0_20px_50px_-10px_rgba(0,0,0,0.85),inset_0_1px_0_rgba(255,255,255,0.22),0_0_0_1px_rgba(16,185,129,0.35)] dark:shadow-[0_16px_40px_-8px_rgba(0,0,0,0.7),inset_0_1px_0_rgba(255,255,255,0.14)]"
>
<input
ref={fileInputRef}
@ -6944,7 +6946,7 @@ const Composer = memo(function Composer({
aria-label={t`Attach file`}
disabled={disabled}
onClick={() => fileInputRef.current?.click()}
className="tactile-press size-10 shrink-0 rounded-full border border-white/10 bg-white/[0.06] text-foreground/80 transition-all hover:bg-white/[0.12] hover:text-white flex items-center justify-center cursor-pointer"
className="tactile-press size-10 shrink-0 rounded-full border border-foreground/10 bg-foreground/[0.06] text-foreground/80 transition-all hover:bg-foreground/[0.12] hover:text-foreground flex items-center justify-center cursor-pointer"
>
<Plus strokeWidth={2.2} className="size-icon-lg" />
</Button>
@ -6952,9 +6954,12 @@ const Composer = memo(function Composer({
{selectedSkill ? (
<span
data-testid="skill-chip"
className="inline-flex max-w-full items-center gap-1.5 rounded-full border border-white/10 bg-white/[0.08] px-3 py-1 text-ui-body font-medium text-foreground backdrop-blur-md"
className="inline-flex max-w-full items-center gap-1.5 rounded-full border border-foreground/10 bg-foreground/[0.08] px-3 py-1 text-ui-body font-medium text-foreground backdrop-blur-md"
>
<Box strokeWidth={1.8} className="shrink-0 text-blue-400 size-icon-md" />
<Box
strokeWidth={1.8}
className="shrink-0 text-blue-700 dark:text-blue-400 size-icon-md"
/>
<span dir="auto" className="truncate">
{selectedSkill.name}
</span>
@ -6973,7 +6978,7 @@ const Composer = memo(function Composer({
key={mentionChipKey(mention)}
data-testid="mention-chip"
data-mention-kind={mention.kind}
className="inline-flex max-w-full items-center gap-1.5 rounded-full border border-white/10 bg-white/[0.08] px-3 py-1 text-ui-body font-medium text-foreground backdrop-blur-md"
className="inline-flex max-w-full items-center gap-1.5 rounded-full border border-foreground/10 bg-foreground/[0.08] px-3 py-1 text-ui-body font-medium text-foreground backdrop-blur-md"
>
<MentionChipIcon mention={mention} />
<span dir="auto" className="truncate">
@ -7086,7 +7091,7 @@ const Composer = memo(function Composer({
aria-label={t`Send`}
disabled={!canSend || disabled}
onClick={send}
className="tactile-press size-10 rounded-full bg-emerald-400 text-slate-950 hover:bg-emerald-300 shadow-[0_0_16px_rgba(16,185,129,0.35)] transition-transform active:scale-90 flex items-center justify-center cursor-pointer"
className="tactile-press size-10 rounded-full bg-primary text-primary-foreground hover:bg-primary/85 dark:bg-emerald-400 dark:text-slate-950 dark:hover:bg-emerald-300 dark:shadow-[0_0_16px_rgba(16,185,129,0.35)] transition-transform active:scale-90 flex items-center justify-center cursor-pointer"
>
<ArrowUp strokeWidth={2.6} className="size-icon-lg" />
</Button>
@ -7096,7 +7101,7 @@ const Composer = memo(function Composer({
aria-label={t`Stop`}
disabled={sending}
onClick={() => void onStop()}
className="tactile-press size-10 rounded-full border border-white/15 bg-white/[0.08] text-foreground/90 shadow-sm transition-colors hover:bg-white/[0.15] hover:text-white flex items-center justify-center cursor-pointer"
className="tactile-press size-10 rounded-full border border-foreground/15 bg-foreground/[0.08] text-foreground/90 shadow-sm transition-colors hover:bg-foreground/[0.15] hover:text-foreground flex items-center justify-center cursor-pointer"
>
<Square strokeWidth={0} fill="currentColor" className="size-icon-sm" />
</Button>
@ -7107,7 +7112,7 @@ const Composer = memo(function Composer({
aria-label={t`Send`}
disabled={!canSend || disabled}
onClick={send}
className="tactile-press size-10 shrink-0 rounded-full bg-emerald-400 text-slate-950 hover:bg-emerald-300 shadow-[0_0_16px_rgba(16,185,129,0.35)] transition-all active:scale-90 disabled:bg-white/[0.08] disabled:text-white/20 disabled:shadow-none flex items-center justify-center cursor-pointer"
className="tactile-press size-10 shrink-0 rounded-full bg-primary text-primary-foreground hover:bg-primary/85 dark:bg-emerald-400 dark:text-slate-950 dark:hover:bg-emerald-300 dark:shadow-[0_0_16px_rgba(16,185,129,0.35)] transition-all active:scale-90 disabled:bg-foreground/[0.08] disabled:text-foreground/20 disabled:shadow-none dark:disabled:bg-foreground/[0.08] dark:disabled:text-foreground/20 dark:disabled:shadow-none flex items-center justify-center cursor-pointer"
>
<ArrowUp strokeWidth={2.6} className="size-icon-lg" />
</Button>
@ -7504,19 +7509,22 @@ const MessageView = memo(function MessageView({
{effectiveSpeakerName && message.role === "bot" && !encounterMode ? (
<div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2.5">
<div className="relative flex items-center justify-center size-7 rounded-xl bg-white/[0.08] border border-white/[0.12] shadow-xs">
<div className="relative flex items-center justify-center size-7 rounded-xl bg-foreground/[0.08] border border-foreground/[0.12] shadow-xs">
<EvooMascot size={22} interactive={false} pose={isLive ? "working" : "idle"} />
</div>
<div className="flex items-center gap-2">
<span className="text-[14.5px] font-semibold text-white tracking-tight" dir="auto">
<span
className="text-[14.5px] font-semibold text-foreground tracking-tight"
dir="auto"
>
{effectiveSpeakerName}
</span>
<span className="rounded-full bg-emerald-500/15 border border-emerald-500/25 px-2 py-0.5 text-ui-label font-medium text-emerald-400">
<span className="rounded-full bg-emerald-500/15 border border-emerald-500/25 px-2 py-0.5 text-ui-label font-medium text-emerald-700 dark:text-emerald-400">
Oo
</span>
</div>
</div>
<span className="text-ui-label font-mono text-zinc-400 tabular-nums">
<span className="text-ui-label font-mono text-muted-foreground tabular-nums">
{new Date(message.createdAt).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
@ -7872,8 +7880,8 @@ const MessageView = memo(function MessageView({
data-testid="message-user-bubble"
className={
encounterMode
? "max-w-full whitespace-pre-wrap wrap-anywhere rounded-2xl border border-white/10 bg-white/[0.06] px-3.5 py-2 text-ui-body-sm leading-relaxed text-foreground/95"
: "chat-bubble-user max-w-full whitespace-pre-wrap wrap-anywhere rounded-[24px] px-6 py-4 text-[17px] leading-[1.62] text-[#FAFAFA] shadow-md"
? "max-w-full whitespace-pre-wrap wrap-anywhere rounded-2xl border border-foreground/10 bg-foreground/[0.06] px-3.5 py-2 text-ui-body-sm leading-relaxed text-foreground/95"
: "chat-bubble-user max-w-full whitespace-pre-wrap wrap-anywhere rounded-[24px] px-6 py-4 text-[17px] leading-[1.62] text-chat-user-foreground dark:text-[#FAFAFA] dark:shadow-md"
}
dir="auto"
>
@ -7895,7 +7903,7 @@ const MessageView = memo(function MessageView({
dir="auto"
>
<ChatMarkdown>{block.text}</ChatMarkdown>
<div className="mt-3 pt-2.5 flex items-center justify-between border-t border-white/[0.08] text-ui-label text-muted-foreground/75">
<div className="mt-3 pt-2.5 flex items-center justify-between border-t border-foreground/[0.08] text-ui-label text-muted-foreground/75">
<div className="flex items-center gap-3">
<button
type="button"
@ -7904,7 +7912,7 @@ const MessageView = memo(function MessageView({
navigator.clipboard.writeText(block.text);
}
}}
className="inline-flex items-center gap-1.5 hover:text-white transition-colors cursor-pointer text-ui-body-sm font-medium"
className="inline-flex items-center gap-1.5 hover:text-foreground transition-colors cursor-pointer text-ui-body-sm font-medium"
title={t`Antwort kopieren`}
>
<Copy size={13.5} />
@ -7915,14 +7923,14 @@ const MessageView = memo(function MessageView({
type="button"
aria-label={speaking ? t`Stop speaking` : t`Speak this reply`}
onClick={onSpeak}
className="inline-flex items-center gap-1.5 hover:text-white transition-colors cursor-pointer text-ui-body-sm font-medium"
className="inline-flex items-center gap-1.5 hover:text-foreground transition-colors cursor-pointer text-ui-body-sm font-medium"
>
<Volume2 size={13.5} />
<span>{speaking ? t`Stop` : t`Vorlesen`}</span>
</button>
) : null}
</div>
<span className="tabular-nums font-mono text-ui-label text-zinc-400">
<span className="tabular-nums font-mono text-ui-label text-muted-foreground">
{new Date(message.createdAt).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
@ -7940,7 +7948,7 @@ const MessageView = memo(function MessageView({
{block.lines.map((line) => (
<div key={line.k} className="flex items-baseline gap-2.5 text-ui-body">
<span className="text-success">✓</span>
<span className="font-semibold text-white">{line.k}</span>
<span className="font-semibold text-foreground">{line.k}</span>
<span className="text-muted-foreground">→</span>
<span>{line.v}</span>
</div>

View File

@ -84,17 +84,17 @@ export function ChoiceCard({
aria-label={option.label}
disabled={Boolean(block.answerId) || pending}
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
? "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
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
? "bg-blue-500 text-white"
: "bg-white/10 text-foreground/80"
: "bg-foreground/10 text-foreground/80"
}`}
>
{option.letter}
@ -105,7 +105,7 @@ export function ChoiceCard({
{option.label}
</span>
{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}
</button>
))}

View File

@ -1,20 +1,23 @@
/** Selected sidebar row: a raised paper row in light, a lit glass row in dark. */
export const selectedSidebarRowClass =
"border border-sidebar-border bg-sidebar-accent shadow-xs dark:border-emerald-500/30 dark:bg-foreground/[0.09] dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.14),0_4px_16px_rgba(0,0,0,0.4)]";
/** One look for sidebar conversation rows: the main conversation entry and the bot roster. */
export function rosterRowClass(selected: boolean): string {
return `flex w-full items-center gap-3 rounded-2xl px-3 py-3 mb-[2px] text-start transition-all duration-150 cursor-pointer min-h-[52px] ${
selected
? "bg-white/[0.09] shadow-[inset_0_1px_0_rgba(255,255,255,0.14),0_4px_16px_rgba(0,0,0,0.4)] border border-emerald-500/30"
: "hover:bg-white/[0.05] border border-transparent"
selected ? selectedSidebarRowClass : "hover:bg-foreground/[0.05] border border-transparent"
}`;
}
export function rosterNameClass(unread: boolean): string {
return unread ? "font-bold text-white" : "font-semibold text-white/95";
return unread ? "font-bold text-foreground" : "font-semibold text-foreground/95";
}
export const rosterTimeClass = "text-ui-body-sm text-zinc-400 tabular-nums font-medium";
export const rosterTimeClass = "text-ui-body-sm text-muted-foreground tabular-nums font-medium";
export const rosterUnreadDotClass = "inline-block h-2 w-2 rounded-full bg-emerald-400";
export const rosterUnreadDotClass =
"inline-block h-2 w-2 rounded-full bg-emerald-500 dark:bg-emerald-400";
export function rosterPreviewClass(unread: boolean): string {
return unread ? "font-medium text-white" : "text-zinc-300";
return unread ? "font-medium text-foreground" : "text-foreground/75";
}

View File

@ -76,6 +76,45 @@ body,
-webkit-backdrop-filter: blur(20px);
}
/* Light theme: paper surfaces with hairline borders and soft, short shadows
instead of dark glass. */
html[data-theme="light"],
html[data-theme="light"] body,
html[data-theme="light"] #root {
background: var(--sidebar);
}
[data-theme="light"] .apple-glass,
[data-theme="light"] .apple-glass-card {
background: color-mix(in srgb, var(--card) 88%, transparent);
border-color: var(--border);
box-shadow:
0 1px 2px rgba(24, 24, 22, 0.04),
0 8px 24px -12px rgba(24, 24, 22, 0.1);
}
[data-theme="light"] .apple-island {
background: color-mix(in srgb, var(--card) 92%, transparent);
border-color: var(--input);
box-shadow:
0 1px 2px rgba(24, 24, 22, 0.05),
0 12px 32px -14px rgba(24, 24, 22, 0.16);
}
[data-theme="light"] .chat-bubble-user {
background: var(--chat-user);
border-color: transparent;
box-shadow: none;
}
[data-theme="light"] .chat-bubble-bot {
background: var(--card);
border-color: var(--border);
box-shadow: 0 1px 2px rgba(24, 24, 22, 0.04);
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
/* Shell layout utilities — token-backed, use instead of one-off pixel classes */
@layer components {
.rk-shell-section-label {
@ -99,7 +138,7 @@ body,
.rk-chat-markdown {
font-size: var(--font-size-reading);
line-height: var(--line-height-reading);
color: #f8fafc;
color: var(--foreground);
letter-spacing: -0.011em;
font-weight: 400;
}
@ -122,14 +161,20 @@ body,
font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
[data-theme="light"] .rk-chat-markdown pre {
background: var(--muted);
border-color: var(--border);
box-shadow: none;
}
.sidebar-roster-row[data-selected="true"],
.sidebar-roster-row:hover {
background-color: var(--accent);
}
.rk-chat-markdown code:not(pre code) {
background: rgba(255, 255, 255, 0.09);
border: 1px solid rgba(255, 255, 255, 0.13);
background: color-mix(in srgb, var(--foreground) 7%, transparent);
border: 1px solid color-mix(in srgb, var(--foreground) 11%, transparent);
color: var(--foreground);
font-weight: 500;
border-radius: 6px;
@ -143,7 +188,7 @@ body,
font-weight: 700;
line-height: 1.3;
letter-spacing: -0.025em;
color: #ffffff;
color: var(--foreground);
margin: 1.6em 0 0.6em;
}
@ -152,7 +197,7 @@ body,
font-weight: 600;
line-height: 1.35;
letter-spacing: -0.02em;
color: #ffffff;
color: var(--foreground);
margin: 1.4em 0 0.5em;
}
@ -161,7 +206,7 @@ body,
font-weight: 600;
line-height: 1.4;
letter-spacing: -0.015em;
color: #ffffff;
color: var(--foreground);
margin: 1.2em 0 0.4em;
}
@ -177,7 +222,7 @@ body,
}
.rk-chat-markdown strong {
color: #ffffff;
color: var(--foreground);
font-weight: 600;
}
@ -193,6 +238,13 @@ body,
padding-bottom: 0.4em;
}
[data-theme="light"] .rk-chat-markdown blockquote {
border-left-color: var(--input);
color: var(--muted-foreground);
background: none;
border-radius: 0;
}
:root {
--spring-snappy: cubic-bezier(0.175, 0.885, 0.32, 1.275);
--spring-smooth: cubic-bezier(0.16, 1, 0.3, 1);
@ -210,6 +262,10 @@ body,
box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.18);
}
[data-theme="light"] .tactile-press:hover {
box-shadow: none;
}
.tactile-press:active {
transform: scale(0.965);
transition-duration: 0.06s;
@ -235,11 +291,11 @@ body,
background: transparent;
}
.rk-scroll::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.14);
background: var(--scrollbar);
border-radius: 3px;
}
.rk-scroll::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.28);
background: var(--scrollbar-hover);
}
.app-drag {

View File

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

View File

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

View File

@ -459,8 +459,8 @@ export function GrokShapePreview({
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",
selected
? "ring-2 ring-primary ring-offset-2 ring-offset-popover bg-white/10"
: "hover:bg-white/5",
? "ring-2 ring-primary ring-offset-2 ring-offset-popover bg-foreground/10"
: "hover:bg-foreground/5",
)}
>
<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 }}
>
<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 }}
>
<span className="tracking-[-0.03em]">e</span>

View File

@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { buttonVariants } from "../components/ui/button";
import { cn } from "./utils";
describe("cn", () => {
it("treats the ui type scale as font size, not color", () => {
expect(cn("text-primary-foreground", "text-ui-body")).toBe(
"text-primary-foreground text-ui-body",
);
expect(cn("text-ui-body", "text-base")).toBe("text-base");
});
it("keeps the variant label color on resized buttons", () => {
const classes = cn(buttonVariants({ size: "lg", className: "text-base" })).split(" ");
expect(classes).toContain("text-primary-foreground");
expect(classes).toContain("text-base");
expect(classes).not.toContain("text-ui-body");
});
});

View File

@ -1,5 +1,27 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { extendTailwindMerge } from "tailwind-merge";
// The shared type scale (`text-ui-*`) is a font size, not a color. Without this,
// `text-ui-body` would replace `text-primary-foreground` and hide button labels.
const twMerge = extendTailwindMerge({
extend: {
classGroups: {
"font-size": [
{
text: [
"ui-caption",
"ui-label",
"ui-body-sm",
"ui-body",
"ui-reading",
"ui-title-sm",
"ui-title",
],
},
],
},
},
});
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));