fix(fonts): markdown prose + links honour the live UI font (T-475)

The markdown span builders are context-free statics, so they pinned the
bundled clideUiFamily const and ignored the Appearance UI-font setting —
Claude's conversation prose stayed on the default face regardless of the
pick. Thread the resolved UI family through the same ClideMarkdownHooks
carrier already used for mono (T-472): build() resolves it from context and
every prose/link span reads hooks.ui. Adds a regression test asserting prose
and inline code follow the families from the ClideSettingsScope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 09:35:51 +02:00
co-authored by Claude Opus 4.8
parent 258090926d
commit 90546cb446
5 changed files with 57 additions and 8 deletions
+23 -8
View File
@@ -35,7 +35,15 @@ typedef FileTapCallback = void Function(String path, int? line);
/// inert (the text renders, just not interactive).
@immutable
class ClideMarkdownHooks {
const ClideMarkdownHooks({this.onRecordTap, this.onImageToken, this.onLinkTap, this.resolveFileRef, this.onOpenFile, this.mono = clideMonoFamily});
const ClideMarkdownHooks({
this.onRecordTap,
this.onImageToken,
this.onLinkTap,
this.resolveFileRef,
this.onOpenFile,
this.mono = clideMonoFamily,
this.ui = clideUiFamily,
});
/// Tap a governance/ticket ref (T-281, D-77, …) → open the record (T-279).
final RecordTapCallback? onRecordTap;
@@ -59,6 +67,11 @@ class ClideMarkdownHooks {
/// even though the span builders are context-free statics (T-472).
final String mono;
/// The live UI family (Settings → Appearance, T-460), threaded the same way
/// so markdown prose and link spans honour the chosen UI font instead of
/// pinning the bundled default (T-475).
final String ui;
static const none = ClideMarkdownHooks();
}
@@ -123,6 +136,7 @@ class ClideMarkdown extends StatelessWidget {
resolveFileRef: resolveFileRef,
onOpenFile: onOpenFile,
mono: ClideSettings.fonts.monoOf(context),
ui: ClideSettings.fonts.uiOf(context),
);
final widgets = _buildNodes(nodes, tokens, hooks);
return Column(crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: widgets);
@@ -154,7 +168,7 @@ class ClideMarkdown extends StatelessWidget {
Text.rich(
TextSpan(
style: TextStyle(
fontFamily: clideUiFamily,
fontFamily: hooks.ui,
fontFamilyFallback: clideUiFamilyFallback,
fontWeight: clideUiDefaultWeight,
color: tokens.globalForeground,
@@ -344,7 +358,7 @@ class ClideMarkdown extends StatelessWidget {
}
return TextSpan(
style: TextStyle(
fontFamily: clideUiFamily,
fontFamily: hooks.ui,
fontFamilyFallback: clideUiFamilyFallback,
fontWeight: fontWeight ?? clideUiDefaultWeight,
color: tokens.globalForeground,
@@ -379,7 +393,7 @@ class ClideMarkdown extends StatelessWidget {
// editor (T-300); other inline code renders verbatim.
if (hooks.resolveFileRef != null && hooks.onOpenFile != null) {
final ref = _codeFileRef(raw, hooks.resolveFileRef!);
if (ref != null) return _fileLinkSpan(raw, ref.$1, ref.$2, tokens, hooks.onOpenFile!, hooks.mono, mono: true);
if (ref != null) return _fileLinkSpan(raw, ref.$1, ref.$2, tokens, hooks.onOpenFile!, hooks.mono, hooks.ui, mono: true);
}
return TextSpan(
text: raw,
@@ -401,7 +415,7 @@ class ClideMarkdown extends StatelessWidget {
if (hooks.resolveFileRef != null && hooks.onOpenFile != null && href != null) {
final (path, line) = _splitFileRef(href);
final abs = hooks.resolveFileRef!(path);
if (abs != null) return _fileLinkSpan(text, abs, line, tokens, hooks.onOpenFile!, hooks.mono);
if (abs != null) return _fileLinkSpan(text, abs, line, tokens, hooks.onOpenFile!, hooks.mono, hooks.ui);
}
return TextSpan(
text: text,
@@ -482,7 +496,7 @@ class ClideMarkdown extends StatelessWidget {
final abs = hooks.resolveFileRef!(m.group(1)!);
if (abs == null) continue;
final line = m.group(2) == null ? null : int.tryParse(m.group(2)!);
hits.add(_LinkHit(m.start, m.end, _fileLinkSpan(text.substring(m.start, m.end), abs, line, tokens, hooks.onOpenFile!, hooks.mono)));
hits.add(_LinkHit(m.start, m.end, _fileLinkSpan(text.substring(m.start, m.end), abs, line, tokens, hooks.onOpenFile!, hooks.mono, hooks.ui)));
}
}
if (hits.isEmpty) return [TextSpan(text: text)];
@@ -560,7 +574,8 @@ class ClideMarkdown extends StatelessWidget {
int? line,
SurfaceTokens tokens,
FileTapCallback onOpenFile,
String monoFamily, {
String monoFamily,
String uiFamily, {
bool mono = false,
}) {
return WidgetSpan(
@@ -575,7 +590,7 @@ class ClideMarkdown extends StatelessWidget {
color: tokens.globalFocus,
fontSize: mono ? clideFontMono : _fontSize,
height: _lineHeight,
fontFamily: mono ? monoFamily : clideUiFamily,
fontFamily: mono ? monoFamily : uiFamily,
fontFamilyFallback: mono ? null : clideUiFamilyFallback,
decoration: hovered ? TextDecoration.underline : null,
decorationColor: tokens.globalFocus,