fix(frontend): render ordered lists in markdown descriptions (#10973)

* 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

* fix(frontend): address review nits on the markdown prose fix

- default `GfmMarkdown` to the `sm` stack rather than `xs`: the AI-agent tool
  Message pane takes the default and has no ancestor font size, so `xs` left it
  smaller than its own label. The group note, whose wrapper is `text-2xs`, opts
  down explicitly.
- regenerate `static/tailwind_full.css`, which raw apps are served and which
  still carried the deleted list rules.
- correct the marker-color rationale: the typography config already maps markers
  to tertiary, so the rule steps them up rather than rescuing them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S6G5gDXJnm6uqch4uCPkPE

* fix(frontend): make the note color override an arbitrary value

`text-inherit` is not generated: this config replaces the Tailwind color palette
outright and defines no `inherit` key, so `[&_*]:!text-inherit` compiled to
nothing and notes still rendered in the prose stack's `text-primary`. Verified in
the browser: a yellow note's list items now compute to `text-yellow-900`, matching
the wrapper and the edit-mode textarea, in both themes.

Also drop the `static/tailwind_full.css` regeneration. That file was generated with
tailwind 3.4.1 against a config predating the typography theme overrides; rebuilding
it today sweeps in 250KB of unrelated churn and would flip every raw app's `.prose`
palette from stock gray to Windmill tokens. Its staleness predates this PR and is
its own change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S6G5gDXJnm6uqch4uCPkPE

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Diego Imbert
2026-09-05 10:39:44 +00:00
committed by GitHub
co-authored by Claude Opus 5
parent 1901d3193b
commit a0295b20c4
7 changed files with 29 additions and 27 deletions
+7 -16
View File
@@ -209,33 +209,24 @@
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 {
/* List markers, horizontal rules and blockquote bars take the tertiary/light
tokens the typography config maps them to, which is too faint to read on
the denser markdown surfaces (e.g. the AI chat). Step them up one. */
.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 = 'sm' }: 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,13 +127,13 @@
{: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}
onpointerdown={editMode ? stopPropagation(() => {}) : undefined}
>
<GfmMarkdown md={note} noPadding />
<GfmMarkdown md={note} prose="xs" noPadding />
</div>
{:else}
<!-- svelte-ignore a11y_no_static_element_interactions -->
@@ -105,6 +105,15 @@ 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.
// Arbitrary value, not `text-inherit`: this config replaces the color palette outright and
// defines no `inherit` key, so the named utility would be silently generated as nothing.
// (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,7 +295,8 @@
<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 />
+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