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:
parent
0aae8fb578
commit
d905d0e5e8
|
|
@ -38,8 +38,8 @@ 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 재빌드 완료
|
||||
- UI: **iMessage-inspired light default** + soft charcoal dark (진행 중 배포) —
|
||||
기본 `ThemeMode.light`, 내 버블 `#007AFF`, 다크 캔버스 `#141418`
|
||||
- **발견(2026-07-31): Track C 콘텐츠 갭** — `PRD.md` P0 대비 단톡 따라잡기·관계별 페르소나·
|
||||
스팸 감지 미구현. Master 액션(FCM 시크릿, 실기기 탭, 웹 재배포)과 별개로 지금 바로 코드 착수 가능
|
||||
- 실 FCM 기기 수신 · Android 실기기 탭 · 사람 PoC 실행은 남음
|
||||
|
|
|
|||
|
|
@ -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<SessionState>(
|
||||
builder: (context, s, _) {
|
||||
|
|
|
|||
|
|
@ -405,30 +405,43 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
),
|
||||
_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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -156,16 +156,16 @@ class _ConversationListScreenState extends State<ConversationListScreen> {
|
|||
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];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue