fix(tui): visually distinguish markdown table rows from prose (#15534)

Tables rendered through `<Md>` had no separator and no header weight,
so they read as a paragraph with extra whitespace.  This adds two tiny,
border-free changes that survive Ink's grapheme-approximate column
widths better than a full outline:

* Bold the header row, keeping the existing amber colour.
* Insert a dim `─`-dashed rule between the header and body rows.

We deliberately stay away from a full outline — column widths are
measured via `stripInlineMarkup(...).length`, which is grapheme-aware
but still off by a cell on East Asian wide characters and emoji-mid-
cell strings.  A header rule plus the existing 2-space column gap
gives the visual hierarchy the issue asks for without amplifying that
inaccuracy into a misaligned border.

Validation: `npm run type-check` clean, `npm test --run` 389/389.
This commit is contained in:
Brooklyn Nicholson 2026-04-28 13:56:39 -05:00
parent 0d957a8d48
commit 9eabc24e24

View file

@ -1,5 +1,5 @@
import { Box, Link, Text } from '@hermes/ink' import { Box, Link, Text } from '@hermes/ink'
import { memo, type ReactNode, useMemo } from 'react' import { Fragment, memo, type ReactNode, useMemo } from 'react'
import { ensureEmojiPresentation } from '../lib/emoji.js' import { ensureEmojiPresentation } from '../lib/emoji.js'
import { highlightLine, isHighlightable } from '../lib/syntax.js' import { highlightLine, isHighlightable } from '../lib/syntax.js'
@ -97,18 +97,33 @@ export const stripInlineMarkup = (v: string) =>
const renderTable = (k: number, rows: string[][], t: Theme) => { const renderTable = (k: number, rows: string[][], t: Theme) => {
const widths = rows[0]!.map((_, ci) => Math.max(...rows.map(r => stripInlineMarkup(r[ci] ?? '').length))) const widths = rows[0]!.map((_, ci) => Math.max(...rows.map(r => stripInlineMarkup(r[ci] ?? '').length)))
// Thin divider under the header. Without it tables look like prose
// with extra spacing because the header is just amber-coloured text
// (#15534). We avoid full borders on purpose — column widths are
// grapheme-approximate so a real outline often misaligns; one dim
// dashed rule under row 0 plus tab-style column gaps reads cleanly
// on every terminal we tested.
const sep = widths.map(w => '─'.repeat(Math.max(1, w))).join(' ')
return ( return (
<Box flexDirection="column" key={k} paddingLeft={2}> <Box flexDirection="column" key={k} paddingLeft={2}>
{rows.map((row, ri) => ( {rows.map((row, ri) => (
<Box key={ri}> <Fragment key={ri}>
<Box>
{widths.map((w, ci) => ( {widths.map((w, ci) => (
<Text color={ri === 0 ? t.color.amber : undefined} key={ci}> <Text bold={ri === 0} color={ri === 0 ? t.color.amber : undefined} key={ci}>
<MdInline t={t} text={row[ci] ?? ''} /> <MdInline t={t} text={row[ci] ?? ''} />
{' '.repeat(Math.max(0, w - stripInlineMarkup(row[ci] ?? '').length))} {' '.repeat(Math.max(0, w - stripInlineMarkup(row[ci] ?? '').length))}
{ci < widths.length - 1 ? ' ' : ''} {ci < widths.length - 1 ? ' ' : ''}
</Text> </Text>
))} ))}
</Box> </Box>
{ri === 0 && rows.length > 1 ? (
<Text color={t.color.dim} dimColor>
{sep}
</Text>
) : null}
</Fragment>
))} ))}
</Box> </Box>
) )