Files
sure/mobile/lib/theme/sure_theme.dart
ghost e38632632c feat(mobile): standardize money typography and semantic amount color (#2331)
* feat(mobile): standardize money typography and semantic amount color

Add a brightness-aware SureColors theme extension and a MoneyText/SureMoney
primitive (semantic success/destructive/subdued tokens + tabular figures for
column-aligned digits), then migrate the transaction lists and balance cards
off raw Colors.green/red/grey.

Step 2 of the mobile design-system sequence (#2235), after #2237's theme
foundation. Primitive-first: screens consume shared tokens/typography.

* fix: review feedback — brightness-aware token fallback, de-flake Setting tests, Pipelock localhost FP

- SureColors.of falls back to the palette matching the active brightness (not
  always light) when the extension is missing, so dark surfaces stay correct.
- Clear the rails-settings-cached cache before each test; its in-memory cache
  survives the per-test transaction rollback, leaking Setting.* across tests and
  flaking Settings::HostingsControllerTest (stale empty string vs nil).
  Full unit suite: 4952 runs, 0 failures.
- Suppress the localhost test-DB DATABASE_URL false positive with line-level
  `# pipelock:ignore` in ci.yml + llm-evals.yml instead of excluding whole files,
  so those workflows stay scanned for real secrets.
2026-06-14 21:37:30 +02:00

94 lines
3.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,
secondary: tokens.info,
onSecondary: tokens.textInverse,
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),
),
),
),
);
return base.copyWith(
textTheme: base.textTheme.apply(
bodyColor: tokens.textPrimary,
displayColor: tokens.textPrimary,
),
primaryTextTheme: base.primaryTextTheme.apply(
bodyColor: tokens.textPrimary,
displayColor: tokens.textPrimary,
),
);
}
}