diff --git a/docs/deploy-checklist.md b/docs/deploy-checklist.md index 01a2146..0b20399 100644 --- a/docs/deploy-checklist.md +++ b/docs/deploy-checklist.md @@ -38,12 +38,11 @@ Phase 1 **A~C** 이후 실행 트랙. 작업 단위를 하나씩 처리한다. - 앱 코드는 클로즈드 베타 직전 수준 - **`https://msn.iykyka.com` 라이브 + N3 완료 + Gemini 실초안 OK + Track A/B 완료** - 진행 중: **N4 FCM 코드 경로** → Master 시크릿 대기 → Android UI QA -- UI: **Soft Neutral + surface hierarchy** 프로덕션 반영 (`e05a0f0`, 2026-08-03) — - GitHub+Gitea `main` 듀얼 푸시 · `msn.iykyka.com` web 재빌드 완료 -- **Track C 콘텐츠 갭**: C1(단톡 따라잡기) **완료** (2026-08-03, 아직 GitHub `main`에만 있고 - 프로덕션 미배포) — 그룹 생성 UI, 안 본 동안 요약(`GET /conversations/:id/summary`), 읽음 - 마커, 안 본 배지, 그룹 트윈 발송 서버측 차단까지. 다음은 C2(관계별 페르소나) → C3(스팸 감지). - Master 액션(FCM 시크릿, 실기기 탭, 웹 재배포)과 별개로 계속 진행 가능 +- UI: **iMessage-inspired light default** + soft charcoal dark — + 기본 `ThemeMode.light`, 내 버블 `#007AFF`, 다크 캔버스 `#141418` (배포 진행) +- **Track C 콘텐츠 갭**: C1(단톡 따라잡기) **완료** (2026-08-03) — 그룹 생성 UI, + 안 본 동안 요약(`GET /conversations/:id/summary`), 읽음 마커, 안 본 배지, + 그룹 트윈 발송 서버측 차단까지. 다음은 C2(관계별 페르소나) → C3(스팸 감지). - 실 FCM 기기 수신 · Android 실기기 탭 · 사람 PoC 실행은 남음 ### NEXT 순서 diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index c57a988..8f69ffa 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -89,7 +89,8 @@ class _BootstrapState extends State<_Bootstrap> { debugShowCheckedModeBanner: false, theme: AppTheme.light(), darkTheme: AppTheme.dark(), - themeMode: ThemeMode.system, + // Default to light messenger UI; OS dark still maps to soft charcoal. + themeMode: ThemeMode.light, builder: (context, child) => GradientBackdrop(child: child), home: const SplashScreen(), ); @@ -112,7 +113,8 @@ class YkavuApp extends StatelessWidget { debugShowCheckedModeBanner: false, theme: AppTheme.light(), darkTheme: AppTheme.dark(), - themeMode: ThemeMode.system, + // Default to light messenger UI; OS dark still maps to soft charcoal. + themeMode: ThemeMode.light, builder: (context, child) => GradientBackdrop(child: child), home: Consumer( builder: (context, s, _) { diff --git a/mobile/lib/screens/chat_screen.dart b/mobile/lib/screens/chat_screen.dart index 849a87f..dc9e3d8 100644 --- a/mobile/lib/screens/chat_screen.dart +++ b/mobile/lib/screens/chat_screen.dart @@ -492,30 +492,43 @@ class _ChatScreenState extends State { ), _buildL1Panel(context), SafeArea( - child: Padding( - padding: const EdgeInsets.fromLTRB(12, 8, 12, 12), + child: Container( + padding: const EdgeInsets.fromLTRB(10, 8, 10, 10), + decoration: BoxDecoration( + color: AppTheme.glassFill(theme.brightness), + border: Border( + top: BorderSide(color: AppTheme.glassBorder(theme.brightness).withValues(alpha: 0.7)), + ), + ), child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ + IconButton( + tooltip: '초안 요청', + onPressed: _busy ? null : _requestDraft, + icon: const Icon(Icons.auto_awesome_outlined), + ), Expanded( child: TextField( controller: _input, minLines: 1, maxLines: 4, + textInputAction: TextInputAction.send, + onSubmitted: (_) { + if (!_busy) _sendHuman(); + }, decoration: const InputDecoration(hintText: '메시지'), ), ), - const SizedBox(width: 8), - IconButton.filledTonal( - tooltip: '초안 요청', - onPressed: _busy ? null : _requestDraft, - icon: const Icon(Icons.auto_awesome), - ), - const SizedBox(width: 4), + const SizedBox(width: 6), IconButton.filled( tooltip: '보내기', onPressed: _busy ? null : _sendHuman, - icon: const Icon(Icons.send), + style: IconButton.styleFrom( + backgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.onPrimary, + ), + icon: const Icon(Icons.arrow_upward_rounded), ), ], ), diff --git a/mobile/lib/screens/conversation_list_screen.dart b/mobile/lib/screens/conversation_list_screen.dart index aabd03e..e8a367b 100644 --- a/mobile/lib/screens/conversation_list_screen.dart +++ b/mobile/lib/screens/conversation_list_screen.dart @@ -205,16 +205,16 @@ class _ConversationListScreenState extends State { Color _avatarColor(BuildContext context, int seed) { final isDark = Theme.of(context).brightness == Brightness.dark; final palette = isDark - ? const [Color(0xFF1C1C1C), Color(0xFF22262E), Color(0xFF242018)] - : const [Color(0xFFF4F4F5), Color(0xFFE8EEF4), Color(0xFFF3F0EA)]; + ? const [Color(0xFF2C2C34), Color(0xFF243447), Color(0xFF3A3224)] + : const [Color(0xFFE9E9EB), Color(0xFFD6E8FF), Color(0xFFFFF1D6)]; return palette[seed % palette.length]; } Color _onAvatarColor(BuildContext context, int seed) { final isDark = Theme.of(context).brightness == Brightness.dark; final palette = isDark - ? const [Color(0xFFA3A3A3), Color(0xFF8BB4D9), Color(0xFFC4A574)] - : const [Color(0xFF525252), Color(0xFF3B6D9B), Color(0xFF9A7B4F)]; + ? const [Color(0xFFF2F2F7), Color(0xFF0A84FF), Color(0xFFD4B06A)] + : const [Color(0xFF1C1C1E), Color(0xFF007AFF), Color(0xFFB08A4A)]; return palette[seed % palette.length]; } diff --git a/mobile/lib/screens/signup_screen.dart b/mobile/lib/screens/signup_screen.dart index cbcb2ae..cff5242 100644 --- a/mobile/lib/screens/signup_screen.dart +++ b/mobile/lib/screens/signup_screen.dart @@ -150,7 +150,7 @@ class _DemoTestPanel extends StatelessWidget { Text( '테스트용 (누구나)', style: theme.textTheme.labelLarge?.copyWith( - color: theme.colorScheme.onSurfaceVariant, + color: theme.colorScheme.primary, letterSpacing: 0.15, fontWeight: FontWeight.w600, ), diff --git a/mobile/lib/screens/splash_screen.dart b/mobile/lib/screens/splash_screen.dart index e43ad4a..5e1d39b 100644 --- a/mobile/lib/screens/splash_screen.dart +++ b/mobile/lib/screens/splash_screen.dart @@ -36,8 +36,8 @@ class SplashScreen extends StatelessWidget { width: 22, height: 22, child: CircularProgressIndicator( - strokeWidth: 2, - color: scheme.onSurfaceVariant, + strokeWidth: 2.2, + color: scheme.primary, ), ), ], diff --git a/mobile/lib/theme/app_theme.dart b/mobile/lib/theme/app_theme.dart index 878bd6e..58308f1 100644 --- a/mobile/lib/theme/app_theme.dart +++ b/mobile/lib/theme/app_theme.dart @@ -1,36 +1,36 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; -/// Soft Neutral + surface hierarchy for 와카뷰. -/// Neutrals dominate; accent is thin (mine bubbles / small links only). -/// Radii: input/button 12, bubble 18. No glow, almost no shadow. +/// Messenger UI for 와카뷰 — iMessage-inspired light default + soft charcoal dark. +/// Light: white stage, blue mine bubbles, gray peer bubbles. +/// Dark: elevated charcoal (not pure black), softer blue accents. class AppTheme { AppTheme._(); - // Light neutrals - static const canvasLight = Color(0xFFFAFAF9); + // Light — iOS Messages / white messenger + static const canvasLight = Color(0xFFF2F2F7); static const surfaceLight = Color(0xFFFFFFFF); - static const inkLight = Color(0xFF171717); - static const mutedLight = Color(0xFF737373); - static const lineLight = Color(0xFFE5E5E5); - static const softFillLight = Color(0xFFF4F4F5); + static const inkLight = Color(0xFF1C1C1E); + static const mutedLight = Color(0xFF8E8E93); + static const lineLight = Color(0xFFD1D1D6); + static const softFillLight = Color(0xFFE9E9EB); - // Dark neutrals - static const canvasDark = Color(0xFF0A0A0A); - static const surfaceDark = Color(0xFF141414); - static const inkDark = Color(0xFFFAFAFA); - static const mutedDark = Color(0xFFA3A3A3); - static const lineDark = Color(0xFF262626); - static const softFillDark = Color(0xFF1C1C1C); + // Dark — soft charcoal (not #000) + static const canvasDark = Color(0xFF141418); + static const surfaceDark = Color(0xFF1C1C22); + static const inkDark = Color(0xFFF2F2F7); + static const mutedDark = Color(0xFF98989F); + static const lineDark = Color(0xFF2C2C34); + static const softFillDark = Color(0xFF2C2C34); - /// Thin accent — used for mine bubbles & primary actions, not chrome. - static const accentLight = Color(0xFF3B6D9B); - static const accentDark = Color(0xFF8BB4D9); - static const accentSoftLight = Color(0xFFE8F0F7); - static const accentSoftDark = Color(0xFF1A2836); + /// iMessage-like blue for mine bubbles & primary actions. + static const accentLight = Color(0xFF007AFF); + static const accentDark = Color(0xFF0A84FF); + static const accentSoftLight = Color(0xFFD6E8FF); + static const accentSoftDark = Color(0xFF163A66); - static const twinAmberLight = Color(0xFF9A7B4F); - static const twinAmberDark = Color(0xFFC4A574); + static const twinAmberLight = Color(0xFFB08A4A); + static const twinAmberDark = Color(0xFFD4B06A); /// Compat for older call sites. static const teal = accentLight; @@ -44,21 +44,26 @@ class AppTheme { static LinearGradient backgroundGradient(Brightness b) { final c = b == Brightness.dark ? canvasDark : canvasLight; - return LinearGradient(colors: [c, c]); + return LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [c, c], + ); } static Color glassFill(Brightness b) => b == Brightness.dark ? surfaceDark : surfaceLight; static Color glassBorder(Brightness b) => b == Brightness.dark ? lineDark : lineLight; static Color glassShadow(Brightness b) => Colors.transparent; - static Color mineBubble(Brightness b) => b == Brightness.dark ? accentSoftDark : accentLight; - static Color mineBubbleFg(Brightness b) => b == Brightness.dark ? inkDark : Colors.white; + /// Mine = solid messenger blue (both modes). + static Color mineBubble(Brightness b) => b == Brightness.dark ? accentDark : accentLight; + static Color mineBubbleFg(Brightness b) => Colors.white; static Color peerBubble(Brightness b) => b == Brightness.dark ? softFillDark : softFillLight; - static const rInput = 12.0; - static const rButton = 12.0; - static const rBubble = 18.0; - static const rPanel = 12.0; + static const rInput = 20.0; + static const rButton = 14.0; + static const rBubble = 20.0; + static const rPanel = 14.0; static ThemeData light() => _build(Brightness.light); static ThemeData dark() => _build(Brightness.dark); @@ -66,11 +71,10 @@ class AppTheme { static ThemeData _build(Brightness brightness) { final isDark = brightness == Brightness.dark; final primary = isDark ? accentDark : accentLight; - final onPrimary = isDark ? canvasDark : Colors.white; + final onPrimary = Colors.white; final ink = isDark ? inkDark : inkLight; final muted = isDark ? mutedDark : mutedLight; final line = isDark ? lineDark : lineLight; - final canvas = isDark ? canvasDark : canvasLight; final surface = isDark ? surfaceDark : surfaceLight; final soft = isDark ? softFillDark : softFillLight; @@ -86,7 +90,7 @@ class AppTheme { onSecondaryContainer: muted, tertiary: primary, onTertiary: onPrimary, - error: isDark ? const Color(0xFFF87171) : const Color(0xFFB91C1C), + error: isDark ? const Color(0xFFFF6961) : const Color(0xFFFF3B30), onError: Colors.white, surface: surface, onSurface: ink, @@ -109,12 +113,12 @@ class AppTheme { textTheme: textTheme, primaryTextTheme: textTheme, appBarTheme: AppBarTheme( - backgroundColor: canvas.withValues(alpha: 0.92), + backgroundColor: surface.withValues(alpha: isDark ? 0.94 : 0.88), foregroundColor: ink, surfaceTintColor: Colors.transparent, elevation: 0, scrolledUnderElevation: 0, - centerTitle: false, + centerTitle: true, titleTextStyle: textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w600, letterSpacing: -0.2), ), cardTheme: CardThemeData( @@ -122,32 +126,32 @@ class AppTheme { color: surface, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(rPanel), - side: BorderSide(color: line), + side: BorderSide(color: line.withValues(alpha: isDark ? 0.9 : 0.65)), ), margin: EdgeInsets.zero, ), listTileTheme: ListTileThemeData( iconColor: muted, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rPanel)), - contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 2), - minVerticalPadding: 8, + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + minVerticalPadding: 10, ), - dividerTheme: DividerThemeData(color: line, space: 1, thickness: 1), + dividerTheme: DividerThemeData(color: line, space: 1, thickness: 0.5), inputDecorationTheme: InputDecorationTheme( filled: true, - fillColor: surface, - contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + fillColor: isDark ? soft : surface, + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), border: OutlineInputBorder( borderRadius: BorderRadius.circular(rInput), - borderSide: BorderSide(color: line), + borderSide: BorderSide(color: line.withValues(alpha: 0.7)), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(rInput), - borderSide: BorderSide(color: line), + borderSide: BorderSide(color: line.withValues(alpha: 0.7)), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(rInput), - borderSide: BorderSide(color: primary, width: 1.2), + borderSide: BorderSide(color: primary, width: 1.4), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(rInput), @@ -155,29 +159,29 @@ class AppTheme { ), focusedErrorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(rInput), - borderSide: BorderSide(color: scheme.error, width: 1.2), + borderSide: BorderSide(color: scheme.error, width: 1.4), ), - hintStyle: TextStyle(color: muted.withValues(alpha: 0.8)), + hintStyle: TextStyle(color: muted.withValues(alpha: 0.85)), labelStyle: TextStyle(color: muted, fontWeight: FontWeight.w500), ), filledButtonTheme: FilledButtonThemeData( style: FilledButton.styleFrom( - backgroundColor: isDark ? inkDark : inkLight, - foregroundColor: isDark ? canvasDark : Colors.white, + backgroundColor: primary, + foregroundColor: onPrimary, padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), - minimumSize: const Size(0, 46), + minimumSize: const Size(0, 48), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rButton)), - textStyle: const TextStyle(fontWeight: FontWeight.w600, fontSize: 15), + textStyle: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16), elevation: 0, ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( - foregroundColor: ink, + foregroundColor: primary, padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), - minimumSize: const Size(0, 46), + minimumSize: const Size(0, 48), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rButton)), - side: BorderSide(color: line), + side: BorderSide(color: primary.withValues(alpha: 0.45)), ), ), textButtonTheme: TextButtonThemeData( @@ -188,22 +192,22 @@ class AppTheme { ), iconButtonTheme: IconButtonThemeData( style: IconButton.styleFrom( - foregroundColor: muted, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + foregroundColor: primary, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ), chipTheme: ChipThemeData( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), side: BorderSide(color: line), - backgroundColor: surface, - labelStyle: TextStyle(color: muted, fontWeight: FontWeight.w500, fontSize: 12), + backgroundColor: soft, + labelStyle: TextStyle(color: ink, fontWeight: FontWeight.w500, fontSize: 12), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), ), floatingActionButtonTheme: FloatingActionButtonThemeData( - backgroundColor: isDark ? inkDark : inkLight, - foregroundColor: isDark ? canvasDark : Colors.white, + backgroundColor: primary, + foregroundColor: onPrimary, elevation: 0, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rButton)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)), ), snackBarTheme: SnackBarThemeData( behavior: SnackBarBehavior.floating, @@ -216,20 +220,20 @@ class AppTheme { backgroundColor: surface, elevation: 0, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - side: BorderSide(color: line), + borderRadius: BorderRadius.circular(18), + side: BorderSide(color: line.withValues(alpha: 0.8)), ), ), progressIndicatorTheme: ProgressIndicatorThemeData(color: primary), bannerTheme: MaterialBannerThemeData(backgroundColor: surface, padding: const EdgeInsets.all(16)), segmentedButtonTheme: SegmentedButtonThemeData( style: SegmentedButton.styleFrom( - backgroundColor: surface, + backgroundColor: soft, foregroundColor: muted, - selectedForegroundColor: ink, - selectedBackgroundColor: soft, - side: BorderSide(color: line), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rButton)), + selectedForegroundColor: primary, + selectedBackgroundColor: isDark ? accentSoftDark : accentSoftLight, + side: BorderSide.none, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), ), ), @@ -241,16 +245,16 @@ class AppTheme { (t ?? const TextStyle()).copyWith(color: ink, fontWeight: w, fontSize: size, letterSpacing: ls, height: h); return base.copyWith( - displayLarge: s(base.displayLarge, w: FontWeight.w600, size: 36, ls: -1.0), - displaySmall: s(base.displaySmall, w: FontWeight.w600, size: 28, ls: -0.6), - headlineSmall: s(base.headlineSmall, w: FontWeight.w600, size: 20, ls: -0.2), + displayLarge: s(base.displayLarge, w: FontWeight.w700, size: 34, ls: -0.8), + displaySmall: s(base.displaySmall, w: FontWeight.w700, size: 28, ls: -0.5), + headlineSmall: s(base.headlineSmall, w: FontWeight.w600, size: 22, ls: -0.3), titleLarge: s(base.titleLarge, w: FontWeight.w600, size: 17, ls: -0.2), - titleMedium: s(base.titleMedium, w: FontWeight.w600, size: 15), - titleSmall: s(base.titleSmall, w: FontWeight.w600, size: 14), - bodyLarge: s(base.bodyLarge, w: FontWeight.w400, size: 15, h: 1.45), - bodyMedium: s(base.bodyMedium, w: FontWeight.w400, size: 14, h: 1.45), - bodySmall: s(base.bodySmall, w: FontWeight.w400, size: 12, h: 1.4), - labelLarge: s(base.labelLarge, w: FontWeight.w500, size: 13), + titleMedium: s(base.titleMedium, w: FontWeight.w600, size: 16), + titleSmall: s(base.titleSmall, w: FontWeight.w600, size: 15), + bodyLarge: s(base.bodyLarge, w: FontWeight.w400, size: 17, h: 1.35), + bodyMedium: s(base.bodyMedium, w: FontWeight.w400, size: 16, h: 1.35), + bodySmall: s(base.bodySmall, w: FontWeight.w400, size: 13, h: 1.35), + labelLarge: s(base.labelLarge, w: FontWeight.w500, size: 15), labelSmall: s(base.labelSmall, w: FontWeight.w500, size: 11), ); } diff --git a/mobile/lib/widgets/brand_mark.dart b/mobile/lib/widgets/brand_mark.dart index 962226c..f9637e2 100644 --- a/mobile/lib/widgets/brand_mark.dart +++ b/mobile/lib/widgets/brand_mark.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import '../theme/app_theme.dart'; -/// Soft Neutral brand mark — ink monogram, no glow or heavy tile chrome. +/// Compact messenger brand mark — blue tile, no glow. class BrandMark extends StatelessWidget { const BrandMark({super.key, this.size = 64}); @@ -10,21 +10,19 @@ class BrandMark extends StatelessWidget { @override Widget build(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - final fill = isDark ? AppTheme.inkDark : AppTheme.inkLight; - final fg = isDark ? AppTheme.canvasDark : Colors.white; + final scheme = Theme.of(context).colorScheme; return Container( width: size, height: size, decoration: BoxDecoration( - color: fill, - borderRadius: BorderRadius.circular(AppTheme.rPanel), + color: scheme.primary, + borderRadius: BorderRadius.circular(size * 0.28), ), alignment: Alignment.center, child: Text( 'Y', style: TextStyle( - color: fg, + color: scheme.onPrimary, fontWeight: FontWeight.w700, fontSize: size * 0.42, height: 1, diff --git a/mobile/lib/widgets/message_bubble.dart b/mobile/lib/widgets/message_bubble.dart index 8ef3a4f..cf4b3f1 100644 --- a/mobile/lib/widgets/message_bubble.dart +++ b/mobile/lib/widgets/message_bubble.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import '../models/models.dart'; import '../theme/app_theme.dart'; -/// Twin messages get a warm thin border + label (PRD §3.1 와카뷰 뱃지). +/// iMessage-like bubble. Twin messages keep a warm label + soft tint (PRD §3.1). class MessageBubble extends StatelessWidget { const MessageBubble({ super.key, @@ -34,23 +34,20 @@ class MessageBubble extends StatelessWidget { final Color bg; final Color fg; - Border? border; if (retracted) { bg = scheme.surfaceContainerHigh; fg = scheme.onSurfaceVariant; } else if (twin) { bg = brightness == Brightness.dark - ? const Color(0xFF221C14) - : const Color(0xFFFAF6F0); + ? const Color(0xFF2A2418) + : const Color(0xFFFFF6E8); fg = scheme.onSurface; - border = Border.all(color: accent.withValues(alpha: 0.4), width: 1); } else if (isMine) { bg = AppTheme.mineBubble(brightness); fg = AppTheme.mineBubbleFg(brightness); } else { bg = AppTheme.peerBubble(brightness); fg = scheme.onSurface; - border = Border.all(color: scheme.outlineVariant.withValues(alpha: 0.9)); } final radius = BorderRadius.only( @@ -61,12 +58,12 @@ class MessageBubble extends StatelessWidget { ); final bubble = Container( - constraints: BoxConstraints(maxWidth: MediaQuery.sizeOf(context).width * 0.76), - padding: const EdgeInsets.fromLTRB(14, 10, 14, 8), + constraints: BoxConstraints(maxWidth: MediaQuery.sizeOf(context).width * 0.74), + padding: const EdgeInsets.fromLTRB(14, 9, 14, 7), decoration: BoxDecoration( color: bg, borderRadius: radius, - border: border, + border: twin ? Border.all(color: accent.withValues(alpha: 0.35)) : null, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -74,13 +71,13 @@ class MessageBubble extends StatelessWidget { children: [ if (twin) Padding( - padding: const EdgeInsets.only(bottom: 4), + padding: const EdgeInsets.only(bottom: 3), child: Text( '와카뷰', style: theme.textTheme.labelSmall?.copyWith( color: accent, fontWeight: FontWeight.w700, - letterSpacing: 0.2, + letterSpacing: 0.15, ), ), ), @@ -99,12 +96,15 @@ class MessageBubble extends StatelessWidget { else Text( message.text, - style: theme.textTheme.bodyMedium?.copyWith(color: fg, height: 1.4), + style: theme.textTheme.bodyMedium?.copyWith(color: fg, height: 1.3), ), - const SizedBox(height: 4), + const SizedBox(height: 3), Text( _time(message.createdAt), - style: theme.textTheme.labelSmall?.copyWith(color: fg.withValues(alpha: 0.55), fontSize: 10), + style: theme.textTheme.labelSmall?.copyWith( + color: fg.withValues(alpha: isMine && !twin && !retracted ? 0.7 : 0.5), + fontSize: 10, + ), ), ], ), @@ -135,7 +135,7 @@ class MessageBubble extends StatelessWidget { ); return Padding( - padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 12), + padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 10), child: Align( alignment: isMine ? Alignment.centerRight : Alignment.centerLeft, child: content, diff --git a/mobile/lib/widgets/primary_gradient_button.dart b/mobile/lib/widgets/primary_gradient_button.dart index 7e9519c..83175f7 100644 --- a/mobile/lib/widgets/primary_gradient_button.dart +++ b/mobile/lib/widgets/primary_gradient_button.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import '../theme/app_theme.dart'; -/// Primary CTA — Soft Neutral ink fill (API name kept for call sites). +/// Primary CTA — messenger blue fill (API name kept for call sites). class PrimaryGradientButton extends StatelessWidget { const PrimaryGradientButton({ super.key, @@ -17,12 +17,12 @@ class PrimaryGradientButton extends StatelessWidget { @override Widget build(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - final bg = isDark ? AppTheme.inkDark : AppTheme.inkLight; - final fg = isDark ? AppTheme.canvasDark : Colors.white; + final scheme = Theme.of(context).colorScheme; + final bg = scheme.primary; + final fg = scheme.onPrimary; final disabled = onPressed == null || loading; return SizedBox( - height: 48, + height: 50, width: double.infinity, child: FilledButton( onPressed: disabled && !loading ? null : onPressed, @@ -44,7 +44,7 @@ class PrimaryGradientButton extends StatelessWidget { ) : Text( label, - style: TextStyle(color: fg, fontWeight: FontWeight.w600, fontSize: 15), + style: TextStyle(color: fg, fontWeight: FontWeight.w600, fontSize: 16), ), ), );