Add app source and tests

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
alvocool
2026-10-11 10:15:30 +03:00
parent 7e246255c2
commit 38881d1fe1
135 changed files with 21057 additions and 0 deletions

569
lib/Peach/utils/Colors.dart Normal file
View File

@@ -0,0 +1,569 @@
import 'package:flutter/material.dart';
// ─────────────────────────────────────────────────────────────────────────────
// Named palette. Every colour used anywhere in the app is named here — never
// write a hex literal at a call site. Brightness-aware surfaces still come from
// [JpColors] via `context.jp`; these constants are the fixed brand tones and
// the defaults used where no context is available.
// ─────────────────────────────────────────────────────────────────────────────
var colorWhite = const Color(0xFFFFFFFF);
var colorPrimary = const Color(0xFFE8451F); // vermilion — brand primary
var colorPrimaryDark = const Color(0xFF111823); // near-black ink
var colorPrimaryDark2 = const Color(0xFF0E1521);
var colorPrimaryLight = const Color(0xFFF3F4F7);
var colorPrimaryLight2 = const Color(0xFFF5F6F9);
var colorAccent = const Color(0xFF3B82F6); // blue — web/deploy differentiation
var colorPositive = const Color(0xFF17C79A);
var colorNegative = const Color(0xFFE5484D);
var colorWarmYellow = const Color(0xFFF5A623);
var colorViolet = const Color(0xFF8B7CFF);
var colorCyan = const Color(0xFF38BDF8);
var colorGrey = const Color(0xFF94A3B8);
var colorGrey2 = const Color(0xFF586372);
var colorGrey3 = const Color(0xFF9AA3B0);
var colorBorder = const Color(0xFFE5E8ED);
var colorConsole = const Color(0xFF070A10);
/// The terminal palette. Console surfaces (log output, unit-file previews,
/// command blocks) stay dark in **both** app themes — like a code block, they
/// don't invert — so these are fixed tones rather than [JpColors] entries.
/// The background itself is still `jp.consoleBg`, which the light theme lifts
/// slightly.
class ConsoleColors {
const ConsoleColors._();
/// Hairline frame and dividers inside a console panel.
static const frame = Color(0xFF232B36);
/// Slightly lifted frame, for controls sitting on the console.
static const frameRaised = Color(0xFF2E3947);
/// Primary console body copy — the monospace output itself.
static const body = Color(0xFFD3D8E0);
/// Header/label copy and enabled control glyphs.
static const header = Color(0xFFB9C1CE);
/// Secondary counters and captions.
static const muted = Color(0xFF6B7482);
/// Disabled controls and placeholder copy.
static const disabled = Color(0xFF5A6472);
// Log severity tones, tuned for legibility on [JpColors.consoleBg].
static const error = Color(0xFFFF8B8B);
static const warn = Color(0xFFFFD066);
static const success = Color(0xFF3DE0AE);
static const step = Color(0xFF8AD0FF);
static const info = Color(0xFF9AA4B2);
/// The pulsing "LIVE" indicator on a streaming console.
static const live = Color(0xFFFF6B6B);
}
/// Builds the 50→900 swatch Material needs from a single brand colour.
MaterialColor createMaterialColor(Color color) {
final strengths = <double>[.05];
final swatch = <int, Color>{};
final int r = (color.r * 255).round(),
g = (color.g * 255).round(),
b = (color.b * 255).round();
for (int i = 1; i < 10; i++) {
strengths.add(0.1 * i);
}
for (final strength in strengths) {
final double ds = 0.5 - strength;
swatch[(strength * 1000).round()] = Color.fromRGBO(
r + ((ds < 0 ? r : (255 - r)) * ds).round(),
g + ((ds < 0 ? g : (255 - g)) * ds).round(),
b + ((ds < 0 ? b : (255 - b)) * ds).round(),
1,
);
}
return MaterialColor(color.toARGB32(), swatch);
}
/// `#RRGGBB` (or `#AARRGGBB`) → [Color].
Color getColorFromHex(String hex) {
var value = hex.replaceFirst('#', '');
if (value.length == 6) value = 'FF$value';
return Color(int.parse(value, radix: 16));
}
/// [Color] → `#AARRGGBB`.
String colorToHex(Color color) =>
'#${color.toARGB32().toRadixString(16).padLeft(8, '0').toUpperCase()}';
/// Brightness-aware neutral tokens (surfaces, borders, text). Brand accents
/// live on [PeachTheme] and read well on both light and dark.
@immutable
class JpColors extends ThemeExtension<JpColors> {
final Color bg;
final Color surface;
final Color surface2;
final Color border;
final Color text;
final Color textDim;
final Color textFaint;
final Color consoleBg;
const JpColors({
required this.bg,
required this.surface,
required this.surface2,
required this.border,
required this.text,
required this.textDim,
required this.textFaint,
required this.consoleBg,
});
// Deep, cool-neutral ink. More separation between each step so panels read
// crisply and hairline borders stay visible — the Linear/Vercel register.
static const dark = JpColors(
bg: Color(0xFF0A0E14),
surface: Color(0xFF121821),
surface2: Color(0xFF1B222D),
border: Color(0xFF283040),
text: Color(0xFFF3F5F8),
textDim: Color(0xFFAEB7C4),
textFaint: Color(0xFF6B7585),
consoleBg: Color(0xFF070A10),
);
// Clean, soft, premium light — off-white canvas, pure-white cards, crisp
// hairlines. Avoids the flat grey that reads "template".
static const light = JpColors(
bg: Color(0xFFF3F4F7),
surface: Color(0xFFFFFFFF),
surface2: Color(0xFFF5F6F9),
border: Color(0xFFE5E8ED),
text: Color(0xFF0E1521),
textDim: Color(0xFF586372),
textFaint: Color(0xFF9AA3B0),
consoleBg: Color(0xFF0E1521),
);
@override
JpColors copyWith({
Color? bg,
Color? surface,
Color? surface2,
Color? border,
Color? text,
Color? textDim,
Color? textFaint,
Color? consoleBg,
}) => JpColors(
bg: bg ?? this.bg,
surface: surface ?? this.surface,
surface2: surface2 ?? this.surface2,
border: border ?? this.border,
text: text ?? this.text,
textDim: textDim ?? this.textDim,
textFaint: textFaint ?? this.textFaint,
consoleBg: consoleBg ?? this.consoleBg,
);
@override
JpColors lerp(ThemeExtension<JpColors>? other, double t) {
if (other is! JpColors) return this;
return JpColors(
bg: Color.lerp(bg, other.bg, t)!,
surface: Color.lerp(surface, other.surface, t)!,
surface2: Color.lerp(surface2, other.surface2, t)!,
border: Color.lerp(border, other.border, t)!,
text: Color.lerp(text, other.text, t)!,
textDim: Color.lerp(textDim, other.textDim, t)!,
textFaint: Color.lerp(textFaint, other.textFaint, t)!,
consoleBg: Color.lerp(consoleBg, other.consoleBg, t)!,
);
}
}
extension JpColorsContext on BuildContext {
JpColors get jp => Theme.of(this).extension<JpColors>()!;
}
/// PeachDeployments theme: modern rounded "cockpit", light + dark.
class PeachTheme {
// "expand" brand palette: vermilion red-orange primary, ink dark, warm gray.
static const seed = Color(0xFFE8451F); // vermilion red-orange
static const ink = Color(0xFF111823); // near-black
static const accent = Color(0xFF3B82F6); // blue (web differentiation)
static const danger = Color(0xFFE5484D);
static const warn = Color(0xFFF5A623);
static const slate = Color(0xFF94A3B8); // neutral chart tone
static const violet = Color(0xFF8B7CFF);
static const mint = Color(0xFF17C79A); // fresh positive tone
static const cyan = Color(0xFF38BDF8);
static const pink = Color(0xFFEC4899); // completes the categorical set
static const _display = 'Sora';
static const _body = 'Urbanist';
// Warm brand gradient for CTAs, rings and progress.
static const brandGradient = LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFFFF6A3D), Color(0xFFE23C16)],
);
static const peachGradient = LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFFFFE0D0), Color(0xFFFFC1A3)],
);
static ThemeData light() => _build(Brightness.light, JpColors.light);
static ThemeData dark() => _build(Brightness.dark, JpColors.dark);
/// A colored glow (for accent elements).
static List<BoxShadow> glow(
Color c, {
double blur = 26,
double spread = -2,
double opacity = 0.45,
}) => [
BoxShadow(
color: c.withValues(alpha: opacity),
blurRadius: blur,
spreadRadius: spread,
offset: Offset(0, blur * 0.35),
),
];
/// Soft, layered depth shadow — barely-there but present, the premium
/// register (not the heavy drop shadow of template UIs).
static List<BoxShadow> softShadow(Brightness b) => b == Brightness.dark
? const [
BoxShadow(
color: Color(0x4D000000),
blurRadius: 28,
spreadRadius: -8,
offset: Offset(0, 18),
),
BoxShadow(
color: Color(0x26000000),
blurRadius: 4,
offset: Offset(0, 1),
),
]
: const [
BoxShadow(
color: Color(0x141B2A44),
blurRadius: 24,
spreadRadius: -6,
offset: Offset(0, 12),
),
BoxShadow(
color: Color(0x0A1B2A44),
blurRadius: 2,
offset: Offset(0, 1),
),
];
/// A hair-lighter tint of [surface] for the top edge of a card, giving an
/// opaque "lit from above" bevel without any translucency.
static Color _topSheen(Color surface, Brightness b) => b == Brightness.dark
? Color.alphaBlend(Colors.white.withValues(alpha: 0.05), surface)
: surface;
/// Elevated SOLID panel decoration used by the custom cards. Opaque, with a
/// subtle top-to-bottom sheen and a crisp hairline — no translucency.
static BoxDecoration panel(
BuildContext context, {
Color? glowColor,
double radius = 20,
}) {
final jp = context.jp;
final b = Theme.of(context).brightness;
return BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [_topSheen(jp.surface, b), jp.surface],
),
borderRadius: BorderRadius.circular(radius),
border: Border.all(color: jp.border),
boxShadow: [
...softShadow(b),
if (glowColor != null) ...glow(glowColor, opacity: 0.16, blur: 30),
],
);
}
static ThemeData _build(Brightness b, JpColors jp) {
final base = ThemeData(brightness: b, useMaterial3: true);
final scheme = ColorScheme.fromSeed(seedColor: seed, brightness: b)
.copyWith(
surface: jp.surface,
secondary: accent,
primary: seed,
onPrimary: Colors.white,
error: danger,
);
final textTheme = base.textTheme
.apply(fontFamily: _body, bodyColor: jp.text, displayColor: jp.text)
.copyWith(
// Tight, confident display tracking — the hallmark of premium type.
displaySmall: base.textTheme.displaySmall?.copyWith(
fontFamily: _display,
fontWeight: FontWeight.w800,
letterSpacing: -0.8,
color: jp.text,
),
headlineMedium: base.textTheme.headlineMedium?.copyWith(
fontFamily: _display,
fontWeight: FontWeight.w800,
letterSpacing: -0.6,
color: jp.text,
),
headlineSmall: base.textTheme.headlineSmall?.copyWith(
fontFamily: _display,
fontWeight: FontWeight.w700,
letterSpacing: -0.4,
color: jp.text,
),
titleLarge: base.textTheme.titleLarge?.copyWith(
fontFamily: _display,
fontWeight: FontWeight.w700,
letterSpacing: -0.2,
color: jp.text,
),
titleMedium: base.textTheme.titleMedium?.copyWith(
fontFamily: _display,
fontWeight: FontWeight.w700,
color: jp.text,
),
bodyLarge: base.textTheme.bodyLarge?.copyWith(
height: 1.45,
color: jp.text,
),
bodyMedium: base.textTheme.bodyMedium?.copyWith(
height: 1.45,
color: jp.textDim,
),
labelLarge: base.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
),
);
return base.copyWith(
colorScheme: scheme,
extensions: [jp],
// Transparent so the glow backdrop shows through app-wide.
scaffoldBackgroundColor: Colors.transparent,
canvasColor: jp.bg,
textTheme: textTheme,
splashFactory: InkSparkle.splashFactory,
appBarTheme: AppBarTheme(
backgroundColor: jp.bg,
foregroundColor: jp.text,
surfaceTintColor: Colors.transparent,
elevation: 0,
scrolledUnderElevation: 0,
centerTitle: false,
titleTextStyle: TextStyle(
fontFamily: _display,
fontSize: 20,
fontWeight: FontWeight.w700,
letterSpacing: -0.3,
color: jp.text,
),
iconTheme: IconThemeData(color: jp.textDim),
),
iconTheme: IconThemeData(color: jp.textDim),
dividerTheme: DividerThemeData(color: jp.border, thickness: 1),
listTileTheme: ListTileThemeData(
iconColor: jp.textDim,
textColor: jp.text,
),
tabBarTheme: TabBarThemeData(
labelColor: seed,
unselectedLabelColor: jp.textFaint,
indicatorColor: seed,
indicatorSize: TabBarIndicatorSize.label,
dividerColor: jp.border,
overlayColor: WidgetStateProperty.all(seed.withValues(alpha: 0.06)),
),
popupMenuTheme: PopupMenuThemeData(
color: jp.surface,
surfaceTintColor: Colors.transparent,
elevation: 12,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
side: BorderSide(color: jp.border),
),
),
dialogTheme: DialogThemeData(
backgroundColor: jp.surface,
surfaceTintColor: Colors.transparent,
elevation: 24,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(color: jp.border),
),
),
snackBarTheme: SnackBarThemeData(
behavior: SnackBarBehavior.floating,
backgroundColor: b == Brightness.dark
? jp.surface2
: const Color(0xFF161D28),
contentTextStyle: const TextStyle(
color: Colors.white,
fontFamily: _body,
),
elevation: 12,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
side: BorderSide(
color: b == Brightness.dark ? jp.border : Colors.transparent,
),
),
),
cardTheme: CardThemeData(
color: jp.surface,
elevation: 0,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(color: jp.border),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: jp.surface2,
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 14,
),
hintStyle: TextStyle(color: jp.textFaint),
labelStyle: TextStyle(color: jp.textDim),
floatingLabelStyle: const TextStyle(
color: seed,
fontWeight: FontWeight.w700,
),
prefixIconColor: jp.textFaint,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(13),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(13),
borderSide: BorderSide(color: jp.border),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(13),
borderSide: const BorderSide(color: seed, width: 1.6),
),
),
filledButtonTheme: FilledButtonThemeData(
style:
FilledButton.styleFrom(
backgroundColor: seed,
foregroundColor: Colors.white,
disabledBackgroundColor: seed.withValues(alpha: 0.32),
disabledForegroundColor: Colors.white70,
elevation: 0,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(13),
),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
textStyle: const TextStyle(
fontFamily: _body,
fontWeight: FontWeight.w700,
fontSize: 14,
letterSpacing: 0.2,
),
).copyWith(
// Subtle brand shadow only when the button is actually enabled.
elevation: WidgetStateProperty.resolveWith(
(s) => s.contains(WidgetState.disabled) ? 0 : 6,
),
shadowColor: WidgetStateProperty.all(
seed.withValues(alpha: 0.45),
),
overlayColor: WidgetStateProperty.all(
Colors.white.withValues(alpha: 0.10),
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: jp.text,
side: BorderSide(color: jp.border),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(13),
),
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 15),
textStyle: const TextStyle(
fontFamily: _body,
fontWeight: FontWeight.w700,
fontSize: 14,
),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: seed,
textStyle: const TextStyle(
fontFamily: _body,
fontWeight: FontWeight.w700,
),
),
),
floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: seed,
foregroundColor: Colors.white,
elevation: 8,
highlightElevation: 10,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
chipTheme: base.chipTheme.copyWith(
backgroundColor: jp.surface2,
side: BorderSide(color: jp.border),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(9)),
labelStyle: TextStyle(color: jp.textDim, fontWeight: FontWeight.w600),
),
switchTheme: SwitchThemeData(
thumbColor: WidgetStateProperty.resolveWith(
(s) => s.contains(WidgetState.selected) ? seed : null,
),
trackColor: WidgetStateProperty.resolveWith(
(s) => s.contains(WidgetState.selected)
? seed.withValues(alpha: 0.4)
: null,
),
),
progressIndicatorTheme: ProgressIndicatorThemeData(
color: seed,
linearTrackColor: jp.surface2,
circularTrackColor: Colors.transparent,
),
tooltipTheme: TooltipThemeData(
decoration: BoxDecoration(
color: b == Brightness.dark ? jp.surface2 : const Color(0xFF161D28),
borderRadius: BorderRadius.circular(9),
border: Border.all(
color: b == Brightness.dark ? jp.border : Colors.transparent,
),
),
textStyle: const TextStyle(
color: Colors.white,
fontFamily: _body,
fontSize: 12,
),
),
);
}
}