authorgravatar for 63693423+abcdmku@users.noreply.github.comabcdmku <63693423+abcdmku@users.noreply.github.com> 2026-08-22 09:30:58-05:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-22 10:30:58-04:00
log2c4158f87a1b6a586d0aa5e0338f122cb7887c4f
tree6bbc0f72e82354fe297815b1ea433f537864461c
parent0ede2ed0de8214c0e9168dcf1936e68e5c730e10
signature Signed by PGP key B5690EEEBB952194

fix(web): handle wide ordered-list marker edge cases (#7856)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

3 files changed, 26 insertions(+), 13 deletions(-)

apps/web/src/components/ChatMarkdown.test.tsx+8
...@@ -24,13 +24,21 @@ describe("orderedListGutterStyle", () => {...@@ -24,13 +24,21 @@ describe("orderedListGutterStyle", () => {
24 it("accounts for a non-default start attribute", () => {24 it("accounts for a non-default start attribute", () => {
25 // start=95 + 9 items => last marker is "103", three digits.25 // start=95 + 9 items => last marker is "103", three digits.
26 expect(orderedListGutterStyle(9, 95)).toEqual({ "--list-gutter": "4ch" });26 expect(orderedListGutterStyle(9, 95)).toEqual({ "--list-gutter": "4ch" });
27 expect(orderedListGutterStyle(5, "999995")).toEqual({ "--list-gutter": "7ch" });
27 });28 });
2829
29 it("scales further for four-digit markers", () => {30 it("scales further for four-digit markers", () => {
30 expect(orderedListGutterStyle(1000, undefined)).toEqual({ "--list-gutter": "5ch" });31 expect(orderedListGutterStyle(1000, undefined)).toEqual({ "--list-gutter": "5ch" });
31 });32 });
3233
34 it("uses the widest marker and includes a negative start's minus sign", () => {
35 expect(orderedListGutterStyle(1001, -1000)).toEqual({ "--list-gutter": "6ch" });
36 expect(orderedListGutterStyle(3, -15)).toEqual({ "--list-gutter": "4ch" });
37 expect(orderedListGutterStyle(3, -5)).toBeUndefined();
38 });
39
33 it("treats a missing/zero item count as a single item", () => {40 it("treats a missing/zero item count as a single item", () => {
34 expect(orderedListGutterStyle(0, undefined)).toBeUndefined();41 expect(orderedListGutterStyle(0, undefined)).toBeUndefined();
42 expect(orderedListGutterStyle(0, 100)).toEqual({ "--list-gutter": "4ch" });
35 });43 });
36});44});
apps/web/src/components/ChatMarkdown.tsx+13-11
...@@ -159,22 +159,24 @@ function findTaskListMarkerOffset(markdown: string, listItemStart: number): numb...@@ -159,22 +159,24 @@ function findTaskListMarkerOffset(markdown: string, listItemStart: number): numb
159}159}
160160
161/**161/**
162 * The default `1.25rem` marker gutter (`.chat-markdown ol`) fits two-digit162 * The default `1.25rem` marker gutter (`.chat-markdown ol`) fits markers up to
163 * decimal markers. Once a list's last item reaches three digits (item 100+),163 * two characters wide. Once a marker reaches three characters (item 100+),
164 * `list-style-position: outside` paints the marker wider than that gutter and164 * `list-style-position: outside` paints it wider than that gutter and clips
165 * the leading digit gets clipped by the item's own overflow. Rather than165 * the leading character against the item's own overflow. Rather than widening
166 * widening the gutter for every list, only lists whose last marker is 3+166 * the gutter for every list, only lists whose widest marker is 3+ characters
167 * digits get a wider `--list-gutter`, sized to that marker's digit count.167 * get a wider `--list-gutter`. The width includes a negative marker's minus
168 * sign.
168 */169 */
169export function orderedListGutterStyle(170export function orderedListGutterStyle(
170 itemCount: number,171 itemCount: number,
171 start: number | undefined,172 start: unknown,
172): { "--list-gutter": string } | undefined {173): { "--list-gutter": string } | undefined {
173 const firstNumber = typeof start === "number" && Number.isFinite(start) ? start : 1;174 const parsedStart = Number.parseInt(String(start ?? 1), 10);
175 const firstNumber = Number.isNaN(parsedStart) ? 1 : parsedStart;
174 const lastNumber = firstNumber + Math.max(itemCount - 1, 0);176 const lastNumber = firstNumber + Math.max(itemCount - 1, 0);
175 const digits = String(Math.abs(lastNumber)).length;177 const markerWidth = Math.max(String(firstNumber).length, String(lastNumber).length);
176 if (digits <= 2) return undefined;178 if (markerWidth <= 2) return undefined;
177 return { "--list-gutter": `${digits + 1}ch` };179 return { "--list-gutter": `${markerWidth + 1}ch` };
178}180}
179181
180const CHAT_MARKDOWN_SANITIZE_SCHEMA = {182const CHAT_MARKDOWN_SANITIZE_SCHEMA = {
apps/web/src/index.css+5-2
...@@ -2040,7 +2040,7 @@ code {...@@ -2040,7 +2040,7 @@ code {
20402040
2041/* --list-gutter defaults to the same 1.25rem as .chat-markdown ul, but2041/* --list-gutter defaults to the same 1.25rem as .chat-markdown ul, but
2042 ChatMarkdown's `ol` renderer widens it (via inline style) for lists whose2042 ChatMarkdown's `ol` renderer widens it (via inline style) for lists whose
2043 last marker is 3+ digits, so item 100+ isn't clipped by list-style-position:2043 widest marker is 3+ characters, so item 100+ isn't clipped by list-style-position:
2044 outside painting the marker past the padding box. Reset it here too so a2044 outside painting the marker past the padding box. Reset it here too so a
2045 nested ol without its own widened marker doesn't inherit the outer one. */2045 nested ol without its own widened marker doesn't inherit the outer one. */
2046.chat-markdown ol {2046.chat-markdown ol {
...@@ -2049,6 +2049,10 @@ code {...@@ -2049,6 +2049,10 @@ code {
2049 list-style-type: decimal;2049 list-style-type: decimal;
2050}2050}
20512051
2052.chat-markdown ol > li::marker {
2053 font-variant-numeric: tabular-nums;
2054}
2055
2052.chat-markdown ul ul {2056.chat-markdown ul ul {
2053 list-style-type: circle;2057 list-style-type: circle;
2054}2058}
...@@ -2114,7 +2118,6 @@ code {...@@ -2114,7 +2118,6 @@ code {
21142118
2115.chat-markdown section[data-footnotes] ol {2119.chat-markdown section[data-footnotes] ol {
2116 margin: 0;2120 margin: 0;
2117 padding-left: 1.25rem;
2118}2121}
21192122
2120.chat-markdown section[data-footnotes] li + li {2123.chat-markdown section[data-footnotes] li + li {