Polish frontend: design tokens, theming, AppBar titles, empty/loading states

Frontend usability and aesthetics pass across web and mobile:

- Add shared layout tokens (theme/dimens.dart: Insets + Breakpoints) and
  replace magic-number spacing and per-file breakpoint consts.
- Route both themes through buildAppTheme (theme/theme_builder.dart): explicit
  useMaterial3 and a shared branded textTheme (Barlow Condensed for
  display/headline/title styles); logo now reads its style from the theme.
- Move screen titles into the shared AppBar (route-derived) and drop the
  redundant in-body headers; upgrade Settings buttons to M3 FilledButton.
- Add reusable EmptyState and skeleton loaders; Devices empty state links to
  scanner status, notifications get filter-aware messages.
- Add a first-run welcome intro and Save-disabled helper text in Settings.
- Make device Filter/Sort adaptive: bottom sheet on phones, dialog on wide.
- Collapse the device-list filter chips and Sort/Filter buttons into one row;
  rename the home Scanners card title and align its style.

Tests: add EmptyState/skeleton widget tests and Settings first-run cases;
update tests for the FilledButton swap and relocated titles. 89 passing,
dart analyze clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
rzuasti
2026-06-04 08:39:37 -04:00
co-authored by Claude Opus 4.8
parent 5f6f5ad679
commit 9367c359d7
20 changed files with 661 additions and 274 deletions
+60 -61
View File
@@ -1,93 +1,92 @@
import 'package:flutter/material.dart';
import 'app_colors.dart';
import 'theme_builder.dart';
abstract final class CatppuccinMochaColors {
static const Color crust = Color(0xFF11111b);
static const Color mantle = Color(0xFF181825);
static const Color base = Color(0xFF1e1e2e);
static const Color surface0 = Color(0xFF313244);
static const Color surface1 = Color(0xFF45475a);
static const Color surface2 = Color(0xFF585b70);
static const Color overlay0 = Color(0xFF6c7086);
static const Color overlay1 = Color(0xFF7f849c);
static const Color overlay2 = Color(0xFF9399b2);
static const Color subtext0 = Color(0xFFa6adc8);
static const Color subtext1 = Color(0xFFbac2de);
static const Color text = Color(0xFFcdd6f4);
static const Color crust = Color(0xFF11111b);
static const Color mantle = Color(0xFF181825);
static const Color base = Color(0xFF1e1e2e);
static const Color surface0 = Color(0xFF313244);
static const Color surface1 = Color(0xFF45475a);
static const Color surface2 = Color(0xFF585b70);
static const Color overlay0 = Color(0xFF6c7086);
static const Color overlay1 = Color(0xFF7f849c);
static const Color overlay2 = Color(0xFF9399b2);
static const Color subtext0 = Color(0xFFa6adc8);
static const Color subtext1 = Color(0xFFbac2de);
static const Color text = Color(0xFFcdd6f4);
static const Color rosewater = Color(0xFFf5e0dc);
static const Color flamingo = Color(0xFFf2cdcd);
static const Color pink = Color(0xFFf5c2e7);
static const Color mauve = Color(0xFFcba6f7);
static const Color red = Color(0xFFf38ba8);
static const Color maroon = Color(0xFFeba0ac);
static const Color peach = Color(0xFFfab387);
static const Color yellow = Color(0xFFf9e2af);
static const Color green = Color(0xFFa6e3a1);
static const Color teal = Color(0xFF94e2d5);
static const Color sky = Color(0xFF89dceb);
static const Color sapphire = Color(0xFF74c7ec);
static const Color blue = Color(0xFF89b4fa);
static const Color lavender = Color(0xFFb4befe);
static const Color flamingo = Color(0xFFf2cdcd);
static const Color pink = Color(0xFFf5c2e7);
static const Color mauve = Color(0xFFcba6f7);
static const Color red = Color(0xFFf38ba8);
static const Color maroon = Color(0xFFeba0ac);
static const Color peach = Color(0xFFfab387);
static const Color yellow = Color(0xFFf9e2af);
static const Color green = Color(0xFFa6e3a1);
static const Color teal = Color(0xFF94e2d5);
static const Color sky = Color(0xFF89dceb);
static const Color sapphire = Color(0xFF74c7ec);
static const Color blue = Color(0xFF89b4fa);
static const Color lavender = Color(0xFFb4befe);
}
const ColorScheme _catppuccinMochaColorScheme = ColorScheme(
brightness: Brightness.dark,
// Surfaces — AppBar (darkest) → NavRail → body (lightest)
surface: CatppuccinMochaColors.base,
surfaceContainerLowest: CatppuccinMochaColors.crust,
surfaceContainerLow: CatppuccinMochaColors.mantle,
surfaceContainer: CatppuccinMochaColors.surface0,
surfaceContainerHigh: CatppuccinMochaColors.surface1,
surface: CatppuccinMochaColors.base,
surfaceContainerLowest: CatppuccinMochaColors.crust,
surfaceContainerLow: CatppuccinMochaColors.mantle,
surfaceContainer: CatppuccinMochaColors.surface0,
surfaceContainerHigh: CatppuccinMochaColors.surface1,
surfaceContainerHighest: CatppuccinMochaColors.surface2,
onSurface: CatppuccinMochaColors.text,
onSurfaceVariant: CatppuccinMochaColors.subtext1,
outline: CatppuccinMochaColors.overlay1,
outlineVariant: CatppuccinMochaColors.overlay0,
onSurface: CatppuccinMochaColors.text,
onSurfaceVariant: CatppuccinMochaColors.subtext1,
outline: CatppuccinMochaColors.overlay1,
outlineVariant: CatppuccinMochaColors.overlay0,
// Primary — Save button, selected nav indicators
primary: CatppuccinMochaColors.mauve,
onPrimary: CatppuccinMochaColors.crust,
primaryContainer: CatppuccinMochaColors.surface0,
primary: CatppuccinMochaColors.mauve,
onPrimary: CatppuccinMochaColors.crust,
primaryContainer: CatppuccinMochaColors.surface0,
onPrimaryContainer: CatppuccinMochaColors.text,
// Secondary — Test button (not-passing state)
secondary: CatppuccinMochaColors.blue,
onSecondary: CatppuccinMochaColors.crust,
secondaryContainer: CatppuccinMochaColors.surface1,
secondary: CatppuccinMochaColors.blue,
onSecondary: CatppuccinMochaColors.crust,
secondaryContainer: CatppuccinMochaColors.surface1,
onSecondaryContainer: CatppuccinMochaColors.text,
// Tertiary — swipe-to-unread background
tertiary: CatppuccinMochaColors.teal,
onTertiary: CatppuccinMochaColors.crust,
tertiaryContainer: CatppuccinMochaColors.surface0,
tertiary: CatppuccinMochaColors.teal,
onTertiary: CatppuccinMochaColors.crust,
tertiaryContainer: CatppuccinMochaColors.surface0,
onTertiaryContainer: CatppuccinMochaColors.text,
// Error
error: CatppuccinMochaColors.red,
onError: CatppuccinMochaColors.crust,
errorContainer: CatppuccinMochaColors.maroon,
error: CatppuccinMochaColors.red,
onError: CatppuccinMochaColors.crust,
errorContainer: CatppuccinMochaColors.maroon,
onErrorContainer: CatppuccinMochaColors.crust,
// Inverse / scrim
inverseSurface: CatppuccinMochaColors.text,
inverseSurface: CatppuccinMochaColors.text,
onInverseSurface: CatppuccinMochaColors.crust,
inversePrimary: CatppuccinMochaColors.mauve,
scrim: CatppuccinMochaColors.crust,
shadow: CatppuccinMochaColors.crust,
inversePrimary: CatppuccinMochaColors.mauve,
scrim: CatppuccinMochaColors.crust,
shadow: CatppuccinMochaColors.crust,
);
final ThemeData catppuccinMochaDarkTheme = ThemeData(
final ThemeData catppuccinMochaDarkTheme = buildAppTheme(
colorScheme: _catppuccinMochaColorScheme,
extensions: [
const AppColorExtension(
success: CatppuccinMochaColors.green,
onSuccess: CatppuccinMochaColors.crust,
warning: CatppuccinMochaColors.peach,
onWarning: CatppuccinMochaColors.crust,
info: CatppuccinMochaColors.blue,
onInfo: CatppuccinMochaColors.crust,
),
],
appColors: const AppColorExtension(
success: CatppuccinMochaColors.green,
onSuccess: CatppuccinMochaColors.crust,
warning: CatppuccinMochaColors.peach,
onWarning: CatppuccinMochaColors.crust,
info: CatppuccinMochaColors.blue,
onInfo: CatppuccinMochaColors.crust,
),
);
+28
View File
@@ -0,0 +1,28 @@
/// Shared layout tokens for consistent spacing and responsive breakpoints.
///
/// Use these instead of hand-written magic numbers so spacing stays uniform
/// across screens and breakpoints are defined in a single place.
library;
/// Spacing scale (logical pixels). Used for padding, margins and gaps.
abstract final class Insets {
static const double xs = 4;
static const double sm = 8;
static const double md = 12;
static const double lg = 16;
static const double xl = 20;
static const double xxl = 24;
static const double xxxl = 32;
}
/// Responsive layout breakpoints, aligned with Material 3 window size classes.
abstract final class Breakpoints {
/// Compact → medium: switch from bottom NavigationBar to NavigationRail.
static const double medium = 600;
/// Medium → expanded: extend the NavigationRail with labels.
static const double expanded = 840;
/// Home screen switches to its two-column layout at this width.
static const double twoColumn = 700;
}
+56 -57
View File
@@ -1,30 +1,31 @@
import 'package:flutter/material.dart';
import 'app_colors.dart';
import 'theme_builder.dart';
abstract final class GruvboxColors {
static const Color bgHard = Color(0xFF1d2021);
static const Color bg = Color(0xFF282828);
static const Color bgSoft = Color(0xFF32302f);
static const Color bg1 = Color(0xFF3c3836);
static const Color bg2 = Color(0xFF504945);
static const Color bg3 = Color(0xFF665c54);
static const Color bg4 = Color(0xFF7c6f64);
static const Color fg = Color(0xFFebdbb2);
static const Color fg2 = Color(0xFFd5c4a1);
static const Color gray = Color(0xFF928374);
static const Color red = Color(0xFFcc241d);
static const Color brightRed = Color(0xFFfb4934);
static const Color green = Color(0xFF98971a);
static const Color brightGreen = Color(0xFFb8bb26);
static const Color yellow = Color(0xFFd79921);
static const Color bgHard = Color(0xFF1d2021);
static const Color bg = Color(0xFF282828);
static const Color bgSoft = Color(0xFF32302f);
static const Color bg1 = Color(0xFF3c3836);
static const Color bg2 = Color(0xFF504945);
static const Color bg3 = Color(0xFF665c54);
static const Color bg4 = Color(0xFF7c6f64);
static const Color fg = Color(0xFFebdbb2);
static const Color fg2 = Color(0xFFd5c4a1);
static const Color gray = Color(0xFF928374);
static const Color red = Color(0xFFcc241d);
static const Color brightRed = Color(0xFFfb4934);
static const Color green = Color(0xFF98971a);
static const Color brightGreen = Color(0xFFb8bb26);
static const Color yellow = Color(0xFFd79921);
static const Color brightYellow = Color(0xFFfabd2f);
static const Color blue = Color(0xFF458588);
static const Color brightBlue = Color(0xFF83a598);
static const Color purple = Color(0xFFb16286);
static const Color blue = Color(0xFF458588);
static const Color brightBlue = Color(0xFF83a598);
static const Color purple = Color(0xFFb16286);
static const Color brightPurple = Color(0xFFd3869b);
static const Color aqua = Color(0xFF689d6a);
static const Color brightAqua = Color(0xFF8ec07c);
static const Color orange = Color(0xFFd65d0e);
static const Color aqua = Color(0xFF689d6a);
static const Color brightAqua = Color(0xFF8ec07c);
static const Color orange = Color(0xFFd65d0e);
static const Color brightOrange = Color(0xFFfe8019);
}
@@ -32,59 +33,57 @@ const ColorScheme _gruvboxColorScheme = ColorScheme(
brightness: Brightness.dark,
// Surfaces — AppBar (darkest) → NavRail → body (lightest)
surface: GruvboxColors.bgSoft,
surfaceContainerLowest: GruvboxColors.bgHard,
surfaceContainerLow: GruvboxColors.bg,
surfaceContainer: GruvboxColors.bg1,
surfaceContainerHigh: GruvboxColors.bg1,
surface: GruvboxColors.bgSoft,
surfaceContainerLowest: GruvboxColors.bgHard,
surfaceContainerLow: GruvboxColors.bg,
surfaceContainer: GruvboxColors.bg1,
surfaceContainerHigh: GruvboxColors.bg1,
surfaceContainerHighest: GruvboxColors.bg2,
onSurface: GruvboxColors.fg,
onSurfaceVariant: GruvboxColors.fg2,
outline: GruvboxColors.gray,
outlineVariant: GruvboxColors.bg4,
onSurface: GruvboxColors.fg,
onSurfaceVariant: GruvboxColors.fg2,
outline: GruvboxColors.gray,
outlineVariant: GruvboxColors.bg4,
// Primary — Save button, selected nav indicators
primary: GruvboxColors.brightOrange,
onPrimary: GruvboxColors.bgHard,
primaryContainer: GruvboxColors.bg1,
primary: GruvboxColors.brightOrange,
onPrimary: GruvboxColors.bgHard,
primaryContainer: GruvboxColors.bg1,
onPrimaryContainer: GruvboxColors.fg,
// Secondary — Test button (not-passing state)
secondary: GruvboxColors.blue,
onSecondary: GruvboxColors.fg,
secondaryContainer: GruvboxColors.bg2,
secondary: GruvboxColors.blue,
onSecondary: GruvboxColors.fg,
secondaryContainer: GruvboxColors.bg2,
onSecondaryContainer: GruvboxColors.fg,
// Tertiary — swipe-to-unread background
tertiary: GruvboxColors.aqua,
onTertiary: GruvboxColors.bgHard,
tertiaryContainer: GruvboxColors.bg1,
tertiary: GruvboxColors.aqua,
onTertiary: GruvboxColors.bgHard,
tertiaryContainer: GruvboxColors.bg1,
onTertiaryContainer: GruvboxColors.fg,
// Error
error: GruvboxColors.brightRed,
onError: GruvboxColors.bgHard,
errorContainer: GruvboxColors.red,
error: GruvboxColors.brightRed,
onError: GruvboxColors.bgHard,
errorContainer: GruvboxColors.red,
onErrorContainer: GruvboxColors.fg,
// Inverse / scrim
inverseSurface: GruvboxColors.fg,
inverseSurface: GruvboxColors.fg,
onInverseSurface: GruvboxColors.bgHard,
inversePrimary: GruvboxColors.orange,
scrim: GruvboxColors.bgHard,
shadow: GruvboxColors.bgHard,
inversePrimary: GruvboxColors.orange,
scrim: GruvboxColors.bgHard,
shadow: GruvboxColors.bgHard,
);
final ThemeData gruvboxDarkTheme = ThemeData(
final ThemeData gruvboxDarkTheme = buildAppTheme(
colorScheme: _gruvboxColorScheme,
extensions: [
const AppColorExtension(
success: GruvboxColors.brightGreen,
onSuccess: GruvboxColors.bgHard,
warning: GruvboxColors.brightYellow,
onWarning: GruvboxColors.bgHard,
info: GruvboxColors.brightBlue,
onInfo: GruvboxColors.bgHard,
),
],
appColors: const AppColorExtension(
success: GruvboxColors.brightGreen,
onSuccess: GruvboxColors.bgHard,
warning: GruvboxColors.brightYellow,
onWarning: GruvboxColors.bgHard,
info: GruvboxColors.brightBlue,
onInfo: GruvboxColors.bgHard,
),
);
+36
View File
@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'app_colors.dart';
/// Builds a Material 3 [ThemeData] from a [ColorScheme] and the app's custom
/// [AppColorExtension], applying the shared OOTT typography.
///
/// Both bundled themes (Catppuccin Mocha, Gruvbox Dark) go through here so they
/// stay visually consistent — only their colors differ.
ThemeData buildAppTheme({
required ColorScheme colorScheme,
required AppColorExtension appColors,
}) {
final base = ThemeData(colorScheme: colorScheme, useMaterial3: true);
return base.copyWith(
textTheme: _brandTextTheme(base.textTheme),
extensions: [appColors],
);
}
/// Applies the OOTT brand font (Barlow Condensed) to display, headline and
/// large-title styles while leaving body/label styles on the default face for
/// readability. Sizes and colors from [base] are preserved.
TextTheme _brandTextTheme(TextTheme base) {
final branded = GoogleFonts.barlowCondensedTextTheme(base);
return base.copyWith(
displayLarge: branded.displayLarge,
displayMedium: branded.displayMedium,
displaySmall: branded.displaySmall,
headlineLarge: branded.headlineLarge,
headlineMedium: branded.headlineMedium,
headlineSmall: branded.headlineSmall,
titleLarge: branded.titleLarge,
);
}