From 2729ab897f3b770ecdb26546dc7b5c72d3e40b2c Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 2 May 2026 16:56:38 -0400 Subject: [PATCH] feat(flutter): FabledSwordTheme ThemeExtension + Material 3 ThemeData Widgets read tokens via Theme.of(ctx).extension(). Material 3 ColorScheme bound to FabledSword tokens. Body text uses GoogleFonts.interTextTheme() so the fonts work without bundling .ttf files (offline-from-cold-launch is a separate question once #357 lands). --- flutter_client/lib/app.dart | 7 +- flutter_client/lib/theme/theme_data.dart | 29 ++++++++ flutter_client/lib/theme/theme_extension.dart | 68 +++++++++++++++++++ .../test/theme/theme_extension_test.dart | 26 +++++++ 4 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 flutter_client/lib/theme/theme_data.dart create mode 100644 flutter_client/lib/theme/theme_extension.dart create mode 100644 flutter_client/test/theme/theme_extension_test.dart diff --git a/flutter_client/lib/app.dart b/flutter_client/lib/app.dart index b8c34d52..4a870c5e 100644 --- a/flutter_client/lib/app.dart +++ b/flutter_client/lib/app.dart @@ -1,13 +1,16 @@ import 'package:flutter/material.dart'; +import 'theme/theme_data.dart'; + class MinstrelApp extends StatelessWidget { const MinstrelApp({super.key}); @override Widget build(BuildContext context) { - return const MaterialApp( + return MaterialApp( title: 'Minstrel', - home: Scaffold( + theme: buildThemeData(), + home: const Scaffold( body: Center(child: Text('Minstrel — scaffold')), ), ); diff --git a/flutter_client/lib/theme/theme_data.dart b/flutter_client/lib/theme/theme_data.dart new file mode 100644 index 00000000..3b13b8c6 --- /dev/null +++ b/flutter_client/lib/theme/theme_data.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import 'theme_extension.dart'; +import 'tokens.dart'; + +ThemeData buildThemeData() { + final fs = FabledSwordTheme.fromTokens(); + final colorScheme = ColorScheme.dark( + surface: fs.iron, + onSurface: fs.parchment, + primary: fs.accent, + onPrimary: fs.parchment, + secondary: fs.moss, + error: fs.error, + ); + + return ThemeData( + useMaterial3: true, + colorScheme: colorScheme, + scaffoldBackgroundColor: fs.obsidian, + textTheme: GoogleFonts.interTextTheme().apply( + bodyColor: fs.parchment, + displayColor: fs.parchment, + ), + fontFamily: FabledSwordTokens.fontBody, + extensions: >[fs], + ); +} diff --git a/flutter_client/lib/theme/theme_extension.dart b/flutter_client/lib/theme/theme_extension.dart new file mode 100644 index 00000000..f325b664 --- /dev/null +++ b/flutter_client/lib/theme/theme_extension.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; + +import 'tokens.dart'; + +class FabledSwordTheme extends ThemeExtension { + const FabledSwordTheme({ + required this.accent, + required this.obsidian, + required this.iron, + required this.slate, + required this.pewter, + required this.parchment, + required this.vellum, + required this.ash, + required this.moss, + required this.bronze, + required this.oxblood, + required this.warning, + required this.error, + required this.info, + required this.display, + required this.body, + required this.mono, + }); + + final Color accent; + final Color obsidian, iron, slate, pewter; + final Color parchment, vellum, ash; + final Color moss, bronze, oxblood; + final Color warning, error, info; + final TextStyle display, body, mono; + + static FabledSwordTheme fromTokens() => FabledSwordTheme( + accent: FabledSwordTokens.accent, + obsidian: FabledSwordTokens.obsidian, + iron: FabledSwordTokens.iron, + slate: FabledSwordTokens.slate, + pewter: FabledSwordTokens.pewter, + parchment: FabledSwordTokens.parchment, + vellum: FabledSwordTokens.vellum, + ash: FabledSwordTokens.ash, + moss: FabledSwordTokens.moss, + bronze: FabledSwordTokens.bronze, + oxblood: FabledSwordTokens.oxblood, + warning: FabledSwordTokens.warning, + error: FabledSwordTokens.error, + info: FabledSwordTokens.info, + display: const TextStyle(fontFamily: FabledSwordTokens.fontDisplay), + body: const TextStyle(fontFamily: FabledSwordTokens.fontBody), + mono: const TextStyle(fontFamily: FabledSwordTokens.fontMono), + ); + + @override + FabledSwordTheme copyWith({ + Color? accent, + }) => + FabledSwordTheme( + accent: accent ?? this.accent, + obsidian: obsidian, iron: iron, slate: slate, pewter: pewter, + parchment: parchment, vellum: vellum, ash: ash, + moss: moss, bronze: bronze, oxblood: oxblood, + warning: warning, error: error, info: info, + display: display, body: body, mono: mono, + ); + + @override + FabledSwordTheme lerp(ThemeExtension? other, double t) => this; +} diff --git a/flutter_client/test/theme/theme_extension_test.dart b/flutter_client/test/theme/theme_extension_test.dart new file mode 100644 index 00000000..03637c25 --- /dev/null +++ b/flutter_client/test/theme/theme_extension_test.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:minstrel/theme/theme_data.dart'; +import 'package:minstrel/theme/theme_extension.dart'; +import 'package:minstrel/theme/tokens.dart'; + +void main() { + testWidgets('Theme exposes FabledSwordTheme with expected accent', (tester) async { + late FabledSwordTheme captured; + await tester.pumpWidget( + MaterialApp( + theme: buildThemeData(), + home: Builder( + builder: (ctx) { + captured = Theme.of(ctx).extension()!; + return const SizedBox.shrink(); + }, + ), + ), + ); + expect(captured.accent, FabledSwordTokens.accent); + expect(captured.parchment, FabledSwordTokens.parchment); + expect(captured.body.fontFamily, FabledSwordTokens.fontBody); + }); +}