mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 03:45:46 -06:00
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:
committed by
GitHub
parent
3d016975c3
commit
3dd95d7bf7
@@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user