mirror of
https://github.com/block/buzz.git
synced 2026-10-28 04:51:40 +00:00
## Summary Polish mobile Home and Solo: group Search/Inbox beside a separate profile avatar, move Inbox and Settings to bottom-up pages with Close controls, and place tabs opposite the plus/compose action. Morph plus to compose clockwise when entering Solo and reverse counterclockwise when returning Home, with a directional quarter-turn of the icon inside a level button and a subtle scale pulse, respecting reduced motion. Align Search’s Close control with Inbox, simplify its placeholder, and initialize selected filter contrast correctly. Collapse the plus menu when leaving Home. Center Solo’s native-glass New chat empty state and keep it stationary behind the keyboard. Standardize Tabler icons and neutral backgrounds, retain SF Back/Close on iOS, use native Liquid Glass headers, tab container, and actions (solid grey selected tab, slightly darker on iOS) on iOS and black actions on Android, and smooth the expanded menu corners. Repair native header safe-area/return layout and compose movement after cancellation. ### Related issue None found for this UI refinement; follows #8337. ### Testing - Formatting, analysis, full mobile suite: 3,487 passed, 4 skipped; 3 push-configuration tests passed. File-size checks passed. - Base UI built and installed on Pixel and iPhone in place. Pixel navigation/New chat cancellation checked. Subsequent glass and rotation refinements are previewed on iPhone; motion feel awaits human confirmation. - Draft pending human confirmation of the final Settings entrance/return behavior on iPhone. ### Pixel snapshots | Solo | Expanded actions | | --- | --- | | <img src="https://raw.githubusercontent.com/block/buzz/16dc0f8af971e63d788fee20974997fab925dec9/docs/screenshots/mobile-home-solo/pixel-solo.png" width="280" alt="Solo empty state with New chat" /> | <img src="https://raw.githubusercontent.com/block/buzz/16dc0f8af971e63d788fee20974997fab925dec9/docs/screenshots/mobile-home-solo/pixel-actions.png" width="280" alt="Expanded channel actions with smooth corners" /> | --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
268 lines
8.5 KiB
Dart
268 lines
8.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:buzz/shared/theme/theme.dart';
|
|
import 'package:buzz/shared/widgets/frosted_app_bar.dart';
|
|
|
|
void main() {
|
|
group('Buzz theme catalog entries', () {
|
|
test('both halves are in the catalog', () {
|
|
expect(findTheme(buzzThemeName), isNotNull);
|
|
expect(findTheme(buzzDarkThemeName), isNotNull);
|
|
});
|
|
|
|
test('borrow the GitHub palettes', () {
|
|
final buzz = findTheme(buzzThemeName)!;
|
|
final github = findTheme('github-light')!;
|
|
expect(buzz.bg, github.bg);
|
|
expect(buzz.fg, github.fg);
|
|
expect(buzz.comment, github.comment);
|
|
|
|
final buzzDark = findTheme(buzzDarkThemeName)!;
|
|
final githubDark = findTheme('github-dark')!;
|
|
expect(buzzDark.bg, githubDark.bg);
|
|
expect(buzzDark.fg, githubDark.fg);
|
|
expect(buzzDark.comment, githubDark.comment);
|
|
});
|
|
|
|
test('are a light/dark pair', () {
|
|
expect(findTheme(buzzThemeName)!.isDark, isFalse);
|
|
expect(findTheme(buzzDarkThemeName)!.isDark, isTrue);
|
|
expect(themePairFor(buzzThemeName), buzzDarkThemeName);
|
|
expect(themePairFor(buzzDarkThemeName), buzzThemeName);
|
|
});
|
|
|
|
test('appear as a single System-mode option labelled "Buzz"', () {
|
|
final paired = themeGroups().paired.map((t) => t.name);
|
|
expect(paired, contains(buzzThemeName));
|
|
expect(paired, isNot(contains(buzzDarkThemeName)));
|
|
expect(pairedThemeLabel(buzzThemeName), 'Buzz');
|
|
expect(themeSelectionLabel(buzzThemeName, ThemeMode.system), 'Buzz');
|
|
expect(themeSelectionLabel(buzzDarkThemeName, ThemeMode.system), 'Buzz');
|
|
});
|
|
|
|
test('forces neutral rendering without changing the stored accent', () {
|
|
const storedAccent = '#ef4444';
|
|
|
|
expect(
|
|
effectiveAccentIndex(buzzThemeName, storedAccent),
|
|
neutralAccentIndex,
|
|
);
|
|
expect(
|
|
effectiveAccentIndex(buzzDarkThemeName, storedAccent),
|
|
neutralAccentIndex,
|
|
);
|
|
expect(
|
|
effectiveAccentIndex('github-light', storedAccent),
|
|
accentIndexForWireValue(storedAccent),
|
|
);
|
|
expect(storedAccent, '#ef4444');
|
|
});
|
|
|
|
test('resolve across brightnesses like any other pair', () {
|
|
final resolved = resolveSchemes(buzzThemeName, ThemeMode.system);
|
|
expect(resolved.forcedMode, isNull);
|
|
expect(resolved.light.brightness, Brightness.light);
|
|
expect(resolved.dark.brightness, Brightness.dark);
|
|
expect(resolved.lightTheme?.name, buzzThemeName);
|
|
expect(resolved.darkTheme?.name, buzzDarkThemeName);
|
|
|
|
expect(
|
|
effectiveTheme(buzzThemeName, ThemeMode.dark)?.name,
|
|
buzzDarkThemeName,
|
|
);
|
|
expect(
|
|
effectiveTheme(buzzDarkThemeName, ThemeMode.light)?.name,
|
|
buzzThemeName,
|
|
);
|
|
});
|
|
|
|
test(
|
|
'fallbacks expose the effective Buzz theme for gradient selection',
|
|
() {
|
|
final coerced = resolveSchemes('nord', ThemeMode.light);
|
|
expect(coerced.lightTheme?.name, buzzThemeName);
|
|
expect(
|
|
buzzTopSectionGradient(
|
|
coerced.lightTheme!.name,
|
|
coerced.light.brightness,
|
|
),
|
|
isNotNull,
|
|
);
|
|
|
|
final unknown = resolveSchemes('not-a-theme', ThemeMode.light);
|
|
expect(unknown.lightTheme?.name, buzzThemeName);
|
|
expect(
|
|
buzzTopSectionGradient(
|
|
unknown.lightTheme!.name,
|
|
unknown.light.brightness,
|
|
),
|
|
isNotNull,
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
group('buzzTopSectionGradient', () {
|
|
test('is null for non-Buzz themes', () {
|
|
expect(buzzTopSectionGradient('github-light', Brightness.light), isNull);
|
|
expect(buzzTopSectionGradient('nord', Brightness.dark), isNull);
|
|
});
|
|
|
|
test('paints top to bottom for both halves of the pair', () {
|
|
for (final name in [buzzThemeName, buzzDarkThemeName]) {
|
|
final gradient = buzzTopSectionGradient(name, Brightness.light);
|
|
expect(gradient, isNotNull, reason: '$name should be gradient-backed');
|
|
expect(gradient!.begin, Alignment.topCenter);
|
|
expect(gradient.end, Alignment.bottomCenter);
|
|
expect(gradient.colors, hasLength(2));
|
|
}
|
|
});
|
|
|
|
test('brightness selects the stops, not the theme name', () {
|
|
// Both halves enable the gradient, so System mode keeps it on across an
|
|
// OS switch — the applied brightness alone decides which stops are used.
|
|
final light = buzzTopSectionGradient(buzzThemeName, Brightness.light)!;
|
|
final dark = buzzTopSectionGradient(buzzThemeName, Brightness.dark)!;
|
|
|
|
expect(light.colors, isNot(dark.colors));
|
|
expect(
|
|
buzzTopSectionGradient(buzzDarkThemeName, Brightness.dark)!.colors,
|
|
dark.colors,
|
|
);
|
|
expect(
|
|
buzzTopSectionGradient(buzzDarkThemeName, Brightness.light)!.colors,
|
|
light.colors,
|
|
);
|
|
});
|
|
|
|
test('is opaque so the color replaces the frosted fill', () {
|
|
for (final brightness in Brightness.values) {
|
|
final gradient = buzzTopSectionGradient(buzzThemeName, brightness)!;
|
|
for (final color in gradient.colors) {
|
|
expect(color.a, 1.0);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
group('theme threading', () {
|
|
BoxDecoration barDecoration(WidgetTester tester) {
|
|
final container = tester
|
|
.widgetList<Container>(
|
|
find.descendant(
|
|
of: find.byType(FrostedAppBar),
|
|
matching: find.byType(Container),
|
|
),
|
|
)
|
|
.first;
|
|
return container.decoration! as BoxDecoration;
|
|
}
|
|
|
|
Widget harness(ThemeData theme) => MaterialApp(
|
|
theme: theme,
|
|
home: Builder(
|
|
builder: (context) => Stack(
|
|
children: [
|
|
FrostedAppBar(
|
|
gradient: context.appColors.topSectionGradient,
|
|
title: const Text('Home'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
testWidgets('AppTheme carries the gradient to the top section', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
harness(
|
|
AppTheme.light(
|
|
topSectionGradient: buzzTopSectionGradient(
|
|
buzzThemeName,
|
|
Brightness.light,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final decoration = barDecoration(tester);
|
|
expect(decoration.gradient, isNotNull);
|
|
// A BoxDecoration cannot paint a color and a gradient at once.
|
|
expect(decoration.color, isNull);
|
|
});
|
|
|
|
testWidgets('non-Buzz themes keep the frosted surface fill', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(harness(AppTheme.light()));
|
|
|
|
final decoration = barDecoration(tester);
|
|
expect(decoration.gradient, isNull);
|
|
expect(decoration.color, isNotNull);
|
|
});
|
|
|
|
testWidgets('Buzz section labels use 80% neutral foreground', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
harness(
|
|
AppTheme.light(
|
|
topSectionGradient: buzzTopSectionGradient(
|
|
buzzThemeName,
|
|
Brightness.light,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final context = tester.element(find.text('Home'));
|
|
expect(
|
|
navigationSectionForeground(context),
|
|
Colors.black.withValues(alpha: 0.8),
|
|
);
|
|
});
|
|
|
|
testWidgets('navigation roles inherit non-Buzz theme tokens', (
|
|
tester,
|
|
) async {
|
|
const primaryForeground = Color(0xFF123456);
|
|
const secondaryForeground = Color(0xFF789ABC);
|
|
const searchSurface = Color(0xFFDEF012);
|
|
final theme = ThemeData(
|
|
colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple).copyWith(
|
|
onSurface: primaryForeground,
|
|
onSurfaceVariant: secondaryForeground,
|
|
surfaceContainerHighest: searchSurface,
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: theme,
|
|
home: const Scaffold(body: SizedBox()),
|
|
),
|
|
);
|
|
|
|
final context = tester.element(find.byType(SizedBox));
|
|
expect(navigationPrimaryForeground(context), primaryForeground);
|
|
expect(navigationSecondaryForeground(context), secondaryForeground);
|
|
expect(navigationSectionForeground(context), secondaryForeground);
|
|
expect(navigationSearchSurface(context), const Color(0xFFF0F0F0));
|
|
expect(
|
|
navigationDivider(context, 0.15),
|
|
primaryForeground.withValues(alpha: 0.15),
|
|
);
|
|
});
|
|
});
|
|
|
|
group('isBuzzTheme', () {
|
|
test('matches only the Buzz pair', () {
|
|
expect(isBuzzTheme(buzzThemeName), isTrue);
|
|
expect(isBuzzTheme(buzzDarkThemeName), isTrue);
|
|
expect(isBuzzTheme('github-light'), isFalse);
|
|
expect(isBuzzTheme(''), isFalse);
|
|
});
|
|
});
|
|
}
|