From e82c8bb7b4ebe5dd2d23c33085e0c53997b71b45 Mon Sep 17 00:00:00 2001 From: uvtqt Date: Fri, 13 Nov 2026 19:13:58 +0100 Subject: [PATCH 1/2] Cycle: let the day-in-cycle card wrap instead of overflowing The card's headline row put the cycle day and "counted from your last logged start" side by side with nothing allowed to wrap, and its header put the label before a Spacer and the phase pill. At a 390 pt phone width the headline overflowed at default size in the test font, and both rows overflowed at large system text (iOS reaches 3.1x). The caption is Flexible now, and the header is a Wrap: label and pill side by side when they fit, the pill on its own line when not. Co-Authored-By: Claude Opus 5.5 --- lib/ui2/screens/cycle_screen.dart | 30 +++++++---- test/cycle_day_row_overflow_test.dart | 71 +++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 9 deletions(-) create mode 100644 test/cycle_day_row_overflow_test.dart diff --git a/lib/ui2/screens/cycle_screen.dart b/lib/ui2/screens/cycle_screen.dart index f9f18cdfe..cf29376f1 100644 --- a/lib/ui2/screens/cycle_screen.dart +++ b/lib/ui2/screens/cycle_screen.dart @@ -479,13 +479,20 @@ class _CycleTabState extends State with RevisionReload { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( + // A Wrap, not a Row with a Spacer: at large text sizes or in a + // longer language the label and the phase pill do not fit one + // line, and the row overflowed. Side by side when they fit, the + // pill on its own line when they don't. + Wrap( + alignment: WrapAlignment.spaceBetween, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: S.x2, + runSpacing: S.x2, children: [ Text( l?.cycleDayInThisCycle ?? 'DAY IN THIS CYCLE', style: F.over.copyWith(color: p.ink3), ), - const Spacer(), if (d.phase != 'unknown') Pill(_phaseLabel(l, d.phase), C.pink), ], ), @@ -496,13 +503,18 @@ class _CycleTabState extends State with RevisionReload { children: [ Text('${d.cycleDay}', style: F.n34.copyWith(color: p.ink)), const SizedBox(width: S.x2), - Text( - d.medianLength == null - ? (l?.cycleCountedFromLastStart ?? - 'counted from your last logged start') - : (l?.cycleOfAboutDays(d.medianLength!.round()) ?? - 'of about ${d.medianLength!.round()}'), - style: F.cap.copyWith(color: p.ink3), + // Wraps under itself rather than running off the card: + // "counted from your last logged start" is most of a line at + // the default size already. + Flexible( + child: Text( + d.medianLength == null + ? (l?.cycleCountedFromLastStart ?? + 'counted from your last logged start') + : (l?.cycleOfAboutDays(d.medianLength!.round()) ?? + 'of about ${d.medianLength!.round()}'), + style: F.cap.copyWith(color: p.ink3), + ), ), ], ), diff --git a/test/cycle_day_row_overflow_test.dart b/test/cycle_day_row_overflow_test.dart new file mode 100644 index 000000000..bcaf2ea35 --- /dev/null +++ b/test/cycle_day_row_overflow_test.dart @@ -0,0 +1,71 @@ +// The cycle screen's "day in this cycle" card must fit a phone at any text +// size. +// +// The headline row put the day number and "counted from your last logged +// start" side by side with nothing allowed to wrap, and the header row put +// its label before a Spacer and the phase pill. At a 390 pt width that +// overflowed by 147 pt in the test font, and by less, but still, with larger +// system text (iOS goes to 3.1x) or a longer language. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; + +import 'package:openstrap_edge/data/local_repository.dart'; +import 'package:openstrap_edge/state/app_state.dart'; +import 'package:openstrap_edge/ui2/screens/cycle_screen.dart'; +import 'package:openstrap_edge/ui2/ui2.dart'; + +class _Repo extends LocalRepository { + _Repo({this.median}); + final num? median; + + @override + Future> getCycle() async => { + 'enabled': true, + 'phase': 'follicular', + 'cycle_day': 5, + 'median_length': ?median, + 'logs': [ + {'date': '2026-11-09', 'kind': 'start'}, + ], + }; + + @override + Future>> getCycleSymptoms() async => const {}; +} + +Future _pump(WidgetTester t, {num? median, double scale = 1}) async { + t.view.physicalSize = const Size(390 * 3, 4000 * 3); + t.view.devicePixelRatio = 3; + addTearDown(t.view.reset); + final app = AppState.forTesting()..repo = _Repo(median: median); + addTearDown(app.dispose); + await t.pumpWidget(ChangeNotifierProvider.value( + value: app, + child: MediaQuery( + data: MediaQueryData(textScaler: TextScaler.linear(scale)), + child: MaterialApp( + theme: buildTheme(Brightness.light), + home: const Scaffold(body: CycleTab()), + ), + ), + )); + await t.pumpAndSettle(); +} + +void main() { + for (final scale in const [1.0, 3.1]) { + testWidgets('no median yet, ${scale}x text', (t) async { + // The pump is the assertion: a RenderFlex overflow fails it. + await _pump(t, scale: scale); + expect(find.textContaining('counted from your last logged start'), + findsOneWidget); + }); + + testWidgets('with a median, ${scale}x text', (t) async { + await _pump(t, median: 29, scale: scale); + expect(find.textContaining('of about 29'), findsOneWidget); + }); + } +} From 637bd536f98761b41100d1f4e9330afa3ed18b07 Mon Sep 17 00:00:00 2001 From: uvtqt Date: Fri, 13 Nov 2026 19:49:36 +0100 Subject: [PATCH 2/2] Cycle: let the phase pill wrap at large text Review on #713: at large text a long phase name ended in an ellipsis. The pill wraps there now and stays one line at default sizes. Co-Authored-By: Claude Opus 5.5 --- lib/ui2/screens/cycle_screen.dart | 5 ++++- test/cycle_day_row_overflow_test.dart | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/ui2/screens/cycle_screen.dart b/lib/ui2/screens/cycle_screen.dart index cf29376f1..0a985b75c 100644 --- a/lib/ui2/screens/cycle_screen.dart +++ b/lib/ui2/screens/cycle_screen.dart @@ -493,7 +493,10 @@ class _CycleTabState extends State with RevisionReload { l?.cycleDayInThisCycle ?? 'DAY IN THIS CYCLE', style: F.over.copyWith(color: p.ink3), ), - if (d.phase != 'unknown') Pill(_phaseLabel(l, d.phase), C.pink), + if (d.phase != 'unknown') + // Wraps at large text rather than cutting a long phase name + // short with an ellipsis; one line at default sizes. + Pill(_phaseLabel(l, d.phase), C.pink, wrap: bigText(c)), ], ), const SizedBox(height: S.x2), diff --git a/test/cycle_day_row_overflow_test.dart b/test/cycle_day_row_overflow_test.dart index bcaf2ea35..df014dac2 100644 --- a/test/cycle_day_row_overflow_test.dart +++ b/test/cycle_day_row_overflow_test.dart @@ -66,6 +66,8 @@ void main() { testWidgets('with a median, ${scale}x text', (t) async { await _pump(t, median: 29, scale: scale); expect(find.textContaining('of about 29'), findsOneWidget); + // The phase name wraps at large text instead of ending in an ellipsis. + expect(t.widget(find.byType(Pill).first).wrap, scale > 1.3); }); } }