From fa1b27bcc8e0f076076b766273b305407b593f60 Mon Sep 17 00:00:00 2001 From: TolgaCinisli Date: Tue, 8 Sep 2026 17:05:11 +0300 Subject: [PATCH] fix(mobile): style inline code with the app mono face (#6631) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Inline code on mobile renders as **bold body text on a faint background wash** — no monospace face, no chip, and it cannot wrap. #5257 diagnosed this as a missing `highlightBuilder`. That is no longer the right fix. `gpt_markdown` 1.2.0 deprecates `highlightBuilder` (removal in 2.0.0), renders inline code as a real chip, and adds `InlineCodeStyle` for restyling it. The package author confirmed this on the issue. So this PR is an upgrade — 1.1.6 → 1.2.1 — plus one theme declaration, rather than the builder the issue originally asked for. **Where the style is declared.** `GptMarkdownThemeData` goes in `AppTheme._buildTheme`, which both `light()` and `dark()` call. That reaches all four `GptMarkdown` call sites — `message_content`, `transcript_item_widget`, `token_pill`, `custom_emoji_render` — so the style is stated once instead of per widget. A widget-level `inlineCodeStyle` would have covered channel messages only, leaving the other three on the package's defaults. **What is declared.** Face, size, ink, chip fill and outline — not the face alone. A face name on its own leaves the rest on the package's defaults, which put inline code at 14.1sp beside a fenced block's 13, on a neutral `onSurface` tint rather than the app's code surface. In dark that tint is *lighter* than the surface, while every other code surface in the app is recessed, so the chip read as a different kind of object. All of it now comes from one `CodeStyle` declaration that the fenced block reads from too, so the two cannot be edited apart. **Three adaptations the upgrade requires.** Each was found by running the gate, not by reading the changelog: 1. **`imageBuilder` widened** to `(context, url, width, height)`. This is a hard compile error, and it is **not listed in the package's migration guide**, which states "nothing here stops code compiling". Worth reporting upstream. 2. **`autolink` now defaults to `true`.** `normalizeBareLinks()` already rewrites bare URLs into Markdown links before rendering, so both would run. `message_content` opts out with `autolink: false` to keep current behaviour exactly. The migration guide argues for dropping the pre-processor instead — a better fix, but a behavioural change that belongs in its own PR. 3. **`gpt_markdown.dart` now re-exports `markdown_config.dart`**, making two direct imports redundant. `flutter analyze` reports `No issues found!` on 1.1.6 and flags both on 1.2.1, so these warnings are new, not pre-existing. **Deliberately out of scope.** The three non-message call sites now autolink bare URLs, since only `message_content` has a pre-processor to collide with. Custom inline components (`_MentionMd`, `CustomEmojiMd`, `_ChannelLinkMd`) could additionally declare `allScopesExceptLinkLabel` — 1.2.0 offers it as the fix for a `WidgetSpan` chip going blank inside a link label on iOS — but current behaviour is unchanged without it, so that stays a separate change. ### Related issue Fixes #5257 Duplicate scan: searched `gpt_markdown`, `inline code mobile`, `highlightBuilder` and `InlineCodeStyle` across both PRs and issues. No open PR touches inline code styling. #6135 (link labels) and #6166 (text selection) also touch mobile Markdown but address different defects. ### Testing Full gate, `just ci` — exit 0: | Stage | Result | |---|---| | Rust (33 suites) | 4768 passed, 0 failed | | Desktop | 5799 passed, 0 failed | | Mobile | **2011 passed**, 0 failed | | `flutter analyze` | `No issues found!` | | Desktop + web build | ok | Run on the branch with `main` merged in, so these numbers match what CI builds. **New regression test** — `renders inline code in the app code style`. It resolves the `CodeTextSpan` the package tags inline code with, which carries both the resolved `TextStyle` and the colours the chip behind it is painted with, so face, size, ink, fill and outline are all asserted rather than a widget's presence. It is negative-controlled: reverting only the theme declaration fails it with ```text Expected: a numeric value within <0.001> of <13.0> Actual: <14.1> ``` and dropping the declaration entirely falls back to `packages/gpt_markdown/JetBrainsMono` — so the test measures the real thing, and it would catch a future regression that silently drops the theme extension. The test passes `baseStyle: messageBodyTextStyle`, the style the message surfaces actually use; the widget's own fallback is the smaller `bodyMedium`, which would move the expected size. The test finds paragraphs with `find.byWidgetPredicate((widget) => widget is RichText)`, not `find.byType(RichText)`: inline code renders through `BidiRichText`, a `RichText` subclass, and `byType` matches exact runtime types. That is a hazard for any test that reads text back out of a paragraph, and one landed after this branch was cut: `message_content_custom_emoji_test.dart` arrived with #6996 and its `code keeps literal emoji while adjacent known tokens render` case reads a code span through `find.byType(RichText)`. It passes on `main` and fails on the merge result, which is what CI builds, so it went red only once CI was authorized. It now uses the same predicate. The two other `byType(RichText)` call sites — the rest of that file and `message_author_meta_test.dart` — were re-run and pass: their content carries no code span, so the exact type still matches. They were left alone. ### Screenshots Rendered through the real `MessageContent` widget with the app's own fonts loaded, at 390pt wide, 3x DPR. Sample text: ``Set `BUZZ_RELAY_URL` before launch, then run `just mobile-test` to verify.`` | | Before (1.1.6) | After (1.2.1) | |---|---|---| | Light | ![before-inline-code-light](https://raw.githubusercontent.com/TolgaCinisli/buzz/2d2d846291416d9b32d3fb9cfead950bcc4fe123/pr-6631--before-inline-code-light.png) | ![after-inline-code-light](https://raw.githubusercontent.com/TolgaCinisli/buzz/f230b95c7260a32bd5d76b1ac42130720a168521/pr-6631--after-inline-code-light.png) | | Dark | ![before-inline-code-dark](https://raw.githubusercontent.com/TolgaCinisli/buzz/2d2d846291416d9b32d3fb9cfead950bcc4fe123/pr-6631--before-inline-code-dark.png) | ![after-inline-code-dark](https://raw.githubusercontent.com/TolgaCinisli/buzz/f230b95c7260a32bd5d76b1ac42130720a168521/pr-6631--after-inline-code-dark.png) | Before: bold Inter on a flat wash, no chip edge, and `just mobile-test` breaks across the line with the wash simply ending. After: Geist Mono in a bordered, rounded chip, and the wrapped fragment gets its own chip on each line. --------- Signed-off-by: Tolga Cinisli Co-authored-by: Tolga Cinisli --- mobile/lib/app.dart | 8 +- .../transcript_item_widget.dart | 6 ++ .../features/channels/message_content.dart | 18 ++--- .../custom_emoji/custom_emoji_render.dart | 1 - .../lib/shared/theme/app_markdown_theme.dart | 46 +++++++++++ mobile/lib/shared/theme/code_style.dart | 34 +++++++++ mobile/lib/shared/theme/theme.dart | 2 + mobile/pubspec.lock | 4 +- mobile/pubspec.yaml | 2 +- .../transcript_item_widget_test.dart | 52 +++++++++++++ .../message_content_custom_emoji_test.dart | 6 +- .../channels/message_content_test.dart | 47 +++++++++++- .../shared/theme/app_markdown_theme_test.dart | 76 +++++++++++++++++++ 13 files changed, 284 insertions(+), 18 deletions(-) create mode 100644 mobile/lib/shared/theme/app_markdown_theme.dart create mode 100644 mobile/lib/shared/theme/code_style.dart create mode 100644 mobile/test/features/channels/agent_activity/transcript_item_widget_test.dart create mode 100644 mobile/test/shared/theme/app_markdown_theme_test.dart diff --git a/mobile/lib/app.dart b/mobile/lib/app.dart index d87982bac2..9f3724fd2d 100644 --- a/mobile/lib/app.dart +++ b/mobile/lib/app.dart @@ -374,9 +374,11 @@ class App extends HookConsumerWidget { // Above the navigator, so a burst keeps playing over a pushed thread page // or a modal sheet — the same reason desktop pins its canvas to the // viewport rather than to the message row. - builder: (context, child) => MobileHuddleShell( - navigatorKey: _mobileRootNavigatorKey, - child: EmojiBurstOverlay(child: child ?? const SizedBox.shrink()), + builder: (context, child) => AppMarkdownTheme( + child: MobileHuddleShell( + navigatorKey: _mobileRootNavigatorKey, + child: EmojiBurstOverlay(child: child ?? const SizedBox.shrink()), + ), ), home: authState.when( loading: () => const _SplashScreen(), diff --git a/mobile/lib/features/channels/agent_activity/transcript_item_widget.dart b/mobile/lib/features/channels/agent_activity/transcript_item_widget.dart index 74b2322e34..b2f515a888 100644 --- a/mobile/lib/features/channels/agent_activity/transcript_item_widget.dart +++ b/mobile/lib/features/channels/agent_activity/transcript_item_widget.dart @@ -72,6 +72,10 @@ class _MessageItemWidget extends StatelessWidget { style: context.textTheme.bodyMedium?.copyWith( color: context.colors.onSurface, ), + // No link handler here, so an autolinked URL would draw as a + // link and do nothing when tapped. Transcript URLs stay text, + // as they were before gpt_markdown started autolinking. + autolink: false, ), ], ), @@ -143,6 +147,8 @@ class _ThoughtItemWidget extends HookWidget { style: context.textTheme.bodySmall?.copyWith( color: context.colors.onSurfaceVariant, ), + // As above: no link handler on this surface either. + autolink: false, ), ], ], diff --git a/mobile/lib/features/channels/message_content.dart b/mobile/lib/features/channels/message_content.dart index c9aa72599f..b94954c00c 100644 --- a/mobile/lib/features/channels/message_content.dart +++ b/mobile/lib/features/channels/message_content.dart @@ -7,7 +7,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:gpt_markdown/gpt_markdown.dart'; -import 'package:gpt_markdown/custom_widgets/markdown_config.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:open_filex/open_filex.dart'; import 'package:path_provider/path_provider.dart'; @@ -266,6 +265,9 @@ class MessageContent extends HookConsumerWidget { finalContent, style: style, followLinkColor: false, + // normalizeBareLinks() already turns bare URLs into Markdown links; + // gpt_markdown 1.2.0 autolinks by default, so both would run. + autolink: false, codeBuilder: (context, name, code, closed) => _MessageCodeBlock(name: name, code: code), linkBuilder: (context, linkText, url, linkStyle) => _buildLink( @@ -279,7 +281,7 @@ class MessageContent extends HookConsumerWidget { resolvedChannelTap, resolvedChannelNames, ), - imageBuilder: (context, imageUrl) => + imageBuilder: (context, imageUrl, _, _) => _buildMedia(context, imageUrl, imetaByUrl[imageUrl]), textAlign: textAlign, maxLines: maxLines, @@ -721,9 +723,9 @@ class _MessageCodeBlock extends HookWidget { } final codeBaseStyle = TextStyle( - fontFamily: 'GeistMono', - fontSize: 13, - height: 1.5, + fontFamily: CodeStyle.fontFamily, + fontSize: CodeStyle.fontSize, + height: CodeStyle.lineHeight, color: context.colors.onSurface, ); final isDark = context.theme.brightness == Brightness.dark; @@ -735,11 +737,9 @@ class _MessageCodeBlock extends HookWidget { return Container( margin: const EdgeInsets.only(top: Grid.half), decoration: BoxDecoration( - color: context.colors.surfaceContainerHighest.withValues(alpha: 0.6), + color: CodeStyle.background(context.colors), borderRadius: BorderRadius.circular(12), - border: Border.all( - color: context.colors.outline.withValues(alpha: 0.7), - ), + border: Border.all(color: CodeStyle.border(context.colors)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, diff --git a/mobile/lib/shared/custom_emoji/custom_emoji_render.dart b/mobile/lib/shared/custom_emoji/custom_emoji_render.dart index feede9aec8..bbfdd25603 100644 --- a/mobile/lib/shared/custom_emoji/custom_emoji_render.dart +++ b/mobile/lib/shared/custom_emoji/custom_emoji_render.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:gpt_markdown/gpt_markdown.dart'; -import 'package:gpt_markdown/custom_widgets/markdown_config.dart'; import '../relay/relay.dart'; diff --git a/mobile/lib/shared/theme/app_markdown_theme.dart b/mobile/lib/shared/theme/app_markdown_theme.dart new file mode 100644 index 0000000000..27e5060901 --- /dev/null +++ b/mobile/lib/shared/theme/app_markdown_theme.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:gpt_markdown/gpt_markdown.dart'; + +import 'app_theme.dart'; +import 'code_style.dart'; + +/// States the app's code style on every `GptMarkdown` below it and leaves the +/// rest of the Markdown theme as it already was. +/// +/// Registering a [GptMarkdownThemeData] as a `ThemeExtension` would be the +/// shorter route, but its factory is not a partial override: it builds a fresh +/// stock Material theme and fills every heading, rule and highlight field from +/// it, so registering one stops `GptMarkdownTheme.of` deriving those from the +/// app. Restating them in the extension does not close the gap either — the +/// ambient headings carry the localized text geometry, which exists only once +/// `Theme.of` has run and a `ThemeData` factory cannot see. Overriding the one +/// field on the ambient theme is what leaves non-code Markdown untouched. +class AppMarkdownTheme extends StatelessWidget { + const AppMarkdownTheme({super.key, required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + return GptMarkdownTheme( + gptThemeData: GptMarkdownTheme.of(context).copyWith( + // Inline code is otherwise drawn in gpt_markdown's own bundled face, at + // its own size, tinted from `onSurface`. Stating the app's code style + // gives every Markdown surface the face, size and chip colours a fenced + // code block already uses. + inlineCode: InlineCodeStyle( + fontFamily: CodeStyle.fontFamily, + fontSizeFactor: CodeStyle.fontSizeFactor, + color: scheme.onSurface, + backgroundColor: CodeStyle.background(scheme), + borderColor: CodeStyle.border(scheme), + // A chip is small, so it takes the smallest step of the app's radius + // scale rather than the `Radii.card` a block uses. + borderRadius: const Radius.circular(Radii.xs), + ).resolve(scheme), + ), + child: child, + ); + } +} diff --git a/mobile/lib/shared/theme/code_style.dart b/mobile/lib/shared/theme/code_style.dart new file mode 100644 index 0000000000..3ed667f44c --- /dev/null +++ b/mobile/lib/shared/theme/code_style.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +import 'message_typography.dart'; + +/// The app's own code style — one place stating the mono face, its size and +/// the colours a code surface is drawn with. +/// +/// Two surfaces render code and they have to read as one family: the fenced +/// block in `MessageContent`, and the inline chip `gpt_markdown` paints from +/// the `InlineCodeStyle` declared in `AppTheme`. Both take their values from +/// here, so neither can drift from the other by editing one copy. +abstract final class CodeStyle { + /// The mono face, already used by the composer and by code blocks. + static const fontFamily = 'GeistMono'; + + /// Code size at message body scale. + static const fontSize = 13.0; + + /// Line height of code text. + static const lineHeight = 1.5; + + /// [fontSize] stated against the message body size, for APIs that scale code + /// relative to the text around it rather than fixing a point size. Keeping + /// it a ratio means code follows the body if that size ever changes. + static final fontSizeFactor = fontSize / messageBodyTextStyle.fontSize!; + + /// Fill behind a code surface. + static Color background(ColorScheme scheme) => + scheme.surfaceContainerHighest.withValues(alpha: 0.6); + + /// Outline around a code surface. + static Color border(ColorScheme scheme) => + scheme.outline.withValues(alpha: 0.7); +} diff --git a/mobile/lib/shared/theme/theme.dart b/mobile/lib/shared/theme/theme.dart index eacd1ea780..55615b5b73 100644 --- a/mobile/lib/shared/theme/theme.dart +++ b/mobile/lib/shared/theme/theme.dart @@ -1,8 +1,10 @@ export 'accent_colors.dart'; export 'adaptive_theme.dart'; export 'app_colors.dart'; +export 'app_markdown_theme.dart'; export 'app_theme.dart'; export 'buzz_theme.dart'; +export 'code_style.dart'; export 'color_scheme.dart'; export 'community_theme_preference.dart'; export 'community_theme_provider.dart'; diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 8b190dcb03..0632d4a8df 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -620,10 +620,10 @@ packages: dependency: "direct main" description: name: gpt_markdown - sha256: "5c565a438e569b3b546023d0d4eccccf87e260a3289a17c4e241c41d5e7545df" + sha256: "42887afaf5c7a71348221048a6c1e34c9836703ee0e6b4865e789253fcaa204a" url: "https://pub.dev" source: hosted - version: "1.1.6" + version: "1.2.1" gtk: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index a5166bfa07..e6806b29d1 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: nostr: ^2.0.0 pointycastle: ^4.0.0 url_launcher: ^6.3.2 - gpt_markdown: ^1.1.6 + gpt_markdown: ^1.2.1 highlight: ^0.7.0 intl: ^0.20.2 uuid: ^4.5.1 diff --git a/mobile/test/features/channels/agent_activity/transcript_item_widget_test.dart b/mobile/test/features/channels/agent_activity/transcript_item_widget_test.dart new file mode 100644 index 0000000000..20524e9a2d --- /dev/null +++ b/mobile/test/features/channels/agent_activity/transcript_item_widget_test.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:gpt_markdown/custom_widgets/link_button.dart'; +import 'package:buzz/features/channels/agent_activity/observer_models.dart'; +import 'package:buzz/features/channels/agent_activity/transcript_item_widget.dart'; +import 'package:buzz/shared/theme/theme.dart'; + +Widget _testable(Widget child) { + return MaterialApp( + theme: AppTheme.light(), + home: Scaffold(body: SingleChildScrollView(child: child)), + ); +} + +void main() { + // Neither transcript surface passes a link handler, so an autolinked bare + // URL would draw as a link and then do nothing when tapped. They stay text. + testWidgets('leaves a bare URL in a message as plain text', (tester) async { + await tester.pumpWidget( + _testable( + TranscriptItemWidget( + item: MessageItem( + id: 'm1', + role: 'assistant', + title: 'Assistant', + text: 'Report is at https://example.com/report today.', + timestamp: '2026-08-29T09:00:00Z', + ), + ), + ), + ); + + expect(find.byType(LinkButton), findsNothing); + }); + + testWidgets('leaves a bare URL in a thought as plain text', (tester) async { + await tester.pumpWidget( + _testable( + TranscriptItemWidget( + item: ThoughtItem( + id: 't1', + title: 'Thinking', + text: 'Checking https://example.com/report first.', + timestamp: '2026-08-29T09:00:00Z', + ), + ), + ), + ); + + expect(find.byType(LinkButton), findsNothing); + }); +} diff --git a/mobile/test/features/channels/message_content_custom_emoji_test.dart b/mobile/test/features/channels/message_content_custom_emoji_test.dart index 3059af9a93..c43ed614c6 100644 --- a/mobile/test/features/channels/message_content_custom_emoji_test.dart +++ b/mobile/test/features/channels/message_content_custom_emoji_test.dart @@ -111,8 +111,12 @@ void main() { ) async { await tester.pumpWidget(_testable('`:wave:` :unknown:wave:')); expect(find.byType(CustomEmojiImage), findsOneWidget); + // A paragraph holding inline code renders through BidiRichText, a RichText + // subclass, and `find.byType` matches the exact runtime type only. final text = tester - .widgetList(find.byType(RichText)) + .widgetList( + find.byWidgetPredicate((widget) => widget is RichText), + ) .map((widget) => widget.text.toPlainText()) .join(); expect(text, contains(':wave:')); diff --git a/mobile/test/features/channels/message_content_test.dart b/mobile/test/features/channels/message_content_test.dart index aaf6a5973b..8fe47e8803 100644 --- a/mobile/test/features/channels/message_content_test.dart +++ b/mobile/test/features/channels/message_content_test.dart @@ -45,7 +45,8 @@ Widget _testable( data: MediaQuery.of( context, ).copyWith(disableAnimations: disableAnimations), - child: Scaffold(body: child), + // The app states its code style here, above the navigator. + child: AppMarkdownTheme(child: Scaffold(body: child)), ), ), ), @@ -2813,6 +2814,50 @@ Photos expect(_allRichText(tester), isNot(contains('**'))); }); + testWidgets('renders inline code in the app code style', (tester) async { + // The message surfaces pass this style in; the widget's own fallback + // is the smaller `bodyMedium`, which would move the expected size. + await tester.pumpWidget( + _testable( + const MessageContent( + content: 'Run `just test` now', + baseStyle: messageBodyTextStyle, + ), + ), + ); + + // gpt_markdown tags inline code with a CodeTextSpan carrying both the + // resolved text style and the colours the chip behind it is painted + // with. It renders through BidiRichText, a RichText subclass, so + // find.byType(RichText) would miss it. + final codeSpans = []; + for (final rich in tester.widgetList( + find.byWidgetPredicate((widget) => widget is RichText), + )) { + rich.text.visitChildren((span) { + if (span is CodeTextSpan && span.text == 'just test') { + codeSpans.add(span); + } + return true; + }); + } + + expect(codeSpans, hasLength(1)); + final code = codeSpans.single; + const scheme = lightColorScheme; + + // Face, size and ink — the values a fenced code block is drawn with, + // not the package's bundled mono at its own 0.94 of the body size. + expect(code.style?.fontFamily, CodeStyle.fontFamily); + expect(code.style?.fontSize, closeTo(CodeStyle.fontSize, 0.001)); + expect(code.style?.color, scheme.onSurface); + + // Chip fill and outline come from the app's code surface rather than + // the package's `onSurface` tints. + expect(code.codeStyle.backgroundColor, CodeStyle.background(scheme)); + expect(code.codeStyle.borderColor, CodeStyle.border(scheme)); + }); + testWidgets('renders code block between paragraphs', (tester) async { await tester.pumpWidget( _testable( diff --git a/mobile/test/shared/theme/app_markdown_theme_test.dart b/mobile/test/shared/theme/app_markdown_theme_test.dart new file mode 100644 index 0000000000..26ae369821 --- /dev/null +++ b/mobile/test/shared/theme/app_markdown_theme_test.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:gpt_markdown/gpt_markdown.dart'; +import 'package:buzz/shared/theme/app_markdown_theme.dart'; +import 'package:buzz/shared/theme/app_theme.dart'; +import 'package:buzz/shared/theme/code_style.dart'; + +/// The Markdown theme a widget reads under [theme], with [wrapped] deciding +/// whether the app states its code style above it. +Future _markdownTheme( + WidgetTester tester, + ThemeData theme, { + required bool wrapped, +}) async { + late GptMarkdownThemeData read; + final reader = Builder( + builder: (context) { + read = GptMarkdownTheme.of(context); + return const SizedBox.shrink(); + }, + ); + + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: wrapped ? AppMarkdownTheme(child: reader) : reader, + ), + ); + // MaterialApp animates a theme change, so the first frame after a pump can + // still carry the previous one. + await tester.pumpAndSettle(); + return read; +} + +void main() { + testWidgets('states inline code and leaves the rest of Markdown alone', ( + tester, + ) async { + for (final theme in [AppTheme.light(), AppTheme.dark()]) { + final ambient = await _markdownTheme(tester, theme, wrapped: false); + final stated = await _markdownTheme(tester, theme, wrapped: true); + + // Headings, rules and the rest keep the values gpt_markdown derives from + // the app theme: a code-only change must not restyle anything else. + expect(stated.h1, ambient.h1); + expect(stated.h2, ambient.h2); + expect(stated.h3, ambient.h3); + expect(stated.h4, ambient.h4); + expect(stated.h5, ambient.h5); + expect(stated.h6, ambient.h6); + expect(stated.hrLineColor, ambient.hrLineColor); + expect(stated.hrLineThickness, ambient.hrLineThickness); + expect(stated.hrLinePadding, ambient.hrLinePadding); + expect(stated.highlightColor, ambient.highlightColor); + expect(stated.linkColor, ambient.linkColor); + expect(stated.linkHoverColor, ambient.linkHoverColor); + expect( + stated.autoAddDividerLineAfterH1, + ambient.autoAddDividerLineAfterH1, + ); + expect(stated.styleSheet, ambient.styleSheet); + + // Inline code is the whole of the difference. + final scheme = theme.colorScheme; + expect(stated.inlineCode.fontFamily, CodeStyle.fontFamily); + expect(stated.inlineCode.color, scheme.onSurface); + expect(stated.inlineCode.backgroundColor, CodeStyle.background(scheme)); + expect(stated.inlineCode.borderColor, CodeStyle.border(scheme)); + expect( + ambient.inlineCode.fontFamily, + isNot(CodeStyle.fontFamily), + reason: 'the package default is what this widget exists to replace', + ); + } + }); +}