Two reports:
1. "In Diagnose ist viel nicht übersetzt, wie pending, chain
etc." The Doctor summary strip (Modules / Approvals / Audit
/ Services tiles) and the modules panel still rendered
English wire labels: "pending", "chain", "declared",
"loaded", "empty", "attention", "No pending approvals",
"No host services declared", and the matching "{n} modules
· {m} capabilities" / "{n} approvals awaiting review"
strings.
Fix: 13 new ARB keys covering the summary tiles, modules
panel, and services panel; doctor.dart now reads them via
AppLocalizations. German operators no longer see English
labels on Diagnose.
2. "Im Light mode ist der kontrast zur überschrift falsch,
store ist weiß auf weiß." The AppBar title rendered
white-on-white in light mode.
Cause: `appBarTheme.titleTextStyle = textTheme.headlineSmall`
passed a TextStyle built fresh from GoogleFonts.inter(...)
with `color: null`. Material's "merge foregroundColor at
draw time" path didn't always populate it — depended on
build configuration. The colour fell through to whatever
the surrounding DefaultTextStyle had, which on the
light-mode AppBar was the surface colour.
Fix: bake the foreground colour straight into the title
style via `copyWith(color: scheme.onSurface)`. Also pin
`iconTheme: IconThemeData(color: scheme.onSurface)` so
actions-row icons get the same treatment defensively.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
244 lines
8.1 KiB
Dart
244 lines
8.1 KiB
Dart
// Theme assembly. ColorScheme + typography + component themes.
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
|
|
import 'tokens.dart';
|
|
|
|
class FaiTheme {
|
|
FaiTheme._();
|
|
|
|
/// Inter for UI, JetBrains Mono for technical strings (IDs,
|
|
/// paths, capability refs). Both via google_fonts so they
|
|
/// bundle on first run; for production we'd ship them as
|
|
/// app assets.
|
|
static TextTheme _textTheme(Brightness b) {
|
|
final base = b == Brightness.dark
|
|
? Typography.whiteCupertino
|
|
: Typography.blackCupertino;
|
|
return GoogleFonts.interTextTheme(base).copyWith(
|
|
displaySmall: GoogleFonts.inter(
|
|
fontSize: 28,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -0.4,
|
|
),
|
|
headlineSmall: GoogleFonts.inter(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -0.2,
|
|
),
|
|
titleMedium: GoogleFonts.inter(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: 0,
|
|
),
|
|
titleSmall: GoogleFonts.inter(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
bodyLarge: GoogleFonts.inter(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w400,
|
|
height: 1.45,
|
|
),
|
|
bodyMedium: GoogleFonts.inter(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w400,
|
|
height: 1.4,
|
|
),
|
|
bodySmall: GoogleFonts.inter(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w400,
|
|
height: 1.4,
|
|
),
|
|
labelMedium: GoogleFonts.inter(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
labelSmall: GoogleFonts.inter(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w500,
|
|
letterSpacing: 0.2,
|
|
),
|
|
);
|
|
}
|
|
|
|
/// JetBrains Mono for monospaced technical content. Used by
|
|
/// FaiMono helper.
|
|
static TextStyle mono({
|
|
double size = 12,
|
|
FontWeight weight = FontWeight.w400,
|
|
Color? color,
|
|
}) =>
|
|
GoogleFonts.jetBrainsMono(
|
|
fontSize: size,
|
|
fontWeight: weight,
|
|
color: color,
|
|
height: 1.4,
|
|
);
|
|
|
|
static ThemeData dark() {
|
|
final scheme = ColorScheme(
|
|
brightness: Brightness.dark,
|
|
primary: FaiColors.accent,
|
|
onPrimary: FaiColors.black,
|
|
primaryContainer: FaiColors.accentMuted,
|
|
onPrimaryContainer: FaiColors.textStrong,
|
|
secondary: FaiColors.accentDeep,
|
|
onSecondary: FaiColors.black,
|
|
secondaryContainer: FaiColors.surfaceHigh,
|
|
onSecondaryContainer: FaiColors.text,
|
|
tertiary: FaiColors.accent,
|
|
onTertiary: FaiColors.black,
|
|
tertiaryContainer: FaiColors.surfaceHigh,
|
|
onTertiaryContainer: FaiColors.text,
|
|
error: FaiColors.danger,
|
|
onError: FaiColors.textStrong,
|
|
errorContainer: const Color(0xFF3F1518),
|
|
onErrorContainer: const Color(0xFFFCA5A5),
|
|
surface: FaiColors.black,
|
|
onSurface: FaiColors.text,
|
|
onSurfaceVariant: FaiColors.muted,
|
|
outline: FaiColors.border,
|
|
outlineVariant: FaiColors.border,
|
|
surfaceContainerLowest: FaiColors.black,
|
|
surfaceContainerLow: FaiColors.surface,
|
|
surfaceContainer: FaiColors.surface,
|
|
surfaceContainerHigh: FaiColors.surfaceHigh,
|
|
surfaceContainerHighest: FaiColors.surfaceHigh,
|
|
);
|
|
return _build(scheme);
|
|
}
|
|
|
|
static ThemeData light() {
|
|
final scheme = ColorScheme(
|
|
brightness: Brightness.light,
|
|
primary: FaiColors.accentDeep,
|
|
onPrimary: FaiColors.lightSurface,
|
|
primaryContainer: const Color(0xFFE0F2FE),
|
|
onPrimaryContainer: FaiColors.accentMuted,
|
|
secondary: FaiColors.accentMuted,
|
|
onSecondary: FaiColors.lightSurface,
|
|
secondaryContainer: FaiColors.lightSurfaceHigh,
|
|
onSecondaryContainer: FaiColors.lightText,
|
|
tertiary: FaiColors.accentDeep,
|
|
onTertiary: FaiColors.lightSurface,
|
|
tertiaryContainer: const Color(0xFFE0F2FE),
|
|
onTertiaryContainer: FaiColors.accentMuted,
|
|
error: FaiColors.danger,
|
|
onError: FaiColors.lightSurface,
|
|
errorContainer: const Color(0xFFFEE2E2),
|
|
onErrorContainer: const Color(0xFF7F1D1D),
|
|
surface: FaiColors.lightCanvas,
|
|
onSurface: FaiColors.lightText,
|
|
onSurfaceVariant: FaiColors.lightMuted,
|
|
outline: FaiColors.lightBorder,
|
|
outlineVariant: FaiColors.lightBorder,
|
|
surfaceContainerLowest: FaiColors.lightCanvas,
|
|
surfaceContainerLow: FaiColors.lightSurface,
|
|
surfaceContainer: FaiColors.lightSurface,
|
|
surfaceContainerHigh: FaiColors.lightSurfaceHigh,
|
|
surfaceContainerHighest: FaiColors.lightSurfaceHigh,
|
|
);
|
|
return _build(scheme);
|
|
}
|
|
|
|
static ThemeData _build(ColorScheme scheme) {
|
|
final textTheme = _textTheme(scheme.brightness);
|
|
return ThemeData(
|
|
useMaterial3: true,
|
|
colorScheme: scheme,
|
|
scaffoldBackgroundColor: scheme.surface,
|
|
textTheme: textTheme,
|
|
primaryTextTheme: textTheme,
|
|
appBarTheme: AppBarTheme(
|
|
backgroundColor: scheme.surface,
|
|
foregroundColor: scheme.onSurface,
|
|
iconTheme: IconThemeData(color: scheme.onSurface),
|
|
elevation: 0,
|
|
scrolledUnderElevation: 0,
|
|
centerTitle: false,
|
|
// Bake the foreground colour straight into the title
|
|
// style. `headlineSmall` is built from GoogleFonts.inter
|
|
// with no color slot, and Material's "merge foreground
|
|
// colour at draw time" path leaves it null in some
|
|
// light-mode builds — the title rendered white-on-white.
|
|
titleTextStyle: textTheme.headlineSmall?.copyWith(
|
|
color: scheme.onSurface,
|
|
),
|
|
toolbarHeight: 64,
|
|
),
|
|
cardTheme: CardThemeData(
|
|
color: scheme.surfaceContainer,
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(FaiRadius.md),
|
|
side: BorderSide(color: scheme.outlineVariant),
|
|
),
|
|
margin: EdgeInsets.zero,
|
|
),
|
|
filledButtonTheme: FilledButtonThemeData(
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: scheme.primary,
|
|
foregroundColor: scheme.onPrimary,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(FaiRadius.sm),
|
|
),
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: FaiSpace.lg,
|
|
vertical: FaiSpace.md,
|
|
),
|
|
textStyle: textTheme.labelMedium,
|
|
animationDuration: FaiMotion.fast,
|
|
),
|
|
),
|
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: scheme.onSurface,
|
|
side: BorderSide(color: scheme.outline),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(FaiRadius.sm),
|
|
),
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: FaiSpace.lg,
|
|
vertical: FaiSpace.md,
|
|
),
|
|
textStyle: textTheme.labelMedium,
|
|
animationDuration: FaiMotion.fast,
|
|
),
|
|
),
|
|
iconButtonTheme: IconButtonThemeData(
|
|
style: IconButton.styleFrom(
|
|
foregroundColor: scheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
navigationRailTheme: NavigationRailThemeData(
|
|
backgroundColor: scheme.surfaceContainerLow,
|
|
indicatorColor: scheme.primaryContainer,
|
|
selectedIconTheme: IconThemeData(color: scheme.primary, size: 22),
|
|
unselectedIconTheme:
|
|
IconThemeData(color: scheme.onSurfaceVariant, size: 22),
|
|
selectedLabelTextStyle:
|
|
textTheme.labelMedium?.copyWith(color: scheme.primary),
|
|
unselectedLabelTextStyle: textTheme.labelMedium
|
|
?.copyWith(color: scheme.onSurfaceVariant),
|
|
useIndicator: true,
|
|
),
|
|
dividerColor: scheme.outlineVariant,
|
|
dividerTheme: DividerThemeData(
|
|
color: scheme.outlineVariant,
|
|
thickness: 1,
|
|
space: 1,
|
|
),
|
|
snackBarTheme: SnackBarThemeData(
|
|
backgroundColor: scheme.surfaceContainerHigh,
|
|
contentTextStyle:
|
|
textTheme.bodyMedium?.copyWith(color: scheme.onSurface),
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(FaiRadius.sm),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|