tane/apps/app_seeds/lib/ui/theme.dart
vjrj 7ae6becd8f feat(i18n): full RTL/CJK support — fonts, Japanese locale, directional fixes
Bundle Noto Sans Arabic + Noto Sans JP (SIL OFL 1.1) as per-glyph fallbacks
for both the UI text theme and every generated PDF, so RTL (Arabic) and CJK
render on all platforms — including desktop, where the system font may lack
them — instead of tofu. A shared pdf_fonts.dart centralizes the PDF theme and
the three label/catalog/recovery services reuse it.

Add Japanese (ja) as the CJK reference locale — fittingly, the app's own name
is Japanese (種, tane, 'seed'). Core strings translated; the rest fall back to
English via slang. Wired into supportedLocales (automatic) and the settings
picker.

Fix three physical Alignment.centerLeft -> AlignmentDirectional.centerStart in
variety_detail_screen, and make month-abbreviation and avatar-initial
truncation grapheme-safe (.characters) so CJK text is never cut mid-character.

Tests: CJK/Arabic PDF glyph render, ja locale resolution + English fallback.
The chat-bubble mirroring flagged by the audit was verified correct (already
respects Directionality) and left unchanged.
2026-07-14 11:11:19 +02:00

116 lines
4.8 KiB
Dart

import 'package:flutter/material.dart';
/// Material 3 palette from the Seedees v2 redesign
/// (docs/mockups/seedees_material3.html): green app bars over a pale-green
/// canvas, a saturated green for primary actions (FAB, buttons, links, category
/// headers) and a soft green container for selected/tonal surfaces.
const seedGreen = Color(0xFF2F7D34); // primary (actions, links, headers)
const seedGreenDark = Color(0xFF22521E); // status bar / darker green
// App-bar green. Nudged down from the mockups' #3E8C38 so its white title
// meets WCAG AA (4.5:1) — see test/ui/theme_contrast_test.dart.
const seedAppBar = Color(0xFF3A8434);
const seedCanvas = Color(0xFFE7F1E0); // surface / scaffold
const seedPrimaryContainer = Color(0xFFD3E8C8); // tonal green container
const seedOnPrimaryContainer = Color(0xFF2F6D2A);
const seedField = Color(0xFFF3F7EE); // outlined field fill
const seedAvatar = Color(0xFFCFE7C3); // list avatar disc
const seedOnAvatar = Color(0xFF3B7A34);
const seedOutline = Color(0xFFC3CBB6);
const seedDivider = Color(0xFFE4E9DD);
const seedTitle = Color(0xFF1B2416); // heading text on canvas
const seedOnSurface = Color(0xFF1B2416);
const seedOnSurfaceVariant = Color(0xFF5C6B52); // scientific names, secondary
// Hints and secondary text/icons. Darkened from the mockups' #8A9880 (which
// fell below WCAG AA) to keep ≥4.5:1 on white, the canvas and the field fill —
// see test/ui/theme_contrast_test.dart.
const seedMuted = Color(0xFF617057);
const seedRating = Color(0xFFE8A200); // stars
const seedFavorite = Color(0xFFE53935); // saved/favorite heart (pops on green)
const seedWarning = Color(0xFFA84600); // overdue nudges (AA on white & canvas)
ThemeData buildTaneTheme() {
final scheme =
ColorScheme.fromSeed(
seedColor: seedGreen,
brightness: Brightness.light,
).copyWith(
primary: seedGreen,
onPrimary: Colors.white,
primaryContainer: seedPrimaryContainer,
onPrimaryContainer: seedOnPrimaryContainer,
secondaryContainer: seedPrimaryContainer,
onSecondaryContainer: seedOnPrimaryContainer,
tertiary: seedRating,
onTertiary: Colors.white,
surface: seedCanvas,
onSurface: seedOnSurface,
surfaceContainerHighest: seedField,
onSurfaceVariant: seedOnSurfaceVariant,
outline: seedMuted,
outlineVariant: seedOutline,
);
return ThemeData(
useMaterial3: true,
colorScheme: scheme,
// Per-glyph script fallbacks so Arabic (RTL) and CJK text render even where
// the platform's default font lacks them (notably desktop). The primary
// family stays the system default for Latin; these only fill the gaps. The
// ranges are disjoint, so order is immaterial. Fonts bundled in pubspec.
fontFamilyFallback: const ['Noto Sans JP', 'Noto Sans Arabic'],
scaffoldBackgroundColor: seedCanvas,
// Green app bars — aligned with the v2 mockups and the real app.
appBarTheme: const AppBarTheme(
backgroundColor: seedAppBar,
foregroundColor: Colors.white,
surfaceTintColor: Colors.transparent,
elevation: 0,
scrolledUnderElevation: 0,
centerTitle: false,
iconTheme: IconThemeData(color: Colors.white),
titleTextStyle: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
// Squircle FAB (20px radius) as in the mockups, in the saturated primary.
floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: seedGreen,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
),
drawerTheme: const DrawerThemeData(
backgroundColor: Colors.white,
surfaceTintColor: Colors.transparent,
),
bottomSheetTheme: const BottomSheetThemeData(
backgroundColor: seedCanvas,
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(26)),
),
),
// Pill "Save" actions across sheets and dialogs, as in the v3 mockups.
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: seedGreen,
foregroundColor: Colors.white,
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 13),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(foregroundColor: seedGreen),
),
// Green floating labels on the edit-seed / add-batch text fields. (Borders
// are left to each field so the inventory search keeps its borderless pill.)
inputDecorationTheme: const InputDecorationTheme(
floatingLabelStyle: TextStyle(color: seedGreen),
),
dividerTheme: const DividerThemeData(
space: 1,
thickness: 1,
color: seedDivider,
),
);
}