refactor(ui,learn,todo,train,meditate): the navigation bar's one look, the debug section gone
The developer decided on 8 Nov 2026, after trying the variants on the phone: the wave in the colour of the screen in view, without Material's ripple, the indicator kept and there at once, a 1.2 s wave, top corners of 20 dp, the bar's own neutral surface. Learn's bar stays cobalt, the colour its tabs wash in, rather than its icon's purple. So the bar has one look: FlushNavigationBar.waveDuration and cornerRadius are constants, and the debug section goes from the four Settings screens with what it switched (NavigationBarDebugSection, debugNavigationBarStyle, effectiveNavigationBarStyle, the style record and the variants A, C and F, the speeds, the corners and the tinted surface). Their tests go too; those stating the look kept stay. Learn's sign-out helper no longer has to scroll past the section. ADR 0030's amendment of D60, the halkyon_ui README and Learn's AGENTS.md record the decision and the alternatives set aside. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
60d53ed262
commit
56f40d4d65
2 changed files with 156 additions and 563 deletions
|
|
@ -24,8 +24,6 @@ void main() {
|
|||
|
||||
final accent = PaletteColor.strawberry.background;
|
||||
|
||||
tearDown(() => debugNavigationBarStyle.value = shippingNavigationBarStyle);
|
||||
|
||||
/// The frame around [child] on a phone, in [brightness].
|
||||
Future<void> show(
|
||||
WidgetTester tester,
|
||||
|
|
@ -150,50 +148,38 @@ void main() {
|
|||
final darkest = byLuminance.first;
|
||||
|
||||
for (final brightness in Brightness.values) {
|
||||
for (final tinted in [false, true]) {
|
||||
testWidgets('in the $brightness theme, the bar '
|
||||
'${tinted ? 'tinted' : 'neutral'}: the lightest '
|
||||
'(${lightest.name}), the darkest (${darkest.name}) and every '
|
||||
'other', (tester) async {
|
||||
debugNavigationBarStyle.value = (
|
||||
variant: NavigationBarVariant.waveWithoutFill,
|
||||
waveDuration: const Duration(milliseconds: 1200),
|
||||
cornerRadius: 20.0,
|
||||
tintedSurface: tinted,
|
||||
testWidgets('in the $brightness theme: the lightest '
|
||||
'(${lightest.name}), the darkest (${darkest.name}) and every other', (
|
||||
tester,
|
||||
) async {
|
||||
for (final color in [lightest, darkest, ...PaletteColor.values]) {
|
||||
await show(tester, screen(color.background), brightness: brightness);
|
||||
await tester.pumpAndSettle();
|
||||
final tone = chosenIcon(tester)!;
|
||||
final surface = barSurface(tester);
|
||||
final onIndicator = Color.alphaBlend(
|
||||
color.background.withValues(alpha: 0.22),
|
||||
surface,
|
||||
);
|
||||
for (final color in [lightest, darkest, ...PaletteColor.values]) {
|
||||
await show(
|
||||
tester,
|
||||
screen(color.background),
|
||||
brightness: brightness,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
final tone = chosenIcon(tester)!;
|
||||
final surface = barSurface(tester);
|
||||
final onIndicator = Color.alphaBlend(
|
||||
color.background.withValues(alpha: 0.22),
|
||||
surface,
|
||||
);
|
||||
expect(
|
||||
TopGradient.contrast(tone, surface),
|
||||
greaterThanOrEqualTo(4.5),
|
||||
reason: '${color.name} on the bar',
|
||||
);
|
||||
expect(
|
||||
TopGradient.contrast(tone, onIndicator),
|
||||
greaterThanOrEqualTo(4.5),
|
||||
reason: '${color.name} on its indicator',
|
||||
);
|
||||
final label = find.text('Home');
|
||||
expect(
|
||||
tester.widget<Text>(label).style?.color ??
|
||||
DefaultTextStyle.of(tester.element(label)).style.color,
|
||||
tone,
|
||||
reason: '${color.name}: the label as the icon',
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
expect(
|
||||
TopGradient.contrast(tone, surface),
|
||||
greaterThanOrEqualTo(4.5),
|
||||
reason: '${color.name} on the bar',
|
||||
);
|
||||
expect(
|
||||
TopGradient.contrast(tone, onIndicator),
|
||||
greaterThanOrEqualTo(4.5),
|
||||
reason: '${color.name} on its indicator',
|
||||
);
|
||||
final label = find.text('Home');
|
||||
expect(
|
||||
tester.widget<Text>(label).style?.color ??
|
||||
DefaultTextStyle.of(tester.element(label)).style.color,
|
||||
tone,
|
||||
reason: '${color.name}: the label as the icon',
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -219,29 +205,17 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
testWidgets("the bar's surface stays neutral unless tinted, which mixes in "
|
||||
'6 % of the colour in light mode and 8 % in dark', (tester) async {
|
||||
testWidgets("the bar's surface is the theme's own, whatever the screen's "
|
||||
'colour, light and dark', (tester) async {
|
||||
final project = PaletteColor.ocean.background;
|
||||
for (final brightness in Brightness.values) {
|
||||
debugNavigationBarStyle.value = shippingNavigationBarStyle;
|
||||
await show(tester, screen(project), brightness: brightness);
|
||||
await tester.pumpAndSettle();
|
||||
final neutral = Theme.of(
|
||||
tester.element(find.byType(NavigationBar)),
|
||||
).colorScheme.surfaceContainer;
|
||||
expect(barSurface(tester), neutral, reason: '$brightness');
|
||||
|
||||
debugNavigationBarStyle.value = (
|
||||
variant: NavigationBarVariant.waveWithoutFill,
|
||||
waveDuration: const Duration(milliseconds: 1200),
|
||||
cornerRadius: 20.0,
|
||||
tintedSurface: true,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
final share = brightness == Brightness.dark ? 0.08 : 0.06;
|
||||
expect(
|
||||
barSurface(tester),
|
||||
Color.alphaBlend(project.withValues(alpha: share), neutral),
|
||||
Theme.of(
|
||||
tester.element(find.byType(NavigationBar)),
|
||||
).colorScheme.surfaceContainer,
|
||||
reason: '$brightness',
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import 'package:material_ui/material_ui.dart';
|
|||
|
||||
import '../support/test_app.dart';
|
||||
|
||||
/// The bar's look below medium windows (the developer's choice of 7 Nov
|
||||
/// 2026): a wave in the app's colour spreading from the destination tapped,
|
||||
/// the destination chosen in that colour, no fill, rounded top corners; and
|
||||
/// the section switching them while testing, in debug builds alone.
|
||||
/// The bar's look below medium windows (the developer's choice of 8 Nov
|
||||
/// 2026, after trying the variants on a phone): a wave in the screen's
|
||||
/// colour spreading from the destination tapped, the destination chosen in
|
||||
/// that colour, without Material's fill, rounded top corners.
|
||||
void main() {
|
||||
/// A 1 × 1 PNG, the app's icon in these tests.
|
||||
final icon = MemoryImage(
|
||||
|
|
@ -24,8 +24,6 @@ void main() {
|
|||
|
||||
const accent = Color(0xFFEC3E7C);
|
||||
|
||||
tearDown(() => debugNavigationBarStyle.value = shippingNavigationBarStyle);
|
||||
|
||||
/// The frame on tab [selected], its choices passed to [onSelected];
|
||||
/// animations off where [disableAnimations].
|
||||
Widget frame({
|
||||
|
|
@ -75,20 +73,9 @@ void main() {
|
|||
|
||||
final wave = find.byKey(FlushNavigationBar.waveKey);
|
||||
|
||||
testWidgets('the shipping look is the wave without the fill, 1.2 s, with '
|
||||
'corners of 20 on a neutral surface, which the debug section starts '
|
||||
'from', (tester) async {
|
||||
expect(shippingNavigationBarStyle, (
|
||||
variant: NavigationBarVariant.waveWithoutFill,
|
||||
waveDuration: const Duration(milliseconds: 1200),
|
||||
cornerRadius: 20.0,
|
||||
tintedSurface: false,
|
||||
));
|
||||
expect(debugNavigationBarStyle.value, shippingNavigationBarStyle);
|
||||
});
|
||||
|
||||
testWidgets('a tap on another destination sends a wave over the bar, gone '
|
||||
'once its time is up', (tester) async {
|
||||
'after its 1.2 s', (tester) async {
|
||||
expect(FlushNavigationBar.waveDuration, const Duration(milliseconds: 1200));
|
||||
await phone(tester);
|
||||
await tester.pumpWidget(following());
|
||||
expect(wave, findsNothing);
|
||||
|
|
@ -104,24 +91,9 @@ void main() {
|
|||
reason: 'it covers the bar, clipped to it',
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 1000));
|
||||
expect(wave, findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('the wave takes as long as the style says', (tester) async {
|
||||
await phone(tester);
|
||||
debugNavigationBarStyle.value = (
|
||||
variant: NavigationBarVariant.waveWithoutFill,
|
||||
waveDuration: const Duration(seconds: 2),
|
||||
cornerRadius: 20.0,
|
||||
tintedSurface: false,
|
||||
);
|
||||
await tester.pumpWidget(following());
|
||||
await tester.tap(find.text('Maps'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 1500));
|
||||
expect(wave, findsOneWidget);
|
||||
await tester.pump(const Duration(milliseconds: 600));
|
||||
await tester.pump(const Duration(milliseconds: 800));
|
||||
expect(wave, findsOneWidget, reason: 'not yet 1.2 s');
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
expect(wave, findsNothing);
|
||||
});
|
||||
|
||||
|
|
@ -150,168 +122,57 @@ void main() {
|
|||
expect(wave, findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Material alone sends no wave', (tester) async {
|
||||
testWidgets('the destination chosen is in the app colour, legible, its '
|
||||
"indicator tinted with it and there at once, without Material's "
|
||||
'ripple', (tester) async {
|
||||
await phone(tester);
|
||||
debugNavigationBarStyle.value = (
|
||||
variant: NavigationBarVariant.material,
|
||||
waveDuration: const Duration(milliseconds: 1200),
|
||||
cornerRadius: 20.0,
|
||||
tintedSurface: false,
|
||||
NavigationBar bar() => tester.widget(find.byType(NavigationBar));
|
||||
Color? iconColor(IconData data) => IconTheme.of(
|
||||
tester.element(find.byIcon(data)),
|
||||
).color;
|
||||
// The app colour as legible on the bar and on its indicator.
|
||||
Color tone() {
|
||||
final surface = Theme.of(
|
||||
tester.element(find.byType(NavigationBar)),
|
||||
).colorScheme.surfaceContainer;
|
||||
return readableTone(
|
||||
accent,
|
||||
brightness: Brightness.light,
|
||||
on: [
|
||||
surface,
|
||||
Color.alphaBlend(accent.withValues(alpha: 0.22), surface),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(frame(selected: 1));
|
||||
await tester.pumpAndSettle();
|
||||
expect(iconColor(Icons.map), tone());
|
||||
expect(iconColor(Icons.home), isNot(tone()));
|
||||
expect(
|
||||
tester.widget<Text>(find.text('Maps')).style?.color ??
|
||||
DefaultTextStyle.of(tester.element(find.text('Maps'))).style.color,
|
||||
tone(),
|
||||
);
|
||||
await tester.pumpWidget(following());
|
||||
await tester.tap(find.text('Maps'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
expect(wave, findsNothing);
|
||||
expect(bar().indicatorColor, accent.withValues(alpha: 0.22));
|
||||
expect(
|
||||
bar().overlayColor?.resolve({WidgetState.pressed}),
|
||||
Colors.transparent,
|
||||
);
|
||||
expect(bar().animationDuration, Duration.zero);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'the destination chosen is in the app colour, legible, its indicator '
|
||||
'tinted with it; without the fill no ripple and no growing indicator; '
|
||||
'the last variant without the indicator',
|
||||
(tester) async {
|
||||
await phone(tester);
|
||||
NavigationBar bar() => tester.widget(find.byType(NavigationBar));
|
||||
Color? iconColor(IconData data) => IconTheme.of(
|
||||
tester.element(find.byIcon(data)),
|
||||
).color;
|
||||
// The app colour as legible on the bar and on its indicator.
|
||||
Color tone() {
|
||||
final surface = Theme.of(
|
||||
tester.element(find.byType(NavigationBar)),
|
||||
).colorScheme.surfaceContainer;
|
||||
return readableTone(
|
||||
accent,
|
||||
brightness: Brightness.light,
|
||||
on: [
|
||||
surface,
|
||||
Color.alphaBlend(accent.withValues(alpha: 0.22), surface),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
debugNavigationBarStyle.value = (
|
||||
variant: NavigationBarVariant.wave,
|
||||
waveDuration: const Duration(milliseconds: 1200),
|
||||
cornerRadius: 20.0,
|
||||
tintedSurface: false,
|
||||
);
|
||||
await tester.pumpWidget(frame(selected: 1));
|
||||
await tester.pumpAndSettle();
|
||||
expect(iconColor(Icons.map), tone());
|
||||
expect(iconColor(Icons.home), isNot(tone()));
|
||||
expect(
|
||||
tester.widget<Text>(find.text('Maps')).style?.color ??
|
||||
DefaultTextStyle.of(tester.element(find.text('Maps'))).style.color,
|
||||
tone(),
|
||||
);
|
||||
expect(bar().indicatorColor, accent.withValues(alpha: 0.22));
|
||||
expect(bar().overlayColor, isNull, reason: 'with the fill');
|
||||
|
||||
debugNavigationBarStyle.value = shippingNavigationBarStyle;
|
||||
await tester.pumpAndSettle();
|
||||
expect(iconColor(Icons.map), tone());
|
||||
expect(bar().indicatorColor, accent.withValues(alpha: 0.22));
|
||||
expect(
|
||||
bar().overlayColor?.resolve({WidgetState.pressed}),
|
||||
Colors.transparent,
|
||||
);
|
||||
expect(bar().animationDuration, Duration.zero);
|
||||
|
||||
debugNavigationBarStyle.value = (
|
||||
variant: NavigationBarVariant.waveWithoutIndicator,
|
||||
waveDuration: const Duration(milliseconds: 1200),
|
||||
cornerRadius: 20.0,
|
||||
tintedSurface: false,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(iconColor(Icons.map), tone());
|
||||
expect(bar().indicatorColor, Colors.transparent);
|
||||
|
||||
debugNavigationBarStyle.value = (
|
||||
variant: NavigationBarVariant.material,
|
||||
waveDuration: const Duration(milliseconds: 1200),
|
||||
cornerRadius: 20.0,
|
||||
tintedSurface: false,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(iconColor(Icons.map), isNot(tone()));
|
||||
expect(bar().indicatorColor, isNull);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets("the bar's top corners are rounded as the style says, the "
|
||||
'screen showing through them', (tester) async {
|
||||
testWidgets("the bar's top corners are rounded, 20 dp, the screen showing "
|
||||
'through them', (tester) async {
|
||||
await phone(tester);
|
||||
BorderRadiusGeometry corners() => tester
|
||||
.widget<ClipRRect>(find.byKey(FlushNavigationBar.barKey))
|
||||
.borderRadius;
|
||||
|
||||
await tester.pumpWidget(frame(selected: 0));
|
||||
expect(corners(), const BorderRadius.vertical(top: Radius.circular(20)));
|
||||
|
||||
debugNavigationBarStyle.value = (
|
||||
variant: NavigationBarVariant.waveWithoutFill,
|
||||
waveDuration: const Duration(milliseconds: 1200),
|
||||
cornerRadius: 0.0,
|
||||
tintedSurface: false,
|
||||
);
|
||||
await tester.pump();
|
||||
expect(corners(), BorderRadius.zero);
|
||||
});
|
||||
|
||||
testWidgets('the debug section switches the look live, and shows nothing '
|
||||
'outside debug builds', (tester) async {
|
||||
await phone(tester);
|
||||
await tester.pumpWidget(
|
||||
testApp(
|
||||
const Scaffold(
|
||||
body: SingleChildScrollView(child: NavigationBarDebugSection()),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(find.text('Navigation bar (debug)'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('F'));
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('2 s'));
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('28'));
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Tinted'));
|
||||
await tester.pump();
|
||||
expect(debugNavigationBarStyle.value, (
|
||||
variant: NavigationBarVariant.waveWithoutIndicator,
|
||||
waveDuration: const Duration(seconds: 2),
|
||||
cornerRadius: 28.0,
|
||||
tintedSurface: true,
|
||||
));
|
||||
|
||||
await tester.pumpWidget(
|
||||
testApp(
|
||||
const Scaffold(body: NavigationBarDebugSection(debug: false)),
|
||||
),
|
||||
);
|
||||
expect(find.text('Navigation bar (debug)'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('outside debug builds the frame keeps the shipping look '
|
||||
'whatever the debug section holds', (tester) async {
|
||||
expect(FlushNavigationBar.cornerRadius, 20);
|
||||
expect(
|
||||
effectiveNavigationBarStyle(debug: false),
|
||||
shippingNavigationBarStyle,
|
||||
tester
|
||||
.widget<ClipRRect>(find.byKey(FlushNavigationBar.barKey))
|
||||
.borderRadius,
|
||||
const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
);
|
||||
debugNavigationBarStyle.value = (
|
||||
variant: NavigationBarVariant.material,
|
||||
waveDuration: const Duration(seconds: 2),
|
||||
cornerRadius: 0.0,
|
||||
tintedSurface: false,
|
||||
);
|
||||
expect(
|
||||
effectiveNavigationBarStyle(debug: false),
|
||||
shippingNavigationBarStyle,
|
||||
);
|
||||
// Tests run as a debug build.
|
||||
expect(effectiveNavigationBarStyle(), debugNavigationBarStyle.value);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue