authorgravatar for 35393046+lnieuwenhuis@users.noreply.github.comLars Nieuwenhuis <35393046+lnieuwenhuis@users.noreply.github.com> 2026-08-15 12:44:01+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-15 12:44:01+02:00
logeaa6c4712fe11f0396e549b1873f163dc202d229
tree8dfda75f414e3363ed9b7298c03f50782ca7808a
parenta6ac27e7fd965f26ee0c61946e056f0fd889818c
signature Signed by PGP key B5690EEEBB952194

fix(web): widen ordered-list marker gutter for 3+ digit item numbers (#6527)


3 files changed, 77 insertions(+), 2 deletions(-)

apps/web/src/components/ChatMarkdown.test.tsx created+36
......@@ -0,0 +1,36 @@
1import { describe, expect, it } from "vite-plus/test";
2
3import { orderedListGutterStyle } from "./ChatMarkdown";
4
5describe("orderedListGutterStyle", () => {
6 it("leaves the default gutter alone for single-digit lists", () => {
7 expect(orderedListGutterStyle(9, undefined)).toBeUndefined();
8 });
9
10 it("leaves the default gutter alone for two-digit lists", () => {
11 expect(orderedListGutterStyle(99, undefined)).toBeUndefined();
12 });
13
14 it("leaves the default gutter alone for a two-digit list that starts above 1", () => {
15 // start=50 + 49 items => last marker is "98", still two digits.
16 expect(orderedListGutterStyle(49, 50)).toBeUndefined();
17 });
18
19 it("widens the gutter once the last marker reaches three digits", () => {
20 // item 100 is the bug from #6512: a 100-item list starting at 1.
21 expect(orderedListGutterStyle(100, undefined)).toEqual({ "--list-gutter": "4ch" });
22 });
23
24 it("accounts for a non-default start attribute", () => {
25 // start=95 + 9 items => last marker is "103", three digits.
26 expect(orderedListGutterStyle(9, 95)).toEqual({ "--list-gutter": "4ch" });
27 });
28
29 it("scales further for four-digit markers", () => {
30 expect(orderedListGutterStyle(1000, undefined)).toEqual({ "--list-gutter": "5ch" });
31 });
32
33 it("treats a missing/zero item count as a single item", () => {
34 expect(orderedListGutterStyle(0, undefined)).toBeUndefined();
35 });
36});
apps/web/src/components/ChatMarkdown.tsx+29
......@@ -146,6 +146,26 @@ function findTaskListMarkerOffset(markdown: string, listItemStart: number): numb
146146 if (!match?.[1]) return null;
147147 return listItemStart + firstLine.indexOf(match[1]);
148148}
149
150/**
151 * The default `1.25rem` marker gutter (`.chat-markdown ol`) fits two-digit
152 * decimal markers. Once a list's last item reaches three digits (item 100+),
153 * `list-style-position: outside` paints the marker wider than that gutter and
154 * the leading digit gets clipped by the item's own overflow. Rather than
155 * widening the gutter for every list, only lists whose last marker is 3+
156 * digits get a wider `--list-gutter`, sized to that marker's digit count.
157 */
158export function orderedListGutterStyle(
159 itemCount: number,
160 start: number | undefined,
161): { "--list-gutter": string } | undefined {
162 const firstNumber = typeof start === "number" && Number.isFinite(start) ? start : 1;
163 const lastNumber = firstNumber + Math.max(itemCount - 1, 0);
164 const digits = String(Math.abs(lastNumber)).length;
165 if (digits <= 2) return undefined;
166 return { "--list-gutter": `${digits + 1}ch` };
167}
168
149169const CHAT_MARKDOWN_SANITIZE_SCHEMA = {
150170 ...defaultSchema,
151171 attributes: {
......@@ -1506,6 +1526,15 @@ function ChatMarkdown({
15061526 </div>
15071527 );
15081528 },
1529 ol({ node, start, style, ...props }) {
1530 const itemCount =
1531 node?.children?.filter((child) => child.type === "element" && child.tagName === "li")
1532 .length ?? 0;
1533 const gutterStyle = orderedListGutterStyle(itemCount, start);
1534 return (
1535 <ol {...props} start={start} style={gutterStyle ? { ...style, ...gutterStyle } : style} />
1536 );
1537 },
15091538 li({ node, children, ...props }) {
15101539 const listItemStart = node?.position?.start.offset;
15111540 const markerOffset =
apps/web/src/index.css+12-2
......@@ -1630,12 +1630,22 @@ code {
16301630}
16311631
16321632.chat-markdown ul {
1633 /* Reset for nested uls under a widened ol — --list-gutter is an inherited
1634 custom property, so without this a task-list under a 3+ digit ordered
1635 list would inherit the outer gutter instead of its own default. */
1636 --list-gutter: 1.25rem;
16331637 padding-left: 1.25rem;
16341638 list-style-type: disc;
16351639}
16361640
1641/* --list-gutter defaults to the same 1.25rem as .chat-markdown ul, but
1642 ChatMarkdown's `ol` renderer widens it (via inline style) for lists whose
1643 last marker is 3+ digits, so item 100+ isn't clipped by list-style-position:
1644 outside painting the marker past the padding box. Reset it here too so a
1645 nested ol without its own widened marker doesn't inherit the outer one. */
16371646.chat-markdown ol {
1638 padding-left: 1.25rem;
1647 --list-gutter: 1.25rem;
1648 padding-left: var(--list-gutter, 1.25rem);
16391649 list-style-type: decimal;
16401650}
16411651
......@@ -1665,7 +1675,7 @@ code {
16651675}
16661676
16671677.chat-markdown li.task-list-item input[type="checkbox"] {
1668 margin: 0 0.35em 0.15em -1.25rem;
1678 margin: 0 0.35em 0.15em calc(-1 * var(--list-gutter, 1.25rem));
16691679 vertical-align: middle;
16701680}
16711681