sojorn/sojorn_app/lib/theme/app_theme.dart
Patrick Britton 3c4680bdd7 Initial commit: Complete threaded conversation system with inline replies
**Major Features Added:**
- **Inline Reply System**: Replace compose screen with inline reply boxes
- **Thread Navigation**: Parent/child navigation with jump functionality
- **Chain Flow UI**: Reply counts, expand/collapse animations, visual hierarchy
- **Enhanced Animations**: Smooth transitions, hover effects, micro-interactions

 **Frontend Changes:**
- **ThreadedCommentWidget**: Complete rewrite with animations and navigation
- **ThreadNode Model**: Added parent references and descendant counting
- **ThreadedConversationScreen**: Integrated navigation handlers
- **PostDetailScreen**: Replaced with threaded conversation view
- **ComposeScreen**: Added reply indicators and context
- **PostActions**: Fixed visibility checks for chain buttons

 **Backend Changes:**
- **API Route**: Added /posts/:id/thread endpoint
- **Post Repository**: Include allow_chain and visibility fields in feed
- **Thread Handler**: Support for fetching post chains

 **UI/UX Improvements:**
- **Reply Context**: Clear indication when replying to specific posts
- **Character Counting**: 500 character limit with live counter
- **Visual Hierarchy**: Depth-based indentation and styling
- **Smooth Animations**: SizeTransition, FadeTransition, hover states
- **Chain Navigation**: Parent/child buttons with visual feedback

 **Technical Enhancements:**
- **Animation Controllers**: Proper lifecycle management
- **State Management**: Clean separation of concerns
- **Navigation Callbacks**: Reusable navigation system
- **Error Handling**: Graceful fallbacks and user feedback

This creates a Reddit-style threaded conversation experience with smooth
animations, inline replies, and intuitive navigation between posts in a chain.
2026-01-30 07:40:19 -06:00

316 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
enum AppThemeType {
basic,
pop,
}
class AppTheme {
// ============================================================================
// BASIC THEME PALETTE (Current/Original Theme)
// ============================================================================
// ANCHORS (The 60%)
static const Color basicNavyBlue = Color(0xFF000383);
static const Color basicNavyText = Color(0xFF000383);
// FLOW & STRUCTURE (The 30%)
static const Color basicEgyptianBlue = Color(0xFF0E38AE);
static const Color basicBrightNavy = Color(0xFF1974D1);
// ACCENTS (The 10%)
static const Color basicRoyalPurple = Color(0xFF7751A8);
static const Color basicKsuPurple = Color(0xFF512889);
// NEUTRALS & BACKGROUNDS
static const Color basicQueenPink = Color(0xFFE5C0DD);
static const Color basicQueenPinkLight = Color(0xFFF9F2F7);
static const Color basicWhite = Color(0xFFFFFFFF);
// ============================================================================
// POP THEME PALETTE - "Awakening" - High Contrast & Energy
// ============================================================================
// 60% - The Anchors (Deep & Grounded)
static const Color popNavyBlue = Color(0xFF000383);
static const Color popNavyText = Color(0xFF0D1050);
// 30% - Structure & Flow (Bright & Defined)
static const Color popEgyptianBlue = Color(0xFF0E38AE);
static const Color popBrightNavy = Color(0xFF1974D1);
// 10% - The Pop (Vibrant Accents)
static const Color popRoyalPurple = Color(0xFF7751A8);
static const Color popKsuPurple = Color(0xFF512889);
// Backgrounds - The "Clean" Space
static const Color popScaffoldBg = Color(0xFFF9F6F9);
static const Color popCardSurface = Colors.white;
// Interaction
static const Color popHighlight = Color(0xFFE5C0DD);
// ============================================================================
// CURRENT THEME COLORS (Dynamic based on selected theme)
// ============================================================================
static AppThemeType _currentThemeType = AppThemeType.basic;
static void setThemeType(AppThemeType type) {
_currentThemeType = type;
}
static bool get isPop => _currentThemeType == AppThemeType.pop;
// Dynamic color getters
static Color get navyBlue => isPop ? popNavyBlue : basicNavyBlue;
static Color get navyText => isPop ? popNavyText : basicNavyText;
static Color get egyptianBlue => isPop ? popEgyptianBlue : basicEgyptianBlue;
static Color get brightNavy => isPop ? popBrightNavy : basicBrightNavy;
static Color get royalPurple => isPop ? popRoyalPurple : basicRoyalPurple;
static Color get ksuPurple => isPop ? popKsuPurple : basicKsuPurple;
static Color get queenPink => isPop ? popHighlight : basicQueenPink;
static Color get queenPinkLight => scaffoldBg; // Alias for backward compatibility
static Color get scaffoldBg => isPop ? popScaffoldBg : basicQueenPinkLight;
static Color get cardSurface => isPop ? popCardSurface : basicWhite;
static const Color white = basicWhite;
// SEMANTIC
static const Color error = Color(0xFFD32F2F);
static const Color success = basicKsuPurple;
static const Color warning = Color(0xFFFBC02D);
static const Color info = Color(0xFF2196F3);
// Trust Tiers
static Color get tierEstablished => egyptianBlue;
static Color get tierTrusted => royalPurple;
static const Color tierNew = Colors.grey;
// ============================================================================
// DIMENSIONS
// ============================================================================
static const double spacingSm = 12.0;
static const double spacingMd = 16.0;
static const double spacingLg = 24.0;
static const double spacingXs = 4.0;
static const double spacing2xs = 2.0;
// Radii
static const double radiusSm = 4.0;
static const double radiusXs = 2.0;
static const double radiusMd = 8.0;
static const double radiusMdValue = 8.0;
static const double radiusFull = 36.0;
// Text Colors - COLOR HIERARCHY: Content neutral, UI branded
static Color get textPrimary => navyText; // Names, handles, UI
static Color get textSecondary => navyText; // Names, handles, UI
static Color get textTertiary => navyText; // Names, handles, UI
static const Color textDisabled = Colors.grey;
static const Color textOnAccent = white;
static Color get border => egyptianBlue;
// Post Content - Neutral for contrast with purple UI
static const Color postContent = Color(0xFF1A1A1A);
static const Color postContentLight = Color(0xFF4A4A4A);
// STRICT SEPARATION
static const double borderWidth = 1.5;
static const double dividerThickness = 2.0;
static const double flowLineWidth = 3.0;
// Post Specific Spacing
static const double spacingPostShort = 16.0;
static const double spacingPostMedium = 24.0;
static const double spacingPostLong = 32.0;
// ============================================================================
// TYPOGRAPHY (Literata + Navy Blue) - STRICT FLAT DESIGN
// ============================================================================
static TextTheme get textTheme => GoogleFonts.literataTextTheme().copyWith(
// Hero Body Text - Main post content (NEUTRAL for contrast with purple UI)
bodyLarge: GoogleFonts.literata(
fontSize: 17,
height: 1.5,
fontWeight: FontWeight.w400,
color: postContent,
),
// Standard Body - Secondary content (NEUTRAL)
bodyMedium: GoogleFonts.literata(
fontSize: 16,
height: 1.5,
fontWeight: FontWeight.w400,
color: postContentLight,
),
// Metadata / UI Elements (Sans-serif)
labelSmall: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w600,
color: egyptianBlue,
letterSpacing: 0.2,
),
labelMedium: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: brightNavy,
letterSpacing: 0,
),
// Author Name - Visual anchor, ExtraBold Navy Blue
labelLarge: GoogleFonts.inter(
fontSize: 15,
fontWeight: FontWeight.w800,
color: navyBlue,
),
// Headlines
headlineSmall: GoogleFonts.literata(
fontSize: 22,
fontWeight: FontWeight.w700,
color: navyBlue,
letterSpacing: -0.5,
),
headlineMedium: GoogleFonts.literata(
fontSize: 26,
fontWeight: FontWeight.w700,
color: navyBlue,
letterSpacing: 0,
),
);
// Backward Compat Getters
static TextStyle get postBody => textTheme.bodyLarge!;
static TextStyle get postBodyShort => textTheme.bodyLarge!.copyWith(fontSize: 22);
static TextStyle get postBodyLong => textTheme.bodyLarge!.copyWith(fontSize: 18);
static TextStyle get postBodyReflective => textTheme.bodyLarge!.copyWith(
fontStyle: FontStyle.italic,
color: ksuPurple
);
// Text Style Getters
static TextStyle get bodyMedium => textTheme.bodyMedium!;
static TextStyle get bodyLarge => textTheme.bodyLarge!;
static TextStyle get headlineMedium => textTheme.headlineMedium!;
static TextStyle get headlineSmall => textTheme.headlineSmall!;
static TextStyle get labelMedium => textTheme.labelMedium!;
static TextStyle get labelSmall => textTheme.labelSmall!;
// ============================================================================
// THEME DATA
// ============================================================================
static ThemeData get lightTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.light,
scaffoldBackgroundColor: scaffoldBg,
primaryColor: navyBlue,
// Color Scheme
colorScheme: ColorScheme.light(
primary: navyBlue,
secondary: brightNavy,
tertiary: royalPurple,
surface: cardSurface,
onSurface: navyText,
error: error,
),
// Text Theme
textTheme: textTheme,
fontFamily: GoogleFonts.literata().fontFamily,
// AppBar (High Contrast)
appBarTheme: AppBarTheme(
backgroundColor: cardSurface,
surfaceTintColor: Colors.transparent,
elevation: 0,
centerTitle: false,
iconTheme: IconThemeData(color: navyBlue),
titleTextStyle: textTheme.headlineSmall,
systemOverlayStyle: SystemUiOverlayStyle.dark,
shape: Border(
bottom: BorderSide(color: egyptianBlue, width: isPop ? 2 : borderWidth),
),
),
// Card Theme (Defined Edges)
cardTheme: CardThemeData(
color: cardSurface,
elevation: 0,
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: egyptianBlue, width: 1),
),
),
// Buttons
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: brightNavy,
foregroundColor: white,
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
textStyle: GoogleFonts.inter(fontWeight: FontWeight.bold),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: egyptianBlue,
textStyle: GoogleFonts.inter(fontWeight: FontWeight.w600),
),
),
// Bottom Nav
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: cardSurface,
selectedItemColor: royalPurple,
unselectedItemColor: const Color(0xFF9EA3B0),
type: BottomNavigationBarType.fixed,
showSelectedLabels: !isPop,
showUnselectedLabels: !isPop,
elevation: isPop ? 10 : 0,
),
// Floating Action Button
floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: brightNavy,
foregroundColor: Colors.white,
elevation: isPop ? 4 : 6,
shape: isPop ? const CircleBorder() : null,
),
// Divider (Hard & Visible)
dividerTheme: DividerThemeData(
color: queenPink,
thickness: 1,
space: 24,
),
// Input Fields
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: cardSurface,
contentPadding: const EdgeInsets.all(16),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: egyptianBlue, width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: egyptianBlue, width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: royalPurple, width: 2),
),
),
);
}
}