mobile bluesky app made with flutter lazurite.stormlightlabs.org/
mobile bluesky flutter
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: shades of purple theme

* add event emitter guard to composer

+481 -66
+12 -1
lib/core/theme/app_theme.dart
··· 3 3 import 'package:lazurite/core/theme/lazurite_theme.dart'; 4 4 import 'package:lazurite/core/theme/nord_theme.dart'; 5 5 import 'package:lazurite/core/theme/oxocarbon_theme.dart'; 6 + import 'package:lazurite/core/theme/purple_theme.dart'; 6 7 import 'package:lazurite/core/theme/rose_pine_theme.dart'; 7 8 8 - enum AppThemePalette { lazurite, oxocarbon, catppuccin, nord, rosePine } 9 + enum AppThemePalette { lazurite, oxocarbon, catppuccin, nord, rosePine, purple } 9 10 10 11 enum AppThemeVariant { light, dark } 11 12 ··· 24 25 return variant == AppThemeVariant.light ? NordTheme.light() : NordTheme.dark(); 25 26 case AppThemePalette.rosePine: 26 27 return variant == AppThemeVariant.light ? RosePineTheme.light() : RosePineTheme.dark(); 28 + case AppThemePalette.purple: 29 + return variant == AppThemeVariant.light ? PurpleTheme.light() : PurpleTheme.dark(); 27 30 } 28 31 } 29 32 ··· 39 42 return 'Nord'; 40 43 case AppThemePalette.rosePine: 41 44 return 'Rosé Pine'; 45 + case AppThemePalette.purple: 46 + return 'Purple'; 42 47 } 43 48 } 44 49 ··· 63 68 return AppThemePalette.nord; 64 69 case 'rosePine': 65 70 return AppThemePalette.rosePine; 71 + case 'purple': 72 + return AppThemePalette.purple; 66 73 default: 67 74 return AppThemePalette.lazurite; 68 75 } ··· 80 87 return 'nord'; 81 88 case AppThemePalette.rosePine: 82 89 return 'rosePine'; 90 + case AppThemePalette.purple: 91 + return 'purple'; 83 92 } 84 93 } 85 94 ··· 115 124 return const [Color(0xFF88c0d0), Color(0xFFa3be8c), Color(0xFFebcb8b), Color(0xFFb48ead)]; 116 125 case AppThemePalette.rosePine: 117 126 return const [Color(0xFFebbcba), Color(0xFFc4a7e7), Color(0xFF9ccfd8), Color(0xFFf6c177)]; 127 + case AppThemePalette.purple: 128 + return const [Color(0xFFA599E9), Color(0xFFB362FF), Color(0xFF9EFFFF), Color(0xFFFAD000)]; 118 129 } 119 130 } 120 131 }
+185
lib/core/theme/purple_theme.dart
··· 1 + import 'package:flutter/material.dart'; 2 + import 'package:lazurite/core/theme/typography.dart'; 3 + 4 + /// Shades of Purple — dark theme by Alexander Keliris (Rigellute). 5 + /// https://github.com/Rigellute/shades-of-purple.vim 6 + class PurpleTheme { 7 + PurpleTheme._(); 8 + 9 + // ── Dark palette ────────────────────────────────────────────────────────── 10 + static const Color sop0 = Color(0xFF1E1E3F); // darkest bg (ColorColumn, WildMenu) 11 + static const Color sop1 = Color(0xFF28284E); // panel bg (LineNr bg, VertSplit bg) 12 + static const Color sop2 = Color(0xFF2D2B55); // main bg (Normal bg) 13 + static const Color sop3 = Color(0xFFA599E9); // muted lavender (LineNr fg, NonText) 14 + static const Color sop4 = Color(0xFFE1EFFF); // main fg (Normal fg) 15 + static const Color sop5 = Color(0xFF9EFFFF); // cyan (Special, Title) 16 + static const Color sop6 = Color(0xFFFAD000); // yellow (Cursor, WarningMsg) 17 + static const Color sop7 = Color(0xFFFF9D00); // orange (Function, Identifier) 18 + static const Color sop8 = Color(0xFFB362FF); // vivid purple (Comment) 19 + static const Color sop9 = Color(0xFFFF628C); // pink-rose (Constant, SpellBad) 20 + static const Color sop10 = Color(0xFFA5FF90); // green (String) 21 + static const Color sop11 = Color(0xFFEC3A37); // red (Error, DiffDelete) 22 + static const Color sop12 = Color(0xFF80FFBB); // teal (Type) 23 + static const Color sop13 = Color(0xFFFB94FF); // light magenta (jsThis, jsFunction) 24 + static const Color sop14 = Color(0xFF6943FF); // blue (terminal blue) 25 + 26 + // Semi-transparent overlay for subtle dark-mode borders/dividers 27 + static const Color darkOutlineVariant = Color(0x26A599E9); // lavender ~15% opacity 28 + 29 + // ── Light palette ───────────────────────────────────────────────────────── 30 + static const Color sopL0 = Color(0xFFF8F6FF); // lightest bg (scaffold) 31 + static const Color sopL1 = Color(0xFFEDE9FF); // panel bg (cards, surfaces) 32 + static const Color sopL2 = Color(0xFFD6CEFF); // border / divider 33 + static const Color sopL3 = Color(0xFF8B7FD4); // muted purple (secondary text) 34 + static const Color sopL4 = Color(0xFF2D2B55); // main fg (body text = sop2) 35 + static const Color sopL5 = Color(0xFF6943FF); // primary accent (sop14) 36 + static const Color sopL6 = Color(0xFF7B6EC0); // secondary accent 37 + 38 + // ── Dark theme ──────────────────────────────────────────────────────────── 39 + static ThemeData dark() { 40 + return ThemeData( 41 + useMaterial3: true, 42 + brightness: Brightness.dark, 43 + colorScheme: const ColorScheme( 44 + brightness: Brightness.dark, 45 + primary: sop3, 46 + onPrimary: sop0, 47 + primaryContainer: sop1, 48 + onPrimaryContainer: sop4, 49 + secondary: sop5, 50 + onSecondary: sop0, 51 + secondaryContainer: sop1, 52 + onSecondaryContainer: sop4, 53 + tertiary: sop8, 54 + onTertiary: sop0, 55 + error: sop11, 56 + onError: sop4, 57 + errorContainer: sop1, 58 + onErrorContainer: sop4, 59 + surface: sop1, 60 + onSurface: sop4, 61 + surfaceContainerHighest: sop2, 62 + outline: sop3, 63 + outlineVariant: darkOutlineVariant, 64 + ), 65 + scaffoldBackgroundColor: sop0, 66 + appBarTheme: AppBarTheme( 67 + backgroundColor: sop0, 68 + foregroundColor: sop4, 69 + surfaceTintColor: sop3, 70 + titleTextStyle: AppTypography.googleSans(fontSize: 18, fontWeight: FontWeight.w600, color: sop4), 71 + ), 72 + cardTheme: const CardThemeData(color: sop1, surfaceTintColor: sop3), 73 + dividerTheme: const DividerThemeData(color: darkOutlineVariant), 74 + iconTheme: const IconThemeData(color: sop3), 75 + listTileTheme: ListTileThemeData( 76 + textColor: sop4, 77 + iconColor: sop3, 78 + titleTextStyle: AppTypography.googleSans(fontSize: 16, fontWeight: FontWeight.w500, color: sop4), 79 + subtitleTextStyle: AppTypography.googleSans(fontSize: 14, color: sop3), 80 + ), 81 + textTheme: AppTypography.textTheme(bodyColor: sop4, headlineColor: sop4, captionColor: sop3), 82 + floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: sop3, foregroundColor: sop0), 83 + elevatedButtonTheme: ElevatedButtonThemeData( 84 + style: ElevatedButton.styleFrom( 85 + backgroundColor: sop3, 86 + foregroundColor: sop0, 87 + textStyle: AppTypography.googleSans(fontSize: 14, fontWeight: FontWeight.w500), 88 + ), 89 + ), 90 + textButtonTheme: TextButtonThemeData( 91 + style: TextButton.styleFrom( 92 + foregroundColor: sop3, 93 + textStyle: AppTypography.googleSans(fontSize: 14, fontWeight: FontWeight.w500), 94 + ), 95 + ), 96 + inputDecorationTheme: InputDecorationTheme( 97 + filled: true, 98 + fillColor: sop1, 99 + border: const OutlineInputBorder(borderSide: BorderSide(color: sop1)), 100 + enabledBorder: const OutlineInputBorder(borderSide: BorderSide(color: sop1)), 101 + focusedBorder: const OutlineInputBorder(borderSide: BorderSide(color: sop3)), 102 + labelStyle: AppTypography.googleSans(color: sop3), 103 + hintStyle: AppTypography.googleSans(color: sop3), 104 + ), 105 + snackBarTheme: SnackBarThemeData( 106 + backgroundColor: sop1, 107 + contentTextStyle: AppTypography.googleSans(color: sop4), 108 + ), 109 + ); 110 + } 111 + 112 + // ── Light theme ─────────────────────────────────────────────────────────── 113 + static ThemeData light() { 114 + return ThemeData( 115 + useMaterial3: true, 116 + brightness: Brightness.light, 117 + colorScheme: const ColorScheme( 118 + brightness: Brightness.light, 119 + primary: sopL5, 120 + onPrimary: sopL0, 121 + primaryContainer: sopL1, 122 + onPrimaryContainer: sopL4, 123 + secondary: sopL6, 124 + onSecondary: sopL0, 125 + secondaryContainer: sopL1, 126 + onSecondaryContainer: sopL4, 127 + tertiary: sop8, 128 + onTertiary: sopL0, 129 + error: sop11, 130 + onError: sopL0, 131 + errorContainer: sopL1, 132 + onErrorContainer: sopL4, 133 + surface: sopL1, 134 + onSurface: sopL4, 135 + surfaceContainerHighest: sopL2, 136 + outline: sopL3, 137 + outlineVariant: sopL2, 138 + ), 139 + scaffoldBackgroundColor: sopL0, 140 + appBarTheme: AppBarTheme( 141 + backgroundColor: sopL0, 142 + foregroundColor: sopL4, 143 + surfaceTintColor: sopL5, 144 + titleTextStyle: AppTypography.googleSans(fontSize: 18, fontWeight: FontWeight.w600, color: sopL4), 145 + ), 146 + cardTheme: const CardThemeData(color: sopL1, surfaceTintColor: sopL5), 147 + dividerTheme: const DividerThemeData(color: sopL2), 148 + iconTheme: const IconThemeData(color: sopL3), 149 + listTileTheme: ListTileThemeData( 150 + textColor: sopL4, 151 + iconColor: sopL3, 152 + titleTextStyle: AppTypography.googleSans(fontSize: 16, fontWeight: FontWeight.w500, color: sopL4), 153 + subtitleTextStyle: AppTypography.googleSans(fontSize: 14, color: sopL3), 154 + ), 155 + textTheme: AppTypography.textTheme(bodyColor: sopL4, headlineColor: sopL4, captionColor: sopL3), 156 + floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: sopL5, foregroundColor: sopL0), 157 + elevatedButtonTheme: ElevatedButtonThemeData( 158 + style: ElevatedButton.styleFrom( 159 + backgroundColor: sopL5, 160 + foregroundColor: sopL0, 161 + textStyle: AppTypography.googleSans(fontSize: 14, fontWeight: FontWeight.w500), 162 + ), 163 + ), 164 + textButtonTheme: TextButtonThemeData( 165 + style: TextButton.styleFrom( 166 + foregroundColor: sopL5, 167 + textStyle: AppTypography.googleSans(fontSize: 14, fontWeight: FontWeight.w500), 168 + ), 169 + ), 170 + inputDecorationTheme: InputDecorationTheme( 171 + filled: true, 172 + fillColor: sopL1, 173 + border: const OutlineInputBorder(borderSide: BorderSide(color: sopL2)), 174 + enabledBorder: const OutlineInputBorder(borderSide: BorderSide(color: sopL2)), 175 + focusedBorder: const OutlineInputBorder(borderSide: BorderSide(color: sopL5)), 176 + labelStyle: AppTypography.googleSans(color: sopL3), 177 + hintStyle: AppTypography.googleSans(color: sopL3), 178 + ), 179 + snackBarTheme: SnackBarThemeData( 180 + backgroundColor: sopL1, 181 + contentTextStyle: AppTypography.googleSans(color: sopL4), 182 + ), 183 + ); 184 + } 185 + }
+3
lib/features/compose/bloc/compose_bloc.dart
··· 62 62 63 63 Future<void> _onTextChanged(TextChanged event, Emitter<ComposeState> emit) async { 64 64 final text = event.text; 65 + if (text == state.text) { 66 + return; 67 + } 65 68 final graphemeCount = text.characters.length; 66 69 final isOverLimit = graphemeCount > kMaxGraphemes; 67 70 final isEmpty = text.trim().isEmpty && state.mediaAttachments.isEmpty && state.videoAttachment == null;
+6 -1
lib/features/compose/presentation/compose_screen.dart
··· 108 108 } 109 109 110 110 void _onTextChanged() { 111 - context.read<ComposeBloc>().add(TextChanged(_textController.text)); 111 + final bloc = context.read<ComposeBloc>(); 112 + final text = _textController.text; 113 + if (bloc.state.text == text) { 114 + return; 115 + } 116 + bloc.add(TextChanged(text)); 112 117 } 113 118 114 119 Future<void> _pickImage() async {
+65 -57
lib/features/feed/presentation/widgets/grid_post_card.dart
··· 70 70 ), 71 71 child: ModeratedBlurOverlay( 72 72 ui: postUi, 73 - child: InkWell( 74 - onTap: onTap, 75 - child: Column( 76 - crossAxisAlignment: CrossAxisAlignment.start, 77 - children: [ 78 - if (primaryImageUrl != null) 79 - ModeratedBlurOverlay( 80 - ui: mediaUi, 81 - fillWidth: false, 82 - child: AspectRatio( 83 - aspectRatio: 1.0, 84 - child: ColorFiltered( 85 - colorFilter: AppColorFilters.greyscale, 86 - child: Image.network( 87 - primaryImageUrl, 88 - fit: BoxFit.cover, 89 - width: double.infinity, 90 - errorBuilder: (_, _, _) => 91 - ColoredBox(color: colorScheme.surfaceContainerHigh, child: const SizedBox.expand()), 73 + child: Column( 74 + crossAxisAlignment: CrossAxisAlignment.start, 75 + children: [ 76 + InkWell( 77 + onTap: onTap, 78 + child: Column( 79 + crossAxisAlignment: CrossAxisAlignment.start, 80 + children: [ 81 + if (primaryImageUrl != null) 82 + ModeratedBlurOverlay( 83 + ui: mediaUi, 84 + fillWidth: false, 85 + child: AspectRatio( 86 + aspectRatio: 1.0, 87 + child: ColorFiltered( 88 + colorFilter: AppColorFilters.greyscale, 89 + child: Image.network( 90 + primaryImageUrl, 91 + fit: BoxFit.cover, 92 + width: double.infinity, 93 + errorBuilder: (_, _, _) => 94 + ColoredBox(color: colorScheme.surfaceContainerHigh, child: const SizedBox.expand()), 95 + ), 96 + ), 92 97 ), 93 98 ), 99 + Padding( 100 + padding: AppInsets.allMd, 101 + child: Column( 102 + crossAxisAlignment: CrossAxisAlignment.start, 103 + children: [ 104 + _buildAuthorRow(context, post.author), 105 + if (postUi.alert || postUi.inform) ...[ 106 + const SizedBox(height: 10), 107 + ModerationBadgeRow(ui: postUi), 108 + ], 109 + if (bodyText.isNotEmpty) ...[ 110 + const SizedBox(height: AppSpacing.xs), 111 + if (primaryImageUrl == null && contentEmbed == null) 112 + FacetText( 113 + text: bodyText, 114 + facets: record?.facets, 115 + style: feedPostBodyTextStyle(context), 116 + maxLines: 6, 117 + overflow: TextOverflow.ellipsis, 118 + ) 119 + else if (!isCompactGrid) 120 + FacetText(text: bodyText, facets: record?.facets, style: feedPostBodyTextStyle(context)) 121 + else 122 + FacetText( 123 + text: bodyText, 124 + facets: record?.facets, 125 + style: feedPostBodyTextStyle(context, compact: true), 126 + maxLines: 2, 127 + overflow: TextOverflow.ellipsis, 128 + ), 129 + ], 130 + if (contentEmbed != null) ...[ 131 + const SizedBox(height: AppSpacing.xs), 132 + _buildEmbedPreview(contentEmbed, compact: isCompactGrid), 133 + ], 134 + ], 135 + ), 94 136 ), 95 - ), 96 - Padding( 97 - padding: AppInsets.allMd, 98 - child: Column( 99 - crossAxisAlignment: CrossAxisAlignment.start, 100 - children: [ 101 - _buildAuthorRow(context, post.author), 102 - if (postUi.alert || postUi.inform) ...[const SizedBox(height: 10), ModerationBadgeRow(ui: postUi)], 103 - if (bodyText.isNotEmpty) ...[ 104 - const SizedBox(height: AppSpacing.xs), 105 - if (primaryImageUrl == null && contentEmbed == null) 106 - FacetText( 107 - text: bodyText, 108 - facets: record?.facets, 109 - style: feedPostBodyTextStyle(context), 110 - maxLines: 6, 111 - overflow: TextOverflow.ellipsis, 112 - ) 113 - else if (!isCompactGrid) 114 - FacetText(text: bodyText, facets: record?.facets, style: feedPostBodyTextStyle(context)) 115 - else 116 - FacetText( 117 - text: bodyText, 118 - facets: record?.facets, 119 - style: feedPostBodyTextStyle(context, compact: true), 120 - maxLines: 2, 121 - overflow: TextOverflow.ellipsis, 122 - ), 123 - ], 124 - if (contentEmbed != null) ...[ 125 - const SizedBox(height: AppSpacing.xs), 126 - _buildEmbedPreview(contentEmbed, compact: isCompactGrid), 127 - ], 128 - ], 129 - ), 137 + ], 130 138 ), 131 - resolvedFooter, 132 - ], 133 - ), 139 + ), 140 + resolvedFooter, 141 + ], 134 142 ), 135 143 ), 136 144 );
+12
test/core/theme/app_theme_test.dart
··· 23 23 test('returns correct name for rosePine', () { 24 24 expect(AppTheme.getPaletteName(AppThemePalette.rosePine), 'Rosé Pine'); 25 25 }); 26 + 27 + test('returns correct name for purple', () { 28 + expect(AppTheme.getPaletteName(AppThemePalette.purple), 'Purple'); 29 + }); 26 30 }); 27 31 28 32 group('getVariantName', () { ··· 56 60 expect(AppTheme.parsePalette('rosePine'), AppThemePalette.rosePine); 57 61 }); 58 62 63 + test('parses purple', () { 64 + expect(AppTheme.parsePalette('purple'), AppThemePalette.purple); 65 + }); 66 + 59 67 test('returns lazurite for null', () { 60 68 expect(AppTheme.parsePalette(null), AppThemePalette.lazurite); 61 69 }); ··· 84 92 85 93 test('converts rosePine to string', () { 86 94 expect(AppTheme.paletteToString(AppThemePalette.rosePine), 'rosePine'); 95 + }); 96 + 97 + test('converts purple to string', () { 98 + expect(AppTheme.paletteToString(AppThemePalette.purple), 'purple'); 87 99 }); 88 100 }); 89 101
+159
test/core/theme/purple_theme_test.dart
··· 1 + import 'package:flutter/material.dart'; 2 + import 'package:flutter_test/flutter_test.dart'; 3 + import 'package:lazurite/core/theme/purple_theme.dart'; 4 + 5 + void main() { 6 + group('PurpleTheme', () { 7 + group('Dark color values', () { 8 + test('sop0 is #1E1E3F', () { 9 + expect(PurpleTheme.sop0, const Color(0xFF1E1E3F)); 10 + }); 11 + 12 + test('sop1 is #28284E', () { 13 + expect(PurpleTheme.sop1, const Color(0xFF28284E)); 14 + }); 15 + 16 + test('sop2 is #2D2B55', () { 17 + expect(PurpleTheme.sop2, const Color(0xFF2D2B55)); 18 + }); 19 + 20 + test('sop3 is #A599E9', () { 21 + expect(PurpleTheme.sop3, const Color(0xFFA599E9)); 22 + }); 23 + 24 + test('sop4 is #E1EFFF', () { 25 + expect(PurpleTheme.sop4, const Color(0xFFE1EFFF)); 26 + }); 27 + 28 + test('sop5 is #9EFFFF', () { 29 + expect(PurpleTheme.sop5, const Color(0xFF9EFFFF)); 30 + }); 31 + 32 + test('sop6 is #FAD000', () { 33 + expect(PurpleTheme.sop6, const Color(0xFFFAD000)); 34 + }); 35 + 36 + test('sop7 is #FF9D00', () { 37 + expect(PurpleTheme.sop7, const Color(0xFFFF9D00)); 38 + }); 39 + 40 + test('sop8 is #B362FF', () { 41 + expect(PurpleTheme.sop8, const Color(0xFFB362FF)); 42 + }); 43 + 44 + test('sop9 is #FF628C', () { 45 + expect(PurpleTheme.sop9, const Color(0xFFFF628C)); 46 + }); 47 + 48 + test('sop10 is #A5FF90', () { 49 + expect(PurpleTheme.sop10, const Color(0xFFA5FF90)); 50 + }); 51 + 52 + test('sop11 is #EC3A37', () { 53 + expect(PurpleTheme.sop11, const Color(0xFFEC3A37)); 54 + }); 55 + 56 + test('sop12 is #80FFBB', () { 57 + expect(PurpleTheme.sop12, const Color(0xFF80FFBB)); 58 + }); 59 + 60 + test('sop13 is #FB94FF', () { 61 + expect(PurpleTheme.sop13, const Color(0xFFFB94FF)); 62 + }); 63 + 64 + test('sop14 is #6943FF', () { 65 + expect(PurpleTheme.sop14, const Color(0xFF6943FF)); 66 + }); 67 + 68 + test('darkOutlineVariant is lavender at ~15% opacity', () { 69 + expect(PurpleTheme.darkOutlineVariant, const Color(0x26A599E9)); 70 + }); 71 + }); 72 + 73 + group('Light color values', () { 74 + test('sopL0 is #F8F6FF', () { 75 + expect(PurpleTheme.sopL0, const Color(0xFFF8F6FF)); 76 + }); 77 + 78 + test('sopL1 is #EDE9FF', () { 79 + expect(PurpleTheme.sopL1, const Color(0xFFEDE9FF)); 80 + }); 81 + 82 + test('sopL2 is #D6CEFF', () { 83 + expect(PurpleTheme.sopL2, const Color(0xFFD6CEFF)); 84 + }); 85 + 86 + test('sopL3 is #8B7FD4', () { 87 + expect(PurpleTheme.sopL3, const Color(0xFF8B7FD4)); 88 + }); 89 + 90 + test('sopL4 is #2D2B55', () { 91 + expect(PurpleTheme.sopL4, const Color(0xFF2D2B55)); 92 + }); 93 + 94 + test('sopL5 is #6943FF', () { 95 + expect(PurpleTheme.sopL5, const Color(0xFF6943FF)); 96 + }); 97 + 98 + test('sopL6 is #7B6EC0', () { 99 + expect(PurpleTheme.sopL6, const Color(0xFF7B6EC0)); 100 + }); 101 + }); 102 + 103 + group('ThemeData', () { 104 + test('dark theme maps expected tokens', () { 105 + final theme = PurpleTheme.dark(); 106 + final scheme = theme.colorScheme; 107 + 108 + expect(theme.useMaterial3, isTrue); 109 + expect(theme.brightness, Brightness.dark); 110 + expect(scheme.primary, PurpleTheme.sop3); 111 + expect(scheme.secondary, PurpleTheme.sop5); 112 + expect(scheme.tertiary, PurpleTheme.sop8); 113 + expect(scheme.surface, PurpleTheme.sop1); 114 + expect(scheme.onSurface, PurpleTheme.sop4); 115 + expect(scheme.surfaceContainerHighest, PurpleTheme.sop2); 116 + expect(scheme.outline, PurpleTheme.sop3); 117 + expect(scheme.outlineVariant, PurpleTheme.darkOutlineVariant); 118 + expect(scheme.error, PurpleTheme.sop11); 119 + expect(theme.scaffoldBackgroundColor, PurpleTheme.sop0); 120 + expect(theme.appBarTheme.backgroundColor, PurpleTheme.sop0); 121 + expect(theme.cardTheme.color, PurpleTheme.sop1); 122 + expect(theme.dividerTheme.color, PurpleTheme.darkOutlineVariant); 123 + expect(theme.iconTheme.color, PurpleTheme.sop3); 124 + expect(theme.listTileTheme.textColor, PurpleTheme.sop4); 125 + expect(theme.floatingActionButtonTheme.backgroundColor, PurpleTheme.sop3); 126 + expect(theme.inputDecorationTheme.filled, isTrue); 127 + expect(theme.inputDecorationTheme.fillColor, PurpleTheme.sop1); 128 + expect(theme.snackBarTheme.backgroundColor, PurpleTheme.sop1); 129 + }); 130 + 131 + test('light theme maps expected tokens', () { 132 + final theme = PurpleTheme.light(); 133 + final scheme = theme.colorScheme; 134 + 135 + expect(theme.useMaterial3, isTrue); 136 + expect(theme.brightness, Brightness.light); 137 + expect(scheme.primary, PurpleTheme.sopL5); 138 + expect(scheme.secondary, PurpleTheme.sopL6); 139 + expect(scheme.tertiary, PurpleTheme.sop8); 140 + expect(scheme.surface, PurpleTheme.sopL1); 141 + expect(scheme.onSurface, PurpleTheme.sopL4); 142 + expect(scheme.surfaceContainerHighest, PurpleTheme.sopL2); 143 + expect(scheme.outline, PurpleTheme.sopL3); 144 + expect(scheme.outlineVariant, PurpleTheme.sopL2); 145 + expect(scheme.error, PurpleTheme.sop11); 146 + expect(theme.scaffoldBackgroundColor, PurpleTheme.sopL0); 147 + expect(theme.appBarTheme.backgroundColor, PurpleTheme.sopL0); 148 + expect(theme.cardTheme.color, PurpleTheme.sopL1); 149 + expect(theme.dividerTheme.color, PurpleTheme.sopL2); 150 + expect(theme.iconTheme.color, PurpleTheme.sopL3); 151 + expect(theme.listTileTheme.textColor, PurpleTheme.sopL4); 152 + expect(theme.floatingActionButtonTheme.backgroundColor, PurpleTheme.sopL5); 153 + expect(theme.inputDecorationTheme.filled, isTrue); 154 + expect(theme.inputDecorationTheme.fillColor, PurpleTheme.sopL1); 155 + expect(theme.snackBarTheme.backgroundColor, PurpleTheme.sopL1); 156 + }); 157 + }); 158 + }); 159 + }
+10 -7
test/features/compose/bloc/compose_bloc_test.dart
··· 145 145 ); 146 146 147 147 blocTest<ComposeBloc, ComposeState>( 148 + 'does not emit when text is unchanged', 149 + build: () => composeBloc, 150 + seed: () => const ComposeState.ready(text: 'Hello world', graphemeCount: 11, isEmpty: false), 151 + act: (bloc) => bloc.add(const TextChanged('Hello world')), 152 + expect: () => <ComposeState>[], 153 + ); 154 + 155 + blocTest<ComposeBloc, ComposeState>( 148 156 'emits overLimit when text exceeds 300 graphemes', 149 157 build: () => composeBloc, 150 158 act: (bloc) => bloc.add(TextChanged('a' * 301)), ··· 157 165 ); 158 166 159 167 blocTest<ComposeBloc, ComposeState>( 160 - 'isEmpty is true when text is empty and no media', 168 + 'does not emit when empty text is unchanged', 161 169 build: () => composeBloc, 162 170 act: (bloc) => bloc.add(const TextChanged('')), 163 - expect: () => [ 164 - isA<ComposeState>() 165 - .having((s) => s.text, 'text', '') 166 - .having((s) => s.isEmpty, 'isEmpty', true) 167 - .having((s) => s.canSubmit, 'canSubmit', false), 168 - ], 171 + expect: () => <ComposeState>[], 169 172 ); 170 173 }); 171 174
+29
test/features/feed/presentation/grid_post_card_test.dart
··· 82 82 expect(tapped, isTrue); 83 83 }); 84 84 85 + testWidgets('tapping footer reply does not trigger card onTap', (tester) async { 86 + var cardTapped = false; 87 + var replyTapped = false; 88 + final post = _makePost(); 89 + 90 + await tester.pumpWidget( 91 + MaterialApp( 92 + home: MediaQuery( 93 + data: const MediaQueryData(size: Size(390, 844)), 94 + child: Scaffold( 95 + body: SingleChildScrollView( 96 + child: GridPostCard( 97 + feedViewPost: post, 98 + onTap: () => cardTapped = true, 99 + footer: PostCardFooter(timestamp: '1H', onReply: () => replyTapped = true), 100 + ), 101 + ), 102 + ), 103 + ), 104 + ), 105 + ); 106 + 107 + await tester.tap(find.byIcon(Icons.chat_bubble_outline)); 108 + await tester.pump(); 109 + 110 + expect(replyTapped, isTrue); 111 + expect(cardTapped, isFalse); 112 + }); 113 + 85 114 testWidgets('text-only posts have no image AspectRatio', (tester) async { 86 115 final post = _makePost(text: 'Text-only post content'); 87 116 await tester.pumpWidget(_buildSubject(post));