From 4b25785803ce2c746d5f93eaa8474a48065cfbd7 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 22 Oct 2026 11:08:44 +0000 Subject: [PATCH 1/4] fix(web): single-digit numbered lists no longer clip in wide fonts Chat markdown kept a fixed 1.25rem gutter for single-digit ordered lists, so a wide interface font painted the outside `1. ` marker past the chat lane's clip edge. The shared ol rule now uses max(1.25rem, 3ch), the rule the composer already had, and the composer's duplicate copy is gone. Co-Authored-By: Claude Opus 5.5 --- apps/web/src/index.css | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 01fa601ab666e..3707b9458aed8 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -1852,13 +1852,14 @@ code { list-style-type: disc; } -/* --list-gutter defaults to the same 1.25rem as .chat-markdown ul, but - ChatMarkdown's `ol` renderer widens it (via inline style) for lists whose - widest marker has multiple characters so it isn't clipped by list-style-position: - outside painting the marker past the padding box. Reset it here too so a - nested ol without its own widened marker doesn't inherit the outer one. */ +/* list-style-position: outside paints the marker past the padding box, so the + gutter must fit a single-digit marker like `1. ` (3ch) even in wide interface + fonts. ChatMarkdown's `ol` renderer widens it (via inline style) for lists + whose widest marker has multiple characters. Setting it here also resets a + nested ol without its own widened marker so it doesn't inherit the outer + one; `ch` resolves against each list's own font. */ :is(.chat-markdown, .composer-tiptap) ol { - --list-gutter: 1.25rem; + --list-gutter: max(1.25rem, 3ch); padding-inline-start: var(--list-gutter, 1.25rem); list-style-type: decimal; } @@ -1875,10 +1876,6 @@ code { /* The prompt font and size can differ from the interface, so its gutter must also follow the prompt's font metrics. */ -.composer-tiptap ol { - --list-gutter: max(1.25rem, 3ch); -} - .composer-tiptap ol:has(> li[data-marker-width="3"]) { --list-gutter: max(1.25rem, 4ch); } From 439fbdfda04106f5820a835ba31b06f9615d0052 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 22 Oct 2026 11:11:58 +0000 Subject: [PATCH 2/4] fix(web): say the 3ch list gutter applies to every font Co-Authored-By: Claude Opus 5.5 --- apps/web/src/index.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 3707b9458aed8..8d132c71b62b9 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -1853,8 +1853,9 @@ code { } /* list-style-position: outside paints the marker past the padding box, so the - gutter must fit a single-digit marker like `1. ` (3ch) even in wide interface - fonts. ChatMarkdown's `ol` renderer widens it (via inline style) for lists + gutter must fit a single-digit marker like `1. `, which takes 3ch in a + monospace font. CSS cannot measure the marker, so proportional fonts get the + same 3ch. ChatMarkdown's `ol` renderer widens it (via inline style) for lists whose widest marker has multiple characters. Setting it here also resets a nested ol without its own widened marker so it doesn't inherit the outer one; `ch` resolves against each list's own font. */ From 19552e10bbd7a05ba65fda9132acbfa0fc892e29 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Nov 2026 01:26:22 +0000 Subject: [PATCH 3/4] fix(web): trim the list gutter comment to the four-line cap AI-assisted; verified with the tests and tools listed above. --- apps/web/src/index.css | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 6f5b931bf9c80..fb65ece08d69b 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -1933,13 +1933,10 @@ code { list-style-type: disc; } -/* list-style-position: outside paints the marker past the padding box, so the - gutter must fit a single-digit marker like `1. `, which takes 3ch in a - monospace font. CSS cannot measure the marker, so proportional fonts get the - same 3ch. ChatMarkdown's `ol` renderer widens it (via inline style) for lists - whose widest marker has multiple characters. Setting it here also resets a - nested ol without its own widened marker so it doesn't inherit the outer - one; `ch` resolves against each list's own font. */ +/* Outside markers paint past the padding box, so the gutter must fit `1. ` + (3ch in a monospace font; CSS can't measure it, so every font gets 3ch). + ChatMarkdown widens it inline for multi-digit markers; setting it here stops + a nested ol inheriting that. `ch` resolves against each list's own font. */ :is(.chat-markdown, .composer-tiptap) ol { --list-gutter: max(1.25rem, 3ch); padding-inline-start: var(--list-gutter, 1.25rem); From 4a8a73b022dc9b62f2866979afbec50bb7047873 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Nov 2026 01:42:14 +0000 Subject: [PATCH 4/4] fix(web): size the list gutter from the measured marker, not 3ch 3ch over-reserves in proportional fonts, where the period and space are narrower than a digit, so lists whose markers already fit were indented further and rewrapped. Measure `0. ` in the interface font instead and keep 1.25rem whenever the marker fits. AI-assisted; verified with the tests and tools listed above. --- apps/web/src/appearanceFonts.browser.test.ts | 36 ++++++++++++++++++++ apps/web/src/appearanceFonts.ts | 19 +++++++++-- apps/web/src/index.css | 13 ++++--- 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/apps/web/src/appearanceFonts.browser.test.ts b/apps/web/src/appearanceFonts.browser.test.ts index 9bdc5c2ef9864..289e48ecad050 100644 --- a/apps/web/src/appearanceFonts.browser.test.ts +++ b/apps/web/src/appearanceFonts.browser.test.ts @@ -52,4 +52,40 @@ describe("applyAppearanceFontVariables", () => { else root.setAttribute("style", style); } }); + + it("widens the numbered-list gutter only when the interface font's marker would clip", () => { + const root = document.documentElement; + const style = root.getAttribute("style"); + const gutter = (sans: string) => { + applyAppearanceFontVariables(root, { + sans, + code: "", + composer: "", + sizeInterface: 16, + sizePrompt: 14, + sizeCode: 13, + smoothing: true, + separate: false, + }); + const markdown = document.createElement("div"); + markdown.className = "chat-markdown"; + markdown.style.fontSize = "14px"; + const list = document.createElement("ol"); + markdown.append(list); + document.body.append(markdown); + try { + return Number.parseFloat(getComputedStyle(list).paddingInlineStart); + } finally { + markdown.remove(); + } + }; + try { + // `0. ` in a 14px monospace font is about 25px, past the 20px default. + expect(gutter("monospace")).toBeGreaterThan(24); + expect(gutter("sans-serif")).toBe(20); + } finally { + if (style === null) root.removeAttribute("style"); + else root.setAttribute("style", style); + } + }); }); diff --git a/apps/web/src/appearanceFonts.ts b/apps/web/src/appearanceFonts.ts index e7c2867cb279f..19f2d88d3c29d 100644 --- a/apps/web/src/appearanceFonts.ts +++ b/apps/web/src/appearanceFonts.ts @@ -88,6 +88,16 @@ export function applyAppearanceFontVariables( } } + const markerWidth = probeWidth( + appearanceFontStack(preferences.sans, DEFAULT_SANS_FONT_STACK), + LIST_MARKER_PROBE_TEXT, + ); + if (markerWidth === null) { + root.style.removeProperty("--list-marker-sans"); + } else { + root.style.setProperty("--list-marker-sans", `${(markerWidth / PROBE_FONT_SIZE).toFixed(3)}em`); + } + root.style.fontSize = `${clampInterfaceFontSize(preferences.sizeInterface)}px`; root.style.setProperty( "--font-size-prompt", @@ -127,15 +137,18 @@ function clampCodeFontSize(value: number): number { } const FONT_PROBE_TEXT = "mmmmmmmmMMWli1O0@# fjord"; +// The widest single-digit `ol` marker; CSS can't measure it to size the list gutter. +const LIST_MARKER_PROBE_TEXT = "0. "; +const PROBE_FONT_SIZE = 16; let fontProbeContext: CanvasRenderingContext2D | null | undefined; -function probeWidth(fontList: string): number | null { +function probeWidth(fontList: string, text = FONT_PROBE_TEXT): number | null { if (fontProbeContext === undefined) { fontProbeContext = document.createElement("canvas").getContext("2d"); } if (fontProbeContext === null) return null; - fontProbeContext.font = `16px ${fontList}`; - return fontProbeContext.measureText(FONT_PROBE_TEXT).width; + fontProbeContext.font = `${PROBE_FONT_SIZE}px ${fontList}`; + return fontProbeContext.measureText(text).width; } /** Probes canvas metrics because document.fonts.check() accepts uninstalled families. */ diff --git a/apps/web/src/index.css b/apps/web/src/index.css index fb65ece08d69b..966eecf7c5036 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -1933,12 +1933,11 @@ code { list-style-type: disc; } -/* Outside markers paint past the padding box, so the gutter must fit `1. ` - (3ch in a monospace font; CSS can't measure it, so every font gets 3ch). - ChatMarkdown widens it inline for multi-digit markers; setting it here stops - a nested ol inheriting that. `ch` resolves against each list's own font. */ +/* Outside markers paint past the padding box, so the gutter must fit `0. `, + measured per font in appearanceFonts. ChatMarkdown widens it inline for + multi-digit markers; setting it here stops a nested ol inheriting that. */ :is(.chat-markdown, .composer-tiptap) ol { - --list-gutter: max(1.25rem, 3ch); + --list-gutter: max(1.25rem, var(--list-marker-sans, 0px)); padding-inline-start: var(--list-gutter, 1.25rem); list-style-type: decimal; } @@ -1955,6 +1954,10 @@ code { /* The prompt font and size can differ from the interface, so its gutter must also follow the prompt's font metrics. */ +.composer-tiptap ol { + --list-gutter: max(1.25rem, 3ch); +} + .composer-tiptap ol:has(> li[data-marker-width="3"]) { --list-gutter: max(1.25rem, 4ch); }