Merge pull request 'Release v26.04.28.1 — Design system foundation port' (#30) from dev into main
This commit was merged in pull request #30.
This commit is contained in:
+143
-29
@@ -2,35 +2,142 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
// ── Colour constants ──────────────────────────────────────────────────────────
|
// ── 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);
|
// Dark mode — Obsidian / Iron / Pewter / Parchment (FabledSword baseline)
|
||||||
const _darkSurface = Color(0xFF16161F);
|
const _darkBackground = Color(0xFF14171A); // Obsidian
|
||||||
const _darkSurfaceVar = Color(0xFF1A1A24);
|
const _darkSurface = Color(0xFF1E2228); // Iron
|
||||||
const _darkPrimary = Color(0xFF7C3AED);
|
const _darkSurfaceVar = Color(0xFF2C313A); // Slate
|
||||||
const _darkOnSurface = Color(0xFFE4E4F0);
|
const _darkPrimary = Color(0xFF5B4A8A); // dusty violet (Scribe accent)
|
||||||
const _darkOnSurfaceVar = Color(0xFF8888A8);
|
const _darkPrimaryDeep = Color(0xFF3F3560); // gradient stop
|
||||||
const _darkOutline = Color(0xFF2A2A3A);
|
const _darkOnSurface = Color(0xFFE8E4D8); // Parchment
|
||||||
|
const _darkOnSurfaceVar = Color(0xFFC2BFB4); // Vellum
|
||||||
|
const _darkOutline = Color(0xFF3F4651); // Pewter
|
||||||
|
|
||||||
const _lightBackground = Color(0xFFF5F5FB);
|
// Light mode — warm parchment (Scribe iteration)
|
||||||
const _lightSurface = Color(0xFFFFFFFF);
|
const _lightBackground = Color(0xFFF5F1E8); // warm cream page
|
||||||
const _lightSurfaceVar = Color(0xFFF0F0F8);
|
const _lightSurface = Color(0xFFFBF8F0); // near-white card
|
||||||
const _lightPrimary = Color(0xFF7C3AED);
|
const _lightSurfaceVar = Color(0xFFEFEAE0); // inset / hover surface
|
||||||
const _lightOnSurface = Color(0xFF1A1A1A);
|
const _lightPrimary = Color(0xFF5B4A8A); // dusty violet (same on both modes)
|
||||||
const _lightOnSurfaceVar = Color(0xFF666666);
|
const _lightOnSurface = Color(0xFF14171A); // deep ink (Obsidian inverted)
|
||||||
const _lightOutline = Color(0xFFDDDDE8);
|
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<ActionColors>()!.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<ActionColors> {
|
||||||
|
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<ActionColors>? 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 ─────────────────────────────────────────────────────────────────
|
// ── 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) {
|
TextTheme _buildTextTheme(TextTheme base) {
|
||||||
|
final inter = GoogleFonts.interTextTheme(base);
|
||||||
final fraunces = GoogleFonts.frauncesTextTheme(base);
|
final fraunces = GoogleFonts.frauncesTextTheme(base);
|
||||||
return base.copyWith(
|
return inter.copyWith(
|
||||||
// Headings / titles use Fraunces
|
displayLarge: fraunces.displayLarge,
|
||||||
|
displayMedium: fraunces.displayMedium,
|
||||||
|
displaySmall: fraunces.displaySmall,
|
||||||
headlineLarge: fraunces.headlineLarge,
|
headlineLarge: fraunces.headlineLarge,
|
||||||
headlineMedium: fraunces.headlineMedium,
|
headlineMedium: fraunces.headlineMedium,
|
||||||
headlineSmall: fraunces.headlineSmall,
|
headlineSmall: fraunces.headlineSmall,
|
||||||
titleLarge: fraunces.titleLarge,
|
titleLarge: fraunces.titleLarge,
|
||||||
titleMedium: fraunces.titleMedium,
|
// titleMedium / titleSmall / body* / label* stay Inter
|
||||||
// Body / labels remain system default
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +148,7 @@ ThemeData fabledDarkTheme() {
|
|||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
primary: _darkPrimary,
|
primary: _darkPrimary,
|
||||||
onPrimary: Colors.white,
|
onPrimary: Colors.white,
|
||||||
primaryContainer: const Color(0xFF5B21B6),
|
primaryContainer: _darkPrimaryDeep,
|
||||||
onPrimaryContainer: _darkOnSurface,
|
onPrimaryContainer: _darkOnSurface,
|
||||||
secondary: _darkPrimary,
|
secondary: _darkPrimary,
|
||||||
onSecondary: Colors.white,
|
onSecondary: Colors.white,
|
||||||
@@ -51,10 +158,10 @@ ThemeData fabledDarkTheme() {
|
|||||||
onTertiary: Colors.white,
|
onTertiary: Colors.white,
|
||||||
tertiaryContainer: _darkSurfaceVar,
|
tertiaryContainer: _darkSurfaceVar,
|
||||||
onTertiaryContainer: _darkOnSurface,
|
onTertiaryContainer: _darkOnSurface,
|
||||||
error: const Color(0xFFEF4444),
|
error: _semanticError,
|
||||||
onError: Colors.white,
|
onError: Colors.white,
|
||||||
errorContainer: const Color(0xFF7F1D1D),
|
errorContainer: _semanticErrorBg,
|
||||||
onErrorContainer: const Color(0xFFFEE2E2),
|
onErrorContainer: _semanticErrorFg,
|
||||||
surface: _darkSurface,
|
surface: _darkSurface,
|
||||||
onSurface: _darkOnSurface,
|
onSurface: _darkOnSurface,
|
||||||
surfaceContainerHighest: _darkSurfaceVar,
|
surfaceContainerHighest: _darkSurfaceVar,
|
||||||
@@ -73,6 +180,7 @@ ThemeData fabledDarkTheme() {
|
|||||||
colorScheme: cs,
|
colorScheme: cs,
|
||||||
scaffoldBackgroundColor: _darkBackground,
|
scaffoldBackgroundColor: _darkBackground,
|
||||||
textTheme: _buildTextTheme(ThemeData.dark().textTheme),
|
textTheme: _buildTextTheme(ThemeData.dark().textTheme),
|
||||||
|
extensions: const [ActionColors._kStandard],
|
||||||
cardTheme: CardThemeData(
|
cardTheme: CardThemeData(
|
||||||
color: _darkSurface,
|
color: _darkSurface,
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
@@ -118,7 +226,9 @@ ThemeData fabledLightTheme() {
|
|||||||
brightness: Brightness.light,
|
brightness: Brightness.light,
|
||||||
primary: _lightPrimary,
|
primary: _lightPrimary,
|
||||||
onPrimary: Colors.white,
|
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,
|
onPrimaryContainer: _lightOnSurface,
|
||||||
secondary: _lightPrimary,
|
secondary: _lightPrimary,
|
||||||
onSecondary: Colors.white,
|
onSecondary: Colors.white,
|
||||||
@@ -128,10 +238,10 @@ ThemeData fabledLightTheme() {
|
|||||||
onTertiary: Colors.white,
|
onTertiary: Colors.white,
|
||||||
tertiaryContainer: _lightSurfaceVar,
|
tertiaryContainer: _lightSurfaceVar,
|
||||||
onTertiaryContainer: _lightOnSurface,
|
onTertiaryContainer: _lightOnSurface,
|
||||||
error: const Color(0xFFDC2626),
|
error: _semanticError,
|
||||||
onError: Colors.white,
|
onError: Colors.white,
|
||||||
errorContainer: const Color(0xFFFEE2E2),
|
errorContainer: const Color(0xFFFEE2E2),
|
||||||
onErrorContainer: const Color(0xFF7F1D1D),
|
onErrorContainer: _semanticErrorBg,
|
||||||
surface: _lightSurface,
|
surface: _lightSurface,
|
||||||
onSurface: _lightOnSurface,
|
onSurface: _lightOnSurface,
|
||||||
surfaceContainerHighest: _lightSurfaceVar,
|
surfaceContainerHighest: _lightSurfaceVar,
|
||||||
@@ -150,6 +260,7 @@ ThemeData fabledLightTheme() {
|
|||||||
colorScheme: cs,
|
colorScheme: cs,
|
||||||
scaffoldBackgroundColor: _lightBackground,
|
scaffoldBackgroundColor: _lightBackground,
|
||||||
textTheme: _buildTextTheme(ThemeData.light().textTheme),
|
textTheme: _buildTextTheme(ThemeData.light().textTheme),
|
||||||
|
extensions: const [ActionColors._kStandard],
|
||||||
cardTheme: CardThemeData(
|
cardTheme: CardThemeData(
|
||||||
color: _lightSurface,
|
color: _lightSurface,
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
@@ -191,7 +302,10 @@ ThemeData fabledLightTheme() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── GradientButton ─────────────────────────────────────────────────────────────
|
// ── 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 {
|
class GradientButton extends StatelessWidget {
|
||||||
final VoidCallback? onPressed;
|
final VoidCallback? onPressed;
|
||||||
@@ -218,15 +332,15 @@ class GradientButton extends StatelessWidget {
|
|||||||
: const LinearGradient(
|
: const LinearGradient(
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
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),
|
borderRadius: BorderRadius.circular(12),
|
||||||
boxShadow: disabled
|
boxShadow: disabled
|
||||||
? null
|
? null
|
||||||
: [
|
: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: const Color(0xFF7C3AED).withValues(alpha: 0.45),
|
color: _darkPrimary.withValues(alpha: 0.45),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 3),
|
offset: const Offset(0, 3),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user