From eaa6c4712fe11f0396e549b1873f163dc202d229 Mon Sep 17 00:00:00 2001 From: Lars Nieuwenhuis <35393046+lnieuwenhuis@users.noreply.github.com> Date: Sat, 15 Aug 2026 12:44:01 +0200 Subject: [PATCH] fix(web): widen ordered-list marker gutter for 3+ digit item numbers (#6527) --- apps/web/src/components/ChatMarkdown.test.tsx | 36 +++++++++++++++++++ apps/web/src/components/ChatMarkdown.tsx | 29 +++++++++++++++ apps/web/src/index.css | 14 ++++++-- 3 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/components/ChatMarkdown.test.tsx diff --git a/apps/web/src/components/ChatMarkdown.test.tsx b/apps/web/src/components/ChatMarkdown.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9499ee5a69152a632b0e574a9c142bf414423c0f --- /dev/null +++ b/apps/web/src/components/ChatMarkdown.test.tsx @@ -0,0 +1,36 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { orderedListGutterStyle } from "./ChatMarkdown"; + +describe("orderedListGutterStyle", () => { + it("leaves the default gutter alone for single-digit lists", () => { + expect(orderedListGutterStyle(9, undefined)).toBeUndefined(); + }); + + it("leaves the default gutter alone for two-digit lists", () => { + expect(orderedListGutterStyle(99, undefined)).toBeUndefined(); + }); + + it("leaves the default gutter alone for a two-digit list that starts above 1", () => { + // start=50 + 49 items => last marker is "98", still two digits. + expect(orderedListGutterStyle(49, 50)).toBeUndefined(); + }); + + it("widens the gutter once the last marker reaches three digits", () => { + // item 100 is the bug from #6512: a 100-item list starting at 1. + expect(orderedListGutterStyle(100, undefined)).toEqual({ "--list-gutter": "4ch" }); + }); + + it("accounts for a non-default start attribute", () => { + // start=95 + 9 items => last marker is "103", three digits. + expect(orderedListGutterStyle(9, 95)).toEqual({ "--list-gutter": "4ch" }); + }); + + it("scales further for four-digit markers", () => { + expect(orderedListGutterStyle(1000, undefined)).toEqual({ "--list-gutter": "5ch" }); + }); + + it("treats a missing/zero item count as a single item", () => { + expect(orderedListGutterStyle(0, undefined)).toBeUndefined(); + }); +}); diff --git a/apps/web/src/components/ChatMarkdown.tsx b/apps/web/src/components/ChatMarkdown.tsx index 53b043f3a8ff32f551bc9701ff8bc0f6c277c810..294a9e22ad755df421379ac1f96329b585703c4b 100644 --- a/apps/web/src/components/ChatMarkdown.tsx +++ b/apps/web/src/components/ChatMarkdown.tsx @@ -146,6 +146,26 @@ function findTaskListMarkerOffset(markdown: string, listItemStart: number): numb if (!match?.[1]) return null; return listItemStart + firstLine.indexOf(match[1]); } + +/** + * The default `1.25rem` marker gutter (`.chat-markdown ol`) fits two-digit + * decimal markers. Once a list's last item reaches three digits (item 100+), + * `list-style-position: outside` paints the marker wider than that gutter and + * the leading digit gets clipped by the item's own overflow. Rather than + * widening the gutter for every list, only lists whose last marker is 3+ + * digits get a wider `--list-gutter`, sized to that marker's digit count. + */ +export function orderedListGutterStyle( + itemCount: number, + start: number | undefined, +): { "--list-gutter": string } | undefined { + const firstNumber = typeof start === "number" && Number.isFinite(start) ? start : 1; + const lastNumber = firstNumber + Math.max(itemCount - 1, 0); + const digits = String(Math.abs(lastNumber)).length; + if (digits <= 2) return undefined; + return { "--list-gutter": `${digits + 1}ch` }; +} + const CHAT_MARKDOWN_SANITIZE_SCHEMA = { ...defaultSchema, attributes: { @@ -1506,6 +1526,15 @@ function ChatMarkdown({ ); }, + ol({ node, start, style, ...props }) { + const itemCount = + node?.children?.filter((child) => child.type === "element" && child.tagName === "li") + .length ?? 0; + const gutterStyle = orderedListGutterStyle(itemCount, start); + return ( +
    + ); + }, li({ node, children, ...props }) { const listItemStart = node?.position?.start.offset; const markerOffset = diff --git a/apps/web/src/index.css b/apps/web/src/index.css index b2c914c0b69f85911b35647426728430ded449c9..299506c30ad28222fada2cbf0f5e72e0ab45c305 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -1630,12 +1630,22 @@ code { } .chat-markdown ul { + /* Reset for nested uls under a widened ol — --list-gutter is an inherited + custom property, so without this a task-list under a 3+ digit ordered + list would inherit the outer gutter instead of its own default. */ + --list-gutter: 1.25rem; padding-left: 1.25rem; 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 + last marker is 3+ digits, so item 100+ 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. */ .chat-markdown ol { - padding-left: 1.25rem; + --list-gutter: 1.25rem; + padding-left: var(--list-gutter, 1.25rem); list-style-type: decimal; } @@ -1665,7 +1675,7 @@ code { } .chat-markdown li.task-list-item input[type="checkbox"] { - margin: 0 0.35em 0.15em -1.25rem; + margin: 0 0.35em 0.15em calc(-1 * var(--list-gutter, 1.25rem)); vertical-align: middle; } -- 2.54.0