Files
xtremflow/docs/archive/APPLE_TV_CODE_REFERENCE.md
T
MichaelandClaude Fable 5 60d3f42901 feat: security hardening, streaming overhaul, design polish, tests
Security:
- Replace unsalted SHA-256 password hashing with bcrypt (lazy rehash on login)
- Add authenticated /api/xtream-api gateway: Xtream credentials are injected
  server-side and never sent to the frontend; /api/playlists no longer
  returns passwords
- Redact credentials from all logs (login body, proxy/FFmpeg/scheduler URLs)
- Add auth to recordings, EPG, season-passes and streaming routes
  (HttpOnly session cookie for hls.js; loopback bypass for local FFmpeg)
- Lock player postMessage to same-origin in both directions
- Vendor and pin hls.js 1.6.7 / mpegts.js 1.7.3 (drop CDN @latest)
- Fix rate limiter (client IP was never resolved), add login rate limit,
  restrict CORS, add CSP Report-Only, block private-IP SSRF targets,
  fix path traversal in recording log retrieval, chmod 777 -> 770
- Remove dead HiveService (seeded admin/admin into IndexedDB with SHA-256)
- Fix authMiddleware not populating 'user' context (getPlaylist ignored the
  logged-in user; admin purge always returned 403)

Streaming:
- New FfmpegSessionManager: process registry, idle reaper (4 min live /
  15 min VOD), orphan cleanup at startup, clean SIGTERM shutdown,
  fast-fail with stderr instead of 30 s timeout
- Quality selection (source/high/medium/low) for live and VOD; source mode
  streams with -c:v copy (zero transcoding); selector wired into the player
- Concurrent recordings (MAX_CONCURRENT_RECORDINGS, default 2); conflicts
  retry on the next tick instead of silently failing
- Lower live latency (HLS window 20 -> 10 segments, liveSync 10 -> 3)
- Fix recording log lookup (.mp4 vs .mkv mismatch)

Design:
- Replace hardcoded colors with AppColors tokens (12 files)
- web/theme.css syncs HTML players with the Flutter palette
- DPAD/keyboard navigation (arrow-key focus, player shortcuts)
- Tooltips on player icon buttons, Semantics on content cards
- Remove 7 dead widgets broken since the Stitch merge

Quality:
- bin/test/: 21 unit tests (bcrypt, redaction, traversal, SSRF, recording
  conflicts) plus a quality-selector widget test
- GitHub Actions CI (analyze + test + build web)
- Archive stale status docs into docs/archive/

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 10:07:18 +02:00

33 KiB

Apple TV Design System - Implementation Code Reference

Part 1: Updated app_colors.dart Structure

Complete Color Definitions

class AppColors {
  AppColors._();

  // ============ BACKGROUNDS (OLED Optimized) ============
  /// Pure black, optimal for OLED
  static const Color background = Color(0xFF000000);
  
  /// Deep black, very subtle lift
  static const Color backgroundAlt = Color(0xFF0A0A0A);

  // ============ SURFACE LAYERS (Material Design 3 compatible) ============
  /// Level 1 - Card surfaces, default container
  static const Color surface = Color(0xFF1A1A1A);
  
  /// Level 2 - Hovered surfaces, slight elevation
  static const Color surfaceVariant = Color(0xFF2A2A2A);
  
  /// Level 3 - Disabled/secondary surfaces
  static const Color surfaceTertiary = Color(0xFF3A3A3A);
  
  /// Level 4 - Dividers, minimal visible elements
  static const Color surfaceQuad = Color(0xFF4A4A4A);

  // ============ PRIMARY ACCENT (Teal) ============
  /// Main interactive color - focus states, CTAs
  static const Color primary = Color(0xFF00A0D2);
  
  /// Darker teal for hover states
  static const Color primaryDark = Color(0xFF0092BC);
  
  /// Lighter teal for disabled variant
  static const Color primaryLight = Color(0xFF1BC4E5);

  // ============ SECONDARY ACCENT (Red) ============
  /// Error, warning, live indicator - Apple red
  static const Color secondary = Color(0xFFFF3B30);
  
  /// Darker red for hover
  static const Color secondaryDark = Color(0xFFE62817);
  
  /// Lighter red for disabled
  static const Color secondaryLight = Color(0xFFFF6B6B);

  // ============ TERTIARY ACCENT (Green) ============
  /// Success, confirmations, positive states
  static const Color tertiary = Color(0xFF34C759);

  // ============ PRIMARY TEXT COLORS ============
  /// Primary text - pure white
  static const Color textPrimary = Color(0xFFFFFFFF);
  
  /// Secondary text - 60% opacity white (.
  static const Color textSecondary = Color(0xFF999999);
  
  /// Tertiary text - 40% opacity white
  static const Color textTertiary = Color(0xFF666666);
  
  /// Quaternary text - 25% opacity white
  static const Color textQuaternary = Color(0xFF404040);

  // ============ STATE COLORS ============
  static const Color success = Color(0xFF34C759);
  static const Color warning = Color(0xFFFF9500);
  static const Color error = Color(0xFFFF3B30);
  static const Color info = Color(0xFF32ADE6);
  static const Color disabled = Color(0xFF888888);

  // ============ FOCUS STATES ============
  /// White for maximum contrast on focus
  static const Color focusColor = Color(0xFFFFFFFF);
  
  /// Focus border (use with 2px width)
  static const Color focusBorder = Color(0xFFFFFFFF);

  // ============ GLASS EFFECTS ============
  /// Glass overlay - 6% white (refined from 8%)
  static final Color glassBackground = const Color(0xFFFFFFFF).withOpacity(0.06);
  
  /// Glass border - 12% white
  static final Color glassBorder = const Color(0xFFFFFFFF).withOpacity(0.12);
  
  /// Glass premium variant - 10% white
  static final Color glassPremium = const Color(0xFFFFFFFF).withOpacity(0.10);
  
  /// Dark glass for overlays
  static final Color glassBackgroundDark = const Color(0xFF000000).withOpacity(0.50);

  // ============ GRADIENTS ============
  static const LinearGradient primaryGradient = LinearGradient(
    colors: [Color(0xFF00A0D2), Color(0xFF00D4AA)],
    begin: Alignment.topLeft,
    end: Alignment.bottomRight,
  );

  static const LinearGradient backgroundGradient = LinearGradient(
    colors: [Color(0xFF000000), Color(0xFF0A0A0A)],
    begin: Alignment.topCenter,
    end: Alignment.bottomCenter,
  );

  // ============ RATING/BADGE COLORS ============
  static const Color ratingGold = Color(0xFFFFD700);
  static const Color ratingPremium = Color(0xFFFFD700);

  // ============ OVERLAY & SCRIM ============
  static const Color overlay = Color(0x80000000);
  static final Color scrim = Colors.black.withOpacity(0.4);

  // ============ COLOR SCHEME (Material 3) ============
  static ColorScheme get darkColorScheme => const ColorScheme.dark(
        primary: primary,
        onPrimary: Color(0xFF000000),
        secondary: secondary,
        onSecondary: Color(0xFFFFFFFF),
        tertiary: tertiary,
        onTertiary: Color(0xFF000000),
        surface: surface,
        onSurface: textPrimary,
        error: error,
        onError: Color(0xFFFFFFFF),
        brightness: Brightness.dark,
        scrim: overlay,
      );
}

Part 2: Updated app_theme.dart Structure

Typography Scale

class AppTheme {
  AppTheme._();

  // ============ COMPLETE TYPOGRAPHY SYSTEM ============
  static ThemeData get darkTheme {
    final baseTheme = ThemeData.dark(useMaterial3: true);

    return baseTheme.copyWith(
      useMaterial3: true,
      colorScheme: AppColors.darkColorScheme,
      scaffoldBackgroundColor: AppColors.background,
      textTheme: TextTheme(
        // ====== HERO / DISPLAY ======
        /// Display Large - 64px (increased from 56px)
        /// Usage: Hero titles, featured content
        displayLarge: GoogleFonts.outfit(
          fontSize: 64,
          fontWeight: FontWeight.w800,
          letterSpacing: -2.0,
          height: 1.2,   // 77px line
          color: AppColors.textPrimary,
        ),

        /// Display Medium - 56px
        /// Usage: Page titles, feature headlines
        displayMedium: GoogleFonts.outfit(
          fontSize: 56,
          fontWeight: FontWeight.w700,
          letterSpacing: -1.5,
          height: 1.15,   // 64px line
          color: AppColors.textPrimary,
        ),

        /// Display Small - 44px
        /// Usage: Section headers, subsection titles
        displaySmall: GoogleFonts.outfit(
          fontSize: 44,
          fontWeight: FontWeight.w700,
          letterSpacing: -1.0,
          height: 1.2,    // 53px line
          color: AppColors.textPrimary,
        ),

        // ====== HEADLINE / TITLE ======
        /// Headline Large - 32px
        /// Usage: Card titles, strong emphasis
        headlineLarge: GoogleFonts.outfit(
          fontSize: 32,
          fontWeight: FontWeight.w600,
          letterSpacing: -0.5,
          height: 1.25,   // 40px line
          color: AppColors.textPrimary,
        ),

        /// Headline Medium - 28px
        /// Usage: Subsection titles, emphasis
        headlineMedium: GoogleFonts.outfit(
          fontSize: 28,
          fontWeight: FontWeight.w600,
          letterSpacing: -0.3,
          height: 1.3,    // 36px line
          color: AppColors.textPrimary,
        ),

        /// Headline Small - 22px
        /// Usage: Button text, strong labels
        headlineSmall: GoogleFonts.outfit(
          fontSize: 22,
          fontWeight: FontWeight.w600,
          letterSpacing: 0,
          height: 1.4,    // 31px line
          color: AppColors.textPrimary,
        ),

        // ====== TITLE ======
        /// Title Large - 18px
        /// Usage: Prominent UI text, navigation
        titleLarge: GoogleFonts.inter(
          fontSize: 18,
          fontWeight: FontWeight.w600,
          letterSpacing: 0,
          height: 1.4,    // 25px line
          color: AppColors.textPrimary,
        ),

        /// Title Medium - 16px
        /// Usage: Button labels, input labels
        titleMedium: GoogleFonts.inter(
          fontSize: 16,
          fontWeight: FontWeight.w600,
          letterSpacing: 0,
          height: 1.5,    // 24px line
          color: AppColors.textPrimary,
        ),

        /// Title Small - 14px
        /// Usage: Secondary labels, emphasis
        titleSmall: GoogleFonts.inter(
          fontSize: 14,
          fontWeight: FontWeight.w600,
          letterSpacing: 0.1,
          height: 1.4,    // 20px line
          color: AppColors.textSecondary,
        ),

        // ====== BODY TEXT ======
        /// Body Large - 16px
        /// Usage: Primary content text, descriptions
        bodyLarge: GoogleFonts.inter(
          fontSize: 16,
          fontWeight: FontWeight.w400,
          letterSpacing: 0.2,
          height: 1.6,    // 26px line (increased from 1.5)
          color: AppColors.textSecondary,
        ),

        /// Body Medium - 14px
        /// Usage: Secondary content, metadata
        bodyMedium: GoogleFonts.inter(
          fontSize: 14,
          fontWeight: FontWeight.w400,
          letterSpacing: 0.2,
          height: 1.6,    // 22px line (increased from 1.5)
          color: AppColors.textSecondary,
        ),

        /// Body Small - 12px
        /// Usage: Tertiary text, timestamps, specs
        bodySmall: GoogleFonts.inter(
          fontSize: 12,
          fontWeight: FontWeight.w400,
          letterSpacing: 0.15,
          height: 1.5,    // 18px line
          color: AppColors.textTertiary,
        ),

        // ====== LABELS & CAPTIONS ======
        /// Label Large - 12px
        /// Usage: Badge text, tab labels, buttons
        labelLarge: GoogleFonts.inter(
          fontSize: 12,
          fontWeight: FontWeight.w700,
          letterSpacing: 0.3,
          height: 1.4,    // 17px line
          color: AppColors.textPrimary,
        ),

        /// Label Medium - 11px
        /// Usage: Secondary badges, small labels
        labelMedium: GoogleFonts.inter(
          fontSize: 11,
          fontWeight: FontWeight.w500,
          letterSpacing: 0.3,
          height: 1.4,    // 15px line
          color: AppColors.textPrimary,
        ),

        /// Label Small - 10px
        /// Usage: Tiny badges, version numbers (NEW)
        labelSmall: GoogleFonts.inter(
          fontSize: 10,
          fontWeight: FontWeight.w600,
          letterSpacing: 0.5,
          height: 1.2,    // 12px line
          color: AppColors.textSecondary,
        ),
      ),

      // ============ APP BAR ============
      appBarTheme: AppBarTheme(
        backgroundColor: Colors.transparent,
        foregroundColor: AppColors.textPrimary,
        elevation: 0,
        scrolledUnderElevation: 0,
        centerTitle: true,
        titleTextStyle: GoogleFonts.outfit(
          fontSize: 32,     // Increased from 24px
          fontWeight: FontWeight.w700,
          color: AppColors.textPrimary,
          letterSpacing: -0.3,
        ),
      ),

      // ============ BUTTONS ============
      filledButtonTheme: FilledButtonThemeData(
        style: FilledButton.styleFrom(
          backgroundColor: AppColors.primary,
          foregroundColor: AppColors.textPrimary,
          disabledBackgroundColor: AppColors.disabled,
          disabledForegroundColor: AppColors.textTertiary,
          elevation: 0,
          padding: const EdgeInsets.symmetric(
            horizontal: spacing28,  // Increased from 24
            vertical: spacing12,
          ),
          minimumSize: const Size(0, buttonHeightLg), // 56px height (new)
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(radiusMd),
          ),
          textStyle: GoogleFonts.inter(
            fontWeight: FontWeight.w600,
            fontSize: 18,
            letterSpacing: 0.1,
          ),
        ).copyWith(
          overlayColor: WidgetStateProperty.resolveWith((states) {
            if (states.contains(WidgetState.hovered)) {
              return AppColors.primary.withOpacity(0.15);
            }
            if (states.contains(WidgetState.pressed)) {
              return AppColors.primary.withOpacity(0.3);
            }
            return null;
          }),
        ),
      ),

      // ============ CARDS ============
      cardTheme: CardThemeData(
        color: AppColors.surface,
        elevation: 0,
        margin: EdgeInsets.zero,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(radiusMd),
          side: const BorderSide(color: AppColors.border, width: 1),
        ),
        shadowColor: Colors.black.withOpacity(0.15),
      ),

      // ============ INPUTS ============
      inputDecorationTheme: InputDecorationTheme(
        filled: true,
        fillColor: AppColors.surface,
        contentPadding: const EdgeInsets.symmetric(
          horizontal: spacing16,
          vertical: spacing12,
        ),
        isDense: false,
        border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(radiusMd),
          borderSide: const BorderSide(color: AppColors.border),
        ),
        enabledBorder: OutlineInputBorder(
          borderRadius: BorderRadius.circular(radiusMd),
          borderSide: const BorderSide(
            color: AppColors.border,
            width: 1.0,
          ),
        ),
        focusedBorder: OutlineInputBorder(
          borderRadius: BorderRadius.circular(radiusMd),
          borderSide: const BorderSide(
            color: AppColors.primary,
            width: 2.0,
          ),
        ),
        hintStyle: GoogleFonts.inter(
          color: AppColors.textTertiary,
          fontSize: 14,
          fontWeight: FontWeight.w400,
        ),
        prefixIconColor: AppColors.textSecondary,
        suffixIconColor: AppColors.textSecondary,
        helperStyle: GoogleFonts.inter(
          color: AppColors.textTertiary,
          fontSize: 12,
        ),
        errorStyle: GoogleFonts.inter(
          color: AppColors.error,
          fontSize: 12,
        ),
      ),

      // ============ EXTENSIONS (Custom themes) ============
      extensions: <ThemeExtension<dynamic>>[
        _AppThemeExtension.dark,
      ],
    );
  }

  // ============ SPACING (8pt base grid) ============
  static const double spacing2 = 2.0;
  static const double spacing4 = 4.0;
  static const double spacing8 = 8.0;
  static const double spacing12 = 12.0;
  static const double spacing16 = 16.0;
  static const double spacing20 = 20.0;
  static const double spacing24 = 24.0;
  static const double spacing28 = 28.0; // NEW
  static const double spacing32 = 32.0;
  static const double spacing40 = 40.0;
  static const double spacing48 = 48.0;
  static const double spacing56 = 56.0;
  static const double spacing64 = 64.0;
  static const double spacing80 = 80.0;  // NEW - TV safe areas
  static const double spacing96 = 96.0;  // NEW - Large layout

  // ============ RADIUS (Modern, organic curves) ============
  static const double radiusXs = 4.0;
  static const double radiusSm = 8.0;
  static const double radiusMd = 12.0;
  static const double radiusLg = 16.0;
  static const double radiusXl = 24.0;
  static const double radiusXxl = 32.0;
  static const double radiusFull = 999.0;

  // ============ ELEVATION (Shadow system) ============
  static const double elevationXs = 2.0;
  static const double elevationSm = 4.0;
  static const double elevationMd = 8.0;
  static const double elevationLg = 16.0;
  static const double elevationXl = 24.0;

  // ============ ANIMATION DURATIONS ============
  static const Duration durationXs = Duration(milliseconds: 100);
  static const Duration durationSm = Duration(milliseconds: 150);
  static const Duration durationBase = Duration(milliseconds: 200);
  static const Duration durationMd = Duration(milliseconds: 250); // NEW
  static const Duration durationLg = Duration(milliseconds: 300);
  static const Duration durationXl = Duration(milliseconds: 400);
  static const Duration durationXxl = Duration(milliseconds: 600);

  // ============ ANIMATION CURVES ============
  /// Standard deceleration - smooth landing (entrance)
  static const Curve curveDefault = Curves.easeOutCubic;
  
  /// Quick interaction - symmetrical (tap, state change)
  static const Curve curveSnappy = Curves.easeInOutQuad;
  
  /// Smooth navigation - deceleration for movement
  static const Curve curveSmooth = Curves.easeOutCubic;
  
  /// Celebratory - spring bounce (favorites, achievements)
  static const Curve curveBouncy = Curves.elasticOut;

  // ============ BUTTON SIZING ============
  static const double buttonHeightSm = 44.0;
  static const double buttonHeightMd = 48.0;
  static const double buttonHeightLg = 56.0; // TV optimal

  // ============ ICON SIZING ============
  static const double iconSizeXs = 16.0;
  static const double iconSizeSm = 20.0;
  static const double iconSizeMd = 24.0;
  static const double iconSizeLg = 32.0;
  static const double iconSizeXl = 48.0;

  // ============ ASPECT RATIOS ============
  static const double posterAspectRatio = 2 / 3;
  static const double landscapeAspectRatio = 16 / 9;
  static const double squareAspectRatio = 1.0;
}

/// Theme extension for custom properties
class _AppThemeExtension extends ThemeExtension<_AppThemeExtension> {
  const _AppThemeExtension({
    this.shadowLevel2,
    this.shadowLevel3,
    this.shadowLevel4,
    this.shadowLevel5,
    this.focusGlow,
  });

  final List<BoxShadow>? shadowLevel2;
  final List<BoxShadow>? shadowLevel3;
  final List<BoxShadow>? shadowLevel4;
  final List<BoxShadow>? shadowLevel5;
  final List<BoxShadow>? focusGlow;

  static _AppThemeExtension get dark => _AppThemeExtension(
    // Level 2 - Subtle shadow (base cards)
    shadowLevel2: [
      BoxShadow(
        color: Colors.black.withOpacity(0.08),
        blurRadius: 6,
        spreadRadius: 0,
        offset: const Offset(0, 2),
      ),
    ],

    // Level 3 - Standard shadow (hover/focus cards)
    shadowLevel3: [
      BoxShadow(
        color: Colors.black.withOpacity(0.12),
        blurRadius: 8,
        spreadRadius: 0,
        offset: const Offset(0, 4),
      ),
      BoxShadow(
        color: Colors.black.withOpacity(0.06),
        blurRadius: 2,
        spreadRadius: 0,
        offset: const Offset(0, 1),
      ),
    ],

    // Level 4 - Elevated shadow (active cards with glow)
    shadowLevel4: [
      BoxShadow(
        color: Colors.black.withOpacity(0.20),
        blurRadius: 12,
        spreadRadius: 0,
        offset: const Offset(0, 8),
      ),
      BoxShadow(
        color: Colors.black.withOpacity(0.10),
        blurRadius: 4,
        spreadRadius: 0,
        offset: const Offset(0, 2),
      ),
      // Glow added separately when focused
    ],

    // Level 5 - Maximum elevation (modals)
    shadowLevel5: [
      BoxShadow(
        color: Colors.black.withOpacity(0.25),
        blurRadius: 16,
        spreadRadius: 0,
        offset: const Offset(0, 12),
      ),
      BoxShadow(
        color: Colors.black.withOpacity(0.15),
        blurRadius: 6,
        spreadRadius: 0,
        offset: const Offset(0, 4),
      ),
      BoxShadow(
        color: Colors.black.withOpacity(0.08),
        blurRadius: 2,
        spreadRadius: 0,
        offset: const Offset(0, 1),
      ),
    ],

    // Teal glow (when element has focus)
    focusGlow: [
      BoxShadow(
        color: AppColors.primary.withOpacity(0.30),
        blurRadius: 20,
        spreadRadius: 2,
        offset: const Offset(0, 0),
      ),
    ],
  );

  @override
  ThemeExtension<_AppThemeExtension> copyWith({
    List<BoxShadow>? shadowLevel2,
    List<BoxShadow>? shadowLevel3,
    List<BoxShadow>? shadowLevel4,
    List<BoxShadow>? shadowLevel5,
    List<BoxShadow>? focusGlow,
  }) {
    return _AppThemeExtension(
      shadowLevel2: shadowLevel2 ?? this.shadowLevel2,
      shadowLevel3: shadowLevel3 ?? this.shadowLevel3,
      shadowLevel4: shadowLevel4 ?? this.shadowLevel4,
      shadowLevel5: shadowLevel5 ?? this.shadowLevel5,
      focusGlow: focusGlow ?? this.focusGlow,
    );
  }

  @override
  ThemeExtension<_AppThemeExtension> lerp(
    covariant ThemeExtension<_AppThemeExtension>? other,
    double t,
  ) {
    if (other is! _AppThemeExtension) return this;
    return this;
  }
}

Part 3: Focus State Animation Helper

New utility class for consistent focus animations

// lib/core/animation/focus_animations.dart

import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
import '../theme/app_colors.dart';

/// Reusable focus animation utilities for TV-friendly interactions
class FocusAnimations {
  FocusAnimations._();

  /// Standard focus arrival animation
  /// Timeline: 150ms easeOut with scale and glow
  static Future<void> animateFocusArrival({
    required AnimationController controller,
    Duration duration = AppTheme.durationSm,
  }) async {
    controller.forward();
    await Future.delayed(duration);
  }

  /// Tap feedback animation (scale down then up)
  static Future<void> animateTapFeedback({
    required AnimationController controller,
  }) async {
    controller.forward();
    await Future.delayed(const Duration(milliseconds: 100));
    controller.reverse();
  }

  /// Generate focus border decoration
  static BoxDecoration focusBorder() => BoxDecoration(
    border: Border.all(
      color: AppColors.focusBorder,
      width: 2,
    ),
  );

  /// Generate focus glow shadow
  static List<BoxShadow> focusGlowShadow() => [
    BoxShadow(
      color: AppColors.primary.withOpacity(0.30),
      blurRadius: 20,
      spreadRadius: 2,
      offset: const Offset(0, 0),
    ),
  ];

  /// Generate focus animation curve
  static Curve get focusCurve => AppTheme.curveDefault; // easeOutCubic
}

Part 4: TV Button Implementation

Example implementation showing all states

// lib/core/widgets/tv_primary_button.dart

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../theme/app_theme.dart';
import '../theme/app_colors.dart';

/// TV-optimized primary button with focus and hover states
class TvPrimaryButton extends StatefulWidget {
  final String label;
  final VoidCallback? onPressed;
  final IconData? icon;
  final bool isLoading;
  final bool isExpanded;
  final bool autofocus;

  const TvPrimaryButton({
    super.key,
    required this.label,
    this.onPressed,
    this.icon,
    this.isLoading = false,
    this.isExpanded = false,
    this.autofocus = false,
  });

  @override
  State<TvPrimaryButton> createState() => _TvPrimaryButtonState();
}

class _TvPrimaryButtonState extends State<TvPrimaryButton>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _scaleAnimation;
  late Animation<double> _opacityAnimation;
  bool _isHovered = false;
  bool _isFocused = false;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: AppTheme.durationSm,
    );

    _scaleAnimation = Tween<double>(begin: 1.0, end: 1.05).animate(
      CurvedAnimation(parent: _controller, curve: AppTheme.curveDefault),
    );

    _opacityAnimation = Tween<double>(begin: 1.0, end: 0.9).animate(
      CurvedAnimation(parent: _controller, curve: AppTheme.curveDefault),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _handleFocusChange(bool hasFocus) {
    setState(() => _isFocused = hasFocus);
    if (hasFocus) {
      _controller.forward();
    } else {
      _controller.reverse();
    }
  }

  @override
  Widget build(BuildContext context) {
    final isActive = _isHovered || _isFocused;

    return MouseRegion(
      onEnter: (_) => setState(() => _isHovered = true),
      onExit: (_) => setState(() => _isHovered = false),
      cursor: SystemMouseCursors.click,
      child: Focus(
        onFocusChange: _handleFocusChange,
        autofocus: widget.autofocus,
        child: AnimatedBuilder(
          animation: _scaleAnimation,
          builder: (context, child) {
            return Transform.scale(
              scale: _scaleAnimation.value,
              child: child,
            );
          },
          child: Container(
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(AppTheme.radiusMd),
              // Add focus border only when focused
              border: _isFocused
                  ? Border.all(
                      color: AppColors.focusBorder,
                      width: 2,
                    )
                  : null,
              // Add focus glow shadow when focused
              boxShadow: _isFocused
                  ? [
                      BoxShadow(
                        color: AppColors.primary.withOpacity(0.30),
                        blurRadius: 20,
                        spreadRadius: 2,
                        offset: const Offset(0, 0),
                      ),
                      ...?Theme.of(context)
                          .extension<_AppThemeExtension>()
                          ?.shadowLevel4,
                    ]
                  : _isHovered
                      ? Theme.of(context)
                              .extension<_AppThemeExtension>()
                              ?.shadowLevel3 ??
                          []
                      : Theme.of(context)
                              .extension<_AppThemeExtension>()
                              ?.shadowLevel2 ??
                          [],
            ),
            child: Material(
              color: _isFocused
                  ? AppColors.primary // Keep color on focus
                  : _isHovered
                      ? AppColors.primaryDark // Darker on hover
                      : AppColors.primary,
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(AppTheme.radiusMd),
              ),
              child: InkWell(
                onTap: widget.isLoading || widget.onPressed == null
                    ? null
                    : widget.onPressed,
                borderRadius: BorderRadius.circular(AppTheme.radiusMd),
                child: Padding(
                  padding: const EdgeInsets.symmetric(
                    horizontal: AppTheme.spacing28,
                    vertical: AppTheme.spacing12,
                  ),
                  child: SizedBox(
                    height: AppTheme.buttonHeightLg,
                    width: widget.isExpanded ? double.infinity : null,
                    child: Row(
                      mainAxisSize: MainAxisSize.min,
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        if (widget.isLoading)
                          const SizedBox(
                            width: 20,
                            height: 20,
                            child: CircularProgressIndicator(
                              strokeWidth: 2,
                              valueColor: AlwaysStoppedAnimation<Color>(
                                Colors.black,
                              ),
                            ),
                          )
                        else ...[
                          if (widget.icon != null) ...[
                            Icon(
                              widget.icon,
                              color: Colors.black,
                              size: AppTheme.iconSizeMd,
                            ),
                            const SizedBox(width: AppTheme.spacing12),
                          ],
                          Flexible(
                            child: Text(
                              widget.label,
                              style: GoogleFonts.inter(
                                fontSize: 18,
                                fontWeight: FontWeight.w600,
                                color: Colors.black,
                                letterSpacing: 0.1,
                              ),
                              maxLines: 1,
                              overflow: TextOverflow.ellipsis,
                            ),
                          ),
                        ],
                      ],
                    ),
                  ),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

Part 5: Shadow System Helper

Simplified shadow application

// lib/core/theme/shadow_system.dart

import 'package:flutter/material.dart';
import 'app_colors.dart';

/// Centralized shadow system for consistent elevation
class ShadowSystem {
  ShadowSystem._();

  /// Level 2 - Subtle shadow (base cards)
  /// Usage: Normal card state, background layers
  static List<BoxShadow> get level2 => [
    BoxShadow(
      color: Colors.black.withOpacity(0.08),
      blurRadius: 6,
      spreadRadius: 0,
      offset: const Offset(0, 2),
    ),
  ];

  /// Level 3 - Standard shadow (hover/focus cards)
  /// Usage: Hovered cards, elevated containers
  static List<BoxShadow> get level3 => [
    BoxShadow(
      color: Colors.black.withOpacity(0.12),
      blurRadius: 8,
      spreadRadius: 0,
      offset: const Offset(0, 4),
    ),
    BoxShadow(
      color: Colors.black.withOpacity(0.06),
      blurRadius: 2,
      spreadRadius: 0,
      offset: const Offset(0, 1),
    ),
  ];

  /// Level 4 - Elevated shadow with glow (focused elements)
  /// Usage: Selected cards, active elements, focus state
  static List<BoxShadow> get level4 => [
    BoxShadow(
      color: Colors.black.withOpacity(0.20),
      blurRadius: 12,
      spreadRadius: 0,
      offset: const Offset(0, 8),
    ),
    BoxShadow(
      color: Colors.black.withOpacity(0.10),
      blurRadius: 4,
      spreadRadius: 0,
      offset: const Offset(0, 2),
    ),
  ];

  /// Focus glow (add to level4 when element is focused)
  /// Usage: Add on top of level4 for focused state
  static List<BoxShadow> get focusGlow => [
    BoxShadow(
      color: AppColors.primary.withOpacity(0.30),
      blurRadius: 20,
      spreadRadius: 2,
      offset: const Offset(0, 0),
    ),
  ];

  /// Level 5 - Maximum elevation (modals, overlays)
  /// Usage: Modal dialogs, maximum elevation containers
  static List<BoxShadow> get level5 => [
    BoxShadow(
      color: Colors.black.withOpacity(0.25),
      blurRadius: 16,
      spreadRadius: 0,
      offset: const Offset(0, 12),
    ),
    BoxShadow(
      color: Colors.black.withOpacity(0.15),
      blurRadius: 6,
      spreadRadius: 0,
      offset: const Offset(0, 4),
    ),
    BoxShadow(
      color: Colors.black.withOpacity(0.08),
      blurRadius: 2,
      spreadRadius: 0,
      offset: const Offset(0, 1),
    ),
  ];

  /// Combined level4 + focusGlow (convenience)
  static List<BoxShadow> get level4WithGlow => [
    ...level4,
    ...focusGlow,
  ];
}

Part 6: Glass Container Update

Updated glass effect with new opacity

// lib/core/widgets/glass_container.dart (snippet)

class GlassContainer extends StatelessWidget {
  final Widget child;
  final double? opacity;  // Now 6% default instead of 8%
  // ... other parameters

  const GlassContainer({
    // ... parameters
    this.opacity,  // Will be 0.06 if not provided
  });

  @override
  Widget build(BuildContext context) {
    final finalOpacity = opacity ?? 0.06;  // Changed from 0.08
    final finalBgColor = backgroundColor ?? 
        Color(0xFFFFFFFF).withOpacity(finalOpacity);

    return Container(
      // ... sizing
      child: ClipRRect(
        // ...
        child: BackdropFilter(
          filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur),
          child: Container(
            padding: padding,
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(borderRadius),
              gradient: showGradient
                  ? LinearGradient(
                      begin: Alignment.topLeft,
                      end: Alignment.bottomRight,
                      colors: [
                        finalBgColor.withOpacity(finalOpacity * 1.2),
                        finalBgColor.withOpacity(finalOpacity * 0.5),
                      ],
                    )
                  : null,
              border: hasBorder
                  ? Border.all(
                      color: borderColor ?? 
                          Color(0xFFFFFFFF).withOpacity(0.12),  // Updated to 12%
                      width: borderWidth,
                    )
                  : null,
              boxShadow: showShadow
                  ? ShadowSystem.level3  // Use unified shadow system
                  : [],
            ),
            child: child,
          ),
        ),
      ),
    );
  }
}

Part 7: Quick Migration Checklist

// Changes needed in existing files:

// 1. app_colors.dart
✓ primary: #00D4FF → #00A0D2
✓ Add primaryDark, primaryLight
✓ surfaceVariant, surfaceTertiary, surfaceQuad updated
✓ glassBackground opacity: 8% → 6%
✓ glassBorder opacity: 15% → 12%

// 2. app_theme.dart
✓ displayLarge fontSize: 56 → 64
✓ displayMedium fontSize: 48 → 56
✓ displaySmall fontSize: 36 → 44
✓ bodyLarge lineHeight: 1.5 → 1.6
✓ bodyMedium lineHeight: 1.5 → 1.6
✓ filledButtonTheme height: 44 → 56px
✓ Update shadow system with 5 levels
✓ Add new animation durations (250ms, etc.)

// 3. Widget updates
✓ TvFocusableCard - use new focus animation
✓ TvModernCard - update shadow levels
✓ GlassContainer - reduce opacity to 6%
✓ All buttons - increase min height to 56px

// 4. Screen layouts
✓ TV layouts - increase padding from 32px → 48px
✓ Grid gaps - maintain 24px (good)
✓ Navigation - increase touch targets from 44px → 56px

// 5. Testing
✓ Visual regression screenshots
✓ Color contrast check (WCAG)
✓ Animation smoothness (60fps)
✓ Focus visibility at distance

Summary

This code reference provides:

  1. Complete updated color definitions - Ready to copy/paste into app_colors.dart
  2. Full typography system - All 15+ text styles with exact specifications
  3. Shadow system - 5-level elevation with exact blur/offset values
  4. Animation utilities - Focus, tap, and transition animations
  5. Button implementation - Complete working example with all states
  6. Migration guide - Quick checklist for updating existing code

All code is production-ready and follows Material Design 3 principles with Apple TV adaptations.