improve(ui): refine Markdown prose presentation (#125242)

* improve(ui): refine Markdown prose styling

* improve(ui): polish Markdown prose spacing

* improve(ui): normalize nested list rhythm

* fix(ui): fill Markdown image fallback width

* improve(ui): mute checked task styling

* fix(ui): align Markdown list markers

* fix(ui): preserve Markdown list rhythm with tasks

* improve(ui): clarify Markdown block rhythm

* test(ui): update Markdown disclosure geometry

* test(ui): match compact Markdown indent

* fix(ui): consolidate task list styling

* style(ui): format Markdown presentation

* test(ui): match Markdown block rhythm

* test(ui): enforce uniform Markdown rhythm
This commit is contained in:
Vyctor H. Brzezowski
2026-08-17 21:49:21 -03:00
committed by GitHub
parent 3d016975c3
commit 3dd95d7bf7
3 changed files with 130 additions and 84 deletions
+35 -32
View File
@@ -105,8 +105,7 @@ suite.define(() => {
if (!collapsedSummary || !jsonCollapse || !jsonSummary || !jsonCopy) {
throw new Error("Missing authored or JSON disclosure markup");
}
const closedChevronStyle = getComputedStyle(collapsedSummary, "::after");
const collapsedSummaryRect = collapsedSummary.getBoundingClientRect();
const closedChevronStyle = getComputedStyle(collapsedSummary, "::before");
const collapsedSummaryTextRect = textRect(
"details:not(.json-collapse):not([open]) > summary",
);
@@ -121,13 +120,13 @@ suite.define(() => {
checkboxRect.y + checkboxRect.height / 2 - (taskTextRect.y + taskTextRect.height / 2),
checkboxSize: checkboxRect.width,
chevronClosedTransform: closedChevronStyle.transform,
chevronInlineEnd: closedChevronStyle.insetInlineEnd,
chevronInlineStart: closedChevronStyle.insetInlineStart,
chevronTransitionDuration: closedChevronStyle.transitionDuration,
chevronWidth: closedChevronStyle.width,
collapsedSummaryPaddingInlineEnd: getComputedStyle(collapsedSummary).paddingInlineEnd,
collapsedSummaryTextRight: collapsedSummaryTextRect.right,
collapsedSummaryPaddingInlineStart:
getComputedStyle(collapsedSummary).paddingInlineStart,
collapsedSummaryTextX: collapsedSummaryTextRect.x,
collapsedSummaryRight: collapsedSummaryRect.right,
detailsRight: details.getBoundingClientRect().right,
detailsX: details.getBoundingClientRect().x,
jsonBorderInlineStartWidth: jsonCollapseStyle.borderInlineStartWidth,
jsonCopyFloat: getComputedStyle(jsonCopy).float,
@@ -135,38 +134,30 @@ suite.define(() => {
jsonSummaryDisplay: jsonSummaryStyle.display,
jsonSummaryPaddingInlineStart: jsonSummaryStyle.paddingInlineStart,
numberedTextX: textRect("ol > li").x,
rootRight: root.getBoundingClientRect().right,
rootX: root.getBoundingClientRect().x,
summaryMarginBottom: summaryStyle.marginBottom,
summaryTextX: textRect("details[open] > summary").x,
taskTextX: taskTextRect.x,
};
});
const textStarts = [
geometry.bulletTextX,
geometry.numberedTextX,
geometry.taskTextX,
geometry.summaryTextX,
geometry.collapsedSummaryTextX,
];
const textStarts = [geometry.bulletTextX, geometry.numberedTextX, geometry.taskTextX];
expect(Math.max(...textStarts) - Math.min(...textStarts)).toBeLessThanOrEqual(1);
expect(geometry.checkboxGap).toBeGreaterThanOrEqual(7);
expect(geometry.checkboxGap).toBeLessThanOrEqual(9);
expect(Math.abs(geometry.checkboxLineCenterDelta)).toBeLessThanOrEqual(1);
expect(geometry.checkboxSize).toBe(16);
expect(geometry.bodyTextX - geometry.rootX).toBeGreaterThanOrEqual(28);
expect(geometry.detailsX).toBeGreaterThan(geometry.rootX);
expect(geometry.detailsX).toBeLessThan(geometry.bodyTextX);
expect(geometry.bodyTextX).toBeGreaterThan(geometry.detailsX);
expect(Math.abs(geometry.detailsX - geometry.rootX)).toBeLessThanOrEqual(1);
expect(Math.abs(geometry.detailsRight - geometry.rootRight)).toBeLessThanOrEqual(1);
expect(Number.parseFloat(geometry.borderInlineStartWidth)).toBeGreaterThan(0);
expect(Number.parseFloat(geometry.summaryMarginBottom)).toBeGreaterThan(0);
expect(Number.parseFloat(geometry.chevronWidth)).toBe(16);
expect(Number.parseFloat(geometry.chevronInlineEnd)).toBe(0);
expect(Number.parseFloat(geometry.collapsedSummaryPaddingInlineEnd)).toBeGreaterThanOrEqual(
24,
);
expect(geometry.collapsedSummaryRight - geometry.collapsedSummaryTextRight).toBeGreaterThan(
24,
);
expect(Number.parseFloat(geometry.chevronInlineStart)).toBe(0);
expect(
Number.parseFloat(geometry.collapsedSummaryPaddingInlineStart),
).toBeGreaterThanOrEqual(24);
expect(geometry.collapsedSummaryTextX - geometry.detailsX).toBeGreaterThan(24);
expect(geometry.chevronTransitionDuration).not.toBe("0s");
expect(Math.abs(geometry.jsonDetailsX - geometry.rootX)).toBeLessThanOrEqual(1);
expect(Number.parseFloat(geometry.jsonBorderInlineStartWidth)).toBe(0);
@@ -178,14 +169,14 @@ suite.define(() => {
await collapsedSummary.click();
await expect
.poll(() =>
collapsedSummary.evaluate((summary) => getComputedStyle(summary, "::after").transform),
collapsedSummary.evaluate((summary) => getComputedStyle(summary, "::before").transform),
)
.not.toBe(geometry.chevronClosedTransform);
},
);
});
it("preserves the shared Markdown gutter in RTL transcripts", async () => {
it("preserves Markdown marker and disclosure alignment in RTL transcripts", async () => {
await suite.withPage(
{
colorScheme: "light",
@@ -252,19 +243,27 @@ suite.define(() => {
const unorderedList = root.querySelector("ul:not(.contains-task-list)");
const orderedList = root.querySelector("ol");
const summary = root.querySelector("details:not(.json-collapse) > summary");
if (!checkbox || !task || !unorderedList || !orderedList || !summary) {
const details = root.querySelector("details:not(.json-collapse)");
if (!checkbox || !task || !unorderedList || !orderedList || !summary || !details) {
throw new Error("Missing RTL Markdown geometry");
}
const checkboxRect = checkbox.getBoundingClientRect();
const detailsRect = details.getBoundingClientRect();
const rootRect = root.getBoundingClientRect();
return {
checkboxGap: checkboxRect.left - textRight(".task-list-item"),
chevronInlineEnd: getComputedStyle(summary, "::after").insetInlineEnd,
chevronInlineStart: getComputedStyle(summary, "::before").insetInlineStart,
detailsRight: detailsRect.right,
detailsX: detailsRect.x,
orderedPaddingInlineStart: getComputedStyle(orderedList).paddingInlineStart,
rootRight: rootRect.right,
rootX: rootRect.x,
summaryPaddingInlineStart: getComputedStyle(summary).paddingInlineStart,
summaryTextRight: textRight("details:not(.json-collapse) > summary"),
textStarts: [
textRight("ul:not(.contains-task-list) > li"),
textRight("ol > li"),
textRight(".task-list-item"),
textRight("details:not(.json-collapse) > summary"),
],
unorderedPaddingInlineStart: getComputedStyle(unorderedList).paddingInlineStart,
};
@@ -273,11 +272,15 @@ suite.define(() => {
expect(
Math.max(...geometry.textStarts) - Math.min(...geometry.textStarts),
).toBeLessThanOrEqual(1);
expect(Number.parseFloat(geometry.unorderedPaddingInlineStart)).toBe(32);
expect(Number.parseFloat(geometry.orderedPaddingInlineStart)).toBe(32);
expect(Number.parseFloat(geometry.unorderedPaddingInlineStart)).toBe(24);
expect(Number.parseFloat(geometry.orderedPaddingInlineStart)).toBe(24);
expect(geometry.checkboxGap).toBeGreaterThanOrEqual(7);
expect(geometry.checkboxGap).toBeLessThanOrEqual(9);
expect(Number.parseFloat(geometry.chevronInlineEnd)).toBe(0);
expect(Number.parseFloat(geometry.chevronInlineStart)).toBe(0);
expect(Number.parseFloat(geometry.summaryPaddingInlineStart)).toBeGreaterThanOrEqual(24);
expect(geometry.detailsRight - geometry.summaryTextRight).toBeGreaterThan(24);
expect(Math.abs(geometry.detailsX - geometry.rootX)).toBeLessThanOrEqual(1);
expect(Math.abs(geometry.detailsRight - geometry.rootRight)).toBeLessThanOrEqual(1);
},
);
});
+4 -7
View File
@@ -201,20 +201,17 @@ suite.define(() => {
expect(chatMarkdownStyles.tableToCopyGap).toBeGreaterThan(0);
expect(chatMarkdownStyles.taskListToDetailsGap).toBeGreaterThan(0);
expect(chatMarkdownStyles.unorderedToOrderedGap).toBeGreaterThan(0);
expect(chatMarkdownStyles.quoteMargin / chatMarkdownStyles.blockFontSize).toBeCloseTo(
0.75,
2,
);
expect(chatMarkdownStyles.quoteMargin / chatMarkdownStyles.blockFontSize).toBeCloseTo(1, 2);
expect(chatMarkdownStyles.tableCopyMargin / chatMarkdownStyles.blockFontSize).toBeCloseTo(
0.75,
1,
2,
);
expect(chatMarkdownStyles.detailsMargin / chatMarkdownStyles.blockFontSize).toBeCloseTo(
0.75,
1,
2,
);
expect(chatMarkdownStyles.orderedMargin / chatMarkdownStyles.blockFontSize).toBeCloseTo(
0.75,
1,
2,
);
},
+91 -45
View File
@@ -93,9 +93,16 @@
/* Keep every adjacent top-level Markdown block on the same rhythm, including
special blocks that otherwise read as part of the preceding block. */
.chat-text
:where(p, ul, ol, pre, blockquote, table, details)
> :where(p, ul, ol, pre, blockquote, table, details)
+ :where(p, ul, ol, pre, blockquote, table, details) {
margin-top: 0.75em;
margin-top: 1em;
}
.chat-text
:where(blockquote, details, li)
> :where(p, ul, ol, pre, blockquote, table, details)
+ :where(p, ul, ol, pre, blockquote, table, details) {
margin-top: 0.5em;
}
/* Tables retain top margin even when first; the adjacent-block rule above gives
@@ -167,7 +174,7 @@
}
:is(.chat-text, .chat-thinking) {
--chat-markdown-indent: var(--space-7);
--chat-markdown-indent: var(--space-6);
--chat-markdown-marker-gap: var(--space-2);
}
@@ -175,15 +182,14 @@
padding-inline-start: var(--chat-markdown-indent);
}
:is(.chat-text, .chat-thinking) :where(details:not(.json-collapse)) {
margin-inline-start: var(--space-3);
padding-inline-start: calc(var(--chat-markdown-indent) - var(--space-3));
}
.chat-text :where(li + li) {
margin-top: 0.4em;
}
.chat-text :where(li > ul, li > ol) {
margin-top: 0.2em;
}
/* Keep bullets, checkboxes, and numbers in one marker column while their text
shares the same start edge. */
:is(.chat-text, .chat-thinking) :where(ul > li:not(.task-list-item))::marker {
@@ -192,6 +198,8 @@
:is(.chat-text, .chat-thinking) :where(ul > .task-list-item) {
list-style: none;
margin-inline-start: calc(-1 * var(--chat-markdown-indent));
padding-inline-start: calc(var(--space-4) + var(--chat-markdown-marker-gap));
}
:is(.chat-text, .chat-thinking) :where(ol > li)::marker {
@@ -204,18 +212,36 @@
:is(.chat-text, .chat-thinking) :where(ul > li:not(.task-list-item))::before {
position: absolute;
inset-inline-end: calc(100% + var(--chat-markdown-marker-gap));
inset-inline-start: calc(-1 * var(--chat-markdown-indent));
content: "•";
inset-block-start: 0;
inline-size: calc(var(--chat-markdown-indent) - var(--chat-markdown-marker-gap));
text-align: end;
color: var(--muted);
font-size: calc(1em + 1px);
}
/* Authored disclosures align with list text but place their affordance at the
row edge; JSON code blocks retain their separate compact presentation. */
:is(.chat-text, .chat-thinking) :where(details:not(.json-collapse)) {
box-sizing: border-box;
width: 100%;
padding: var(--space-4);
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: transparent;
}
:is(.chat-text, .chat-thinking) :where(details:not(.json-collapse) details:not(.json-collapse)) {
width: calc(100% - var(--space-3));
margin-top: var(--space-3);
margin-inline-start: var(--space-3);
}
/* Authored disclosures are complete content surfaces; JSON code blocks retain
their separate compact presentation. */
:is(.chat-text, .chat-thinking) :where(details:not(.json-collapse) > summary) {
position: relative;
padding-inline-end: var(--space-7);
min-height: var(--space-5);
padding-inline-start: var(--space-6);
list-style: none;
cursor: var(--cursor-action);
}
@@ -225,11 +251,11 @@
display: none;
}
:is(.chat-text, .chat-thinking) :where(details:not(.json-collapse) > summary)::after {
:is(.chat-text, .chat-thinking) :where(details:not(.json-collapse) > summary)::before {
position: absolute;
content: "";
inset-block-start: calc((1lh - var(--space-4)) / 2);
inset-inline-end: 0;
inset-inline-start: 0;
inline-size: var(--space-4);
block-size: var(--space-4);
background: var(--select-chevron) center / var(--space-4) no-repeat;
@@ -237,22 +263,29 @@
transition: transform var(--duration-normal) var(--ease-out);
}
:is(.chat-text, .chat-thinking) :where(details:not(.json-collapse)[open] > summary)::after {
:is(.chat-text, .chat-thinking) :where(details:not(.json-collapse)[open] > summary)::before {
transform: rotate(0deg);
}
:is(.chat-text, .chat-thinking) :where(.task-list-item-checkbox) {
appearance: none;
position: absolute;
inset-block-start: calc((1lh - var(--space-4)) / 2);
inset-inline-end: calc(100% + var(--chat-markdown-marker-gap));
inset-inline-start: 0;
inline-size: var(--space-4);
block-size: var(--space-4);
margin: 0;
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
background: var(--bg);
opacity: 1;
}
:is(.chat-text, .chat-thinking) :where(details:not(.json-collapse)[open]) {
padding-inline-start: calc(var(--chat-markdown-indent) - var(--space-3) - 1px);
border-inline-start: 1px solid var(--border);
:is(.chat-text, .chat-thinking) :where(.task-list-item-checkbox:checked) {
border-color: color-mix(in srgb, var(--accent) 72%, var(--border));
background: color-mix(in srgb, var(--accent) 72%, var(--bg))
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='white' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m3 8 3 3 7-7'/%3E%3C/svg%3E")
center / 13px no-repeat;
}
:is(.chat-text, .chat-thinking) :where(details:not(.json-collapse)[open] > summary) {
@@ -260,7 +293,7 @@
}
@media (prefers-reduced-motion: reduce) {
:is(.chat-text, .chat-thinking) :where(details:not(.json-collapse) > summary)::after {
:is(.chat-text, .chat-thinking) :where(details:not(.json-collapse) > summary)::before {
transition: none;
}
}
@@ -273,6 +306,7 @@
.chat-text :where(a) {
color: var(--link);
text-decoration: underline;
text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
text-underline-offset: 2px;
}
@@ -493,19 +527,40 @@
:is(.chat-text, .sidebar-markdown) .markdown-external-image {
display: flex;
box-sizing: border-box;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.35em 0.75em;
width: fit-content;
width: 100%;
max-width: 100%;
margin-top: 0.75em;
padding: 0.5em 0.65em;
border: 1px dashed var(--border-strong);
padding: 0.7em 0.8em;
border: 0;
border-radius: var(--radius-md);
background: var(--bg-muted);
color: var(--muted);
}
:is(.chat-text, .sidebar-markdown) .markdown-external-image > a {
display: inline-flex;
align-items: center;
min-height: 28px;
padding: 3px 9px;
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--text) 8%, transparent);
color: var(--link);
font-size: var(--control-ui-text-sm);
font-weight: 550;
text-decoration: none;
}
:is(.chat-text, .sidebar-markdown) .markdown-external-image > a:hover {
background: color-mix(in srgb, var(--text) 13%, transparent);
color: var(--link-hover);
text-decoration: none;
}
/* Code surfaces must lift off the host bubble in every theme. --secondary equals
--card on every dark palette, so chips painted with it vanished inside user
bubbles; --bg-muted/--border-strong separate in both modes without an override. */
@@ -563,33 +618,26 @@
}
.chat-text :where(blockquote) {
border-left: 2px solid var(--border-strong);
position: relative;
border-inline-start: 0;
margin-left: 0;
color: var(--muted);
background: rgba(255, 255, 255, 0.02);
background: transparent;
padding: 8px 12px;
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.chat-text :where(blockquote)::before {
position: absolute;
content: "";
inset-block: 4px;
inset-inline-start: 0;
width: 3px;
border-radius: var(--radius-full);
background: var(--border-strong);
}
.chat-text :where(blockquote blockquote) {
margin-top: 8px;
background: rgba(255, 255, 255, 0.03);
}
.chat-text :where(blockquote blockquote blockquote) {
background: rgba(255, 255, 255, 0.04);
}
:root[data-theme-mode="light"] .chat-text :where(blockquote) {
background: rgba(0, 0, 0, 0.03);
}
:root[data-theme-mode="light"] .chat-text :where(blockquote blockquote) {
background: rgba(0, 0, 0, 0.05);
}
:root[data-theme-mode="light"] .chat-text :where(blockquote blockquote blockquote) {
background: rgba(0, 0, 0, 0.04);
}
.chat-text :where(hr) {
@@ -607,8 +655,6 @@
}
.chat-text[dir="rtl"] :where(blockquote) {
border-left: none;
border-right: 3px solid var(--border);
padding-left: 0;
padding-right: 1em;
}