From 47567494fa716061c08c4d75f530ab2e8162bfcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?An=C4=B1lcan=20=C3=87ak=C4=B1r?= Date: Thu, 20 Aug 2026 23:44:54 +0300 Subject: [PATCH] fix(page-header): pin the inline title width and drop a class wind ignores Two findings from driving the header on an iPhone. The inline layout collapsed its title. With a leading control, a long title and a titleSuffix, the title measured ZERO width at 402pt while 140pt of the row sat blank after the status badge. The cause was in wind, not here: a `justify-between` Row wrapped every child in `Flexible`, so the icon-sized actions column reserved an equal 185pt share it never painted and the `flex-1` title row had nothing left to take (fluttersdk/wind#175). This adds the regression test at the component level, asserted on the two wrappers rather than on the text: a widget test lays text out in a placeholder font roughly one em per glyph, so comparing a title against a badge measures the harness. `-ml-1` on the back control never did anything. wind supports a negative value only in the position family, and it reports `unknown className '-ml-1' was ignored` for a margin. Removed from the default, the derived theme, and the docblock rather than left in as a class that reads like it is doing work. --- .../configuration/magic_starter_theme.dart | 6 +- .../page_header/page_header_test.dart | 69 +++++++++++++++++++ 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/lib/src/configuration/magic_starter_theme.dart b/lib/src/configuration/magic_starter_theme.dart index c6a1d77..dba79af 100644 --- a/lib/src/configuration/magic_starter_theme.dart +++ b/lib/src/configuration/magic_starter_theme.dart @@ -425,7 +425,7 @@ class MagicStarterPageHeaderTheme { /// size, color, or hover states. /// /// Defaults to - /// `'flex items-center justify-center size-9 -ml-1 text-2xl text-fg-muted hover:text-fg'`. + /// `'flex items-center justify-center size-9 text-2xl text-fg-muted hover:text-fg'`. final String backControlClassName; /// Whether every [MSPageHeader] lays its title and actions out on ONE row. @@ -468,7 +468,7 @@ class MagicStarterPageHeaderTheme { 'text-sm text-gray-600 dark:text-gray-400 line-clamp-2', this.actionContainerClassName = 'flex flex-row items-center gap-2', this.backControlClassName = - 'flex items-center justify-center size-9 -ml-1 text-2xl text-fg-muted hover:text-fg', + 'flex items-center justify-center size-9 text-2xl text-fg-muted hover:text-fg', this.inlineActions = false, }); } @@ -876,7 +876,7 @@ class MagicStarterTheme { titleClassName: 'text-2xl font-bold $fg line-clamp-2', subtitleClassName: 'text-sm $fgMuted line-clamp-2', backControlClassName: - 'flex items-center justify-center size-9 -ml-1 text-2xl $fgMuted hover:$fg', + 'flex items-center justify-center size-9 text-2xl $fgMuted hover:$fg', ), layout: MagicStarterLayoutTheme( sidebarClassName: 'h-full flex flex-col $surface border-r $border', diff --git a/test/ui/components/page_header/page_header_test.dart b/test/ui/components/page_header/page_header_test.dart index d832462..dfc95bb 100644 --- a/test/ui/components/page_header/page_header_test.dart +++ b/test/ui/components/page_header/page_header_test.dart @@ -112,6 +112,75 @@ void main() { expect(find.byKey(suffixKey), findsOneWidget); }); + testWidgets( + 'inlineActions keeps the title readable beside a leading and a suffix', + (tester) async { + // The className assertion below cannot see this: with a leading control, + // a long title and a titleSuffix, the inline row collapsed the title to a + // couple of glyphs on a phone while leaving most of the row EMPTY. + // Measured on an iPhone at 402pt: the title occupied about 10pt and + // everything past the suffix was blank, because `justify-between` handed + // the icon-sized actions column an EQUAL flex share and it reserved half + // the header. + // + // Asserted on the two wrappers, not on the text: a widget test lays text + // out in a placeholder font roughly one em per glyph, so any assertion + // that compares a title against a badge measures the harness. An icon + // column's width and a row's share of the header are layout. + tester.view.devicePixelRatio = 1.0; + tester.view.physicalSize = const Size(402, 900); + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + await tester.pumpWidget( + wrap( + MSPageHeader( + title: 'iOS Sweep Monitor', + subtitle: 'https://example.com', + inlineActions: true, + leading: const Icon(Icons.chevron_left), + titleSuffix: const Text('Operational'), + actions: [const Icon(Icons.more_horiz)], + ), + ), + ); + await tester.pump(); + + final double actionsWidth = tester + .getSize( + find + .ancestor( + of: find.byIcon(Icons.more_horiz), + matching: find.byType(WDiv), + ) + .first, + ) + .width; + final double titleRowWidth = tester + .getSize( + find + .byWidgetPredicate( + (w) => w is WDiv && (w.className ?? '').contains('flex-1'), + ) + .first, + ) + .width; + + expect( + actionsWidth, + lessThan(64), + reason: 'a single icon action is icon-sized; it cannot reserve a share ' + 'of the header it never paints', + ); + expect( + titleRowWidth, + greaterThan(300), + reason: 'the title row is the page identity and takes what the actions ' + 'do not need, so on a 402pt phone it spans nearly the whole header', + ); + }, + ); + testWidgets('inlineActions: true outer WDiv className contains flex-row', (tester) async { await tester.pumpWidget(