Files
sure/mobile/lib/theme/sure_theme.dart
ghost 94d5ad2151 fix(mobile): neutral Sure tokens for FAB/badge/avatar surfaces + themed logo (#2366)
On screens not yet redesigned with SureColors (the Chats list, etc.), Material's
primary/secondary *container* roles were unset, so they fell back to defaults
that read as blue on the FAB, unread-badge, and avatar surfaces.

- Pin `primaryContainer`/`secondaryContainer` to a neutral Sure surface
  (`surfaceInset` + `textPrimary`), and add a `FloatingActionButtonThemeData` so
  FABs use Sure's neutral primary action color (`buttonPrimary`). `primary` /
  `secondary` stay `link` / `info`, so existing link/accent callers are unchanged.
- Add a `SureLogo` widget that renders `logomark.svg` with the wordmark's
  `currentColor` strokes tinted to the theme's secondary text color, and route
  every logomark consumer (nav bar, login) through it — so the mark stays legible
  in dark mode (the hardcoded grey was too dim) and no caller renders the strokes
  as the flutter_svg default (black). The green brand mark keeps its fill.

Refs #2235.
2026-06-16 10:03:28 +02:00

111 lines
4.0 KiB
Dart

import 'package:flutter/material.dart';
import 'sure_colors.dart';
import 'sure_tokens.dart';
class SureTheme {
const SureTheme._();
static ThemeData get light => _build(SureTokens.light, Brightness.light);
static ThemeData get dark => _build(SureTokens.dark, Brightness.dark);
static ThemeData _build(SureTokenPalette tokens, Brightness brightness) {
final colorScheme = ColorScheme(
brightness: brightness,
primary: tokens.link,
onPrimary: tokens.textInverse,
// The container roles were unset, so on screens not yet redesigned with
// SureColors (e.g. Chats) Material defaulted them to a blue that read as
// off-brand on FAB/badge/avatar surfaces. Pin them to a neutral Sure
// surface with primary text on top. `primary`/`secondary` stay link/info so
// existing link/accent callers are unchanged; the prominent neutral primary
// action color is applied to FABs via floatingActionButtonTheme below.
primaryContainer: tokens.surfaceInset,
onPrimaryContainer: tokens.textPrimary,
secondary: tokens.info,
onSecondary: tokens.textInverse,
secondaryContainer: tokens.surfaceInset,
onSecondaryContainer: tokens.textPrimary,
error: tokens.destructive,
onError: tokens.textInverse,
errorContainer: tokens.destructiveSubtle,
onErrorContainer: tokens.textPrimary,
surface: tokens.surface,
onSurface: tokens.textPrimary,
surfaceContainerHighest: tokens.containerInset,
outline: tokens.borderSecondary,
outlineVariant: tokens.borderSubdued,
);
final inputBorder = OutlineInputBorder(
borderRadius: BorderRadius.circular(SureTokens.radiusLg),
borderSide: BorderSide(color: tokens.borderSecondary),
);
final base = ThemeData(
fontFamily: SureTokens.fontSans,
fontFamilyFallback: SureTokens.fontFallback,
colorScheme: colorScheme,
scaffoldBackgroundColor: tokens.surface,
useMaterial3: true,
extensions: <ThemeExtension<dynamic>>[SureColors(tokens)],
appBarTheme: AppBarTheme(
centerTitle: true,
elevation: 0,
backgroundColor: tokens.surface,
foregroundColor: tokens.textPrimary,
surfaceTintColor: Colors.transparent,
),
cardTheme: CardThemeData(
color: tokens.container,
surfaceTintColor: Colors.transparent,
shadowColor: tokens.shadow,
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SureTokens.radiusLg),
),
),
inputDecorationTheme: InputDecorationTheme(
border: inputBorder,
enabledBorder: inputBorder,
focusedBorder: inputBorder.copyWith(
borderSide: BorderSide(color: tokens.borderPrimary),
),
filled: true,
fillColor: tokens.container,
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 50),
backgroundColor: tokens.buttonPrimary,
foregroundColor: tokens.textInverse,
disabledBackgroundColor: tokens.containerInset,
disabledForegroundColor: tokens.textSubdued,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SureTokens.radiusLg),
),
),
),
// The FAB is Sure's primary action surface — use the neutral brand primary
// (near-black in light, white in dark) so it reads as the prominent action
// instead of the subtle primaryContainer the Material 3 default would use.
floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: tokens.buttonPrimary,
foregroundColor: tokens.textInverse,
),
);
return base.copyWith(
textTheme: base.textTheme.apply(
bodyColor: tokens.textPrimary,
displayColor: tokens.textPrimary,
),
primaryTextTheme: base.primaryTextTheme.apply(
bodyColor: tokens.textPrimary,
displayColor: tokens.textPrimary,
),
);
}
}