fix(web): keep chat text readable in the light theme (#104)
Chat markdown hard-coded a near-white text color, so on light surfaces (ask cards, delegated task briefs, peer and file previews) the text was white on white. The desktop conversation panel and the bubble and glass surfaces were hard-coded dark while the text on them follows the theme, so in the light theme the user's own messages rendered as empty pills. - Chat markdown uses the foreground token; heading, code and quote colors come from the shared @rakazo/chat-ui markdown styles instead of white/emerald overrides. Quotes read in the body color, as on mobile, so they stay legible in the dimmed transcript. - User bubbles use the chat-user tokens; bubble and glass card surfaces use card and border tokens. - The conversation panel uses the background token on desktop too, as it already did on mobile. Bot reply footers, summary cards, quick prompt chips, the awareness strip and the chart expand button drop hard-coded white/zinc/emerald classes that would vanish on that panel. - New web E2E measures the contrast of the user's messages, a markdown bot reply, a delegated brief and an ask card in a group transcript in both themes and captures screenshots. Claude-Session: https://claude.ai/code/session_01WnqLpAn8253EY1eCLKVJb2 Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
110984b7bc
commit
3d180dc2dd
154
apps/web/e2e/chat-contrast.spec.ts
Normal file
154
apps/web/e2e/chat-contrast.spec.ts
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import { expect, type Locator, type Page, test } from "@playwright/test";
|
||||||
|
import { captureScreenshot, completeOnboarding, 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;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectReadable(locator: Locator, what: string) {
|
||||||
|
await expect(locator).toBeVisible();
|
||||||
|
await expect
|
||||||
|
.poll(() => textContrast(locator), { message: `${what} is readable` })
|
||||||
|
.toBeGreaterThanOrEqual(MIN_CONTRAST);
|
||||||
|
}
|
||||||
|
|
||||||
|
function runStatus(page: Page, groupId: string) {
|
||||||
|
return rpc<{ run?: { status: string } | null }>(page, "threads/get", { groupId }).then(
|
||||||
|
(snapshot) => snapshot.run?.status ?? "idle",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function send(page: Page, groupId: string, text: string) {
|
||||||
|
await expect
|
||||||
|
.poll(() => runStatus(page, groupId), { timeout: 60_000 })
|
||||||
|
.not.toMatch(/^(queued|running)$/);
|
||||||
|
const composer = page.getByRole("combobox", { name: "Message" });
|
||||||
|
await composer.fill(text);
|
||||||
|
await composer.press("Enter");
|
||||||
|
}
|
||||||
|
|
||||||
|
test("conversation text stays readable in light and dark themes", async ({ page }, testInfo) => {
|
||||||
|
const stamp = Date.now();
|
||||||
|
await page.emulateMedia({ colorScheme: "light" });
|
||||||
|
await signup(page, `chat-contrast-${stamp}@rakazo.test`, "password12", "Contrast QA");
|
||||||
|
await completeOnboarding(page, undefined, { mainConversation: true });
|
||||||
|
const groupId = new URL(page.url()).pathname.split("/").at(-1)!;
|
||||||
|
await rpc(page, "bots/create", {
|
||||||
|
name: "Researcher",
|
||||||
|
title: "",
|
||||||
|
description: "",
|
||||||
|
instructions: "",
|
||||||
|
notifyOnFinish: true,
|
||||||
|
});
|
||||||
|
await page.reload();
|
||||||
|
await waitForChatComposer(page);
|
||||||
|
await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
|
||||||
|
const transcript = page.getByTestId("transcript");
|
||||||
|
|
||||||
|
// The scripted bot echoes the request, so its reply renders this markdown.
|
||||||
|
await send(
|
||||||
|
page,
|
||||||
|
groupId,
|
||||||
|
"Summarize the **owl facts** in `notes/owls.md`\n\n## Owl notes\n\n> Owls turn their heads\n\n```\nowls = 3\n```",
|
||||||
|
);
|
||||||
|
const firstMessage = transcript
|
||||||
|
.getByTestId("message-user-bubble")
|
||||||
|
.filter({ hasText: "Summarize the **owl facts**" });
|
||||||
|
const markdown = transcript
|
||||||
|
.locator(".rk-chat-markdown")
|
||||||
|
.filter({ hasText: "done. i handled: Summarize the owl facts" });
|
||||||
|
const reply = markdown.locator("p").filter({ hasText: "done. i handled" });
|
||||||
|
await expect(reply).toBeVisible({ timeout: 60_000 });
|
||||||
|
|
||||||
|
await send(page, groupId, "delegate to the bot named Researcher: collect three owl facts");
|
||||||
|
const task = transcript.getByTestId("delegated-task-card");
|
||||||
|
await expect(task).toHaveAttribute("data-status", "done", { timeout: 60_000 });
|
||||||
|
await expect(
|
||||||
|
transcript.getByText("closing the task: collect three owl facts.").first(),
|
||||||
|
).toBeVisible({ timeout: 60_000 });
|
||||||
|
|
||||||
|
await send(page, groupId, "ask me which city to use");
|
||||||
|
await expect.poll(() => runStatus(page, groupId), { timeout: 60_000 }).toBe("waiting_input");
|
||||||
|
const question = transcript
|
||||||
|
.locator(".rk-chat-markdown p")
|
||||||
|
.filter({ hasText: /^Which city should I use\?$/ });
|
||||||
|
// threads/get can observe waiting_input before the realtime feed paints the ask card.
|
||||||
|
if ((await question.count()) === 0) {
|
||||||
|
await page.reload({ waitUntil: "domcontentloaded" });
|
||||||
|
await waitForChatComposer(page);
|
||||||
|
}
|
||||||
|
await expect(question).toBeVisible({ timeout: 15_000 });
|
||||||
|
const lastMessage = transcript
|
||||||
|
.getByTestId("message-user-bubble")
|
||||||
|
.filter({ hasText: "ask me which city to use" });
|
||||||
|
await task.locator("summary").click();
|
||||||
|
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 firstMessage.scrollIntoViewIfNeeded();
|
||||||
|
await captureScreenshot(page, testInfo, `chat-contrast-${theme}-reply`);
|
||||||
|
await expectReadable(firstMessage, `${theme}: the user's own message`);
|
||||||
|
await expectReadable(reply, `${theme}: a bot reply`);
|
||||||
|
await expectReadable(reply.locator("strong"), `${theme}: bold text in a bot reply`);
|
||||||
|
await expectReadable(reply.locator("code"), `${theme}: inline code in a bot reply`);
|
||||||
|
await expectReadable(markdown.locator("h2"), `${theme}: a heading in a bot reply`);
|
||||||
|
await expectReadable(markdown.locator("blockquote p"), `${theme}: a quote in a bot reply`);
|
||||||
|
await expectReadable(markdown.locator("pre code"), `${theme}: a code block in a bot reply`);
|
||||||
|
|
||||||
|
await brief.scrollIntoViewIfNeeded();
|
||||||
|
await captureScreenshot(page, testInfo, `chat-contrast-${theme}-task`);
|
||||||
|
await expectReadable(brief, `${theme}: the delegated task brief`);
|
||||||
|
|
||||||
|
await question.scrollIntoViewIfNeeded();
|
||||||
|
await captureScreenshot(page, testInfo, `chat-contrast-${theme}-ask`);
|
||||||
|
await expectReadable(question, `${theme}: the ask card question`);
|
||||||
|
await expectReadable(lastMessage, `${theme}: the latest user message`);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -28,15 +28,15 @@ export function VoiceAwarenessStrip({
|
|||||||
<div
|
<div
|
||||||
role="status"
|
role="status"
|
||||||
data-testid="voice-awareness"
|
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-border bg-muted/40 px-4 py-3 backdrop-blur-md"
|
||||||
>
|
>
|
||||||
<BotAvatar color={botColor} identity={botId} size={32} className="shrink-0" />
|
<BotAvatar color={botColor} identity={botId} size={32} className="shrink-0" />
|
||||||
<div className="min-w-0 flex-1 text-start">
|
<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-muted-foreground">
|
||||||
<Icon strokeWidth={1.8} className="size-icon-sm shrink-0 opacity-80" />
|
<Icon strokeWidth={1.8} className="size-icon-sm shrink-0 opacity-80" />
|
||||||
<span>{awarenessKindLabel(event.kind)}</span>
|
<span>{awarenessKindLabel(event.kind)}</span>
|
||||||
</div>
|
</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">
|
||||||
{label}
|
{label}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -4356,7 +4356,7 @@ export function ShellPage() {
|
|||||||
<main
|
<main
|
||||||
aria-hidden={mobileSidebarOpen || undefined}
|
aria-hidden={mobileSidebarOpen || undefined}
|
||||||
inert={mobileSidebarOpen}
|
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-2xl overflow-hidden transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)]"
|
||||||
>
|
>
|
||||||
{activeNavPillar === "heute" ? (
|
{activeNavPillar === "heute" ? (
|
||||||
<div className="flex h-full flex-1 flex-col overflow-hidden">
|
<div className="flex h-full flex-1 flex-col overflow-hidden">
|
||||||
@ -6733,7 +6733,7 @@ const Composer = memo(function Composer({
|
|||||||
{/* Quick Action Prompt Chips */}
|
{/* Quick Action Prompt Chips */}
|
||||||
{showComposerPlaceholder ? (
|
{showComposerPlaceholder ? (
|
||||||
<div className="mb-3 flex items-center gap-2 overflow-x-auto no-scrollbar py-1">
|
<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">
|
<span className="flex items-center gap-1.5 text-ui-label font-semibold uppercase tracking-wider text-muted-foreground shrink-0 px-2.5 py-1 rounded-full bg-muted/40 border border-border me-1 shadow-xs">
|
||||||
<Sparkles className="text-amber-400 size-icon-sm" />
|
<Sparkles className="text-amber-400 size-icon-sm" />
|
||||||
Quick
|
Quick
|
||||||
</span>
|
</span>
|
||||||
@ -6745,7 +6745,7 @@ const Composer = memo(function Composer({
|
|||||||
updateDraft(promptText);
|
updateDraft(promptText);
|
||||||
focusComposer();
|
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-border bg-muted/40 px-3.5 py-1.5 text-ui-body-sm font-medium text-foreground/85 hover:bg-accent hover:text-foreground transition-all shadow-sm active:scale-95 cursor-pointer"
|
||||||
>
|
>
|
||||||
{promptText}
|
{promptText}
|
||||||
</button>
|
</button>
|
||||||
@ -7872,8 +7872,8 @@ const MessageView = memo(function MessageView({
|
|||||||
data-testid="message-user-bubble"
|
data-testid="message-user-bubble"
|
||||||
className={
|
className={
|
||||||
encounterMode
|
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"
|
? "max-w-full whitespace-pre-wrap wrap-anywhere rounded-2xl bg-chat-user px-3.5 py-2 text-ui-body-sm leading-relaxed text-chat-user-foreground"
|
||||||
: "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"
|
: "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 shadow-md"
|
||||||
}
|
}
|
||||||
dir="auto"
|
dir="auto"
|
||||||
>
|
>
|
||||||
@ -7895,7 +7895,7 @@ const MessageView = memo(function MessageView({
|
|||||||
dir="auto"
|
dir="auto"
|
||||||
>
|
>
|
||||||
<ChatMarkdown>{block.text}</ChatMarkdown>
|
<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-border text-ui-label text-muted-foreground/75">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -7904,7 +7904,7 @@ const MessageView = memo(function MessageView({
|
|||||||
navigator.clipboard.writeText(block.text);
|
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`}
|
title={t`Antwort kopieren`}
|
||||||
>
|
>
|
||||||
<Copy size={13.5} />
|
<Copy size={13.5} />
|
||||||
@ -7915,14 +7915,14 @@ const MessageView = memo(function MessageView({
|
|||||||
type="button"
|
type="button"
|
||||||
aria-label={speaking ? t`Stop speaking` : t`Speak this reply`}
|
aria-label={speaking ? t`Stop speaking` : t`Speak this reply`}
|
||||||
onClick={onSpeak}
|
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} />
|
<Volume2 size={13.5} />
|
||||||
<span>{speaking ? t`Stop` : t`Vorlesen`}</span>
|
<span>{speaking ? t`Stop` : t`Vorlesen`}</span>
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</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([], {
|
{new Date(message.createdAt).toLocaleTimeString([], {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
@ -7940,7 +7940,7 @@ const MessageView = memo(function MessageView({
|
|||||||
{block.lines.map((line) => (
|
{block.lines.map((line) => (
|
||||||
<div key={line.k} className="flex items-baseline gap-2.5 text-ui-body">
|
<div key={line.k} className="flex items-baseline gap-2.5 text-ui-body">
|
||||||
<span className="text-success">✓</span>
|
<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 className="text-muted-foreground">→</span>
|
||||||
<span>{line.v}</span>
|
<span>{line.v}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -428,7 +428,7 @@ export function ChartBlockView({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={() => setExpanded(true)}
|
onClick={() => setExpanded(true)}
|
||||||
className="absolute end-3 top-3 opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100 rounded-full border-white/15 bg-white/10 text-white backdrop-blur-md"
|
className="absolute end-3 top-3 opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100 rounded-full"
|
||||||
>
|
>
|
||||||
<Trans>Expand</Trans>
|
<Trans>Expand</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -51,24 +51,24 @@ body,
|
|||||||
.apple-glass-card {
|
.apple-glass-card {
|
||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(20px);
|
||||||
-webkit-backdrop-filter: blur(20px);
|
-webkit-backdrop-filter: blur(20px);
|
||||||
background: rgba(16, 21, 30, 0.68);
|
background: color-mix(in srgb, var(--card) 68%, transparent);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.09);
|
border: 1px solid var(--border);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
||||||
0 6px 24px rgba(0, 0, 0, 0.35);
|
0 6px 24px rgba(0, 0, 0, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-bubble-user {
|
.chat-bubble-user {
|
||||||
background: linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(15, 23, 42, 0.98) 100%);
|
background: var(--chat-user);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
border: 1px solid var(--border);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 8px 24px -4px rgba(0, 0, 0, 0.5),
|
0 8px 24px -4px rgba(0, 0, 0, 0.5),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-bubble-bot {
|
.chat-bubble-bot {
|
||||||
background: rgba(15, 20, 29, 0.72);
|
background: color-mix(in srgb, var(--card) 72%, transparent);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
border: 1px solid var(--border);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 10px 32px -4px rgba(0, 0, 0, 0.35),
|
0 10px 32px -4px rgba(0, 0, 0, 0.35),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||||
@ -95,11 +95,12 @@ body,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Gemini-Grade Accessible Typography (Apple-like with Inter Variable) */
|
/* Gemini-Grade Accessible Typography (Apple-like with Inter Variable).
|
||||||
|
Heading, code and quote colors come from the shared @rakazo/chat-ui markdown styles. */
|
||||||
.rk-chat-markdown {
|
.rk-chat-markdown {
|
||||||
font-size: var(--font-size-reading);
|
font-size: var(--font-size-reading);
|
||||||
line-height: var(--line-height-reading);
|
line-height: var(--line-height-reading);
|
||||||
color: #f8fafc;
|
color: var(--foreground);
|
||||||
letter-spacing: -0.011em;
|
letter-spacing: -0.011em;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
@ -110,8 +111,6 @@ body,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rk-chat-markdown pre {
|
.rk-chat-markdown pre {
|
||||||
background: rgba(8, 11, 16, 0.96);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.11);
|
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.09),
|
inset 0 1px 0 rgba(255, 255, 255, 0.09),
|
||||||
@ -128,9 +127,6 @@ body,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rk-chat-markdown code:not(pre code) {
|
.rk-chat-markdown code:not(pre code) {
|
||||||
background: rgba(255, 255, 255, 0.09);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.13);
|
|
||||||
color: var(--foreground);
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 0.15em 0.4em;
|
padding: 0.15em 0.4em;
|
||||||
@ -143,7 +139,6 @@ body,
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
letter-spacing: -0.025em;
|
letter-spacing: -0.025em;
|
||||||
color: #ffffff;
|
|
||||||
margin: 1.6em 0 0.6em;
|
margin: 1.6em 0 0.6em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,7 +147,6 @@ body,
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
color: #ffffff;
|
|
||||||
margin: 1.4em 0 0.5em;
|
margin: 1.4em 0 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,7 +155,6 @@ body,
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
letter-spacing: -0.015em;
|
letter-spacing: -0.015em;
|
||||||
color: #ffffff;
|
|
||||||
margin: 1.2em 0 0.4em;
|
margin: 1.2em 0 0.4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,17 +170,13 @@ body,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rk-chat-markdown strong {
|
.rk-chat-markdown strong {
|
||||||
color: #ffffff;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rk-chat-markdown blockquote {
|
.rk-chat-markdown blockquote {
|
||||||
border-left: 3px solid #10b981;
|
|
||||||
padding-left: 1.2em;
|
padding-left: 1.2em;
|
||||||
margin: 1.3em 0;
|
margin: 1.3em 0;
|
||||||
color: #cbd5e1;
|
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
background: rgba(16, 185, 129, 0.05);
|
|
||||||
border-radius: 0 8px 8px 0;
|
border-radius: 0 8px 8px 0;
|
||||||
padding-top: 0.4em;
|
padding-top: 0.4em;
|
||||||
padding-bottom: 0.4em;
|
padding-bottom: 0.4em;
|
||||||
|
|||||||
@ -157,7 +157,6 @@
|
|||||||
.rk-chat-markdown blockquote {
|
.rk-chat-markdown blockquote {
|
||||||
border-left: 3px solid var(--border);
|
border-left: 3px solid var(--border);
|
||||||
padding-left: 0.85em;
|
padding-left: 0.85em;
|
||||||
color: var(--muted-foreground);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rk-chat-markdown table {
|
.rk-chat-markdown table {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user