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:
Claude 2026-07-31 08:16:44 +00:00
parent ce2e38e566
commit 79bce5e671
No known key found for this signature in database
10 changed files with 391 additions and 128 deletions

View File

@ -4,6 +4,8 @@ import 'package:provider/provider.dart';
import '../models/models.dart'; import '../models/models.dart';
import '../services/api_client.dart'; import '../services/api_client.dart';
import '../state/session_state.dart'; import '../state/session_state.dart';
import '../theme/app_theme.dart';
import '../widgets/gradient_text.dart';
import 'autonomy_settings_screen.dart'; import 'autonomy_settings_screen.dart';
import 'chat_screen.dart'; import 'chat_screen.dart';
import 'contacts_screen.dart'; import 'contacts_screen.dart';
@ -103,7 +105,7 @@ class _ConversationListScreenState extends State<ConversationListScreen> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('와카뷰'), title: GradientText('와카뷰', style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w800)),
actions: [ actions: [
IconButton( IconButton(
tooltip: '사후 알림', tooltip: '사후 알림',
@ -174,11 +176,26 @@ class _ConversationListScreenState extends State<ConversationListScreen> {
), ),
if (_rooms.isEmpty) if (_rooms.isEmpty)
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 64, horizontal: 32), padding: const EdgeInsets.fromLTRB(32, 48, 32, 32),
child: Column( child: Column(
children: [ children: [
Icon(Icons.chat_bubble_outline, size: 40, color: theme.colorScheme.outline), Container(
const SizedBox(height: 12), 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( Text(
'대화방이 없습니다', '대화방이 없습니다',
style: theme.textTheme.titleMedium, style: theme.textTheme.titleMedium,

View File

@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../state/session_state.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. /// 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. /// 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( child: ListView(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
children: [ children: [
Container( Align(
alignment: Alignment.centerLeft,
child: Container(
width: 56, width: 56,
height: 56, height: 56,
decoration: BoxDecoration(color: scheme.primaryContainer, borderRadius: BorderRadius.circular(16)), decoration: BoxDecoration(
child: Icon(Icons.record_voice_over_outlined, size: 28, color: scheme.onPrimaryContainer), 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), const SizedBox(height: 16),
Text('와카뷰가 따라 쓸 말투', style: theme.textTheme.headlineSmall), Text('와카뷰가 따라 쓸 말투', style: theme.textTheme.headlineSmall),
@ -104,9 +115,9 @@ class _OnboardingToneScreenState extends State<OnboardingToneScreen> {
const SizedBox(height: 12), const SizedBox(height: 12),
], ],
const SizedBox(height: 12), const SizedBox(height: 12),
FilledButton( PrimaryGradientButton(
label: '이 말투로 시작',
onPressed: () => _save(markDone: true), onPressed: () => _save(markDone: true),
child: const Text('이 말투로 시작'),
), ),
], ],
), ),

View File

@ -5,6 +5,9 @@ import 'package:provider/provider.dart';
import '../config.dart'; import '../config.dart';
import '../state/session_state.dart'; import '../state/session_state.dart';
import '../theme/app_theme.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 { class SignupScreen extends StatefulWidget {
const SignupScreen({super.key}); const SignupScreen({super.key});
@ -42,20 +45,10 @@ class _SignupScreenState extends State<SignupScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const Spacer(flex: 3), const Spacer(flex: 2),
Center( const Center(child: BrandMark(size: 72)),
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 SizedBox(height: 20), const SizedBox(height: 20),
Text( GradientText(
'와카뷰', '와카뷰',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: theme.textTheme.displaySmall?.copyWith(fontWeight: FontWeight.w800), style: theme.textTheme.displaySmall?.copyWith(fontWeight: FontWeight.w800),
@ -66,7 +59,7 @@ class _SignupScreenState extends State<SignupScreen> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: theme.textTheme.bodyLarge?.copyWith(color: scheme.onSurfaceVariant), style: theme.textTheme.bodyLarge?.copyWith(color: scheme.onSurfaceVariant),
), ),
const Spacer(flex: 2), const Spacer(flex: 1),
Text('초대 코드로 클로즈드 베타에 참여합니다', style: theme.textTheme.labelLarge), Text('초대 코드로 클로즈드 베타에 참여합니다', style: theme.textTheme.labelLarge),
const SizedBox(height: 12), const SizedBox(height: 12),
_DemoTestPanel( _DemoTestPanel(
@ -109,13 +102,12 @@ class _SignupScreenState extends State<SignupScreen> {
], ],
const SizedBox(height: 20), const SizedBox(height: 20),
_PressScale( _PressScale(
child: FilledButton( child: PrimaryGradientButton(
label: '시작하기',
loading: session.loading,
onPressed: session.loading onPressed: session.loading
? null ? null
: () => session.signup(_invite.text.trim(), _name.text.trim()), : () => 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), const SizedBox(height: 32),
@ -140,7 +132,7 @@ class _DemoTestPanel extends StatelessWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
final brightness = theme.brightness; final brightness = theme.brightness;
return Material( return Material(
color: AppTheme.glassFill(brightness), color: Colors.transparent,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
child: InkWell( child: InkWell(
onTap: onFill, onTap: onFill,
@ -148,8 +140,12 @@ class _DemoTestPanel extends StatelessWidget {
child: Container( child: Container(
padding: const EdgeInsets.fromLTRB(16, 14, 12, 14), padding: const EdgeInsets.fromLTRB(16, 14, 12, 14),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppTheme.glassFill(brightness),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppTheme.glassBorder(brightness)), border: Border.all(color: AppTheme.glassBorder(brightness)),
boxShadow: [
BoxShadow(color: AppTheme.glassShadow(brightness), blurRadius: 20, offset: const Offset(0, 8)),
],
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,

View File

@ -1,5 +1,8 @@
import 'package:flutter/material.dart'; 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 / /// Shown while `SessionState.restore()` runs, before the real app (signup /
/// onboarding / conversation list) is ready to pick. Purely presentational /// onboarding / conversation list) is ready to pick. Purely presentational
/// no navigation logic lives here, `main.dart` swaps it out once restore() /// no navigation logic lives here, `main.dart` swaps it out once restore()
@ -17,31 +20,9 @@ class SplashScreen extends StatelessWidget {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Container( const BrandMark(size: 72),
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 SizedBox(height: 24), const SizedBox(height: 24),
Text( GradientText(
'와카뷰', '와카뷰',
style: theme.textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.w800), style: theme.textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.w800),
), ),

View File

@ -1,46 +1,72 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// Central design tokens for (Ykavu) soft-gradient + glassmorphism /// Central design tokens for (Ykavu) "aurora glass" direction.
/// direction. `backgroundGradient` paints behind every screen (wired into /// `backgroundGradient` + the glow orbs in [GradientBackdrop] paint behind
/// `MaterialApp.builder`); cards/inputs/app bars are semi-transparent /// every screen (wired into `MaterialApp.builder`); cards/inputs/app bars
/// "glass" surfaces that let the gradient read through. `twinAccent` stays a /// are semi-transparent "glass" surfaces with a soft colored shadow so they
/// separate warm accent so a twin-written bubble never reads as something /// visibly lift off that background. `twinAccent` stays a separate warm
/// the human actually typed (PRD §3.1 ). /// accent so a twin-written bubble never reads as something the human
/// actually typed (PRD §3.1 ).
class AppTheme { class AppTheme {
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) => static Color twinAccent(Brightness brightness) =>
brightness == Brightness.dark ? Colors.amber.shade300 : Colors.amber.shade800; brightness == Brightness.dark ? Colors.amber.shade300 : Colors.amber.shade800;
/// Soft diagonal gradient painted behind every screen. Light: pastel /// Diagonal aurora gradient painted behind every screen. Light: violet
/// lavender sky pink. Dark: desaturated indigo navy plum. /// 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) { static LinearGradient backgroundGradient(Brightness brightness) {
if (brightness == Brightness.dark) { if (brightness == Brightness.dark) {
return const LinearGradient( return const LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [Color(0xFF1B1730), Color(0xFF161C34), Color(0xFF2A1830)], colors: [Color(0xFF201A3D), Color(0xFF16213E), Color(0xFF351C3C)],
); );
} }
return const LinearGradient( return const LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, 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 /// "Glass" fill for cards/panels semi-transparent so the gradient
/// behind still reads through, with a faint light border for the classic /// behind still reads through, with a bright border for the classic
/// glassmorphism edge highlight. /// 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 static Color glassFill(Brightness brightness) => brightness == Brightness.dark
? Colors.white.withOpacity(0.06) ? Colors.white.withValues(alpha: 0.08)
: Colors.white.withOpacity(0.55); : Colors.white.withValues(alpha: 0.72);
static Color glassBorder(Brightness brightness) => brightness == Brightness.dark static Color glassBorder(Brightness brightness) => brightness == Brightness.dark
? Colors.white.withOpacity(0.10) ? Colors.white.withValues(alpha: 0.16)
: Colors.white.withOpacity(0.65); : 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 light() => _build(Brightness.light);
static ThemeData dark() => _build(Brightness.dark); static ThemeData dark() => _build(Brightness.dark);
@ -49,6 +75,7 @@ class AppTheme {
final scheme = ColorScheme.fromSeed(seedColor: _seed, brightness: brightness); final scheme = ColorScheme.fromSeed(seedColor: _seed, brightness: brightness);
final glass = glassFill(brightness); final glass = glassFill(brightness);
final glassEdge = glassBorder(brightness); final glassEdge = glassBorder(brightness);
final glassShadowColor = glassShadow(brightness);
final glassShape = RoundedRectangleBorder( final glassShape = RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18), borderRadius: BorderRadius.circular(18),
side: BorderSide(color: glassEdge, width: 1), side: BorderSide(color: glassEdge, width: 1),
@ -78,7 +105,8 @@ class AppTheme {
), ),
), ),
cardTheme: CardThemeData( cardTheme: CardThemeData(
elevation: 0, elevation: 8,
shadowColor: glassShadowColor,
color: glass, color: glass,
shape: glassShape, shape: glassShape,
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
@ -113,7 +141,7 @@ class AppTheme {
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: scheme.error, width: 1.6), 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( filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
@ -158,7 +186,7 @@ class AppTheme {
floatingActionButtonTheme: FloatingActionButtonThemeData( floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: scheme.primary, backgroundColor: scheme.primary,
foregroundColor: scheme.onPrimary, foregroundColor: scheme.onPrimary,
elevation: 1, elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
), ),
snackBarTheme: SnackBarThemeData( snackBarTheme: SnackBarThemeData(
@ -172,7 +200,7 @@ class AppTheme {
dialogTheme: DialogThemeData( dialogTheme: DialogThemeData(
backgroundColor: brightness == Brightness.dark backgroundColor: brightness == Brightness.dark
? const Color(0xFF211D3D) ? const Color(0xFF211D3D)
: Colors.white.withOpacity(0.92), : Colors.white.withValues(alpha: 0.95),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
), ),
progressIndicatorTheme: ProgressIndicatorThemeData(color: scheme.primary), progressIndicatorTheme: ProgressIndicatorThemeData(color: scheme.primary),
@ -181,6 +209,7 @@ class AppTheme {
static TextTheme _textTheme() { static TextTheme _textTheme() {
return const TextTheme( return const TextTheme(
displayLarge: TextStyle(fontWeight: FontWeight.w800, letterSpacing: -1.2),
displaySmall: TextStyle(fontWeight: FontWeight.w800, letterSpacing: -0.5), displaySmall: TextStyle(fontWeight: FontWeight.w800, letterSpacing: -0.5),
headlineSmall: TextStyle(fontWeight: FontWeight.w800, letterSpacing: -0.3), headlineSmall: TextStyle(fontWeight: FontWeight.w800, letterSpacing: -0.3),
titleLarge: TextStyle(fontWeight: FontWeight.w700), titleLarge: TextStyle(fontWeight: FontWeight.w700),

View File

@ -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,
),
),
);
}
}

View File

@ -1,11 +1,14 @@
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../theme/app_theme.dart'; import '../theme/app_theme.dart';
/// Paints the app-wide soft gradient behind [child]. Wired into /// Paints the app-wide aurora gradient + soft blurred glow orbs behind
/// `MaterialApp.builder` so every screen gets it automatically without each /// [child]. Wired into `MaterialApp.builder` so every screen gets it
/// Scaffold needing its own background screens just need a transparent /// automatically without each Scaffold needing its own background
/// `scaffoldBackgroundColor` (set globally in [AppTheme]). /// screens just need a transparent `scaffoldBackgroundColor` (set globally
/// in [AppTheme]).
class GradientBackdrop extends StatelessWidget { class GradientBackdrop extends StatelessWidget {
const GradientBackdrop({super.key, required this.child}); const GradientBackdrop({super.key, required this.child});
@ -13,9 +16,44 @@ class GradientBackdrop extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DecoratedBox( final brightness = Theme.of(context).brightness;
decoration: BoxDecoration(gradient: AppTheme.backgroundGradient(Theme.of(context).brightness)), return Stack(
child: child, 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),
),
),
); );
} }
} }

View File

@ -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),
);
}
}

View File

@ -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),
),
),
),
),
);
}
}

View File

@ -5,18 +5,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _fe_analyzer_shared name: _fe_analyzer_shared
sha256: f0bb5d1648339c8308cc0b9838d8456b3cfe5c91f9dc1a735b4d003269e5da9a sha256: "3b19a47f6ea7c2632760777c78174f47f6aec1e05f0cd611380d4593b8af1dbc"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "88.0.0" version: "96.0.0"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
name: analyzer name: analyzer
sha256: "0b7b9c329d2879f8f05d6c05b32ee9ec025f39b077864bdb5ac9a7b63418a98f" sha256: "0c516bc4ad36a1a75759e54d5047cb9d15cded4459df01aa35a0b5ec7db2c2a0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.1.1" version: "10.2.0"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -29,10 +29,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: async name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.13.0" version: "2.13.1"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@ -61,10 +61,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: build_daemon name: build_daemon
sha256: fd754058c342243718d5171a95f352cfc9fcf0cba8cfa26df67cb13a5836db78 sha256: "8c0535c3b2f625619f4dd1036ef1127f2e77bfedf89ed2eb2676ef076e0b6712"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.2" version: "4.1.3"
build_runner: build_runner:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -129,6 +129,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.2" 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: collection:
dependency: transitive dependency: transitive
description: description:
@ -157,18 +165,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: cupertino_icons name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.8" version: "1.0.9"
dart_style: dart_style:
dependency: transitive dependency: transitive
description: description:
name: dart_style name: dart_style
sha256: c87dfe3d56f183ffe9106a18aebc6db431fc7c98c31a54b952a77f3d54a85697 sha256: "29f7ecc274a86d32920b1d9cfc7502fa87220da41ec60b55f329559d5732e2b2"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.2" version: "3.1.7"
drift: drift:
dependency: "direct main" dependency: "direct main"
description: description:
@ -201,6 +209,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" 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: file:
dependency: transitive dependency: transitive
description: description:
@ -274,10 +290,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: flutter_secure_storage_windows name: flutter_secure_storage_windows
sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613" sha256: "471951813a97006d899db4948acc654a4f28c440083ea08178935ce20b173ec1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.0" version: "4.2.2"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -300,10 +316,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: google_fonts name: google_fonts
sha256: "517b20870220c48752eafa0ba1a797a092fb22df0d89535fd9991e86ee2cdd9c" sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.3.2" version: "6.3.3"
graphs: graphs:
dependency: transitive dependency: transitive
description: description:
@ -312,6 +328,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.3.2"
hooks:
dependency: transitive
description:
name: hooks
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
http: http:
dependency: "direct main" dependency: "direct main"
description: description:
@ -344,14 +368,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.5" 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: json_annotation:
dependency: transitive dependency: transitive
description: description:
name: json_annotation name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.9.0" version: "4.12.0"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@ -432,6 +480,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" 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: package_config:
dependency: transitive dependency: transitive
description: description:
@ -452,42 +508,42 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: path_provider name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.5" version: "2.1.6"
path_provider_android: path_provider_android:
dependency: transitive dependency: transitive
description: description:
name: path_provider_android name: path_provider_android
sha256: "3b4c1fc3aa55ddc9cd4aa6759984330d5c8e66aa7702a6223c61540dc6380c37" sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.19" version: "2.3.1"
path_provider_foundation: path_provider_foundation:
dependency: transitive dependency: transitive
description: description:
name: path_provider_foundation name: path_provider_foundation
sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.2" version: "2.6.0"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
name: path_provider_linux name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.1" version: "2.2.2"
path_provider_platform_interface: path_provider_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: path_provider_platform_interface name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" version: "2.1.3"
path_provider_windows: path_provider_windows:
dependency: transitive dependency: transitive
description: description:
@ -552,30 +608,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.0" 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: shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:
name: shared_preferences name: shared_preferences
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.3" version: "2.5.5"
shared_preferences_android: shared_preferences_android:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_android name: shared_preferences_android
sha256: bd14436108211b0d4ee5038689a56d4ae3620fd72fd6036e113bf1345bc74d9e sha256: "0634e64bd719f89c012f392938e173521f535d3ecaf66558fa94a056d22b5cc7"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.13" version: "2.4.27"
shared_preferences_foundation: shared_preferences_foundation:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_foundation name: shared_preferences_foundation
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.4" version: "2.5.6"
shared_preferences_linux: shared_preferences_linux:
dependency: transitive dependency: transitive
description: description:
@ -588,10 +652,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_platform_interface name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.1" version: "2.4.2"
shared_preferences_web: shared_preferences_web:
dependency: transitive dependency: transitive
description: description:
@ -633,18 +697,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: source_gen name: source_gen
sha256: "1d562a3c1f713904ebbed50d2760217fd8a51ca170ac4b05b0db490699dbac17" sha256: a603f1fb984a7391ae5978d1b92bfaaa08b350dca5c825256f925818f7943bf5
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.2.0" version: "4.2.4"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:
name: source_span name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.1" version: "1.10.2"
sqlcipher_flutter_libs: sqlcipher_flutter_libs:
dependency: "direct main" dependency: "direct main"
description: description:
@ -737,10 +801,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.0.0" version: "15.2.0"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:
@ -777,10 +841,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.15.0" version: "6.3.0"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
@ -798,5 +862,5 @@ packages:
source: hosted source: hosted
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.10.0-0 <4.0.0" dart: ">=3.12.0 <4.0.0"
flutter: ">=3.29.0" flutter: ">=3.44.0"