fix(markdown): derive table and task list colours from the theme

This commit is contained in:
Mohamed Boudra
2026-10-15 15:57:03 +02:00
parent f5fb20cc55
commit 134b796859
3 changed files with 158 additions and 79 deletions
@@ -1,4 +1,6 @@
import { describe, expect, it } from "vitest";
import { darkTheme, lightTheme } from "@/styles/theme";
import { createMarkdownStyles } from "./styles";
import { prepareMarkdownSource } from "./source";
describe("shared Markdown source adaptation", () => {
@@ -139,3 +141,57 @@ it.each(['""', "''", "()"])("preserves an authored empty link title: %s", (title
const source = `[Open](/repo/file.ts ${title})`;
expect(prepareMarkdownSource(source, "/repo").render()).toBe(source);
});
describe("shared Markdown theme colours", () => {
it.each([
["dark", darkTheme],
["light", lightTheme],
] as const)("uses %s theme colours for tables and task labels", (_name, theme) => {
const { markdownStyle: style } = createMarkdownStyles(theme);
expect(style.table).toMatchObject({
color: theme.colors.foreground,
headerTextColor: theme.colors.foreground,
headerBackgroundColor: theme.colors.surface2,
rowEvenBackgroundColor: theme.colors.surface0,
rowOddBackgroundColor: theme.colors.surface1,
borderColor: theme.colors.border,
});
expect(style.list?.color).toBe(theme.colors.foreground);
expect(style.taskList).toMatchObject({
checkedTextColor: theme.colors.foregroundMuted,
checkedColor: theme.colors.accent,
checkmarkColor: theme.colors.accentForeground,
borderColor: theme.colors.foregroundMuted,
});
});
});
it.each([
["dark", darkTheme],
["light", lightTheme],
] as const)(
"uses %s theme colours for quotes, admonitions, code, links and dividers",
(_name, theme) => {
const { markdownStyle: style } = createMarkdownStyles(theme);
expect(style.blockquote).toMatchObject({
color: theme.colors.foregroundMuted,
backgroundColor: theme.colors.surface2,
borderColor: theme.colors.border,
admonitions: {
note: { color: theme.colors.accentBright, backgroundColor: theme.colors.surface2 },
tip: { color: theme.colors.statusSuccess, backgroundColor: theme.colors.surface2 },
important: { color: theme.colors.statusMerged, backgroundColor: theme.colors.surface2 },
warning: { color: theme.colors.statusWarning, backgroundColor: theme.colors.surface2 },
caution: { color: theme.colors.statusDanger, backgroundColor: theme.colors.surface2 },
},
});
expect(style.code).toMatchObject({
color: theme.colors.foreground,
backgroundColor: theme.colors.surface2,
borderColor: "transparent",
});
expect(style.link?.color).toBe(theme.colors.accent);
expect(style.linkVariants?.["^(?!https?://).+"]?.color).toBe(theme.colors.accent);
expect(style.thematicBreak?.color).toBe(theme.colors.border);
},
);
@@ -3,7 +3,6 @@ import { markdownCopyImageSourcesDataSet } from "@/assistant-selection-copy/mark
import { memo, useMemo, useState } from "react";
import {
EnrichedMarkdownText,
type MarkdownStyle,
type LinkPressEvent,
type CodeBlockRendererProps,
} from "react-native-enriched-markdown";
@@ -17,6 +16,7 @@ import { HighlightedCodeBlock } from "@/components/highlighted-code-block";
import { AttachmentLightbox, type ImageLightboxSource } from "@/components/attachment-lightbox";
import type { Theme } from "@/styles/theme";
import { isWeb } from "@/constants/platform";
import { createMarkdownStyles } from "./styles";
import { useMarkdownSource, type MarkdownImageContext } from "./images";
import { MermaidFence, MermaidStreamingContext, splitMermaidFences } from "./fence/mermaid";
@@ -24,84 +24,8 @@ import { MermaidFence, MermaidStreamingContext, splitMermaidFences } from "./fen
const ThemedMarkdown = withUnistyles(EnrichedMarkdownText);
const MD4C_FLAGS = { latexMath: false, hardSoftBreaks: true, admonitions: true };
function markdownStyle(theme: Theme, compact = false): { markdownStyle: MarkdownStyle } {
const fontSize = compact ? theme.fontSize.base : theme.fontSize.content;
const paragraph = {
fontSize,
fontFamily: theme.fontFamily.ui,
lineHeight: Math.round(fontSize * 1.4),
color: theme.colors.foreground,
marginTop: 0,
marginBottom: theme.spacing[3],
};
const heading = (scale: number) => ({
...paragraph,
fontSize: Math.round(fontSize * scale),
lineHeight: Math.round(fontSize * scale * 1.3),
fontWeight: "bold",
marginTop: theme.spacing[3],
});
return {
markdownStyle: {
paragraph,
h1: heading(1.6),
h2: heading(1.4),
h3: heading(1.2),
h4: heading(1.1),
h5: heading(1),
h6: heading(1),
list: {
...paragraph,
bulletColor: theme.colors.foreground,
markerColor: theme.colors.foregroundMuted,
gapWidth: 7,
itemSpacing: 4,
marginLeft: 20,
},
blockquote: {
...paragraph,
color: theme.colors.foregroundMuted,
backgroundColor: theme.colors.surface2,
borderColor: theme.colors.border,
borderWidth: 3,
padding: 12,
gapWidth: 8,
},
link: { color: theme.colors.accent, underline: false },
linkVariants: {
"^(?!https?://).+": {
color: theme.colors.accent,
pill: { borderRadius: 4, paddingHorizontal: 3 },
},
},
code: {
fontFamily: theme.fontFamily.mono === "ui-monospace" && !isWeb ? "" : theme.fontFamily.mono,
fontSize: theme.fontSize.code,
color: theme.colors.foreground,
backgroundColor: theme.colors.surface2,
borderColor: "transparent",
},
codeBlock: {
...paragraph,
fontFamily: theme.fontFamily.mono === "ui-monospace" && !isWeb ? "" : theme.fontFamily.mono,
fontSize: theme.fontSize.code,
backgroundColor: theme.colors.surface2,
borderColor: theme.colors.border,
borderRadius: 8,
padding: 12,
},
table: {
...paragraph,
borderColor: theme.colors.border,
headerBackgroundColor: theme.colors.surface2,
},
image: { height: 320, resizeMode: "contain", borderRadius: 8 },
thematicBreak: { color: theme.colors.border, height: 1, marginTop: 12, marginBottom: 12 },
},
};
}
const regularStyle = (theme: Theme) => markdownStyle(theme);
const compactStyle = (theme: Theme) => markdownStyle(theme, true);
const regularStyle = (theme: Theme) => createMarkdownStyles(theme);
const compactStyle = (theme: Theme) => createMarkdownStyles(theme, true);
const ThemedCodeBlock = withUnistyles(HighlightedCodeBlock);
const codeStyle = (theme: Theme) => ({
inheritedStyles: {
@@ -0,0 +1,99 @@
import type { MarkdownStyle } from "react-native-enriched-markdown";
import type { Theme } from "@/styles/theme";
import { isWeb } from "@/constants/platform";
export function createMarkdownStyles(
theme: Theme,
compact = false,
): { markdownStyle: MarkdownStyle } {
const fontSize = compact ? theme.fontSize.base : theme.fontSize.content;
const paragraph = {
fontSize,
fontFamily: theme.fontFamily.ui,
lineHeight: Math.round(fontSize * 1.4),
color: theme.colors.foreground,
marginTop: 0,
marginBottom: theme.spacing[3],
};
const heading = (scale: number) => ({
...paragraph,
fontSize: Math.round(fontSize * scale),
lineHeight: Math.round(fontSize * scale * 1.3),
fontWeight: "bold",
marginTop: theme.spacing[3],
});
return {
markdownStyle: {
paragraph,
h1: heading(1.6),
h2: heading(1.4),
h3: heading(1.2),
h4: heading(1.1),
h5: heading(1),
h6: heading(1),
list: {
...paragraph,
bulletColor: theme.colors.foreground,
markerColor: theme.colors.foregroundMuted,
gapWidth: 7,
itemSpacing: 4,
marginLeft: 20,
},
blockquote: {
...paragraph,
color: theme.colors.foregroundMuted,
backgroundColor: theme.colors.surface2,
borderColor: theme.colors.border,
borderWidth: 3,
padding: 12,
gapWidth: 8,
admonitions: {
note: { color: theme.colors.accentBright, backgroundColor: theme.colors.surface2 },
tip: { color: theme.colors.statusSuccess, backgroundColor: theme.colors.surface2 },
important: { color: theme.colors.statusMerged, backgroundColor: theme.colors.surface2 },
warning: { color: theme.colors.statusWarning, backgroundColor: theme.colors.surface2 },
caution: { color: theme.colors.statusDanger, backgroundColor: theme.colors.surface2 },
},
},
link: { color: theme.colors.accent, underline: false },
linkVariants: {
"^(?!https?://).+": {
color: theme.colors.accent,
pill: { borderRadius: 4, paddingHorizontal: 3 },
},
},
code: {
fontFamily: theme.fontFamily.mono === "ui-monospace" && !isWeb ? "" : theme.fontFamily.mono,
fontSize: theme.fontSize.code,
color: theme.colors.foreground,
backgroundColor: theme.colors.surface2,
borderColor: "transparent",
},
codeBlock: {
...paragraph,
fontFamily: theme.fontFamily.mono === "ui-monospace" && !isWeb ? "" : theme.fontFamily.mono,
fontSize: theme.fontSize.code,
backgroundColor: theme.colors.surface2,
borderColor: theme.colors.border,
borderRadius: 8,
padding: 12,
},
table: {
...paragraph,
borderColor: theme.colors.border,
headerBackgroundColor: theme.colors.surface2,
headerTextColor: theme.colors.foreground,
rowEvenBackgroundColor: theme.colors.surface0,
rowOddBackgroundColor: theme.colors.surface1,
},
taskList: {
checkedTextColor: theme.colors.foregroundMuted,
checkedColor: theme.colors.accent,
checkmarkColor: theme.colors.accentForeground,
borderColor: theme.colors.foregroundMuted,
},
image: { height: 320, resizeMode: "contain", borderRadius: 8 },
thematicBreak: { color: theme.colors.border, height: 1, marginTop: 12, marginBottom: 12 },
},
};
}