360 lines
11 KiB
Dart
360 lines
11 KiB
Dart
/// 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 '../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: colors.surfaceContainerLow,
|
||
borderRadius: BorderRadius.circular(16),
|
||
border: Border.all(color: colors.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: colors.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.surface : colors.surfaceContainer,
|
||
borderRadius: BorderRadius.circular(20),
|
||
border: Border.all(color: colors.outlineVariant),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: colors.shadow.withValues(alpha: 0.08),
|
||
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,
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|