Files
valuecards/lib/screens/welcome_screen.dart
T
2026-09-18 00:01:42 +02:00

361 lines
11 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// Welcome screen with stacked sample cards and guide drawer.
library;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/values_sort_provider.dart';
import '../utils/app_theme.dart';
import '../widgets/practice_chrome.dart';
class WelcomeScreen extends StatelessWidget {
const WelcomeScreen({super.key});
@override
Widget build(BuildContext context) {
final text = Theme.of(context).textTheme;
final colors = Theme.of(context).colorScheme;
return PracticeScaffold(
header: const Padding(
padding: EdgeInsets.fromLTRB(12, 0, 12, 0),
child: BrandMark(),
),
footer: Column(
children: [
GhostPillButton(
label: 'How this practice works',
icon: Icons.info_outline,
onPressed: () => _showGuide(context),
),
PrimaryPillButton(
label: 'Begin Reflection',
icon: Icons.arrow_forward,
onPressed: () =>
context.read<ValuesSortController>().beginPractice(),
),
],
),
child: ListView(
children: [
const SizedBox(height: 8),
const _CardFan(),
const SizedBox(height: 20),
Text(
'Discover what matters most to you',
textAlign: TextAlign.center,
style: text.displaySmall,
),
const SizedBox(height: 10),
Text(
'Through a gentle, guided card-sort practice, clarify your intrinsic '
'values and illuminate the principles that guide your everyday decisions.',
textAlign: TextAlign.center,
style: text.bodyLarge?.copyWith(color: colors.onSurfaceVariant),
),
const SizedBox(height: 18),
Wrap(
alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 8,
runSpacing: 6,
children: [
_Meta(icon: Icons.schedule, label: '10–15 min'),
Text('•', style: text.labelMedium),
_Meta(icon: Icons.timelapse, label: 'Self-paced'),
Text('•', style: text.labelMedium),
_Meta(icon: Icons.lock_outline, label: 'Private & reflective'),
],
),
],
),
);
}
Future<void> _showGuide(BuildContext context) async {
final controller = context.read<ValuesSortController>();
await showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
builder: (ctx) {
final text = Theme.of(ctx).textTheme;
final colors = Theme.of(ctx).colorScheme;
return Padding(
padding: EdgeInsets.only(
left: 24,
right: 24,
top: 8,
bottom: MediaQuery.paddingOf(ctx).bottom + 24,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.diversity_3, color: colors.tertiary),
const SizedBox(width: 8),
Expanded(
child: Text(
'The Practice of Sorting',
style: text.titleLarge,
),
),
IconButton(
onPressed: () => Navigator.pop(ctx),
icon: const Icon(Icons.close),
),
],
),
const SizedBox(height: 8),
Text(
'Grounded in contemplative psychology, this sorting practice '
'helps clarify the core tenets that give your life meaning and direction.',
style: text.bodyLarge?.copyWith(color: colors.onSurfaceVariant),
),
const SizedBox(height: 16),
const _GuideStep(
index: 1,
title: 'One card at a time',
body:
'Review cards with diverse values one at a time, checking in with your intuition without overthinking.',
),
const _GuideStep(
index: 2,
title: 'Sort into reflective piles',
body:
'Place each card into Most, Very, Important, Somewhat, or Not Important — trusting your first honest response.',
),
const _GuideStep(
index: 3,
title: 'Distill your top values',
body:
'Narrow down to your core top values, then reflect so they can guide daily decisions with clarity.',
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppTheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppTheme.outlineVariant),
),
child: Row(
children: [
Icon(Icons.spa, color: colors.tertiary, size: 18),
const SizedBox(width: 10),
Expanded(
child: Text(
'There are no right or wrong answers—only your authentic presence.',
style: text.bodyMedium?.copyWith(
fontStyle: FontStyle.italic,
),
),
),
],
),
),
const SizedBox(height: 16),
PrimaryPillButton(
label: "Got it, let's begin",
onPressed: () {
Navigator.pop(ctx);
controller.beginPractice();
},
),
],
),
);
},
);
}
}
class _Meta extends StatelessWidget {
const _Meta({required this.icon, required this.label});
final IconData icon;
final String label;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final text = Theme.of(context).textTheme;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 15, color: colors.tertiary),
const SizedBox(width: 4),
Text(label, style: text.labelMedium),
],
);
}
}
class _GuideStep extends StatelessWidget {
const _GuideStep({
required this.index,
required this.title,
required this.body,
});
final int index;
final String title;
final String body;
@override
Widget build(BuildContext context) {
final text = Theme.of(context).textTheme;
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.only(bottom: 14),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 12,
backgroundColor: AppTheme.surfaceContainerHigh,
child: Text(
'$index',
style: text.labelMedium?.copyWith(
color: colors.primary,
fontWeight: FontWeight.w700,
),
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: text.titleMedium),
const SizedBox(height: 2),
Text(body, style: text.bodyMedium),
],
),
),
],
),
);
}
}
class _CardFan extends StatelessWidget {
const _CardFan();
@override
Widget build(BuildContext context) {
final text = Theme.of(context).textTheme;
final colors = Theme.of(context).colorScheme;
Widget mini({
required String domain,
required String title,
required String body,
required double angle,
required double dx,
required double dy,
bool hero = false,
}) {
return Transform.translate(
offset: Offset(dx, dy),
child: Transform.rotate(
angle: angle,
child: Container(
width: hero ? 210 : 180,
height: hero ? 155 : 135,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: hero ? Colors.white : AppTheme.surfaceContainer,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppTheme.outlineVariant),
boxShadow: [
BoxShadow(
color: const Color(0x142C2825),
blurRadius: hero ? 16 : 8,
offset: const Offset(0, 4),
),
],
),
child: ClipRect(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
domain.toUpperCase(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: text.labelSmall?.copyWith(
color: hero ? colors.secondary : colors.onSurfaceVariant,
letterSpacing: 1.2,
),
),
),
if (hero)
Icon(Icons.spa, size: 16, color: colors.primary),
],
),
const SizedBox(height: 4),
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: (hero ? text.headlineMedium : text.titleLarge)
?.copyWith(fontSize: hero ? 22 : 18, height: 1.1),
),
const SizedBox(height: 4),
Expanded(
child: Text(
body,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: text.bodySmall,
),
),
],
),
),
),
),
);
}
return SizedBox(
height: 210,
child: Stack(
alignment: Alignment.center,
children: [
mini(
domain: 'Experience',
title: 'Adventure',
body: 'To have new and exciting experiences.',
angle: -0.21,
dx: -48,
dy: 10,
),
mini(
domain: 'Relation',
title: 'Compassion',
body: 'To feel and act on concern for others.',
angle: 0.21,
dx: 48,
dy: 8,
),
mini(
domain: 'Core Ground',
title: 'Genuineness',
body: 'To act in a manner that is true to who I am.',
angle: 0.02,
dx: 0,
dy: 4,
hero: true,
),
],
),
);
}
}