diff --git a/apps/web/e2e/chat-contrast.spec.ts b/apps/web/e2e/chat-contrast.spec.ts index 0cc05d4..c950ab6 100644 --- a/apps/web/e2e/chat-contrast.spec.ts +++ b/apps/web/e2e/chat-contrast.spec.ts @@ -1,58 +1,112 @@ import { expect, type Locator, type Page, test } from "@playwright/test"; -import { captureScreenshot, completeOnboarding, rpc, signup, waitForChatComposer } from "./helpers"; +import { + captureScreenshot, + completeOnboarding, + createNamedBot, + openNewBot, + rpc, + signup, + waitForChatComposer, +} from "./helpers"; // WCAG AA for body text. const MIN_CONTRAST = 4.5; test.use({ viewport: { width: 1280, height: 1000 } }); -/** Contrast of an element's text against the surface it is painted on. */ -function textContrast(locator: Locator) { - return locator.evaluate((element) => { - const context = document.createElement("canvas").getContext("2d", { willReadFrequently: true }); - if (!context) throw new Error("No 2D canvas context"); - // The canvas normalizes every CSS color syntax (oklab, color-mix, ...) to sRGB. - const rgba = (color: string) => { - context.clearRect(0, 0, 1, 1); - context.fillStyle = color; - context.fillRect(0, 0, 1, 1); - const [r = 0, g = 0, b = 0, a = 0] = context.getImageData(0, 0, 1, 1).data; - return [r / 255, g / 255, b / 255, a / 255] as const; - }; - // One pixel of `color`, painted inside `from` and composited through every ancestor's - // background color and opacity onto the white canvas. - const paint = (color: readonly [number, number, number, number], from: Element) => { - let [r, g, b, a] = color; - for (let node: Element | null = from; node; node = node.parentElement) { - const style = getComputedStyle(node); - const [br, bg, bb, ba] = rgba(style.backgroundColor); - const alpha = a + ba * (1 - a); - if (alpha > 0) { - r = (r * a + br * ba * (1 - a)) / alpha; - g = (g * a + bg * ba * (1 - a)) / alpha; - b = (b * a + bb * ba * (1 - a)) / alpha; +/** + * Texts in `locator` below WCAG AA against the surface they are painted on. With `inside`, every + * visible text and typed value within it counts, except in disabled controls, which WCAG exempts. + */ +function unreadableTexts(locator: Locator, inside: boolean) { + return locator.evaluate( + (root, { inside, min }) => { + const context = document + .createElement("canvas") + .getContext("2d", { willReadFrequently: true }); + if (!context) throw new Error("No 2D canvas context"); + // The canvas normalizes every CSS color syntax (oklab, color-mix, ...) to sRGB. + const rgba = (color: string) => { + context.clearRect(0, 0, 1, 1); + context.fillStyle = color; + context.fillRect(0, 0, 1, 1); + const [r = 0, g = 0, b = 0, a = 0] = context.getImageData(0, 0, 1, 1).data; + return [r / 255, g / 255, b / 255, a / 255] as const; + }; + // One pixel of `color`, painted inside `from` and composited through every ancestor's + // background color and opacity onto the white canvas. + const paint = (color: readonly [number, number, number, number], from: Element) => { + let [r, g, b, a] = color; + for (let node: Element | null = from; node; node = node.parentElement) { + const style = getComputedStyle(node); + const [br, bg, bb, ba] = rgba(style.backgroundColor); + const alpha = a + ba * (1 - a); + if (alpha > 0) { + r = (r * a + br * ba * (1 - a)) / alpha; + g = (g * a + bg * ba * (1 - a)) / alpha; + b = (b * a + bb * ba * (1 - a)) / alpha; + } + a = alpha * Number(style.opacity); } - a = alpha * Number(style.opacity); - } - return [r * a + 1 - a, g * a + 1 - a, b * a + 1 - a] as const; - }; - const luminance = (rgb: readonly [number, number, number]) => { - const [r = 0, g = 0, b = 0] = rgb.map((channel) => - channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4, - ); - return 0.2126 * r + 0.7152 * g + 0.0722 * b; - }; - const text = luminance(paint(rgba(getComputedStyle(element).color), element)); - const surface = luminance(paint([0, 0, 0, 0], element)); - return (Math.max(text, surface) + 0.05) / (Math.min(text, surface) + 0.05); - }); + return [r * a + 1 - a, g * a + 1 - a, b * a + 1 - a] as const; + }; + const luminance = (rgb: readonly [number, number, number]) => { + const [r = 0, g = 0, b = 0] = rgb.map((channel) => + channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4, + ); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; + }; + const contrast = (element: Element) => { + const text = luminance(paint(rgba(getComputedStyle(element).color), element)); + const surface = luminance(paint([0, 0, 0, 0], element)); + return (Math.max(text, surface) + 0.05) / (Math.min(text, surface) + 0.05); + }; + // What the element itself shows: a typed value, or its own text nodes. + const ownText = (element: Element) => { + if (element instanceof HTMLTextAreaElement) return element.value; + if (element instanceof HTMLInputElement) { + return /^(text|search|url|email|tel|number|password)$/.test(element.type) + ? element.value + : ""; + } + return Array.from(element.childNodes, (node) => + node.nodeType === Node.TEXT_NODE ? node.textContent : "", + ) + .join("") + .trim(); + }; + const shown = (element: Element) => { + const { width, height } = element.getBoundingClientRect(); + return width > 1 && height > 1 && element.checkVisibility(); + }; + const elements = inside + ? [root, ...root.querySelectorAll("*")].filter( + (element) => ownText(element) && shown(element) && !element.closest(":disabled"), + ) + : [root]; + return elements.flatMap((element) => { + const ratio = contrast(element); + const text = ownText(element) || element.textContent?.trim() || ""; + return ratio < min ? [`${ratio.toFixed(2)}:1 ${text.slice(0, 48)}`] : []; + }); + }, + { inside, min: MIN_CONTRAST }, + ); } -async function expectReadable(locator: Locator, what: string) { +async function expectReadable(locator: Locator, what: string, { inside = false } = {}) { await expect(locator).toBeVisible(); await expect - .poll(() => textContrast(locator), { message: `${what} is readable` }) - .toBeGreaterThanOrEqual(MIN_CONTRAST); + .poll(() => unreadableTexts(locator, inside), { message: `${what} is readable` }) + .toEqual([]); +} + +async function inEachTheme(page: Page, check: (theme: "light" | "dark") => Promise) { + for (const theme of ["light", "dark"] as const) { + await page.emulateMedia({ colorScheme: theme }); + await expect(page.locator("html")).toHaveAttribute("data-theme", theme); + await check(theme); + } } function runStatus(page: Page, groupId: string) { @@ -128,10 +182,7 @@ test("conversation text stays readable in light and dark themes", async ({ page const brief = task.locator(".rk-chat-markdown p"); await expect(brief).toHaveText("collect three owl facts"); - for (const theme of ["light", "dark"] as const) { - await page.emulateMedia({ colorScheme: theme }); - await expect(page.locator("html")).toHaveAttribute("data-theme", theme); - + await inEachTheme(page, async (theme) => { await firstMessage.scrollIntoViewIfNeeded(); await captureScreenshot(page, testInfo, `chat-contrast-${theme}-reply`); await expectReadable(firstMessage, `${theme}: the user's own message`); @@ -150,5 +201,62 @@ test("conversation text stays readable in light and dark themes", async ({ page await captureScreenshot(page, testInfo, `chat-contrast-${theme}-ask`); await expectReadable(question, `${theme}: the ask card question`); await expectReadable(lastMessage, `${theme}: the latest user message`); - } + }); +}); + +test("shell surfaces stay readable in light and dark themes", async ({ page }, testInfo) => { + await page.emulateMedia({ colorScheme: "light" }); + await signup(page, `shell-contrast-${Date.now()}@rakazo.test`, "password12", "Contrast QA"); + await completeOnboarding(page); + await createNamedBot(page, "Researcher"); + const composer = page.getByRole("combobox", { name: "Message" }); + await composer.fill("Draft a note about owls"); + const sidebar = page.getByTestId("bots-sidebar"); + const sidePanel = page.getByTestId("side-panel"); + + await inEachTheme(page, async (theme) => { + await captureScreenshot(page, testInfo, `shell-contrast-${theme}`); + await expectReadable(composer, `${theme}: the composer draft`); + await expectReadable(page.getByTestId("composer-bar"), `${theme}: the composer`, { + inside: true, + }); + await expectReadable( + sidebar.getByRole("button", { name: "Heute", exact: true }), + `${theme}: Heute`, + { inside: true }, + ); + await expectReadable( + sidebar.locator("[data-main-conversation]"), + `${theme}: the main conversation row`, + { inside: true }, + ); + await expectReadable( + sidebar.getByRole("button", { name: "Researcher, 1:1 conversation" }), + `${theme}: the selected bot row`, + { inside: true }, + ); + await expectReadable(page.getByTestId("trading-mode-status"), `${theme}: trading status`, { + inside: true, + }); + await expectReadable(page.getByTestId("user-menu-trigger"), `${theme}: the user menu`, { + inside: true, + }); + }); + + await openNewBot(page); + await page.getByTestId("create-bot-form").locator("label:has-text('Name') input").fill("Owl"); + await inEachTheme(page, async (theme) => { + await captureScreenshot(page, testInfo, `shell-contrast-${theme}-new-bot`); + await expectReadable(sidePanel, `${theme}: the new bot form`, { inside: true }); + }); + + await page.getByRole("button", { name: "Cancel new bot" }).click(); + await page.getByTestId("bot-settings-trigger").click(); + const advanced = page.getByTestId("bot-settings-advanced"); + await advanced.locator("summary").click(); + await expect(advanced.getByText("Open work")).toBeVisible(); + await inEachTheme(page, async (theme) => { + await captureScreenshot(page, testInfo, `shell-contrast-${theme}-settings`); + await expectReadable(sidePanel, `${theme}: the bot settings form`, { inside: true }); + }); }); diff --git a/apps/web/src/components/stage/OoStage.tsx b/apps/web/src/components/stage/OoStage.tsx index 1fa1f26..25fa582 100644 --- a/apps/web/src/components/stage/OoStage.tsx +++ b/apps/web/src/components/stage/OoStage.tsx @@ -175,7 +175,7 @@ export function OoStage({ {(hasMessages || phase !== "idle") && (
@@ -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"}`} >
-
+
-
+
Open work
{items.length === 0 ? ( -
+
None yet
) : ( diff --git a/apps/web/src/pages/Shell.tsx b/apps/web/src/pages/Shell.tsx index bf25ff9..ae34218 100644 --- a/apps/web/src/pages/Shell.tsx +++ b/apps/web/src/pages/Shell.tsx @@ -3354,7 +3354,7 @@ export function ShellPage() {
{ if ( mobileSidebarOpen || @@ -3428,7 +3428,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-sidebar-border 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 @@ -3565,13 +3565,9 @@ export function ShellPage() { setActiveNavPillar("heute"); setMobileSidebarOpen(false); }} - 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]" - }`} + className={rosterRowClass(activeNavPillar === "heute")} > -
+
{heuteActionCount > 0 ? (
@@ -3851,7 +3845,7 @@ export function ShellPage() { {needsYouChipLabel(item.chat.status)} ) : item.chat.title ? ( - + {item.chat.title} ) : null} @@ -3905,7 +3899,7 @@ export function ShellPage() { })}
{/* PROJEKTE SECTION */} -
+
{t`Projekte`}

@@ -4092,15 +4086,11 @@ export function ShellPage() { position: { x: event.clientX, y: event.clientY }, }); }} - 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" - : 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" - }`} + className={rosterRowClass( + item.kind === "bot" + ? !inGroup && active?.id === item.chat.id + : inGroup && activeGroup?.id === item.chat.id, + )} > {item.kind === "bot" ? ( {item.chat.name}

- + {formatRosterTime(item.chat.updatedAt)} {item.chat.unread ? (
{cleanRosterPreview(item.chat.preview)}
@@ -4718,7 +4704,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-2xl md:relative ${ panel && ( active || @@ -4822,8 +4808,8 @@ export function ShellPage() { /> {isDesktop ? ( devServerRunning ? ( - - + + Dev ) : ( @@ -4998,11 +4984,11 @@ export function ShellPage() {
@@ -6136,13 +6122,13 @@ const Transcript = memo(function Transcript({ onClick={() => { onPromptSelect?.(t`📈 Scanne den Markt nach aktuellen Alpha-Signalen`); }} - 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" + className="apple-glass-card flex flex-col items-start p-4 rounded-2xl text-start transition-all hover:scale-[1.02] cursor-pointer" > 📈 - + {t`Alpha-Signale`} - + {t`Volatilität & Orderbuch-Liquidität`} @@ -6151,13 +6137,13 @@ const Transcript = memo(function Transcript({ onClick={() => { onPromptSelect?.(t`💎 Führe ein Steve Jobs UX & Architektur-Audit durch`); }} - 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" + className="apple-glass-card flex flex-col items-start p-4 rounded-2xl text-start transition-all hover:scale-[1.02] cursor-pointer" > 💎 - + {t`Steve Jobs Audit`} - + {t`Schlichtheit, Eleganz & Detailtiefe`} @@ -6928,7 +6914,7 @@ const Composer = memo(function Composer({ ) : null}
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-border bg-foreground/5 text-foreground/80 transition-all hover:bg-foreground/10 hover:text-foreground flex items-center justify-center cursor-pointer" > @@ -6952,9 +6938,9 @@ const Composer = memo(function Composer({ {selectedSkill ? ( - + {selectedSkill.name} @@ -6973,7 +6959,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-border bg-foreground/[0.08] px-3 py-1 text-ui-body font-medium text-foreground backdrop-blur-md" > @@ -7053,7 +7039,7 @@ const Composer = memo(function Composer({ autoComplete="off" dir="auto" rows={1} - className="max-h-36 min-h-[28px] min-w-[8rem] flex-1 resize-none overflow-y-auto bg-transparent py-1 text-[16.5px] leading-[1.55] text-foreground outline-none placeholder:text-muted-foreground/75 disabled:opacity-40 font-normal selection:bg-emerald-500/30" + className="max-h-36 min-h-[28px] min-w-[8rem] flex-1 resize-none overflow-y-auto bg-transparent py-1 text-[16.5px] leading-[1.55] text-foreground outline-none placeholder:text-muted-foreground/75 disabled:opacity-40 font-normal" />
{onVoice ? ( @@ -7086,7 +7072,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 transition-transform active:scale-90 flex items-center justify-center cursor-pointer" > @@ -7096,7 +7082,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-border bg-foreground/[0.08] text-foreground/90 shadow-sm transition-colors hover:bg-foreground/15 hover:text-foreground flex items-center justify-center cursor-pointer" > @@ -7107,7 +7093,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 transition-all active:scale-90 disabled:bg-foreground/[0.08] disabled:text-foreground/20 flex items-center justify-center cursor-pointer" > @@ -7504,19 +7490,22 @@ const MessageView = memo(function MessageView({ {effectiveSpeakerName && message.role === "bot" && !encounterMode ? (
-
+
- + {effectiveSpeakerName} - + Oo
- + {new Date(message.createdAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", diff --git a/apps/web/src/pages/shell/bot-panel.tsx b/apps/web/src/pages/shell/bot-panel.tsx index fb6563a..5120807 100644 --- a/apps/web/src/pages/shell/bot-panel.tsx +++ b/apps/web/src/pages/shell/bot-panel.tsx @@ -441,7 +441,7 @@ export function BotSettings({ }} />
-
-
+
Get notified when this Bot finishes or needs input
diff --git a/apps/web/src/pages/shell/roster-row-style.ts b/apps/web/src/pages/shell/roster-row-style.ts index f4e68dd..32cc406 100644 --- a/apps/web/src/pages/shell/roster-row-style.ts +++ b/apps/web/src/pages/shell/roster-row-style.ts @@ -1,20 +1,22 @@ /** 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] ${ + 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 - ? "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" + ? "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-transparent hover:bg-sidebar-accent/50" }`; } export function rosterNameClass(unread: boolean): string { - return unread ? "font-bold text-white" : "font-semibold text-white/95"; + return unread ? "font-bold text-sidebar-foreground" : "font-semibold text-sidebar-foreground"; } -export const rosterTimeClass = "text-ui-body-sm text-zinc-400 tabular-nums font-medium"; +// Muted text measures about 4.4:1 on the light sidebar; a dimmed foreground stays above 4.5:1. +export const rosterTimeClass = + "text-ui-body-sm text-sidebar-foreground/65 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-sidebar-foreground"; export function rosterPreviewClass(unread: boolean): string { - return unread ? "font-medium text-white" : "text-zinc-300"; + return unread ? "font-medium text-sidebar-foreground" : "text-sidebar-foreground/85"; } diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 2b69196..6b6e3ea 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -11,13 +11,7 @@ body, #root { height: 100%; margin: 0; - background: #07090e; - background-image: radial-gradient( - circle at 50% 0%, - rgba(16, 185, 129, 0.07) 0%, - rgba(14, 165, 233, 0.04) 40%, - transparent 80% - ); + background: var(--background); color: var(--foreground); font-family: "Inter Variable", -apple-system, BlinkMacSystemFont, "SF Pro Display", "Geist Variable", @@ -41,8 +35,8 @@ body, .apple-island { backdrop-filter: blur(36px) saturate(210%); -webkit-backdrop-filter: blur(36px) saturate(210%); - background: rgba(12, 16, 24, 0.88); - border: 1px solid rgba(255, 255, 255, 0.13); + background: color-mix(in srgb, var(--card) 88%, transparent); + border: 1px solid color-mix(in srgb, var(--foreground) 13%, transparent); box-shadow: 0 18px 44px -8px rgba(0, 0, 0, 0.75), inset 0 1px 0 rgba(255, 255, 255, 0.16); @@ -224,11 +218,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 { diff --git a/packages/ui-web/src/components/ui/tabs.tsx b/packages/ui-web/src/components/ui/tabs.tsx index c1f427d..5fbfc8a 100644 --- a/packages/ui-web/src/components/ui/tabs.tsx +++ b/packages/ui-web/src/components/ui/tabs.tsx @@ -49,7 +49,7 @@ function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {