feat(ui): iMessage-inspired light default + soft charcoal dark

Default ThemeMode.light with white messenger stage, blue mine bubbles,
and gray peer bubbles. Dark theme uses elevated charcoal instead of
pure black; composer and CTAs follow common messenger patterns.

Co-authored-by: okuma <o0kuma@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-08-03 04:28:41 +00:00
parent 0aae8fb578
commit d905d0e5e8
No known key found for this signature in database
10 changed files with 143 additions and 126 deletions

View File

@ -38,8 +38,8 @@ Phase 1 **A~C** 이후 실행 트랙. 작업 단위를 하나씩 처리한다.
- 앱 코드는 클로즈드 베타 직전 수준 - 앱 코드는 클로즈드 베타 직전 수준
- **`https://msn.iykyka.com` 라이브 + N3 완료 + Gemini 실초안 OK + Track A/B 완료** - **`https://msn.iykyka.com` 라이브 + N3 완료 + Gemini 실초안 OK + Track A/B 완료**
- 진행 중: **N4 FCM 코드 경로** → Master 시크릿 대기 → Android UI QA - 진행 중: **N4 FCM 코드 경로** → Master 시크릿 대기 → Android UI QA
- UI: **Soft Neutral + surface hierarchy** 프로덕션 반영 (`e05a0f0`, 2026-08-03) — - UI: **iMessage-inspired light default** + soft charcoal dark (진행 중 배포) —
GitHub+Gitea `main` 듀얼 푸시 · `msn.iykyka.com` web 재빌드 완료 기본 `ThemeMode.light`, 내 버블 `#007AFF`, 다크 캔버스 `#141418`
- **발견(2026-07-31): Track C 콘텐츠 갭**`PRD.md` P0 대비 단톡 따라잡기·관계별 페르소나· - **발견(2026-07-31): Track C 콘텐츠 갭**`PRD.md` P0 대비 단톡 따라잡기·관계별 페르소나·
스팸 감지 미구현. Master 액션(FCM 시크릿, 실기기 탭, 웹 재배포)과 별개로 지금 바로 코드 착수 가능 스팸 감지 미구현. Master 액션(FCM 시크릿, 실기기 탭, 웹 재배포)과 별개로 지금 바로 코드 착수 가능
- 실 FCM 기기 수신 · Android 실기기 탭 · 사람 PoC 실행은 남음 - 실 FCM 기기 수신 · Android 실기기 탭 · 사람 PoC 실행은 남음

View File

@ -89,7 +89,8 @@ class _BootstrapState extends State<_Bootstrap> {
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: AppTheme.light(), theme: AppTheme.light(),
darkTheme: AppTheme.dark(), 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), builder: (context, child) => GradientBackdrop(child: child),
home: const SplashScreen(), home: const SplashScreen(),
); );
@ -112,7 +113,8 @@ class YkavuApp extends StatelessWidget {
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: AppTheme.light(), theme: AppTheme.light(),
darkTheme: AppTheme.dark(), 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), builder: (context, child) => GradientBackdrop(child: child),
home: Consumer<SessionState>( home: Consumer<SessionState>(
builder: (context, s, _) { builder: (context, s, _) {

View File

@ -405,30 +405,43 @@ class _ChatScreenState extends State<ChatScreen> {
), ),
_buildL1Panel(context), _buildL1Panel(context),
SafeArea( SafeArea(
child: Padding( child: Container(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 12), 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( child: Row(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
IconButton(
tooltip: '초안 요청',
onPressed: _busy ? null : _requestDraft,
icon: const Icon(Icons.auto_awesome_outlined),
),
Expanded( Expanded(
child: TextField( child: TextField(
controller: _input, controller: _input,
minLines: 1, minLines: 1,
maxLines: 4, maxLines: 4,
textInputAction: TextInputAction.send,
onSubmitted: (_) {
if (!_busy) _sendHuman();
},
decoration: const InputDecoration(hintText: '메시지'), decoration: const InputDecoration(hintText: '메시지'),
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 6),
IconButton.filledTonal(
tooltip: '초안 요청',
onPressed: _busy ? null : _requestDraft,
icon: const Icon(Icons.auto_awesome),
),
const SizedBox(width: 4),
IconButton.filled( IconButton.filled(
tooltip: '보내기', tooltip: '보내기',
onPressed: _busy ? null : _sendHuman, 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),
), ),
], ],
), ),

View File

@ -156,16 +156,16 @@ class _ConversationListScreenState extends State<ConversationListScreen> {
Color _avatarColor(BuildContext context, int seed) { Color _avatarColor(BuildContext context, int seed) {
final isDark = Theme.of(context).brightness == Brightness.dark; final isDark = Theme.of(context).brightness == Brightness.dark;
final palette = isDark final palette = isDark
? const [Color(0xFF1C1C1C), Color(0xFF22262E), Color(0xFF242018)] ? const [Color(0xFF2C2C34), Color(0xFF243447), Color(0xFF3A3224)]
: const [Color(0xFFF4F4F5), Color(0xFFE8EEF4), Color(0xFFF3F0EA)]; : const [Color(0xFFE9E9EB), Color(0xFFD6E8FF), Color(0xFFFFF1D6)];
return palette[seed % palette.length]; return palette[seed % palette.length];
} }
Color _onAvatarColor(BuildContext context, int seed) { Color _onAvatarColor(BuildContext context, int seed) {
final isDark = Theme.of(context).brightness == Brightness.dark; final isDark = Theme.of(context).brightness == Brightness.dark;
final palette = isDark final palette = isDark
? const [Color(0xFFA3A3A3), Color(0xFF8BB4D9), Color(0xFFC4A574)] ? const [Color(0xFFF2F2F7), Color(0xFF0A84FF), Color(0xFFD4B06A)]
: const [Color(0xFF525252), Color(0xFF3B6D9B), Color(0xFF9A7B4F)]; : const [Color(0xFF1C1C1E), Color(0xFF007AFF), Color(0xFFB08A4A)];
return palette[seed % palette.length]; return palette[seed % palette.length];
} }

View File

@ -150,7 +150,7 @@ class _DemoTestPanel extends StatelessWidget {
Text( Text(
'테스트용 (누구나)', '테스트용 (누구나)',
style: theme.textTheme.labelLarge?.copyWith( style: theme.textTheme.labelLarge?.copyWith(
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.primary,
letterSpacing: 0.15, letterSpacing: 0.15,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),

View File

@ -36,8 +36,8 @@ class SplashScreen extends StatelessWidget {
width: 22, width: 22,
height: 22, height: 22,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2, strokeWidth: 2.2,
color: scheme.onSurfaceVariant, color: scheme.primary,
), ),
), ),
], ],

View File

@ -1,36 +1,36 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
/// Soft Neutral + surface hierarchy for . /// Messenger UI for iMessage-inspired light default + soft charcoal dark.
/// Neutrals dominate; accent is thin (mine bubbles / small links only). /// Light: white stage, blue mine bubbles, gray peer bubbles.
/// Radii: input/button 12, bubble 18. No glow, almost no shadow. /// Dark: elevated charcoal (not pure black), softer blue accents.
class AppTheme { class AppTheme {
AppTheme._(); AppTheme._();
// Light neutrals // Light iOS Messages / white messenger
static const canvasLight = Color(0xFFFAFAF9); static const canvasLight = Color(0xFFF2F2F7);
static const surfaceLight = Color(0xFFFFFFFF); static const surfaceLight = Color(0xFFFFFFFF);
static const inkLight = Color(0xFF171717); static const inkLight = Color(0xFF1C1C1E);
static const mutedLight = Color(0xFF737373); static const mutedLight = Color(0xFF8E8E93);
static const lineLight = Color(0xFFE5E5E5); static const lineLight = Color(0xFFD1D1D6);
static const softFillLight = Color(0xFFF4F4F5); static const softFillLight = Color(0xFFE9E9EB);
// Dark neutrals // Dark soft charcoal (not #000)
static const canvasDark = Color(0xFF0A0A0A); static const canvasDark = Color(0xFF141418);
static const surfaceDark = Color(0xFF141414); static const surfaceDark = Color(0xFF1C1C22);
static const inkDark = Color(0xFFFAFAFA); static const inkDark = Color(0xFFF2F2F7);
static const mutedDark = Color(0xFFA3A3A3); static const mutedDark = Color(0xFF98989F);
static const lineDark = Color(0xFF262626); static const lineDark = Color(0xFF2C2C34);
static const softFillDark = Color(0xFF1C1C1C); static const softFillDark = Color(0xFF2C2C34);
/// Thin accent used for mine bubbles & primary actions, not chrome. /// iMessage-like blue for mine bubbles & primary actions.
static const accentLight = Color(0xFF3B6D9B); static const accentLight = Color(0xFF007AFF);
static const accentDark = Color(0xFF8BB4D9); static const accentDark = Color(0xFF0A84FF);
static const accentSoftLight = Color(0xFFE8F0F7); static const accentSoftLight = Color(0xFFD6E8FF);
static const accentSoftDark = Color(0xFF1A2836); static const accentSoftDark = Color(0xFF163A66);
static const twinAmberLight = Color(0xFF9A7B4F); static const twinAmberLight = Color(0xFFB08A4A);
static const twinAmberDark = Color(0xFFC4A574); static const twinAmberDark = Color(0xFFD4B06A);
/// Compat for older call sites. /// Compat for older call sites.
static const teal = accentLight; static const teal = accentLight;
@ -44,21 +44,26 @@ class AppTheme {
static LinearGradient backgroundGradient(Brightness b) { static LinearGradient backgroundGradient(Brightness b) {
final c = b == Brightness.dark ? canvasDark : canvasLight; 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 glassFill(Brightness b) => b == Brightness.dark ? surfaceDark : surfaceLight;
static Color glassBorder(Brightness b) => b == Brightness.dark ? lineDark : lineLight; static Color glassBorder(Brightness b) => b == Brightness.dark ? lineDark : lineLight;
static Color glassShadow(Brightness b) => Colors.transparent; static Color glassShadow(Brightness b) => Colors.transparent;
static Color mineBubble(Brightness b) => b == Brightness.dark ? accentSoftDark : accentLight; /// Mine = solid messenger blue (both modes).
static Color mineBubbleFg(Brightness b) => b == Brightness.dark ? inkDark : Colors.white; 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 Color peerBubble(Brightness b) => b == Brightness.dark ? softFillDark : softFillLight;
static const rInput = 12.0; static const rInput = 20.0;
static const rButton = 12.0; static const rButton = 14.0;
static const rBubble = 18.0; static const rBubble = 20.0;
static const rPanel = 12.0; static const rPanel = 14.0;
static ThemeData light() => _build(Brightness.light); static ThemeData light() => _build(Brightness.light);
static ThemeData dark() => _build(Brightness.dark); static ThemeData dark() => _build(Brightness.dark);
@ -66,11 +71,10 @@ class AppTheme {
static ThemeData _build(Brightness brightness) { static ThemeData _build(Brightness brightness) {
final isDark = brightness == Brightness.dark; final isDark = brightness == Brightness.dark;
final primary = isDark ? accentDark : accentLight; final primary = isDark ? accentDark : accentLight;
final onPrimary = isDark ? canvasDark : Colors.white; final onPrimary = Colors.white;
final ink = isDark ? inkDark : inkLight; final ink = isDark ? inkDark : inkLight;
final muted = isDark ? mutedDark : mutedLight; final muted = isDark ? mutedDark : mutedLight;
final line = isDark ? lineDark : lineLight; final line = isDark ? lineDark : lineLight;
final canvas = isDark ? canvasDark : canvasLight;
final surface = isDark ? surfaceDark : surfaceLight; final surface = isDark ? surfaceDark : surfaceLight;
final soft = isDark ? softFillDark : softFillLight; final soft = isDark ? softFillDark : softFillLight;
@ -86,7 +90,7 @@ class AppTheme {
onSecondaryContainer: muted, onSecondaryContainer: muted,
tertiary: primary, tertiary: primary,
onTertiary: onPrimary, onTertiary: onPrimary,
error: isDark ? const Color(0xFFF87171) : const Color(0xFFB91C1C), error: isDark ? const Color(0xFFFF6961) : const Color(0xFFFF3B30),
onError: Colors.white, onError: Colors.white,
surface: surface, surface: surface,
onSurface: ink, onSurface: ink,
@ -109,12 +113,12 @@ class AppTheme {
textTheme: textTheme, textTheme: textTheme,
primaryTextTheme: textTheme, primaryTextTheme: textTheme,
appBarTheme: AppBarTheme( appBarTheme: AppBarTheme(
backgroundColor: canvas.withValues(alpha: 0.92), backgroundColor: surface.withValues(alpha: isDark ? 0.94 : 0.88),
foregroundColor: ink, foregroundColor: ink,
surfaceTintColor: Colors.transparent, surfaceTintColor: Colors.transparent,
elevation: 0, elevation: 0,
scrolledUnderElevation: 0, scrolledUnderElevation: 0,
centerTitle: false, centerTitle: true,
titleTextStyle: textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w600, letterSpacing: -0.2), titleTextStyle: textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w600, letterSpacing: -0.2),
), ),
cardTheme: CardThemeData( cardTheme: CardThemeData(
@ -122,32 +126,32 @@ class AppTheme {
color: surface, color: surface,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(rPanel), borderRadius: BorderRadius.circular(rPanel),
side: BorderSide(color: line), side: BorderSide(color: line.withValues(alpha: isDark ? 0.9 : 0.65)),
), ),
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
), ),
listTileTheme: ListTileThemeData( listTileTheme: ListTileThemeData(
iconColor: muted, iconColor: muted,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rPanel)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rPanel)),
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 2), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
minVerticalPadding: 8, minVerticalPadding: 10,
), ),
dividerTheme: DividerThemeData(color: line, space: 1, thickness: 1), dividerTheme: DividerThemeData(color: line, space: 1, thickness: 0.5),
inputDecorationTheme: InputDecorationTheme( inputDecorationTheme: InputDecorationTheme(
filled: true, filled: true,
fillColor: surface, fillColor: isDark ? soft : surface,
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(rInput), borderRadius: BorderRadius.circular(rInput),
borderSide: BorderSide(color: line), borderSide: BorderSide(color: line.withValues(alpha: 0.7)),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(rInput), borderRadius: BorderRadius.circular(rInput),
borderSide: BorderSide(color: line), borderSide: BorderSide(color: line.withValues(alpha: 0.7)),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(rInput), borderRadius: BorderRadius.circular(rInput),
borderSide: BorderSide(color: primary, width: 1.2), borderSide: BorderSide(color: primary, width: 1.4),
), ),
errorBorder: OutlineInputBorder( errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(rInput), borderRadius: BorderRadius.circular(rInput),
@ -155,29 +159,29 @@ class AppTheme {
), ),
focusedErrorBorder: OutlineInputBorder( focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(rInput), 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), labelStyle: TextStyle(color: muted, fontWeight: FontWeight.w500),
), ),
filledButtonTheme: FilledButtonThemeData( filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
backgroundColor: isDark ? inkDark : inkLight, backgroundColor: primary,
foregroundColor: isDark ? canvasDark : Colors.white, foregroundColor: onPrimary,
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
minimumSize: const Size(0, 46), minimumSize: const Size(0, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rButton)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rButton)),
textStyle: const TextStyle(fontWeight: FontWeight.w600, fontSize: 15), textStyle: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16),
elevation: 0, elevation: 0,
), ),
), ),
outlinedButtonTheme: OutlinedButtonThemeData( outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: ink, foregroundColor: primary,
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
minimumSize: const Size(0, 46), minimumSize: const Size(0, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rButton)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rButton)),
side: BorderSide(color: line), side: BorderSide(color: primary.withValues(alpha: 0.45)),
), ),
), ),
textButtonTheme: TextButtonThemeData( textButtonTheme: TextButtonThemeData(
@ -188,22 +192,22 @@ class AppTheme {
), ),
iconButtonTheme: IconButtonThemeData( iconButtonTheme: IconButtonThemeData(
style: IconButton.styleFrom( style: IconButton.styleFrom(
foregroundColor: muted, foregroundColor: primary,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
), ),
), ),
chipTheme: ChipThemeData( chipTheme: ChipThemeData(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
side: BorderSide(color: line), side: BorderSide(color: line),
backgroundColor: surface, backgroundColor: soft,
labelStyle: TextStyle(color: muted, fontWeight: FontWeight.w500, fontSize: 12), labelStyle: TextStyle(color: ink, fontWeight: FontWeight.w500, fontSize: 12),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
), ),
floatingActionButtonTheme: FloatingActionButtonThemeData( floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: isDark ? inkDark : inkLight, backgroundColor: primary,
foregroundColor: isDark ? canvasDark : Colors.white, foregroundColor: onPrimary,
elevation: 0, elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rButton)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
), ),
snackBarTheme: SnackBarThemeData( snackBarTheme: SnackBarThemeData(
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
@ -216,20 +220,20 @@ class AppTheme {
backgroundColor: surface, backgroundColor: surface,
elevation: 0, elevation: 0,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(18),
side: BorderSide(color: line), side: BorderSide(color: line.withValues(alpha: 0.8)),
), ),
), ),
progressIndicatorTheme: ProgressIndicatorThemeData(color: primary), progressIndicatorTheme: ProgressIndicatorThemeData(color: primary),
bannerTheme: MaterialBannerThemeData(backgroundColor: surface, padding: const EdgeInsets.all(16)), bannerTheme: MaterialBannerThemeData(backgroundColor: surface, padding: const EdgeInsets.all(16)),
segmentedButtonTheme: SegmentedButtonThemeData( segmentedButtonTheme: SegmentedButtonThemeData(
style: SegmentedButton.styleFrom( style: SegmentedButton.styleFrom(
backgroundColor: surface, backgroundColor: soft,
foregroundColor: muted, foregroundColor: muted,
selectedForegroundColor: ink, selectedForegroundColor: primary,
selectedBackgroundColor: soft, selectedBackgroundColor: isDark ? accentSoftDark : accentSoftLight,
side: BorderSide(color: line), side: BorderSide.none,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rButton)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), 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); (t ?? const TextStyle()).copyWith(color: ink, fontWeight: w, fontSize: size, letterSpacing: ls, height: h);
return base.copyWith( return base.copyWith(
displayLarge: s(base.displayLarge, w: FontWeight.w600, size: 36, ls: -1.0), displayLarge: s(base.displayLarge, w: FontWeight.w700, size: 34, ls: -0.8),
displaySmall: s(base.displaySmall, w: FontWeight.w600, size: 28, ls: -0.6), displaySmall: s(base.displaySmall, w: FontWeight.w700, size: 28, ls: -0.5),
headlineSmall: s(base.headlineSmall, w: FontWeight.w600, size: 20, ls: -0.2), headlineSmall: s(base.headlineSmall, w: FontWeight.w600, size: 22, ls: -0.3),
titleLarge: s(base.titleLarge, w: FontWeight.w600, size: 17, ls: -0.2), titleLarge: s(base.titleLarge, w: FontWeight.w600, size: 17, ls: -0.2),
titleMedium: s(base.titleMedium, w: FontWeight.w600, size: 15), titleMedium: s(base.titleMedium, w: FontWeight.w600, size: 16),
titleSmall: s(base.titleSmall, w: FontWeight.w600, size: 14), titleSmall: s(base.titleSmall, w: FontWeight.w600, size: 15),
bodyLarge: s(base.bodyLarge, w: FontWeight.w400, size: 15, h: 1.45), bodyLarge: s(base.bodyLarge, w: FontWeight.w400, size: 17, h: 1.35),
bodyMedium: s(base.bodyMedium, w: FontWeight.w400, size: 14, h: 1.45), bodyMedium: s(base.bodyMedium, w: FontWeight.w400, size: 16, h: 1.35),
bodySmall: s(base.bodySmall, w: FontWeight.w400, size: 12, h: 1.4), bodySmall: s(base.bodySmall, w: FontWeight.w400, size: 13, h: 1.35),
labelLarge: s(base.labelLarge, w: FontWeight.w500, size: 13), labelLarge: s(base.labelLarge, w: FontWeight.w500, size: 15),
labelSmall: s(base.labelSmall, w: FontWeight.w500, size: 11), labelSmall: s(base.labelSmall, w: FontWeight.w500, size: 11),
); );
} }

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import '../theme/app_theme.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 { class BrandMark extends StatelessWidget {
const BrandMark({super.key, this.size = 64}); const BrandMark({super.key, this.size = 64});
@ -10,21 +10,19 @@ class BrandMark extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark; final scheme = Theme.of(context).colorScheme;
final fill = isDark ? AppTheme.inkDark : AppTheme.inkLight;
final fg = isDark ? AppTheme.canvasDark : Colors.white;
return Container( return Container(
width: size, width: size,
height: size, height: size,
decoration: BoxDecoration( decoration: BoxDecoration(
color: fill, color: scheme.primary,
borderRadius: BorderRadius.circular(AppTheme.rPanel), borderRadius: BorderRadius.circular(size * 0.28),
), ),
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text(
'Y', 'Y',
style: TextStyle( style: TextStyle(
color: fg, color: scheme.onPrimary,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
fontSize: size * 0.42, fontSize: size * 0.42,
height: 1, height: 1,

View File

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import '../models/models.dart'; import '../models/models.dart';
import '../theme/app_theme.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 { class MessageBubble extends StatelessWidget {
const MessageBubble({ const MessageBubble({
super.key, super.key,
@ -34,23 +34,20 @@ class MessageBubble extends StatelessWidget {
final Color bg; final Color bg;
final Color fg; final Color fg;
Border? border;
if (retracted) { if (retracted) {
bg = scheme.surfaceContainerHigh; bg = scheme.surfaceContainerHigh;
fg = scheme.onSurfaceVariant; fg = scheme.onSurfaceVariant;
} else if (twin) { } else if (twin) {
bg = brightness == Brightness.dark bg = brightness == Brightness.dark
? const Color(0xFF221C14) ? const Color(0xFF2A2418)
: const Color(0xFFFAF6F0); : const Color(0xFFFFF6E8);
fg = scheme.onSurface; fg = scheme.onSurface;
border = Border.all(color: accent.withValues(alpha: 0.4), width: 1);
} else if (isMine) { } else if (isMine) {
bg = AppTheme.mineBubble(brightness); bg = AppTheme.mineBubble(brightness);
fg = AppTheme.mineBubbleFg(brightness); fg = AppTheme.mineBubbleFg(brightness);
} else { } else {
bg = AppTheme.peerBubble(brightness); bg = AppTheme.peerBubble(brightness);
fg = scheme.onSurface; fg = scheme.onSurface;
border = Border.all(color: scheme.outlineVariant.withValues(alpha: 0.9));
} }
final radius = BorderRadius.only( final radius = BorderRadius.only(
@ -61,12 +58,12 @@ class MessageBubble extends StatelessWidget {
); );
final bubble = Container( final bubble = Container(
constraints: BoxConstraints(maxWidth: MediaQuery.sizeOf(context).width * 0.76), constraints: BoxConstraints(maxWidth: MediaQuery.sizeOf(context).width * 0.74),
padding: const EdgeInsets.fromLTRB(14, 10, 14, 8), padding: const EdgeInsets.fromLTRB(14, 9, 14, 7),
decoration: BoxDecoration( decoration: BoxDecoration(
color: bg, color: bg,
borderRadius: radius, borderRadius: radius,
border: border, border: twin ? Border.all(color: accent.withValues(alpha: 0.35)) : null,
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -74,13 +71,13 @@ class MessageBubble extends StatelessWidget {
children: [ children: [
if (twin) if (twin)
Padding( Padding(
padding: const EdgeInsets.only(bottom: 4), padding: const EdgeInsets.only(bottom: 3),
child: Text( child: Text(
'와카뷰', '와카뷰',
style: theme.textTheme.labelSmall?.copyWith( style: theme.textTheme.labelSmall?.copyWith(
color: accent, color: accent,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
letterSpacing: 0.2, letterSpacing: 0.15,
), ),
), ),
), ),
@ -99,12 +96,15 @@ class MessageBubble extends StatelessWidget {
else else
Text( Text(
message.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( Text(
_time(message.createdAt), _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( return Padding(
padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 12), padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 10),
child: Align( child: Align(
alignment: isMine ? Alignment.centerRight : Alignment.centerLeft, alignment: isMine ? Alignment.centerRight : Alignment.centerLeft,
child: content, child: content,

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import '../theme/app_theme.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 { class PrimaryGradientButton extends StatelessWidget {
const PrimaryGradientButton({ const PrimaryGradientButton({
super.key, super.key,
@ -17,12 +17,12 @@ class PrimaryGradientButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark; final scheme = Theme.of(context).colorScheme;
final bg = isDark ? AppTheme.inkDark : AppTheme.inkLight; final bg = scheme.primary;
final fg = isDark ? AppTheme.canvasDark : Colors.white; final fg = scheme.onPrimary;
final disabled = onPressed == null || loading; final disabled = onPressed == null || loading;
return SizedBox( return SizedBox(
height: 48, height: 50,
width: double.infinity, width: double.infinity,
child: FilledButton( child: FilledButton(
onPressed: disabled && !loading ? null : onPressed, onPressed: disabled && !loading ? null : onPressed,
@ -44,7 +44,7 @@ class PrimaryGradientButton extends StatelessWidget {
) )
: Text( : Text(
label, label,
style: TextStyle(color: fg, fontWeight: FontWeight.w600, fontSize: 15), style: TextStyle(color: fg, fontWeight: FontWeight.w600, fontSize: 16),
), ),
), ),
); );