Redesign UI to a bolder aurora-glass system after visual QA with a real Flutter build
Actually installed the Flutter SDK, built the app (web + a local screenshot harness through a real backend), and looked at the rendered screens instead of judging the design from source alone. The previous gradient/glassmorphism pass read as flat and generic: glass cards barely contrasted against the pale background, the brand mark was a stock sparkle icon, and buttons were flat solid fills. Changes: - Stronger, more saturated aurora gradient + blurred glow orbs in GradientBackdrop for real depth instead of a flat 3-stop pastel. - Glass cards/inputs get higher-contrast fill/border and a brand-tinted shadow so they visibly lift off the backdrop. - New BrandMark (gradient monogram), GradientText (gradient-filled wordmark), and PrimaryGradientButton (gradient CTA with shadow) replace the generic Material icon/flat button on splash, signup, onboarding and the conversation list empty state. - Fixed a real pre-existing layout bug found via screenshot: the onboarding header badge was a direct ListView child with an explicit width, which Flutter's sliver list silently stretches to full width regardless of the Container's own width — invisible before because its old flat pale fill blended into the background. Wrapped it in Align to restore its intended size. Verified with flutter analyze (0 errors) and flutter test (all passing), plus manual screenshots of signup, onboarding, conversation list and a live chat exchanged between two demo users through a real core-backend instance.
This commit is contained in:
parent
ce2e38e566
commit
79bce5e671
|
|
@ -4,6 +4,8 @@ import 'package:provider/provider.dart';
|
|||
import '../models/models.dart';
|
||||
import '../services/api_client.dart';
|
||||
import '../state/session_state.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import '../widgets/gradient_text.dart';
|
||||
import 'autonomy_settings_screen.dart';
|
||||
import 'chat_screen.dart';
|
||||
import 'contacts_screen.dart';
|
||||
|
|
@ -103,7 +105,7 @@ class _ConversationListScreenState extends State<ConversationListScreen> {
|
|||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('와카뷰'),
|
||||
title: GradientText('와카뷰', style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w800)),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: '사후 알림',
|
||||
|
|
@ -174,11 +176,26 @@ class _ConversationListScreenState extends State<ConversationListScreen> {
|
|||
),
|
||||
if (_rooms.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 64, horizontal: 32),
|
||||
padding: const EdgeInsets.fromLTRB(32, 48, 32, 32),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(Icons.chat_bubble_outline, size: 40, color: theme.colorScheme.outline),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
width: 72,
|
||||
height: 72,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: AppTheme.brandGradient,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF6D5BD0).withValues(alpha: 0.3),
|
||||
blurRadius: 24,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(Icons.chat_bubble_outline, size: 30, color: Colors.white),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'대화방이 없습니다',
|
||||
style: theme.textTheme.titleMedium,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
|||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../state/session_state.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import '../widgets/primary_gradient_button.dart';
|
||||
|
||||
/// Phase 1 onboarding skeleton: capture a few style samples locally.
|
||||
/// Fine copy / import UX waits for human PoC (#1) — do not invent §3 defaults here.
|
||||
|
|
@ -62,11 +64,20 @@ class _OnboardingToneScreenState extends State<OnboardingToneScreen> {
|
|||
child: ListView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(color: scheme.primaryContainer, borderRadius: BorderRadius.circular(16)),
|
||||
child: Icon(Icons.record_voice_over_outlined, size: 28, color: scheme.onPrimaryContainer),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppTheme.brandGradient,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
boxShadow: [
|
||||
BoxShadow(color: const Color(0xFF6D5BD0).withValues(alpha: 0.3), blurRadius: 18, offset: const Offset(0, 8)),
|
||||
],
|
||||
),
|
||||
child: const Icon(Icons.record_voice_over_outlined, size: 28, color: Colors.white),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('와카뷰가 따라 쓸 말투', style: theme.textTheme.headlineSmall),
|
||||
|
|
@ -104,9 +115,9 @@ class _OnboardingToneScreenState extends State<OnboardingToneScreen> {
|
|||
const SizedBox(height: 12),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
FilledButton(
|
||||
PrimaryGradientButton(
|
||||
label: '이 말투로 시작',
|
||||
onPressed: () => _save(markDone: true),
|
||||
child: const Text('이 말투로 시작'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import 'package:provider/provider.dart';
|
|||
import '../config.dart';
|
||||
import '../state/session_state.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import '../widgets/brand_mark.dart';
|
||||
import '../widgets/gradient_text.dart';
|
||||
import '../widgets/primary_gradient_button.dart';
|
||||
|
||||
class SignupScreen extends StatefulWidget {
|
||||
const SignupScreen({super.key});
|
||||
|
|
@ -42,20 +45,10 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Spacer(flex: 3),
|
||||
Center(
|
||||
child: Container(
|
||||
width: 72,
|
||||
height: 72,
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
),
|
||||
child: Icon(Icons.auto_awesome, size: 34, color: scheme.onPrimaryContainer),
|
||||
),
|
||||
),
|
||||
const Spacer(flex: 2),
|
||||
const Center(child: BrandMark(size: 72)),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
GradientText(
|
||||
'와카뷰',
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.displaySmall?.copyWith(fontWeight: FontWeight.w800),
|
||||
|
|
@ -66,7 +59,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(color: scheme.onSurfaceVariant),
|
||||
),
|
||||
const Spacer(flex: 2),
|
||||
const Spacer(flex: 1),
|
||||
Text('초대 코드로 클로즈드 베타에 참여합니다', style: theme.textTheme.labelLarge),
|
||||
const SizedBox(height: 12),
|
||||
_DemoTestPanel(
|
||||
|
|
@ -109,13 +102,12 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||
],
|
||||
const SizedBox(height: 20),
|
||||
_PressScale(
|
||||
child: FilledButton(
|
||||
child: PrimaryGradientButton(
|
||||
label: '시작하기',
|
||||
loading: session.loading,
|
||||
onPressed: session.loading
|
||||
? null
|
||||
: () => session.signup(_invite.text.trim(), _name.text.trim()),
|
||||
child: session.loading
|
||||
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2))
|
||||
: const Text('시작하기'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
|
@ -140,7 +132,7 @@ class _DemoTestPanel extends StatelessWidget {
|
|||
final theme = Theme.of(context);
|
||||
final brightness = theme.brightness;
|
||||
return Material(
|
||||
color: AppTheme.glassFill(brightness),
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: InkWell(
|
||||
onTap: onFill,
|
||||
|
|
@ -148,8 +140,12 @@ class _DemoTestPanel extends StatelessWidget {
|
|||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 12, 14),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.glassFill(brightness),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppTheme.glassBorder(brightness)),
|
||||
boxShadow: [
|
||||
BoxShadow(color: AppTheme.glassShadow(brightness), blurRadius: 20, offset: const Offset(0, 8)),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../widgets/brand_mark.dart';
|
||||
import '../widgets/gradient_text.dart';
|
||||
|
||||
/// Shown while `SessionState.restore()` runs, before the real app (signup /
|
||||
/// onboarding / conversation list) is ready to pick. Purely presentational —
|
||||
/// no navigation logic lives here, `main.dart` swaps it out once restore()
|
||||
|
|
@ -17,31 +20,9 @@ class SplashScreen extends StatelessWidget {
|
|||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 88,
|
||||
height: 88,
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.primary.withOpacity(0.15),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [scheme.primary, scheme.tertiary],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Icon(Icons.auto_awesome, color: Colors.white, size: 30),
|
||||
),
|
||||
),
|
||||
),
|
||||
const BrandMark(size: 72),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
GradientText(
|
||||
'와카뷰',
|
||||
style: theme.textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.w800),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,46 +1,72 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Central design tokens for 와카뷰 (Ykavu) — soft-gradient + glassmorphism
|
||||
/// direction. `backgroundGradient` paints behind every screen (wired into
|
||||
/// `MaterialApp.builder`); cards/inputs/app bars are semi-transparent
|
||||
/// "glass" surfaces that let the gradient read through. `twinAccent` stays a
|
||||
/// separate warm accent so a twin-written bubble never reads as something
|
||||
/// the human actually typed (PRD §3.1 와카뷰 뱃지).
|
||||
/// Central design tokens for 와카뷰 (Ykavu) — "aurora glass" direction.
|
||||
/// `backgroundGradient` + the glow orbs in [GradientBackdrop] paint behind
|
||||
/// every screen (wired into `MaterialApp.builder`); cards/inputs/app bars
|
||||
/// are semi-transparent "glass" surfaces with a soft colored shadow so they
|
||||
/// visibly lift off that background. `twinAccent` stays a separate warm
|
||||
/// accent so a twin-written bubble never reads as something the human
|
||||
/// actually typed (PRD §3.1 와카뷰 뱃지).
|
||||
class AppTheme {
|
||||
AppTheme._();
|
||||
|
||||
static const _seed = Color(0xFF7C6FF0);
|
||||
static const _seed = Color(0xFF6D5BD0);
|
||||
|
||||
/// Brand gradient used for the wordmark, brand mark and primary CTAs.
|
||||
static const brandGradient = LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [Color(0xFF6D5BD0), Color(0xFFA855C9), Color(0xFFE0609A)],
|
||||
);
|
||||
|
||||
static Color twinAccent(Brightness brightness) =>
|
||||
brightness == Brightness.dark ? Colors.amber.shade300 : Colors.amber.shade800;
|
||||
|
||||
/// Soft diagonal gradient painted behind every screen. Light: pastel
|
||||
/// lavender → sky → pink. Dark: desaturated indigo → navy → plum.
|
||||
/// Diagonal aurora gradient painted behind every screen. Light: violet →
|
||||
/// sky → pink, more saturated than a "safe" pastel so it reads as
|
||||
/// intentional branding rather than a default Material backdrop. Dark:
|
||||
/// deep indigo → navy → plum. Paired with the blurred glow orbs in
|
||||
/// [GradientBackdrop].
|
||||
static LinearGradient backgroundGradient(Brightness brightness) {
|
||||
if (brightness == Brightness.dark) {
|
||||
return const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFF1B1730), Color(0xFF161C34), Color(0xFF2A1830)],
|
||||
colors: [Color(0xFF201A3D), Color(0xFF16213E), Color(0xFF351C3C)],
|
||||
);
|
||||
}
|
||||
return const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFFEDE9FE), Color(0xFFE0F2FE), Color(0xFFFCE7F3)],
|
||||
colors: [Color(0xFFDCCBFF), Color(0xFFC3E4FF), Color(0xFFFFC9E8)],
|
||||
);
|
||||
}
|
||||
|
||||
/// Soft blurred accent blobs layered over [backgroundGradient] by
|
||||
/// [GradientBackdrop] for depth ("aurora glow").
|
||||
static Color glowPrimary(Brightness brightness) => brightness == Brightness.dark
|
||||
? const Color(0xFF8B7CF6).withValues(alpha: 0.35)
|
||||
: const Color(0xFFB79CFF).withValues(alpha: 0.55);
|
||||
|
||||
static Color glowSecondary(Brightness brightness) => brightness == Brightness.dark
|
||||
? const Color(0xFFE879C4).withValues(alpha: 0.25)
|
||||
: const Color(0xFFFFA9DC).withValues(alpha: 0.50);
|
||||
|
||||
/// "Glass" fill for cards/panels — semi-transparent so the gradient
|
||||
/// behind still reads through, with a faint light border for the classic
|
||||
/// glassmorphism edge highlight.
|
||||
/// behind still reads through, with a bright border for the classic
|
||||
/// glassmorphism edge highlight and a tinted shadow so the surface
|
||||
/// visibly lifts off the backdrop instead of blending into it.
|
||||
static Color glassFill(Brightness brightness) => brightness == Brightness.dark
|
||||
? Colors.white.withOpacity(0.06)
|
||||
: Colors.white.withOpacity(0.55);
|
||||
? Colors.white.withValues(alpha: 0.08)
|
||||
: Colors.white.withValues(alpha: 0.72);
|
||||
|
||||
static Color glassBorder(Brightness brightness) => brightness == Brightness.dark
|
||||
? Colors.white.withOpacity(0.10)
|
||||
: Colors.white.withOpacity(0.65);
|
||||
? Colors.white.withValues(alpha: 0.16)
|
||||
: Colors.white.withValues(alpha: 0.85);
|
||||
|
||||
static Color glassShadow(Brightness brightness) => brightness == Brightness.dark
|
||||
? Colors.black.withValues(alpha: 0.45)
|
||||
: const Color(0xFF6D5BD0).withValues(alpha: 0.18);
|
||||
|
||||
static ThemeData light() => _build(Brightness.light);
|
||||
static ThemeData dark() => _build(Brightness.dark);
|
||||
|
|
@ -49,6 +75,7 @@ class AppTheme {
|
|||
final scheme = ColorScheme.fromSeed(seedColor: _seed, brightness: brightness);
|
||||
final glass = glassFill(brightness);
|
||||
final glassEdge = glassBorder(brightness);
|
||||
final glassShadowColor = glassShadow(brightness);
|
||||
final glassShape = RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
side: BorderSide(color: glassEdge, width: 1),
|
||||
|
|
@ -78,7 +105,8 @@ class AppTheme {
|
|||
),
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
elevation: 0,
|
||||
elevation: 8,
|
||||
shadowColor: glassShadowColor,
|
||||
color: glass,
|
||||
shape: glassShape,
|
||||
margin: EdgeInsets.zero,
|
||||
|
|
@ -113,7 +141,7 @@ class AppTheme {
|
|||
borderRadius: BorderRadius.circular(14),
|
||||
borderSide: BorderSide(color: scheme.error, width: 1.6),
|
||||
),
|
||||
hintStyle: TextStyle(color: scheme.onSurfaceVariant.withOpacity(0.7)),
|
||||
hintStyle: TextStyle(color: scheme.onSurfaceVariant.withValues(alpha: 0.7)),
|
||||
),
|
||||
filledButtonTheme: FilledButtonThemeData(
|
||||
style: FilledButton.styleFrom(
|
||||
|
|
@ -158,7 +186,7 @@ class AppTheme {
|
|||
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||
backgroundColor: scheme.primary,
|
||||
foregroundColor: scheme.onPrimary,
|
||||
elevation: 1,
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
|
||||
),
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
|
|
@ -172,7 +200,7 @@ class AppTheme {
|
|||
dialogTheme: DialogThemeData(
|
||||
backgroundColor: brightness == Brightness.dark
|
||||
? const Color(0xFF211D3D)
|
||||
: Colors.white.withOpacity(0.92),
|
||||
: Colors.white.withValues(alpha: 0.95),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
),
|
||||
progressIndicatorTheme: ProgressIndicatorThemeData(color: scheme.primary),
|
||||
|
|
@ -181,6 +209,7 @@ class AppTheme {
|
|||
|
||||
static TextTheme _textTheme() {
|
||||
return const TextTheme(
|
||||
displayLarge: TextStyle(fontWeight: FontWeight.w800, letterSpacing: -1.2),
|
||||
displaySmall: TextStyle(fontWeight: FontWeight.w800, letterSpacing: -0.5),
|
||||
headlineSmall: TextStyle(fontWeight: FontWeight.w800, letterSpacing: -0.3),
|
||||
titleLarge: TextStyle(fontWeight: FontWeight.w700),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../theme/app_theme.dart';
|
||||
|
||||
/// The 와카뷰 app icon mark: a gradient-filled rounded badge with a bold
|
||||
/// "Y" monogram and a soft brand-tinted shadow, used wherever the splash /
|
||||
/// signup / onboarding screens need a small brand anchor instead of a
|
||||
/// generic Material icon.
|
||||
class BrandMark extends StatelessWidget {
|
||||
const BrandMark({super.key, this.size = 64});
|
||||
|
||||
final double size;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final radius = size * 0.32;
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
gradient: AppTheme.brandGradient,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF6D5BD0).withValues(alpha: 0.38),
|
||||
blurRadius: size * 0.35,
|
||||
offset: Offset(0, size * 0.12),
|
||||
),
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'Y',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: size * 0.46,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,14 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../theme/app_theme.dart';
|
||||
|
||||
/// Paints the app-wide soft gradient behind [child]. Wired into
|
||||
/// `MaterialApp.builder` so every screen gets it automatically without each
|
||||
/// Scaffold needing its own background — screens just need a transparent
|
||||
/// `scaffoldBackgroundColor` (set globally in [AppTheme]).
|
||||
/// Paints the app-wide aurora gradient + soft blurred glow orbs behind
|
||||
/// [child]. Wired into `MaterialApp.builder` so every screen gets it
|
||||
/// automatically without each Scaffold needing its own background —
|
||||
/// screens just need a transparent `scaffoldBackgroundColor` (set globally
|
||||
/// in [AppTheme]).
|
||||
class GradientBackdrop extends StatelessWidget {
|
||||
const GradientBackdrop({super.key, required this.child});
|
||||
|
||||
|
|
@ -13,9 +16,44 @@ class GradientBackdrop extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(gradient: AppTheme.backgroundGradient(Theme.of(context).brightness)),
|
||||
child: child,
|
||||
final brightness = Theme.of(context).brightness;
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
DecoratedBox(decoration: BoxDecoration(gradient: AppTheme.backgroundGradient(brightness))),
|
||||
Positioned(
|
||||
top: -90,
|
||||
right: -70,
|
||||
child: _Glow(color: AppTheme.glowPrimary(brightness), size: 260),
|
||||
),
|
||||
Positioned(
|
||||
bottom: -110,
|
||||
left: -90,
|
||||
child: _Glow(color: AppTheme.glowSecondary(brightness), size: 320),
|
||||
),
|
||||
if (child != null) child!,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Glow extends StatelessWidget {
|
||||
const _Glow({required this.color, required this.size});
|
||||
|
||||
final Color color;
|
||||
final double size;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IgnorePointer(
|
||||
child: ImageFiltered(
|
||||
imageFilter: ImageFilter.blur(sigmaX: 60, sigmaY: 60),
|
||||
child: Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../theme/app_theme.dart';
|
||||
|
||||
/// Renders [text] filled with [AppTheme.brandGradient] instead of a flat
|
||||
/// color — used for the "와카뷰" wordmark so the brand reads as designed
|
||||
/// rather than default black-on-gradient body text.
|
||||
class GradientText extends StatelessWidget {
|
||||
const GradientText(this.text, {super.key, this.style, this.textAlign});
|
||||
|
||||
final String text;
|
||||
final TextStyle? style;
|
||||
final TextAlign? textAlign;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ShaderMask(
|
||||
blendMode: BlendMode.srcIn,
|
||||
shaderCallback: (bounds) => AppTheme.brandGradient.createShader(Rect.fromLTWH(0, 0, bounds.width, bounds.height)),
|
||||
child: Text(text, style: style, textAlign: textAlign),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../theme/app_theme.dart';
|
||||
|
||||
/// Primary call-to-action button filled with [AppTheme.brandGradient] and a
|
||||
/// soft brand-tinted shadow, so the one action per screen that matters most
|
||||
/// (시작하기 / 이 말투로 시작 …) visibly outranks the flat glass buttons
|
||||
/// around it.
|
||||
class PrimaryGradientButton extends StatelessWidget {
|
||||
const PrimaryGradientButton({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
this.loading = false,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final VoidCallback? onPressed;
|
||||
final bool loading;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final disabled = onPressed == null;
|
||||
return Opacity(
|
||||
opacity: disabled ? 0.6 : 1,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: onPressed,
|
||||
child: Container(
|
||||
height: 52,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
gradient: AppTheme.brandGradient,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF6D5BD0).withValues(alpha: 0.35),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: loading
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
|
||||
)
|
||||
: Text(
|
||||
label,
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,18 +5,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: f0bb5d1648339c8308cc0b9838d8456b3cfe5c91f9dc1a735b4d003269e5da9a
|
||||
sha256: "3b19a47f6ea7c2632760777c78174f47f6aec1e05f0cd611380d4593b8af1dbc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "88.0.0"
|
||||
version: "96.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "0b7b9c329d2879f8f05d6c05b32ee9ec025f39b077864bdb5ac9a7b63418a98f"
|
||||
sha256: "0c516bc4ad36a1a75759e54d5047cb9d15cded4459df01aa35a0b5ec7db2c2a0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.1"
|
||||
version: "10.2.0"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -29,10 +29,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.0"
|
||||
version: "2.13.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -61,10 +61,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
sha256: fd754058c342243718d5171a95f352cfc9fcf0cba8cfa26df67cb13a5836db78
|
||||
sha256: "8c0535c3b2f625619f4dd1036ef1127f2e77bfedf89ed2eb2676ef076e0b6712"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
version: "4.1.3"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
|
@ -129,6 +129,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
code_assets:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_assets
|
||||
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -157,18 +165,18 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
|
||||
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
version: "1.0.9"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: c87dfe3d56f183ffe9106a18aebc6db431fc7c98c31a54b952a77f3d54a85697
|
||||
sha256: "29f7ecc274a86d32920b1d9cfc7502fa87220da41ec60b55f329559d5732e2b2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
version: "3.1.7"
|
||||
drift:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -201,6 +209,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
ffi_leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi_leak_tracker
|
||||
sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.2"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -274,10 +290,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_windows
|
||||
sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613"
|
||||
sha256: "471951813a97006d899db4948acc654a4f28c440083ea08178935ce20b173ec1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
version: "4.2.2"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
|
@ -300,10 +316,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: google_fonts
|
||||
sha256: "517b20870220c48752eafa0ba1a797a092fb22df0d89535fd9991e86ee2cdd9c"
|
||||
sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.2"
|
||||
version: "6.3.3"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -312,6 +328,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
hooks:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hooks
|
||||
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -344,14 +368,38 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
jni:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni
|
||||
sha256: f038e58b4dc2c9037f50e233175086337e0b305e356d28211bf55f21c504cbd3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
jni_flutter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni_flutter
|
||||
sha256: "7b717011ea40d04fd47c2731d3d1d36eb99eba3435c2753d62489e8c3c9991d5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
jni_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni_util
|
||||
sha256: "1ba86da04a5f2bf18fde2edb235587e70c5b0fc5bd4ba955f46b00942c3fc35f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||
sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.9.0"
|
||||
version: "4.12.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -432,6 +480,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
objective_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: objective_c
|
||||
sha256: b7fb95a6d9a4f009edd63dc5ac69f07420b23a16161c6dd8660290b59c602e8e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.5.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -452,42 +508,42 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
version: "2.1.6"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "3b4c1fc3aa55ddc9cd4aa6759984330d5c8e66aa7702a6223c61540dc6380c37"
|
||||
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.19"
|
||||
version: "2.3.1"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd"
|
||||
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
version: "2.6.0"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
version: "2.2.2"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
version: "2.1.3"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -552,30 +608,38 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
record_use:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_use
|
||||
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
|
||||
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.3"
|
||||
version: "2.5.5"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: bd14436108211b0d4ee5038689a56d4ae3620fd72fd6036e113bf1345bc74d9e
|
||||
sha256: "0634e64bd719f89c012f392938e173521f535d3ecaf66558fa94a056d22b5cc7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.13"
|
||||
version: "2.4.27"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
|
||||
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.4"
|
||||
version: "2.5.6"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -588,10 +652,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
||||
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
version: "2.4.2"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -633,18 +697,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: source_gen
|
||||
sha256: "1d562a3c1f713904ebbed50d2760217fd8a51ca170ac4b05b0db490699dbac17"
|
||||
sha256: a603f1fb984a7391ae5978d1b92bfaaa08b350dca5c825256f925818f7943bf5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.0"
|
||||
version: "4.2.4"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.1"
|
||||
version: "1.10.2"
|
||||
sqlcipher_flutter_libs:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -737,10 +801,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
||||
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.0.0"
|
||||
version: "15.2.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -777,10 +841,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
|
||||
sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.15.0"
|
||||
version: "6.3.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -798,5 +862,5 @@ packages:
|
|||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.10.0-0 <4.0.0"
|
||||
flutter: ">=3.29.0"
|
||||
dart: ">=3.12.0 <4.0.0"
|
||||
flutter: ">=3.44.0"
|
||||
|
|
|
|||
Loading…
Reference in New Issue