feat(flutter): FabledSwordTheme ThemeExtension + Material 3 ThemeData

Widgets read tokens via Theme.of(ctx).extension<FabledSwordTheme>().
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).
This commit is contained in:
2026-05-02 16:56:38 -04:00
parent f97cf6eb38
commit 2729ab897f
4 changed files with 128 additions and 2 deletions
+5 -2
View File
@@ -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')),
),
);
+29
View File
@@ -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: <ThemeExtension<dynamic>>[fs],
);
}
@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'tokens.dart';
class FabledSwordTheme extends ThemeExtension<FabledSwordTheme> {
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<FabledSwordTheme>? other, double t) => this;
}
@@ -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<FabledSwordTheme>()!;
return const SizedBox.shrink();
},
),
),
);
expect(captured.accent, FabledSwordTokens.accent);
expect(captured.parchment, FabledSwordTokens.parchment);
expect(captured.body.fontFamily, FabledSwordTokens.fontBody);
});
}