mirror of
https://github.com/block/buzz.git
synced 2026-10-30 14:01:37 +00:00
## Summary Unify mobile conversations in Messages with All / Solo / Team filters, a collapsing header, and separate Search, New, and default-agent shortcuts. Move workspace actions into the community menu and keep collapsed sections intact across filters. Align Inbox and list typography, spacing, and muted controls. Preserve native glass layering through navigation and give every filtered list enough clearance above the footer. The default-agent shortcut reads the shared encrypted preference and opens an existing chat immediately. When unset, it opens Agents; a star action selects the default through a production signed, self-encrypted preference writer. Save failures remain visible and retryable. Android now uses rounded navigation controls and filters, optically balanced search icons, and expanding New/community menus with leading Tabler icons matching iOS. Preset avatars retain transparent WebM animation through an Android WebView renderer, with lifecycle pausing and poster fallback. ### Related issue None found in searches for mobile navigation issues and open PRs. ### Testing - Android follow-up: ARM64 debug build, Dart analysis, and emulator checks passed for menu placement, selection, icon alignment, search, and avatar playback/pause/resume. The avatar implementation passed 3,840 Flutter tests and native Android tests before subsequent styling iterations. Automated tests for the later menu/icon iterations were skipped at the author’s request; those earlier results do not cover the final styling changes. - Mobile formatting/analysis and full test suite (3,838 passed, 23 skipped, plus 4 push-configuration tests); regression journey covers filtering, collapse retention, and scrolling the last Solo row above the footer. - iPhone 18 Pro simulator (Honeycomb): filters, menus, Inbox, Invite/Back, Switch Community, and default-agent chat/Back exercised. iOS release build and Pairing Preview install/launch passed during iteration, preserving the saved profile. - P1 follow-up: production writer-to-reader widget journey covers selection, publish failure/retry, and the unset footer opening Agents. Simulator Agents open/close passes; its saved community has no selectable agents, so live selection was not verified there. - Independent agent review completed; its Solo footer-clearance finding is fixed. The final clearance adjustment is covered by widget tests; physical-device acceptance of that adjustment and hosted CI remain pending. Final Messages layout:  Switcher layering during iteration (before → after): | Before: filter edge showing through | After: Home hidden behind switcher | | --- | --- | |  |  | --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
356 lines
12 KiB
Dart
356 lines
12 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
|
|
import 'app_colors.dart';
|
|
import 'color_scheme.dart';
|
|
import 'grid.dart';
|
|
import 'text_theme.dart';
|
|
import 'theme_extensions.dart';
|
|
|
|
/// Border radius constants matching desktop shadcn "New York" style.
|
|
/// Desktop uses --radius: 0.625rem (10px) as base:
|
|
/// lg = 10px, md = 8px, sm = 6px
|
|
class Radii {
|
|
/// Capsule radius for 56-point navigation controls.
|
|
static const double navigationCapsule = 28.0;
|
|
|
|
/// Small radius for compact UI elements.
|
|
static const double xs = 4.0;
|
|
static const double lg = 10.0;
|
|
static const double md = 8.0;
|
|
static const double sm = 6.0;
|
|
|
|
/// Shared strong radius for grouped rows, fields, and utility containers.
|
|
static const double container = 22.0;
|
|
static const double card = container; // Backwards-compatible card alias.
|
|
static const double popover = container;
|
|
static const double dialog = 24.0; // desktop uses rounded-3xl for dialogs
|
|
|
|
/// Fully rounds pills, circles, and other capsule shapes.
|
|
static const double full = 999.0;
|
|
}
|
|
|
|
class AppTheme {
|
|
static ThemeData light({
|
|
ColorScheme? colorScheme,
|
|
Gradient? topSectionGradient,
|
|
}) {
|
|
final scheme = colorScheme ?? lightColorScheme;
|
|
final appColors = AppColors(
|
|
success: const Color(0xFF218358), // Desktop green-11
|
|
warning: const Color(0xFFAB6400), // Desktop amber-11
|
|
accent: scheme.tertiary,
|
|
huddleDrawerSurface: const Color(0xFF000000),
|
|
huddleControlSurface: const Color(0xFF333333),
|
|
onHuddleDrawer: const Color(0xFFFAFAFA),
|
|
topSectionGradient: topSectionGradient,
|
|
);
|
|
|
|
return _buildTheme(
|
|
scheme: scheme,
|
|
appColors: appColors,
|
|
brightness: Brightness.light,
|
|
statusBarIconBrightness: Brightness.dark,
|
|
statusBarBrightness: Brightness.light,
|
|
);
|
|
}
|
|
|
|
static ThemeData dark({
|
|
ColorScheme? colorScheme,
|
|
Gradient? topSectionGradient,
|
|
}) {
|
|
final scheme = colorScheme ?? darkColorScheme;
|
|
final appColors = AppColors(
|
|
success: const Color(0xFF3DD68C), // Desktop green-11
|
|
warning: const Color(0xFFFFCA16), // Desktop amber-11
|
|
accent: scheme.tertiary,
|
|
huddleDrawerSurface: scheme.primaryContainer,
|
|
huddleControlSurface: Color.alphaBlend(
|
|
scheme.onPrimaryContainer.withValues(alpha: 0.18),
|
|
scheme.primaryContainer,
|
|
),
|
|
onHuddleDrawer: scheme.onPrimaryContainer,
|
|
topSectionGradient: topSectionGradient,
|
|
);
|
|
|
|
return _buildTheme(
|
|
scheme: scheme,
|
|
appColors: appColors,
|
|
brightness: Brightness.dark,
|
|
statusBarIconBrightness: Brightness.light,
|
|
statusBarBrightness: Brightness.dark,
|
|
);
|
|
}
|
|
|
|
static ThemeData _buildTheme({
|
|
required ColorScheme scheme,
|
|
required AppColors appColors,
|
|
required Brightness brightness,
|
|
required Brightness statusBarIconBrightness,
|
|
required Brightness statusBarBrightness,
|
|
}) {
|
|
return ThemeData(
|
|
useMaterial3: true,
|
|
colorScheme: scheme,
|
|
splashFactory: NoSplash.splashFactory,
|
|
scaffoldBackgroundColor: scheme.surface,
|
|
extensions: [appColors],
|
|
fontFamily: 'Inter',
|
|
textTheme: textTheme,
|
|
appBarTheme: AppBarTheme(
|
|
backgroundColor: Colors.transparent,
|
|
foregroundColor: scheme.onSurface,
|
|
surfaceTintColor: Colors.transparent,
|
|
elevation: 0,
|
|
scrolledUnderElevation: 0,
|
|
titleTextStyle: textTheme.titleMedium?.copyWith(
|
|
color: scheme.onSurface,
|
|
),
|
|
systemOverlayStyle: SystemUiOverlayStyle(
|
|
statusBarColor: Colors.transparent,
|
|
statusBarIconBrightness: statusBarIconBrightness,
|
|
statusBarBrightness: statusBarBrightness,
|
|
),
|
|
),
|
|
|
|
// Bottom navigation: clean style, no indicator pill
|
|
navigationBarTheme: NavigationBarThemeData(
|
|
backgroundColor: scheme.surface,
|
|
elevation: 0,
|
|
indicatorColor: Colors.transparent,
|
|
iconTheme: WidgetStateProperty.resolveWith((states) {
|
|
if (states.contains(WidgetState.selected)) {
|
|
return IconThemeData(color: scheme.primary, size: 24);
|
|
}
|
|
return IconThemeData(color: scheme.onSurfaceVariant, size: 24);
|
|
}),
|
|
labelTextStyle: WidgetStateProperty.resolveWith((states) {
|
|
if (states.contains(WidgetState.selected)) {
|
|
return textTheme.labelSmall?.copyWith(
|
|
color: scheme.primary,
|
|
fontWeight: FontWeight.w600,
|
|
);
|
|
}
|
|
return textTheme.labelSmall?.copyWith(color: scheme.onSurfaceVariant);
|
|
}),
|
|
),
|
|
|
|
// Buttons: desktop uses rounded-md (8px), h-9 (36px), px-4 (16px)
|
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: neutralControlSurface(brightness),
|
|
foregroundColor: scheme.onSurface,
|
|
elevation: 0,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
|
minimumSize: const Size(0, 36),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(Radii.md),
|
|
),
|
|
textStyle: textTheme.labelMedium?.copyWith(
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
filledButtonTheme: FilledButtonThemeData(
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: neutralControlSurface(brightness),
|
|
foregroundColor: scheme.onSurface,
|
|
elevation: 0,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
|
minimumSize: const Size(0, 36),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(Radii.md),
|
|
),
|
|
textStyle: textTheme.labelMedium?.copyWith(
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
|
style: OutlinedButton.styleFrom(
|
|
backgroundColor: neutralControlSurface(brightness),
|
|
foregroundColor: scheme.onSurface,
|
|
side: BorderSide(color: scheme.outline, width: 1),
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
|
minimumSize: const Size(0, 36),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(Radii.md),
|
|
),
|
|
textStyle: textTheme.labelMedium?.copyWith(
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
textButtonTheme: TextButtonThemeData(
|
|
style: TextButton.styleFrom(
|
|
foregroundColor: scheme.onSurface,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
minimumSize: const Size(0, 36),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(Radii.md),
|
|
),
|
|
textStyle: textTheme.labelMedium?.copyWith(
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
|
|
// Cards: desktop uses rounded-lg (10px), flat, no elevation
|
|
cardTheme: CardThemeData(
|
|
color: scheme.surfaceContainerHighest,
|
|
margin: EdgeInsets.zero,
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(Radii.lg),
|
|
),
|
|
),
|
|
|
|
// Inputs: desktop uses outlined style, rounded-md (8px), h-9 (36px)
|
|
inputDecorationTheme: InputDecorationTheme(
|
|
filled: false,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(Radii.md),
|
|
borderSide: BorderSide(color: scheme.outline),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(Radii.md),
|
|
borderSide: BorderSide(color: scheme.outline),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(Radii.md),
|
|
borderSide: BorderSide(color: scheme.primary),
|
|
),
|
|
errorBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(Radii.md),
|
|
borderSide: BorderSide(color: scheme.error),
|
|
),
|
|
focusedErrorBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(Radii.md),
|
|
borderSide: BorderSide(color: scheme.error),
|
|
),
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 10,
|
|
),
|
|
isDense: true,
|
|
),
|
|
|
|
// Dialogs: desktop uses rounded-3xl (24px), custom overlay
|
|
dialogTheme: DialogThemeData(
|
|
backgroundColor: scheme.surface,
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(Radii.dialog),
|
|
side: BorderSide(color: scheme.outline),
|
|
),
|
|
titleTextStyle: textTheme.titleLarge?.copyWith(
|
|
color: scheme.onSurface,
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -0.3,
|
|
),
|
|
contentTextStyle: textTheme.bodyMedium?.copyWith(
|
|
color: scheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
|
|
progressIndicatorTheme: ProgressIndicatorThemeData(
|
|
strokeWidth: 2,
|
|
color: scheme.primary,
|
|
circularTrackColor: scheme.onSurfaceVariant.withValues(alpha: 0.2),
|
|
),
|
|
|
|
listTileTheme: ListTileThemeData(
|
|
titleTextStyle: textTheme.titleSmall?.copyWith(color: scheme.onSurface),
|
|
subtitleTextStyle: textTheme.bodyMedium?.copyWith(
|
|
color: scheme.secondary,
|
|
),
|
|
iconColor: scheme.secondary,
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: Grid.twelve),
|
|
minVerticalPadding: Grid.twelve,
|
|
horizontalTitleGap: Grid.twelve,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(Radii.md),
|
|
),
|
|
),
|
|
|
|
// Chips: desktop uses rounded-sm (6px)
|
|
chipTheme: ChipThemeData(
|
|
labelStyle: textTheme.bodySmall?.copyWith(color: scheme.secondary),
|
|
// M3 resolves the chip container via `color` (WidgetStateProperty);
|
|
// `selectedColor` is the legacy M2 path and is ignored here. Selected
|
|
// filter chips (Search/Activity tabs) use the accent.
|
|
color: WidgetStateProperty.resolveWith((states) {
|
|
if (states.contains(WidgetState.selected)) return scheme.primary;
|
|
return scheme.surfaceContainerHighest;
|
|
}),
|
|
checkmarkColor: scheme.onPrimary,
|
|
shape: RoundedRectangleBorder(
|
|
side: BorderSide.none,
|
|
borderRadius: BorderRadius.circular(Radii.sm),
|
|
),
|
|
side: BorderSide.none,
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
labelPadding: EdgeInsets.zero,
|
|
),
|
|
|
|
// Popup menus use the same corner radius as grouped utility rows.
|
|
popupMenuTheme: PopupMenuThemeData(
|
|
color: scheme.surface.withValues(alpha: 0.98),
|
|
elevation: 8,
|
|
shadowColor: scheme.shadow.withValues(alpha: 0.18),
|
|
surfaceTintColor: Colors.transparent,
|
|
textStyle: textTheme.labelLarge?.copyWith(color: scheme.onSurface),
|
|
labelTextStyle: WidgetStatePropertyAll(
|
|
textTheme.labelLarge?.copyWith(color: scheme.onSurface),
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(Radii.popover),
|
|
side: BorderSide(
|
|
color: Colors.black.withValues(alpha: 0.04),
|
|
width: 1,
|
|
),
|
|
),
|
|
),
|
|
|
|
// Bottom sheet: match dialog radius
|
|
bottomSheetTheme: BottomSheetThemeData(
|
|
backgroundColor: scheme.surface,
|
|
elevation: 0,
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.vertical(
|
|
top: Radius.circular(Radii.dialog),
|
|
),
|
|
),
|
|
),
|
|
|
|
// Tooltips: desktop uses rounded-md, primary bg
|
|
tooltipTheme: TooltipThemeData(
|
|
decoration: BoxDecoration(
|
|
color: scheme.primary,
|
|
borderRadius: BorderRadius.circular(Radii.md),
|
|
),
|
|
textStyle: textTheme.bodySmall?.copyWith(
|
|
color: scheme.onPrimary,
|
|
fontSize: 12,
|
|
),
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
),
|
|
|
|
dividerTheme: DividerThemeData(
|
|
color: scheme.outline,
|
|
thickness: 1,
|
|
space: 1,
|
|
),
|
|
|
|
// Snackbar
|
|
snackBarTheme: SnackBarThemeData(
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(Radii.md),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|