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 21c3816f8d34b..654ef108e8e75 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -1933,13 +1933,11 @@ 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. */ +/* 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: 1.25rem; + --list-gutter: max(1.25rem, var(--list-marker-sans, 0px)); padding-inline-start: var(--list-gutter, 1.25rem); list-style-type: decimal; }