diff --git a/lib/core/theme.dart b/lib/core/theme.dart index df516fa..04a7c21 100644 --- a/lib/core/theme.dart +++ b/lib/core/theme.dart @@ -2,35 +2,142 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; // ── Colour constants ────────────────────────────────────────────────────────── +// +// Mirrors the web frontend's design system (`docs/design-system.md` in +// fabledscribe). Foundation pass shipped on web 2026-04-27 in `7a9a8b7`; +// this is the equivalent palette swap for Flutter. Per-screen "surface +// phase" reclassification (button Hybrid rule, border audit, etc.) is +// deferred — most widgets read `colorScheme.primary` so the palette flip +// alone covers a large surface. -const _darkBackground = Color(0xFF0F0F14); -const _darkSurface = Color(0xFF16161F); -const _darkSurfaceVar = Color(0xFF1A1A24); -const _darkPrimary = Color(0xFF7C3AED); -const _darkOnSurface = Color(0xFFE4E4F0); -const _darkOnSurfaceVar = Color(0xFF8888A8); -const _darkOutline = Color(0xFF2A2A3A); +// Dark mode — Obsidian / Iron / Pewter / Parchment (FabledSword baseline) +const _darkBackground = Color(0xFF14171A); // Obsidian +const _darkSurface = Color(0xFF1E2228); // Iron +const _darkSurfaceVar = Color(0xFF2C313A); // Slate +const _darkPrimary = Color(0xFF5B4A8A); // dusty violet (Scribe accent) +const _darkPrimaryDeep = Color(0xFF3F3560); // gradient stop +const _darkOnSurface = Color(0xFFE8E4D8); // Parchment +const _darkOnSurfaceVar = Color(0xFFC2BFB4); // Vellum +const _darkOutline = Color(0xFF3F4651); // Pewter -const _lightBackground = Color(0xFFF5F5FB); -const _lightSurface = Color(0xFFFFFFFF); -const _lightSurfaceVar = Color(0xFFF0F0F8); -const _lightPrimary = Color(0xFF7C3AED); -const _lightOnSurface = Color(0xFF1A1A1A); -const _lightOnSurfaceVar = Color(0xFF666666); -const _lightOutline = Color(0xFFDDDDE8); +// Light mode — warm parchment (Scribe iteration) +const _lightBackground = Color(0xFFF5F1E8); // warm cream page +const _lightSurface = Color(0xFFFBF8F0); // near-white card +const _lightSurfaceVar = Color(0xFFEFEAE0); // inset / hover surface +const _lightPrimary = Color(0xFF5B4A8A); // dusty violet (same on both modes) +const _lightOnSurface = Color(0xFF14171A); // deep ink (Obsidian inverted) +const _lightOnSurfaceVar = Color(0xFF5A5852); // warm mid grey +const _lightOutline = Color(0xFFD9D6CE); // warm light pewter + +// Semantic — identical across themes +const _semanticError = Color(0xFFC04A1F); // terracotta — validation/error +const _semanticErrorBg = Color(0xFF7E2A1F); // Oxblood-hover for dark error container +const _semanticErrorFg = Color(0xFFFEE2E2); // light text on dark error container + +// Action tokens — Hybrid rule per the doc. These don't fit ColorScheme +// natively (Material's primary/secondary/tertiary slots all carry the +// brand accent), so they live on a ThemeExtension exposed below. +const _actionPrimary = Color(0xFF4A5D3F); // Moss +const _actionPrimaryHover = Color(0xFF5A6F4D); +const _actionSecondary = Color(0xFF8B7355); // Bronze +const _actionSecondaryHover = Color(0xFFA0876A); +const _actionDestructive = Color(0xFF6B2118); // Oxblood +const _actionDestructiveHover = Color(0xFF7E2A1F); +const _actionGhostBorder = Color(0xFF3F4651); // Pewter (same as outline) + +// ── Action ThemeExtension ───────────────────────────────────────────────────── +// Read with: Theme.of(context).extension()!.primary +// +// Flutter's ColorScheme has primary/secondary/tertiary all conceptually +// "branded", whereas the doc's Hybrid rule reserves accent for brand +// moments (Send, empty-state CTAs) and routes action buttons through a +// separate Moss/Bronze/Oxblood/Pewter palette. This extension carries +// those tokens without polluting ColorScheme. + +@immutable +class ActionColors extends ThemeExtension { + final Color primary; // Moss — Save / Confirm + final Color primaryHover; + final Color secondary; // Bronze — Cancel / alternate paths + final Color secondaryHover; + final Color destructive; // Oxblood — Delete / irreversible + final Color destructiveHover; + final Color ghostBorder; // Pewter — tertiary / "later" / "skip" + + const ActionColors({ + required this.primary, + required this.primaryHover, + required this.secondary, + required this.secondaryHover, + required this.destructive, + required this.destructiveHover, + required this.ghostBorder, + }); + + static const _kStandard = ActionColors( + primary: _actionPrimary, + primaryHover: _actionPrimaryHover, + secondary: _actionSecondary, + secondaryHover: _actionSecondaryHover, + destructive: _actionDestructive, + destructiveHover: _actionDestructiveHover, + ghostBorder: _actionGhostBorder, + ); + + @override + ActionColors copyWith({ + Color? primary, + Color? primaryHover, + Color? secondary, + Color? secondaryHover, + Color? destructive, + Color? destructiveHover, + Color? ghostBorder, + }) { + return ActionColors( + primary: primary ?? this.primary, + primaryHover: primaryHover ?? this.primaryHover, + secondary: secondary ?? this.secondary, + secondaryHover: secondaryHover ?? this.secondaryHover, + destructive: destructive ?? this.destructive, + destructiveHover: destructiveHover ?? this.destructiveHover, + ghostBorder: ghostBorder ?? this.ghostBorder, + ); + } + + @override + ActionColors lerp(ThemeExtension? other, double t) { + if (other is! ActionColors) return this; + return ActionColors( + primary: Color.lerp(primary, other.primary, t)!, + primaryHover: Color.lerp(primaryHover, other.primaryHover, t)!, + secondary: Color.lerp(secondary, other.secondary, t)!, + secondaryHover: Color.lerp(secondaryHover, other.secondaryHover, t)!, + destructive: Color.lerp(destructive, other.destructive, t)!, + destructiveHover: Color.lerp(destructiveHover, other.destructiveHover, t)!, + ghostBorder: Color.lerp(ghostBorder, other.ghostBorder, t)!, + ); + } +} // ── Typography ───────────────────────────────────────────────────────────────── +// Inter for body / labels / titleMedium-and-below. +// Fraunces for display / headline / titleLarge — only at ≥18px per the doc. +// JetBrains Mono available via GoogleFonts.jetBrainsMono() at call sites for +// code blocks (Flutter's TextTheme has no dedicated mono slot). TextTheme _buildTextTheme(TextTheme base) { + final inter = GoogleFonts.interTextTheme(base); final fraunces = GoogleFonts.frauncesTextTheme(base); - return base.copyWith( - // Headings / titles use Fraunces + return inter.copyWith( + displayLarge: fraunces.displayLarge, + displayMedium: fraunces.displayMedium, + displaySmall: fraunces.displaySmall, headlineLarge: fraunces.headlineLarge, headlineMedium: fraunces.headlineMedium, headlineSmall: fraunces.headlineSmall, titleLarge: fraunces.titleLarge, - titleMedium: fraunces.titleMedium, - // Body / labels remain system default + // titleMedium / titleSmall / body* / label* stay Inter ); } @@ -41,7 +148,7 @@ ThemeData fabledDarkTheme() { brightness: Brightness.dark, primary: _darkPrimary, onPrimary: Colors.white, - primaryContainer: const Color(0xFF5B21B6), + primaryContainer: _darkPrimaryDeep, onPrimaryContainer: _darkOnSurface, secondary: _darkPrimary, onSecondary: Colors.white, @@ -51,10 +158,10 @@ ThemeData fabledDarkTheme() { onTertiary: Colors.white, tertiaryContainer: _darkSurfaceVar, onTertiaryContainer: _darkOnSurface, - error: const Color(0xFFEF4444), + error: _semanticError, onError: Colors.white, - errorContainer: const Color(0xFF7F1D1D), - onErrorContainer: const Color(0xFFFEE2E2), + errorContainer: _semanticErrorBg, + onErrorContainer: _semanticErrorFg, surface: _darkSurface, onSurface: _darkOnSurface, surfaceContainerHighest: _darkSurfaceVar, @@ -73,6 +180,7 @@ ThemeData fabledDarkTheme() { colorScheme: cs, scaffoldBackgroundColor: _darkBackground, textTheme: _buildTextTheme(ThemeData.dark().textTheme), + extensions: const [ActionColors._kStandard], cardTheme: CardThemeData( color: _darkSurface, elevation: 2, @@ -118,7 +226,9 @@ ThemeData fabledLightTheme() { brightness: Brightness.light, primary: _lightPrimary, onPrimary: Colors.white, - primaryContainer: const Color(0xFFEDE5FF), + // Warm parchment-tinted primary container, replacing the prior cool + // indigo `#EDE5FF`. Used for chip/badge backgrounds at low alpha. + primaryContainer: const Color(0xFFEDE9F4), onPrimaryContainer: _lightOnSurface, secondary: _lightPrimary, onSecondary: Colors.white, @@ -128,10 +238,10 @@ ThemeData fabledLightTheme() { onTertiary: Colors.white, tertiaryContainer: _lightSurfaceVar, onTertiaryContainer: _lightOnSurface, - error: const Color(0xFFDC2626), + error: _semanticError, onError: Colors.white, errorContainer: const Color(0xFFFEE2E2), - onErrorContainer: const Color(0xFF7F1D1D), + onErrorContainer: _semanticErrorBg, surface: _lightSurface, onSurface: _lightOnSurface, surfaceContainerHighest: _lightSurfaceVar, @@ -150,6 +260,7 @@ ThemeData fabledLightTheme() { colorScheme: cs, scaffoldBackgroundColor: _lightBackground, textTheme: _buildTextTheme(ThemeData.light().textTheme), + extensions: const [ActionColors._kStandard], cardTheme: CardThemeData( color: _lightSurface, elevation: 1, @@ -191,7 +302,10 @@ ThemeData fabledLightTheme() { } // ── GradientButton ───────────────────────────────────────────────────────────── -// Use wherever the web app uses the indigo gradient button (send, primary actions). +// Brand-moment CTA equivalent of the web's `--gradient-cta` — chat send, +// journal send, primary "Scribe-feature" actions. Reserve for those moments +// per the doc's Hybrid rule; use FilledButton with ActionColors.primary +// (Moss) for everything else. class GradientButton extends StatelessWidget { final VoidCallback? onPressed; @@ -218,15 +332,15 @@ class GradientButton extends StatelessWidget { : const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [Color(0xFF7C3AED), Color(0xFF5B21B6)], + colors: [_darkPrimary, _darkPrimaryDeep], ), - color: disabled ? const Color(0xFF7C3AED) : null, + color: disabled ? _darkPrimary : null, borderRadius: BorderRadius.circular(12), boxShadow: disabled ? null : [ BoxShadow( - color: const Color(0xFF7C3AED).withValues(alpha: 0.45), + color: _darkPrimary.withValues(alpha: 0.45), blurRadius: 8, offset: const Offset(0, 3), ),