mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +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>
353 lines
12 KiB
Dart
353 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 {
|
|
/// 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 (Pulse/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),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|