fix(frontend): render ordered lists in markdown descriptions

`GfmMarkdown` defaulted to `prose-xs`, which Tailwind Typography does not
define — the class only ever matched four hand-rolled rules in app.css, all
scoped to `ul`. Every surface on that default (script and flow descriptions,
flow-graph notes, markdown job results) therefore rendered `<ol>` with
Preflight's `list-style: none` and no typography at all: no numbers, no
heading or paragraph rhythm.

Route the default through the shared `markdownProse` stacks instead, and cut
the app.css list rules down to the dash glyph so ordered and unordered lists
share Tailwind Typography's indentation and rhythm.

Fixes #10971

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S6G5gDXJnm6uqch4uCPkPE
This commit is contained in:
Diego Imbert
2026-09-04 15:38:34 +02:00
co-authored by Claude Opus 5
parent fda7b3f086
commit fe36aa34f0
9 changed files with 30 additions and 27 deletions
+8 -16
View File
@@ -209,33 +209,25 @@
U+1fac6, U+1fae0-1fae6, U+1fae8-1faea, U+1faef-1faf8;
}
.prose-xs ul {
margin-top: 0.5rem;
list-style-type: '- ';
padding-left: 1.5rem;
}
/* Bullets read as a dash rather than a disc. Only the glyph is overridden:
indentation and vertical rhythm stay with Tailwind Typography so ordered
and unordered lists line up with each other. */
.prose ul {
margin-top: 1.5rem;
list-style-type: '- ';
padding-left: 3rem;
}
/* The '- ' list markers, horizontal rules and blockquote bars otherwise
fall through to Tailwind Typography's default bullet/border colors, which
are nearly invisible on dark backgrounds (e.g. the AI chat). Use
theme-aware tokens so they stay readable in both light and dark mode. */
.prose-xs ul > li::marker,
.prose ul > li::marker {
/* The list markers, horizontal rules and blockquote bars otherwise fall
through to Tailwind Typography's default bullet/border colors, which are
nearly invisible on dark backgrounds (e.g. the AI chat). Use theme-aware
tokens so they stay readable in both light and dark mode. */
.prose :is(ul, ol) > li::marker {
color: rgb(var(--color-text-secondary));
}
.prose-xs hr,
.prose hr {
border-top-color: rgb(var(--color-border-normal));
}
.prose-xs blockquote,
.prose blockquote {
border-left-color: rgb(var(--color-border-normal));
}
@@ -32,6 +32,7 @@
import Alert from './common/alert/Alert.svelte'
import AutoDataTable from './table/AutoDataTable.svelte'
import Markdown from 'svelte-exmarkdown'
import { markdownProse } from './markdownProse'
import Toggle from './Toggle.svelte'
import FileDownload from './common/fileDownload/FileDownload.svelte'
@@ -1229,7 +1230,7 @@
</div>
</div>
{:else if !forceJson && resultKind === 'markdown'}
<div class="prose-xs dark:prose-invert !list-disc !list-outside">
<div class={markdownProse.sm}>
<Markdown md={result?.md ?? result?.markdown} />
</div>
{:else if largeObject || hasBigInt}
@@ -6,12 +6,11 @@
interface Props {
md: string
noPadding?: boolean
/** Shared prose stack to render with. Omitted keeps the legacy `prose-xs`,
* which the flow-graph notes are laid out against. */
/** Shared prose stack to render with. */
prose?: MarkdownProseSize
}
let { md, noPadding, prose }: Props = $props()
let { md, noPadding, prose = 'xs' }: Props = $props()
// Rendering markdown turns `![](url)` into a real `<img>`, i.e. a request. On the
// public replay page the source is a recording from an arbitrary origin and the
@@ -21,7 +20,7 @@
let asPlainText = $derived(isOfflineReplay())
</script>
<div class="{prose ? markdownProse[prose] : '!prose-xs'} {noPadding ? '' : 'pgap'}">
<div class="{markdownProse[prose]} {noPadding ? '' : 'pgap'}">
{#if asPlainText}
<p class="whitespace-pre-wrap">{md}</p>
{:else}
@@ -1,7 +1,7 @@
<script lang="ts">
import GfmMarkdown from '$lib/components/GfmMarkdown.svelte'
import { Check, X } from 'lucide-svelte'
import { NOTE_COLORS, NoteColor } from './noteColors'
import { NOTE_COLORS, NOTE_TEXT_COLOR_OVERRIDE, NoteColor } from './noteColors'
import { stopPropagation, preventDefault } from 'svelte/legacy'
interface Props {
@@ -127,7 +127,7 @@
{:else if note}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="w-full text-2xs break-words overflow-hidden p-2 select-text {noteColorConfig.text} {editMode
class="w-full text-2xs break-words overflow-hidden p-2 select-text {noteColorConfig.text} {NOTE_TEXT_COLOR_OVERRIDE} {editMode
? 'cursor-pointer'
: ''}"
ondblclick={editMode ? stopPropagation(preventDefault(handleDoubleClick)) : undefined}
@@ -105,6 +105,13 @@ export const NOTE_COLORS: Record<NoteColor, NoteColorConfig> = {
}
}
// A note renders its text as markdown, and the prose stack sets body, heading and
// strong colors directly on those elements — which would beat the note color the
// wrapper only passes down by inheritance, and leave the render mismatched against
// the textarea shown while editing. Pin every descendant back to the note color.
// (Kept as a literal: Tailwind's scanner reads class names verbatim from this file.)
export const NOTE_TEXT_COLOR_OVERRIDE = '[&_*]:!text-inherit'
// Color swatch colors for the picker (solid colors for the palette dots)
export const NOTE_COLOR_SWATCHES: Record<NoteColor, string> = {
[NoteColor.YELLOW]: 'bg-yellow-400',
@@ -8,6 +8,7 @@
import {
NoteColor,
NOTE_COLORS,
NOTE_TEXT_COLOR_OVERRIDE,
DEFAULT_NOTE_COLOR,
MIN_NOTE_WIDTH,
MIN_NOTE_HEIGHT
@@ -294,10 +295,11 @@
<div
class={twMerge(
'w-full text-xs rounded-md break-words overflow-hidden',
colorConfig.text
colorConfig.text,
NOTE_TEXT_COLOR_OVERRIDE
)}
>
<GfmMarkdown md={textForDisplay} noPadding />
<GfmMarkdown md={textForDisplay} prose="sm" noPadding />
</div>
{:else}
<div class={twMerge('text-xs italic opacity-60', colorConfig.text)}>
+2 -2
View File
@@ -22,13 +22,13 @@ const base =
// One vertical rhythm for sm/doc; heading margins stay per-preset (fixed, not
// the plugin's em-based ones) so 'doc' can breathe more between sections.
const rhythm = 'prose-sm leading-snug prose-ul:!pl-6'
const rhythm = 'prose-sm leading-snug'
const bodyXs =
'text-primary prose-p:text-primary prose-li:text-primary prose-p:text-xs prose-li:text-xs prose-code:text-xs prose-pre:text-xs prose-table:text-xs'
export const markdownProse = {
xs: `${base} prose-sm leading-snug prose-ul:!pl-5 prose-p:text-2xs prose-li:text-2xs prose-code:text-2xs prose-pre:text-2xs prose-headings:font-medium prose-headings:text-secondary prose-headings:mt-2 prose-headings:mb-1 prose-h1:text-2xs prose-h2:text-2xs prose-h3:text-2xs prose-h4:text-2xs prose-h5:text-2xs prose-h6:text-2xs prose-strong:text-secondary`,
xs: `${base} prose-sm leading-snug prose-p:text-2xs prose-li:text-2xs prose-code:text-2xs prose-pre:text-2xs prose-headings:font-medium prose-headings:text-secondary prose-headings:mt-2 prose-headings:mb-1 prose-h1:text-2xs prose-h2:text-2xs prose-h3:text-2xs prose-h4:text-2xs prose-h5:text-2xs prose-h6:text-2xs prose-strong:text-secondary`,
sm: `${base} ${rhythm} ${bodyXs} prose-headings:mt-3 prose-headings:mb-1 prose-headings:font-medium prose-headings:text-emphasis prose-h1:text-sm prose-h2:text-xs prose-h3:text-xs prose-h4:text-xs prose-h5:text-xs prose-h6:text-xs`,
doc: `${base} ${rhythm} ${bodyXs} prose-headings:mt-8 prose-headings:mb-2 prose-headings:font-semibold prose-headings:text-emphasis prose-h1:text-lg prose-h2:text-base prose-h3:text-sm prose-h4:text-xs prose-h5:text-xs prose-h6:text-xs prose-pre:bg-transparent prose-pre:p-0`
} as const
@@ -666,6 +666,7 @@
<div class="p-4 rounded-md bg-surface-secondary">
<GfmMarkdown
md={defaultIfEmptyString(flow?.description, 'No description')}
prose="sm"
noPadding
/>
</div>
@@ -888,6 +888,7 @@
<div class="p-4 rounded-md bg-surface-secondary">
<GfmMarkdown
md={defaultIfEmptyString(script?.description, 'No description')}
prose="sm"
noPadding
/>
</div>