fix(markdown): separate the code block from the agent bubble in light mode

The code block used --muted. Inside an agent bubble that is the wrong
value: --bubble-agent is oklch(0.97 0 0) in light, which is exactly
--muted. Block and bubble painted the same fill, so the block vanished
and the whole comment read as one flat gray slab. Dark hid the fault
because the bubble is oklch(0.185 0 0) and the block oklch(0.269 0 0).

No fixed token fixes this. The block renders on the page AND inside
bubbles, and --bubble-agent changes per variant: "former" is
oklch(0.955 0 0), and "hairline" is var(--background) itself. Any fixed
choice collides with one of those surfaces.

Use a translucent tint instead. --code-surface shifts whatever is beneath
it rather than replacing it, so one value separates on the page and in
every bubble variant. Light darkens 5% toward the ink; dark lifts 9%.

Add tests: the surface must be a color-mix tint defined in both modes,
and the block must never pin itself to --muted or --bubble-agent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
scotttongandClaude Opus 5 committed 2026-08-17 22:22:21 -07:00
1 parent 27897e754e
commit 48adddb1f8
2 files changed
+46 -8

No files matched your search

@@ -28,7 +28,7 @@ describe("markdown code block styles", () => {
it("rides theme tokens for the rendered code block surface", () => {
const block = cssBlock(".paperclip-markdown pre");
expect(block).toContain("background-color: var(--muted)");
expect(block).toContain("background-color: var(--code-surface)");
expect(block).toContain("color: var(--foreground)");
expect(block).toContain("border: 1px solid var(--border)");
expect(block).toContain("border-radius: var(--radius-lg)");
@@ -37,7 +37,7 @@ describe("markdown code block styles", () => {
it("rides theme tokens for the editor content code block surface", () => {
const block = cssBlock(".paperclip-mdxeditor-content pre");
expect(block).toContain("background: var(--muted)");
expect(block).toContain("background: var(--code-surface)");
expect(block).toContain("color: var(--foreground)");
expect(block).toContain("border: 1px solid var(--border)");
expect(block).toContain("border-radius: var(--radius-lg)");
@@ -46,9 +46,9 @@ describe("markdown code block styles", () => {
it("points the normal and inverted prose variables at the same tokens", () => {
const block = cssBlock(".paperclip-markdown");
expect(block).toContain("--tw-prose-pre-bg: var(--muted)");
expect(block).toContain("--tw-prose-pre-bg: var(--code-surface)");
expect(block).toContain("--tw-prose-pre-code: var(--foreground)");
expect(block).toContain("--tw-prose-invert-pre-bg: var(--muted)");
expect(block).toContain("--tw-prose-invert-pre-bg: var(--code-surface)");
expect(block).toContain("--tw-prose-invert-pre-code: var(--foreground)");
});
@@ -110,3 +110,22 @@ describe("code syntax palette", () => {
}
});
});
describe("code block surface separation", () => {
it("defines the surface as a translucent tint in both modes", () => {
// A fixed token cannot work: --bubble-agent is oklch(0.97 0 0) in light,
// identical to --muted, and its "hairline" variant is var(--background).
// A tint shifts whatever is beneath it, so it separates everywhere.
expect(stylesheet).toMatch(/--code-surface:\s*color-mix\(in oklab, var\(--foreground\) \d+%, transparent\)/);
const darkBlock = stylesheet.slice(stylesheet.indexOf(".dark {"));
expect(darkBlock).toContain("--code-surface:");
});
it("never pins the code surface to the agent bubble fill", () => {
const preBlock = cssBlock(".paperclip-markdown pre");
expect(preBlock).not.toContain("var(--muted)");
expect(preBlock).not.toContain("var(--bubble-agent)");
});
});
+23 -4
View File
@@ -1408,7 +1408,7 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before {
padding: 0;
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background: var(--muted);
background: var(--code-surface);
color: var(--foreground);
overflow-x: auto;
}
@@ -1487,16 +1487,16 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before {
both modes. Override prose CSS variables so prose-invert can't revert to
defaults. */
.paperclip-markdown {
--tw-prose-pre-bg: var(--muted);
--tw-prose-pre-bg: var(--code-surface);
--tw-prose-pre-code: var(--foreground);
--tw-prose-invert-pre-bg: var(--muted);
--tw-prose-invert-pre-bg: var(--code-surface);
--tw-prose-invert-pre-code: var(--foreground);
}
.paperclip-markdown pre {
border: 1px solid var(--border) !important;
border-radius: var(--radius-lg) !important;
background-color: var(--muted) !important;
background-color: var(--code-surface) !important;
color: var(--foreground) !important;
padding: 0.5rem 0.65rem !important;
margin: 0.4rem 0 !important;
@@ -1510,6 +1510,25 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before {
font-size: 1em;
}
/* ── Code block surface ──
The block must separate from whatever it sits on, and that is not always
the page. It also renders inside agent bubbles, whose fill --bubble-agent
is oklch(0.97 0 0) in light — the same value as --muted, which made the
block vanish into the bubble and read as one flat slab. The bubble fill
also changes per variant, and the "hairline" variant IS var(--background),
so no fixed token separates in every case.
A translucent tint shifts whatever is beneath it instead of replacing it,
so one value holds on the page and in every bubble variant, in both modes.
Light darkens toward the ink; dark lifts toward it. */
:root {
--code-surface: color-mix(in oklab, var(--foreground) 5%, transparent);
}
.dark {
--code-surface: color-mix(in oklab, var(--foreground) 9%, transparent);
}
/* ── Code syntax roles ──
Each alias points at a token that already exists and already carries a
`.dark` override, so one definition covers both modes. A custom property