full app done

This commit is contained in:
davmar
2026-09-18 00:01:42 +02:00
parent 8475abeddb
commit 78fd23f69f
74 changed files with 8104 additions and 1963 deletions
+27 -62
View File
@@ -1,67 +1,41 @@
# MyApp
# Values Sort
Generic Flutter Android starter built with Flutter **3.47** (Dart **3.13**), Material 3, null safety, and [Provider](https://pub.dev/packages/provider) for state management.
Flutter Android app for a guided personal-values card-sort practice.
Built with Flutter **3.47** (Dart **3.13**), Material 3, Provider, and local persistence.
Package name: `com.example.myapp`
## Features
## Practice flow
- Splash screen that fades into the main shell
- Bottom navigation: Home, Search, Profile, Settings
- `ListView.builder` catalog with rounded, shadowed cards
- Detail screen with a matching `Hero` animation
- Light / dark / system theme, persisted with `shared_preferences`
- Add-item form with validation
- Mock catalog plus an `http` client aimed at JSONPlaceholder (toggle in Settings)
1. Welcome (+ how-it-works guide)
2. Sort each value into five reflective piles
3. Review / refine piles
4. Rank your most important values
5. Reflect with journal prompts
Progress (stage, piles, ranking, reflections) is saved on device with `shared_preferences`.
## Folder structure
```
android-flutter-app-generic/
├── android/ # Android embedding, Gradle, manifest
├── lib/
│ ├── main.dart # Entry point, Provider scope, MaterialApp
│ ├── models/
│ │ └── item.dart # Item + ItemCategory
│ ├── providers/
│ │ ├── item_provider.dart # Catalog ChangeNotifier
│ │ └── theme_provider.dart # ThemeMode ChangeNotifier
│ ├── screens/
│ │ ├── splash_screen.dart
│ │ ├── main_shell.dart # NavigationBar host
│ │ ├── home_screen.dart
│ │ ├── search_screen.dart
│ │ ├── profile_screen.dart
│ │ ├── settings_screen.dart
│ │ ├── item_detail_screen.dart
│ │ └── add_item_screen.dart
│ ├── services/
│ │ ├── storage_service.dart # shared_preferences wrapper
│ │ ├── item_api.dart # http placeholder
│ │ ├── fake_item_data.dart # Offline sample catalog
│ │ └── item_repository.dart # Mock vs remote switch
│ ├── utils/
│ │ ├── app_theme.dart # Material 3 light/dark themes
│ │ ├── constants.dart
│ │ └── helpers.dart
│ └── widgets/
│ ├── app_text_field.dart
│ ├── empty_state.dart
│ ├── item_card.dart
│ ├── item_hero_badge.dart
│ ├── loading_view.dart
│ └── responsive_body.dart
├── test/
│ └── widget_test.dart
└── pubspec.yaml
lib/
├── main.dart
├── data/values_deck.dart
├── models/
├── providers/
├── screens/ # welcome → sort → overview → rank → reflect
├── services/storage_service.dart
├── utils/
└── widgets/
assets/
├── data/values_deck.json
└── images/emblem.png
stitch/ # exported Stitch HTML/PNG design references
```
## Run the app
You need the [Flutter SDK](https://docs.flutter.dev/get-started/install) (3.47 or newer) and an Android emulator or device.
## Run
```bash
cd android-flutter-app-generic
flutter pub get
flutter run
```
@@ -69,15 +43,6 @@ flutter run
Useful extras:
```bash
flutter devices # list emulators / devices
flutter run -d android # target Android explicitly
flutter analyze # static analysis
flutter test # widget + unit tests
flutter analyze
flutter test
```
On first Android run, Flutter will download a Gradle distribution and compile the app. Accept any Android licenses with `flutter doctor --android-licenses` if prompted.
## Theme and data
- Open **Settings** to switch System / Light / Dark. The choice is stored locally.
- Turn on **Use remote sample API** to fetch posts from `https://jsonplaceholder.typicode.com/posts` through the `http` package. If the request fails, the app falls back to the bundled mock list.
+500
View File
@@ -0,0 +1,500 @@
[
{
"id": "v001",
"title": "Acceptance",
"description": "To be accepted as I am",
"domain": "Values"
},
{
"id": "v002",
"title": "Accuracy",
"description": "To be accurate in my opinions and beliefs",
"domain": "Values"
},
{
"id": "v003",
"title": "Achievement",
"description": "To have important accomplishments",
"domain": "Values"
},
{
"id": "v004",
"title": "Adventure",
"description": "To have new and exciting experiences",
"domain": "Values"
},
{
"id": "v005",
"title": "Attractiveness",
"description": "To be physically attractive",
"domain": "Values"
},
{
"id": "v006",
"title": "Authority",
"description": "To be in charge of and responsible for others",
"domain": "Values"
},
{
"id": "v007",
"title": "Autonomy",
"description": "To be self-determined and independent",
"domain": "Values"
},
{
"id": "v008",
"title": "Beauty",
"description": "To appreciate beauty around me",
"domain": "Values"
},
{
"id": "v009",
"title": "Caring",
"description": "To take care of others",
"domain": "Values"
},
{
"id": "v010",
"title": "Challenge",
"description": "To take on difficult tasks and problems",
"domain": "Values"
},
{
"id": "v011",
"title": "Comfort",
"description": "To have a pleasant and comfortable life",
"domain": "Values"
},
{
"id": "v012",
"title": "Change",
"description": "To have a life full of change and variety",
"domain": "Values"
},
{
"id": "v013",
"title": "Commitment",
"description": "To make enduring, meaningful commitments",
"domain": "Values"
},
{
"id": "v014",
"title": "Compassion",
"description": "To feel and act on concern for others",
"domain": "Values"
},
{
"id": "v015",
"title": "Contribution",
"description": "To make a lasting contribution in the world",
"domain": "Values"
},
{
"id": "v016",
"title": "Cooperation",
"description": "To work collaboratively with others",
"domain": "Values"
},
{
"id": "v017",
"title": "Courtesy",
"description": "To be considerate and polite toward others",
"domain": "Values"
},
{
"id": "v018",
"title": "Creativity",
"description": "To have new and original ideas",
"domain": "Values"
},
{
"id": "v019",
"title": "Dependability",
"description": "To be reliable and trustworthy",
"domain": "Values"
},
{
"id": "v020",
"title": "Duty",
"description": "To carry out my duties and obligations",
"domain": "Values"
},
{
"id": "v021",
"title": "Ecology",
"description": "To live in harmony with the environment",
"domain": "Values"
},
{
"id": "v022",
"title": "Excitement",
"description": "To have a life full of thrills and stimulation",
"domain": "Values"
},
{
"id": "v023",
"title": "Faithfulness",
"description": "To be loyal and true in relationships",
"domain": "Values"
},
{
"id": "v024",
"title": "Fame",
"description": "To be known and recognized",
"domain": "Values"
},
{
"id": "v025",
"title": "Family",
"description": "To have a happy, loving family",
"domain": "Values"
},
{
"id": "v026",
"title": "Fitness",
"description": "To be physically fit and strong",
"domain": "Values"
},
{
"id": "v027",
"title": "Flexibility",
"description": "To adjust to new circumstances easily",
"domain": "Values"
},
{
"id": "v028",
"title": "Forgiveness",
"description": "To be forgiving of others",
"domain": "Values"
},
{
"id": "v029",
"title": "Friendship",
"description": "To have close, supportive friends",
"domain": "Values"
},
{
"id": "v030",
"title": "Fun",
"description": "To play and have fun",
"domain": "Values"
},
{
"id": "v031",
"title": "Generosity",
"description": "To give what I have to others",
"domain": "Values"
},
{
"id": "v032",
"title": "Genuineness",
"description": "To act in a manner that is true to who I am",
"domain": "Values"
},
{
"id": "v033",
"title": "God's Will",
"description": "To seek and obey the will of God",
"domain": "Values"
},
{
"id": "v034",
"title": "Growth",
"description": "To keep changing and growing",
"domain": "Values"
},
{
"id": "v035",
"title": "Health",
"description": "To be physically well and healthy",
"domain": "Values"
},
{
"id": "v036",
"title": "Helpfulness",
"description": "To be helpful to others",
"domain": "Values"
},
{
"id": "v037",
"title": "Honesty",
"description": "To be honest and truthful",
"domain": "Values"
},
{
"id": "v038",
"title": "Hope",
"description": "To maintain a positive and optimistic outlook",
"domain": "Values"
},
{
"id": "v039",
"title": "Humility",
"description": "To be modest and unassuming",
"domain": "Values"
},
{
"id": "v040",
"title": "Humor",
"description": "To see the humorous side of myself and the world",
"domain": "Values"
},
{
"id": "v041",
"title": "Independence",
"description": "To be free from dependence on others",
"domain": "Values"
},
{
"id": "v042",
"title": "Industry",
"description": "To work hard and well at my life tasks",
"domain": "Values"
},
{
"id": "v043",
"title": "Inner Peace",
"description": "To experience personal peace",
"domain": "Values"
},
{
"id": "v044",
"title": "Intimacy",
"description": "To share my innermost experiences with others",
"domain": "Values"
},
{
"id": "v045",
"title": "Justice",
"description": "To promote fair and equal treatment for all",
"domain": "Values"
},
{
"id": "v046",
"title": "Knowledge",
"description": "To learn and contribute valuable knowledge",
"domain": "Values"
},
{
"id": "v047",
"title": "Leisure",
"description": "To take time to relax and enjoy",
"domain": "Values"
},
{
"id": "v048",
"title": "Loved",
"description": "To be loved by those close to me",
"domain": "Values"
},
{
"id": "v049",
"title": "Loving",
"description": "To give love to others",
"domain": "Values"
},
{
"id": "v050",
"title": "Mastery",
"description": "To be competent in my everyday activities",
"domain": "Values"
},
{
"id": "v051",
"title": "Mindfulness",
"description": "To live conscious and mindful of the present moment",
"domain": "Values"
},
{
"id": "v052",
"title": "Moderation",
"description": "To avoid excesses and find a middle ground",
"domain": "Values"
},
{
"id": "v053",
"title": "Monogamy",
"description": "To have one close, loving relationship",
"domain": "Values"
},
{
"id": "v054",
"title": "Non-Conformity",
"description": "To question or challenge authority and norms",
"domain": "Values"
},
{
"id": "v055",
"title": "Nurturance",
"description": "To take care of and nurture others",
"domain": "Values"
},
{
"id": "v056",
"title": "Openness",
"description": "To be open to new experiences, ideas, and options",
"domain": "Values"
},
{
"id": "v057",
"title": "Order",
"description": "To have a life that is well-ordered and organized",
"domain": "Values"
},
{
"id": "v058",
"title": "Passion",
"description": "To have deep feelings about ideas, activities, or people",
"domain": "Values"
},
{
"id": "v059",
"title": "Pleasure",
"description": "To feel good",
"domain": "Values"
},
{
"id": "v060",
"title": "Popularity",
"description": "To be well-liked by many people",
"domain": "Values"
},
{
"id": "v061",
"title": "Power",
"description": "To have control over others",
"domain": "Values"
},
{
"id": "v062",
"title": "Purpose",
"description": "To have meaning and direction in my life",
"domain": "Values"
},
{
"id": "v063",
"title": "Rationality",
"description": "To be guided by reason and logic",
"domain": "Values"
},
{
"id": "v064",
"title": "Realism",
"description": "To see and act realistically and practically",
"domain": "Values"
},
{
"id": "v065",
"title": "Responsibility",
"description": "To make and carry out responsible decisions",
"domain": "Values"
},
{
"id": "v066",
"title": "Risk",
"description": "To take risks and chances",
"domain": "Values"
},
{
"id": "v067",
"title": "Romance",
"description": "To have intense, exciting love in my life",
"domain": "Values"
},
{
"id": "v068",
"title": "Safety",
"description": "To be safe and secure",
"domain": "Values"
},
{
"id": "v069",
"title": "Self-Acceptance",
"description": "To accept myself as I am",
"domain": "Values"
},
{
"id": "v070",
"title": "Self-Control",
"description": "To be disciplined in my own actions",
"domain": "Values"
},
{
"id": "v071",
"title": "Self-Esteem",
"description": "To feel good about myself",
"domain": "Values"
},
{
"id": "v072",
"title": "Self-Knowledge",
"description": "To have a deep and honest understanding of myself",
"domain": "Values"
},
{
"id": "v073",
"title": "Service",
"description": "To be of service to others",
"domain": "Values"
},
{
"id": "v074",
"title": "Sexuality",
"description": "To have an active and satisfying sex life",
"domain": "Values"
},
{
"id": "v075",
"title": "Simplicity",
"description": "To live life simply, with minimal needs",
"domain": "Values"
},
{
"id": "v076",
"title": "Solitude",
"description": "To have time and space where I can be apart from others",
"domain": "Values"
},
{
"id": "v077",
"title": "Spirituality",
"description": "To grow and mature spiritually",
"domain": "Values"
},
{
"id": "v078",
"title": "Stability",
"description": "To have a life that stays fairly consistent",
"domain": "Values"
},
{
"id": "v079",
"title": "Tolerance",
"description": "To accept and respect those who differ from me",
"domain": "Values"
},
{
"id": "v080",
"title": "Tradition",
"description": "To follow respected patterns of the past",
"domain": "Values"
},
{
"id": "v081",
"title": "Virtue",
"description": "To live a morally pure and excellent life",
"domain": "Values"
},
{
"id": "v082",
"title": "Wealth",
"description": "To have plenty of money",
"domain": "Values"
},
{
"id": "v083",
"title": "World Peace",
"description": "To work to promote peace in the world",
"domain": "Values"
}
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

+17
View File
@@ -0,0 +1,17 @@
/// Loads the bundled values deck from assets.
library;
import 'dart:convert';
import 'package:flutter/services.dart';
import '../models/value_card.dart';
/// Reads `assets/data/values_deck.json` and returns shuffled-ready cards.
Future<List<ValueCard>> loadValuesDeck() async {
final raw = await rootBundle.loadString('assets/data/values_deck.json');
final list = jsonDecode(raw) as List<dynamic>;
return list
.map((e) => ValueCard.fromJson(e as Map<String, dynamic>))
.toList(growable: false);
}
+6 -21
View File
@@ -1,13 +1,12 @@
/// Application entry point: wires services, Provider, and [MaterialApp].
/// Application entry point: wires Provider and the Values Sort practice.
library;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/item_provider.dart';
import 'providers/theme_provider.dart';
import 'providers/values_sort_provider.dart';
import 'screens/splash_screen.dart';
import 'services/item_repository.dart';
import 'services/storage_service.dart';
import 'utils/app_theme.dart';
import 'utils/constants.dart';
@@ -18,25 +17,14 @@ Future<void> main() async {
final storage = StorageService();
await storage.init();
runApp(
MyApp(
storage: storage,
repository: ItemRepository(useRemoteData: storage.useRemoteData),
),
);
runApp(MyApp(storage: storage));
}
/// Root widget. Theme and catalog controllers live here so every route
/// can `context.watch` them.
/// Root widget. Theme and sort session live here so every route can watch them.
class MyApp extends StatelessWidget {
const MyApp({
super.key,
required this.storage,
required this.repository,
});
const MyApp({super.key, required this.storage});
final StorageService storage;
final ItemRepository repository;
@override
Widget build(BuildContext context) {
@@ -44,10 +32,7 @@ class MyApp extends StatelessWidget {
providers: [
ChangeNotifierProvider(create: (_) => ThemeController(storage)),
ChangeNotifierProvider(
create: (_) => ItemController(
repository: repository,
storage: storage,
)..loadItems(),
create: (_) => ValuesSortController(storage: storage)..load(),
),
],
child: const _MaterialRoot(),
-130
View File
@@ -1,130 +0,0 @@
/// Domain models for catalog items shown in lists and detail screens.
library;
import 'package:flutter/material.dart';
/// High-level grouping used for filters, icons, and accent colors.
enum ItemCategory {
design,
development,
productivity,
lifestyle,
}
extension ItemCategoryX on ItemCategory {
String get label => switch (this) {
ItemCategory.design => 'Design',
ItemCategory.development => 'Development',
ItemCategory.productivity => 'Productivity',
ItemCategory.lifestyle => 'Lifestyle',
};
IconData get icon => switch (this) {
ItemCategory.design => Icons.palette_outlined,
ItemCategory.development => Icons.code,
ItemCategory.productivity => Icons.bolt_outlined,
ItemCategory.lifestyle => Icons.spa_outlined,
};
/// Stable accent used by [Hero] tiles so the animation has a colored target.
Color get accent => switch (this) {
ItemCategory.design => const Color(0xFF7C3AED),
ItemCategory.development => const Color(0xFF0F766E),
ItemCategory.productivity => const Color(0xFFC2410C),
ItemCategory.lifestyle => const Color(0xFF0369A1),
};
static ItemCategory fromName(String name) {
return ItemCategory.values.firstWhere(
(category) => category.name == name,
orElse: () => ItemCategory.productivity,
);
}
static ItemCategory fromIndex(int index) {
return ItemCategory.values[index.abs() % ItemCategory.values.length];
}
}
/// A catalog entry displayed on Home / Search and opened on the detail route.
class Item {
const Item({
required this.id,
required this.title,
required this.subtitle,
required this.description,
required this.category,
required this.createdAt,
});
final String id;
final String title;
final String subtitle;
final String description;
final ItemCategory category;
final DateTime createdAt;
/// Tag used by the list-to-detail [Hero] animation.
String get heroTag => 'item-hero-$id';
Item copyWith({
String? id,
String? title,
String? subtitle,
String? description,
ItemCategory? category,
DateTime? createdAt,
}) {
return Item(
id: id ?? this.id,
title: title ?? this.title,
subtitle: subtitle ?? this.subtitle,
description: description ?? this.description,
category: category ?? this.category,
createdAt: createdAt ?? this.createdAt,
);
}
/// Maps a JSONPlaceholder `/posts` payload (or any similar JSON object).
factory Item.fromJson(Map<String, dynamic> json) {
final id = json['id']?.toString() ?? '0';
final title = (json['title'] as String? ?? 'Untitled').trim();
final body = (json['body'] as String? ?? '').trim();
final numericId = int.tryParse(id) ?? 0;
return Item(
id: 'remote-$id',
title: _titleCase(title),
subtitle: _firstSentence(body),
description: body.replaceAll('\n', ' '),
category: ItemCategoryX.fromIndex(numericId),
createdAt: DateTime(2026, 1, 1).add(Duration(days: numericId)),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'subtitle': subtitle,
'description': description,
'category': category.name,
'createdAt': createdAt.toIso8601String(),
};
}
static String _titleCase(String value) {
if (value.isEmpty) return value;
return value[0].toUpperCase() + value.substring(1);
}
static String _firstSentence(String value) {
if (value.isEmpty) return 'No summary available.';
final cleaned = value.replaceAll('\n', ' ');
final period = cleaned.indexOf('.');
if (period <= 0) {
return cleaned.length > 80 ? '${cleaned.substring(0, 77)}…' : cleaned;
}
return cleaned.substring(0, period + 1);
}
}
+33
View File
@@ -0,0 +1,33 @@
/// A single values-sort card with title, description, and optional domain.
library;
/// Immutable card drawn from the values deck.
class ValueCard {
const ValueCard({
required this.id,
required this.title,
required this.description,
this.domain = 'Values',
});
final String id;
final String title;
final String description;
final String domain;
factory ValueCard.fromJson(Map<String, dynamic> json) {
return ValueCard(
id: json['id'] as String,
title: json['title'] as String,
description: json['description'] as String? ?? '',
domain: json['domain'] as String? ?? 'Values',
);
}
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'description': description,
'domain': domain,
};
}
+68
View File
@@ -0,0 +1,68 @@
/// The five reflective piles used during the initial card sort.
library;
import 'package:flutter/material.dart';
/// Pile ranks from most to least important (design labels 5→1).
enum ValuePile {
mostImportant,
veryImportant,
important,
somewhatImportant,
notImportant,
}
extension ValuePileX on ValuePile {
/// Short UI label (e.g. "Most Important").
String get label => switch (this) {
ValuePile.mostImportant => 'Most Important',
ValuePile.veryImportant => 'Very Important',
ValuePile.important => 'Important',
ValuePile.somewhatImportant => 'Somewhat Important',
ValuePile.notImportant => 'Not Important',
};
/// Compact chip label used in filters and progress bars.
String get shortLabel => switch (this) {
ValuePile.mostImportant => 'Most',
ValuePile.veryImportant => 'Very',
ValuePile.important => 'Important',
ValuePile.somewhatImportant => 'Somewhat',
ValuePile.notImportant => 'Not',
};
/// Supporting line under pile buttons on the sort screen.
String get subtitle => switch (this) {
ValuePile.mostImportant => 'Non-negotiable core',
ValuePile.veryImportant => 'Deeply held guide',
ValuePile.important => 'Valued aspect',
ValuePile.somewhatImportant => 'Contextual',
ValuePile.notImportant => 'Not a current compass',
};
/// Numeric rank shown in the Stitch designs (5 = most).
int get rank => switch (this) {
ValuePile.mostImportant => 5,
ValuePile.veryImportant => 4,
ValuePile.important => 3,
ValuePile.somewhatImportant => 2,
ValuePile.notImportant => 1,
};
/// Accent color for this pile.
Color get accent => switch (this) {
ValuePile.mostImportant => const Color(0xFFC45C3E),
ValuePile.veryImportant => const Color(0xFFC87D55),
ValuePile.important => const Color(0xFF6B8168),
ValuePile.somewhatImportant => const Color(0xFF5B7A8C),
ValuePile.notImportant => const Color(0xFF8A847C),
};
static ValuePile? fromName(String? name) {
if (name == null) return null;
for (final pile in ValuePile.values) {
if (pile.name == name) return pile;
}
return null;
}
}
-93
View File
@@ -1,93 +0,0 @@
/// [ChangeNotifier] that loads, filters, and mutates the in-memory item list.
library;
import 'package:flutter/foundation.dart';
import '../models/item.dart';
import '../services/item_repository.dart';
import '../services/storage_service.dart';
/// App-wide catalog state. Screens call [loadItems], [addItem], and [search].
class ItemController extends ChangeNotifier {
ItemController({
required this._repository,
required this._storage,
}) {
_repository.useRemoteData = _storage.useRemoteData;
}
final ItemRepository _repository;
final StorageService _storage;
List<Item> _items = const [];
bool _isLoading = false;
String? _error;
List<Item> get items => List.unmodifiable(_items);
bool get isLoading => _isLoading;
String? get error => _error;
bool get useRemoteData => _repository.useRemoteData;
int get count => _items.length;
Future<void> loadItems() async {
_isLoading = true;
_error = null;
notifyListeners();
try {
// Small delay so the loading indicator is visible with local data.
await Future<void>.delayed(const Duration(milliseconds: 250));
_items = await _repository.fetchItems();
} catch (error, stackTrace) {
_error = 'Could not load items.';
debugPrint('ItemController.loadItems failed: $error\n$stackTrace');
} finally {
_isLoading = false;
notifyListeners();
}
}
Future<void> setUseRemoteData(bool value) async {
if (value == _repository.useRemoteData) return;
_repository.useRemoteData = value;
await _storage.setUseRemoteData(value);
notifyListeners();
await loadItems();
}
/// Case-insensitive match against title, subtitle, and category.
List<Item> search(String query) {
final needle = query.trim().toLowerCase();
if (needle.isEmpty) return items;
return _items.where((item) {
return item.title.toLowerCase().contains(needle) ||
item.subtitle.toLowerCase().contains(needle) ||
item.category.label.toLowerCase().contains(needle);
}).toList(growable: false);
}
Item addItem({
required String title,
required String subtitle,
required String description,
required ItemCategory category,
}) {
final created = _repository.createLocal(
title: title,
subtitle: subtitle,
description: description,
category: category,
);
_items = [created, ..._items];
notifyListeners();
return created;
}
Item? findById(String id) {
for (final item in _items) {
if (item.id == id) return item;
}
return null;
}
}
+26 -11
View File
@@ -4,12 +4,11 @@ library;
import 'package:flutter/material.dart';
import '../services/storage_service.dart';
import '../utils/helpers.dart';
/// Provides [themeMode] to [MaterialApp] and writes changes to storage.
class ThemeController extends ChangeNotifier {
ThemeController(this._storage) {
_themeMode = themeModeFromName(_storage.themeModeName);
_themeMode = _fromName(_storage.themeModeName);
}
final StorageService _storage;
@@ -17,21 +16,37 @@ class ThemeController extends ChangeNotifier {
ThemeMode get themeMode => _themeMode;
bool get isDark => _themeMode == ThemeMode.dark;
bool get isDark =>
_themeMode == ThemeMode.dark ||
(_themeMode == ThemeMode.system &&
WidgetsBinding.instance.platformDispatcher.platformBrightness ==
Brightness.dark);
Future<void> setThemeMode(ThemeMode mode) async {
if (mode == _themeMode) return;
_themeMode = mode;
notifyListeners();
await _storage.setThemeModeName(nameFromThemeMode(mode));
await _storage.setThemeModeName(_toName(mode));
}
Future<void> cycleThemeMode() {
final next = switch (_themeMode) {
ThemeMode.system => ThemeMode.light,
ThemeMode.light => ThemeMode.dark,
ThemeMode.dark => ThemeMode.system,
};
return setThemeMode(next);
/// Toggles between light and dark (moon / sun control).
Future<void> toggleLightDark() {
return setThemeMode(isDark ? ThemeMode.light : ThemeMode.dark);
}
}
ThemeMode _fromName(String name) {
return switch (name) {
'dark' => ThemeMode.dark,
'system' => ThemeMode.system,
_ => ThemeMode.light,
};
}
String _toName(ThemeMode mode) {
return switch (mode) {
ThemeMode.light => 'light',
ThemeMode.dark => 'dark',
ThemeMode.system => 'system',
};
}
+455
View File
@@ -0,0 +1,455 @@
/// Session state for the Values Sort practice: sorting, ranking, reflection.
library;
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import '../data/values_deck.dart';
import '../models/value_card.dart';
import '../models/value_pile.dart';
import '../services/storage_service.dart';
/// Contemplative prompts shown during the reflection stage.
const List<String> kReflectionPrompts = [
'What does this value mean to you?',
'Why did you choose this as important?',
'When do you live this value most fully?',
'What gets in the way of honoring this value?',
];
/// Stages in the guided practice flow.
enum SortStage {
welcome,
sorting,
hub,
overview,
pileReview,
ranking,
reflection,
}
/// ChangeNotifier that owns the deck, pile assignments, ranking, and journals.
class ValuesSortController extends ChangeNotifier {
ValuesSortController({required StorageService this._storage});
final StorageService _storage;
bool _loading = true;
bool get loading => _loading;
String? _error;
String? get error => _error;
SortStage _stage = SortStage.welcome;
SortStage get stage => _stage;
List<ValueCard> _deck = const [];
List<ValueCard> get deck => _deck;
/// Remaining unsorted cards (front of list is current).
final List<ValueCard> _unsorted = [];
List<ValueCard> get unsorted => List.unmodifiable(_unsorted);
ValueCard? get currentCard =>
_unsorted.isEmpty ? null : _unsorted.first;
final Map<String, ValuePile> _assignments = {};
Map<String, ValuePile> get assignments => Map.unmodifiable(_assignments);
/// Ordered top values (index 0 = #1 / North Star).
final List<String> _rankedIds = [];
List<String> get rankedIds => List.unmodifiable(_rankedIds);
/// Reflections keyed by `valueId|promptIndex`.
final Map<String, String> _reflections = {};
ValuePile? _reviewPile;
ValuePile? get reviewPile => _reviewPile;
String? _activeReflectionValueId;
String? get activeReflectionValueId => _activeReflectionValueId;
int _activePromptIndex = 0;
int get activePromptIndex => _activePromptIndex;
final List<String> _undoStack = [];
bool _restoring = false;
/// True when a non-welcome session is on disk / in memory.
bool get hasInProgressSession =>
_stage != SortStage.welcome ||
_assignments.isNotEmpty ||
_reflections.isNotEmpty;
Future<void> load({List<ValueCard>? seedDeck}) async {
_loading = true;
_error = null;
notifyListeners();
try {
final cards = seedDeck ?? await loadValuesDeck();
_deck = cards;
_resetSortState();
await _restoreSession();
} catch (e) {
_error = e.toString();
} finally {
_loading = false;
notifyListeners();
}
}
String get _deckFingerprint =>
_deck.map((c) => c.id).join('|');
void _resetSortState() {
_assignments.clear();
_undoStack.clear();
_unsorted
..clear()
..addAll(_deck);
_rankedIds.clear();
_reflections.clear();
_reviewPile = null;
_activeReflectionValueId = null;
_activePromptIndex = 0;
}
void beginPractice() {
if (_deck.isEmpty) return;
_resetSortState();
_stage = SortStage.sorting;
_commit();
}
void restart() {
_resetSortState();
_stage = SortStage.welcome;
_commit(clear: true);
}
int get sortedCount => _assignments.length;
int get totalCount => _deck.length;
double get sortProgress =>
totalCount == 0 ? 0 : sortedCount / totalCount;
bool get isSortComplete =>
_unsorted.isEmpty && _assignments.length == _deck.length;
List<ValueCard> cardsInPile(ValuePile pile) {
return _deck
.where((c) => _assignments[c.id] == pile)
.toList(growable: false);
}
int countInPile(ValuePile pile) => cardsInPile(pile).length;
ValueCard? cardById(String id) {
for (final c in _deck) {
if (c.id == id) return c;
}
return null;
}
void assignCurrent(ValuePile pile) {
final card = currentCard;
if (card == null) return;
_undoStack.add(card.id);
_assignments[card.id] = pile;
_unsorted.removeAt(0);
if (isSortComplete) {
_seedRankingFromMostImportant();
_stage = SortStage.hub;
}
_commit();
}
void undoLastSort() {
if (_undoStack.isEmpty) return;
final id = _undoStack.removeLast();
_assignments.remove(id);
final card = cardById(id);
if (card != null) {
_unsorted.insert(0, card);
}
if (_stage != SortStage.sorting) {
_stage = SortStage.sorting;
}
_commit();
}
void moveCard(String id, ValuePile pile) {
if (!_assignments.containsKey(id) &&
!_unsorted.any((c) => c.id == id)) {
return;
}
_unsorted.removeWhere((c) => c.id == id);
_assignments[id] = pile;
_rankedIds.remove(id);
_syncRankingWithMostImportant();
_commit();
}
void _seedRankingFromMostImportant() {
_rankedIds
..clear()
..addAll(cardsInPile(ValuePile.mostImportant).map((c) => c.id));
}
void _syncRankingWithMostImportant() {
final most = cardsInPile(ValuePile.mostImportant).map((c) => c.id).toSet();
_rankedIds.removeWhere((id) => !most.contains(id));
for (final id in most) {
if (!_rankedIds.contains(id)) {
_rankedIds.add(id);
}
}
}
void goToHub() {
_stage = SortStage.hub;
_commit();
}
void goToOverview() {
_stage = SortStage.overview;
_commit();
}
void openPileReview(ValuePile pile) {
_reviewPile = pile;
_stage = SortStage.pileReview;
_commit();
}
void goToRanking() {
_syncRankingWithMostImportant();
if (_rankedIds.isEmpty) {
final fallback = cardsInPile(ValuePile.veryImportant);
for (final c in fallback.take(8)) {
_assignments[c.id] = ValuePile.mostImportant;
_rankedIds.add(c.id);
}
}
_stage = SortStage.ranking;
_commit();
}
void reorderRanked(int oldIndex, int newIndex) {
final id = _rankedIds.removeAt(oldIndex);
_rankedIds.insert(newIndex, id);
_commit();
}
void moveRank(int index, {required bool up}) {
final target = up ? index - 1 : index + 1;
if (target < 0 || target >= _rankedIds.length) return;
final id = _rankedIds.removeAt(index);
_rankedIds.insert(target, id);
_commit();
}
List<ValueCard> get rankedCards => _rankedIds
.map(cardById)
.whereType<ValueCard>()
.toList(growable: false);
/// Top values used for reflection (up to 5).
List<ValueCard> get coreValues => rankedCards.take(5).toList(growable: false);
void goToReflection({String? valueId}) {
final cores = coreValues;
if (valueId != null && cores.any((c) => c.id == valueId)) {
_activeReflectionValueId = valueId;
} else {
_activeReflectionValueId = cores.isEmpty ? null : cores.first.id;
}
_activePromptIndex = 0;
_stage = SortStage.reflection;
_commit();
}
void selectReflectionValue(String id) {
_activeReflectionValueId = id;
_activePromptIndex = 0;
_commit();
}
void selectPrompt(int index) {
_activePromptIndex = index.clamp(0, kReflectionPrompts.length - 1);
_commit();
}
String reflectionKey(String valueId, int promptIndex) =>
'$valueId|$promptIndex';
String reflectionFor(String valueId, int promptIndex) =>
_reflections[reflectionKey(valueId, promptIndex)] ?? '';
void setReflection(String valueId, int promptIndex, String text) {
final key = reflectionKey(valueId, promptIndex);
if (text.trim().isEmpty) {
_reflections.remove(key);
} else {
_reflections[key] = text.trim();
}
_commit();
}
int answeredPromptCount(String valueId) {
var n = 0;
for (var i = 0; i < kReflectionPrompts.length; i++) {
if (reflectionFor(valueId, i).isNotEmpty) n++;
}
return n;
}
int get totalAnsweredReflections {
var n = 0;
for (final c in coreValues) {
n += answeredPromptCount(c.id);
}
return n;
}
int get totalReflectionSlots =>
coreValues.length * kReflectionPrompts.length;
void _commit({bool clear = false}) {
notifyListeners();
if (_restoring) return;
if (clear || (_stage == SortStage.welcome && _assignments.isEmpty)) {
unawaited(_storage.clearSortSession());
return;
}
unawaited(_persistNow());
}
Future<void> _persistNow() async {
try {
final payload = <String, dynamic>{
'version': 1,
'deckFingerprint': _deckFingerprint,
'stage': _stage.name,
'reviewPile': _reviewPile?.name,
'assignments': {
for (final e in _assignments.entries) e.key: e.value.name,
},
'unsortedIds': _unsorted.map((c) => c.id).toList(growable: false),
'rankedIds': List<String>.from(_rankedIds),
'reflections': Map<String, String>.from(_reflections),
'activeReflectionValueId': _activeReflectionValueId,
'activePromptIndex': _activePromptIndex,
'undoStack': List<String>.from(_undoStack),
};
await _storage.setSortSessionJson(jsonEncode(payload));
} catch (_) {
// Persistence failures should not break the practice UI.
}
}
Future<void> _restoreSession() async {
final raw = _storage.sortSessionJson;
if (raw == null || raw.isEmpty) return;
try {
final data = jsonDecode(raw) as Map<String, dynamic>;
if (data['deckFingerprint'] != _deckFingerprint) {
await _storage.clearSortSession();
return;
}
_restoring = true;
final knownIds = _deck.map((c) => c.id).toSet();
final assignmentsRaw =
(data['assignments'] as Map?)?.cast<String, dynamic>() ?? {};
_assignments.clear();
for (final e in assignmentsRaw.entries) {
final pile = ValuePileX.fromName('${e.value}');
if (pile != null && knownIds.contains(e.key)) {
_assignments[e.key] = pile;
}
}
final unsortedIds =
(data['unsortedIds'] as List?)?.cast<String>() ?? const <String>[];
_unsorted
..clear()
..addAll(
unsortedIds
.map(cardById)
.whereType<ValueCard>(),
);
// Recover any deck cards missing from both maps (deck updates / corruption).
final present = {..._assignments.keys, ..._unsorted.map((c) => c.id)};
for (final card in _deck) {
if (!present.contains(card.id)) {
_unsorted.add(card);
}
}
_rankedIds
..clear()
..addAll(
((data['rankedIds'] as List?)?.cast<String>() ?? const <String>[])
.where(knownIds.contains),
);
_syncRankingWithMostImportant();
_reflections
..clear()
..addAll(
((data['reflections'] as Map?)?.cast<String, dynamic>() ?? {})
.map((k, v) => MapEntry(k, '$v')),
);
_undoStack
..clear()
..addAll(
((data['undoStack'] as List?)?.cast<String>() ?? const <String>[])
.where(knownIds.contains),
);
_reviewPile = ValuePileX.fromName(data['reviewPile'] as String?);
_activeReflectionValueId = data['activeReflectionValueId'] as String?;
if (_activeReflectionValueId != null &&
!knownIds.contains(_activeReflectionValueId)) {
_activeReflectionValueId = null;
}
_activePromptIndex = (data['activePromptIndex'] as num?)?.toInt() ?? 0;
final stageName = data['stage'] as String? ?? SortStage.welcome.name;
_stage = SortStage.values.firstWhere(
(s) => s.name == stageName,
orElse: () => SortStage.welcome,
);
// If restored mid-sort but nothing left unsorted, land on the hub.
if (_stage == SortStage.sorting && isSortComplete) {
_seedRankingFromMostImportant();
_stage = SortStage.hub;
}
if (_stage == SortStage.pileReview && _reviewPile == null) {
_stage = SortStage.hub;
}
} catch (_) {
await _storage.clearSortSession();
_resetSortState();
_stage = SortStage.welcome;
} finally {
_restoring = false;
}
}
@override
void dispose() {
if (!_restoring && hasInProgressSession) {
unawaited(_persistNow());
}
super.dispose();
}
}
-142
View File
@@ -1,142 +0,0 @@
/// Validated form that inserts a new [Item] into [ItemController].
library;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/item.dart';
import '../providers/item_provider.dart';
import '../widgets/app_text_field.dart';
import '../widgets/responsive_body.dart';
class AddItemScreen extends StatefulWidget {
const AddItemScreen({super.key});
@override
State<AddItemScreen> createState() => _AddItemScreenState();
}
class _AddItemScreenState extends State<AddItemScreen> {
final _formKey = GlobalKey<FormState>();
final _title = TextEditingController();
final _subtitle = TextEditingController();
final _description = TextEditingController();
ItemCategory? _category;
bool _submitted = false;
@override
void dispose() {
_title.dispose();
_subtitle.dispose();
_description.dispose();
super.dispose();
}
String? _requiredLength(String? value, {required int min, String? label}) {
final text = value?.trim() ?? '';
if (text.isEmpty) return 'Enter a ${label ?? 'value'}.';
if (text.length < min) {
return '${label ?? 'This field'} must be at least $min characters.';
}
return null;
}
void _submit() {
setState(() => _submitted = true);
final form = _formKey.currentState;
if (form == null || !form.validate() || _category == null) return;
context.read<ItemController>().addItem(
title: _title.text,
subtitle: _subtitle.text,
description: _description.text,
category: _category!,
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Added “${_title.text.trim()}”')),
);
Navigator.of(context).pop();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(title: const Text('Add item')),
body: ResponsiveBody(
child: Form(
key: _formKey,
autovalidateMode: _submitted
? AutovalidateMode.onUserInteraction
: AutovalidateMode.disabled,
child: ListView(
children: [
const SizedBox(height: 8),
Text(
'Create a catalog entry. All fields are required.',
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 20),
AppTextField(
controller: _title,
label: 'Title',
hint: 'Give it a short name',
autofocus: true,
textInputAction: TextInputAction.next,
validator: (value) =>
_requiredLength(value, min: 3, label: 'Title'),
),
const SizedBox(height: 16),
AppTextField(
controller: _subtitle,
label: 'Summary',
hint: 'One sentence overview',
textInputAction: TextInputAction.next,
validator: (value) =>
_requiredLength(value, min: 8, label: 'Summary'),
),
const SizedBox(height: 16),
DropdownButtonFormField<ItemCategory>(
initialValue: _category,
decoration: const InputDecoration(labelText: 'Category'),
items: [
for (final category in ItemCategory.values)
DropdownMenuItem(
value: category,
child: Text(category.label),
),
],
onChanged: (value) => setState(() => _category = value),
validator: (value) =>
value == null ? 'Choose a category.' : null,
),
const SizedBox(height: 16),
AppTextField(
controller: _description,
label: 'Description',
hint: 'A longer note the detail screen will show',
maxLines: 5,
keyboardType: TextInputType.multiline,
validator: (value) =>
_requiredLength(value, min: 16, label: 'Description'),
),
const SizedBox(height: 24),
FilledButton(
onPressed: _submit,
child: const Text('Save item'),
),
const SizedBox(height: 12),
OutlinedButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
const SizedBox(height: 24),
],
),
),
),
);
}
}
+190
View File
@@ -0,0 +1,190 @@
/// Stage 1 — assign each value card into one of five reflective piles.
library;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/value_pile.dart';
import '../providers/values_sort_provider.dart';
import '../widgets/practice_chrome.dart';
import '../widgets/value_card_tile.dart';
class CardSortingScreen extends StatelessWidget {
const CardSortingScreen({super.key});
@override
Widget build(BuildContext context) {
final sort = context.watch<ValuesSortController>();
final text = Theme.of(context).textTheme;
final colors = Theme.of(context).colorScheme;
final card = sort.currentCard;
return PracticeScaffold(
padding: const EdgeInsets.fromLTRB(20, 4, 20, 8),
header: Padding(
padding: const EdgeInsets.fromLTRB(12, 4, 12, 0),
child: Row(
children: [
TextButton.icon(
onPressed: sort.sortedCount == 0 ? null : sort.undoLastSort,
icon: const Icon(Icons.undo, size: 18),
label: const Text('Undo'),
),
const Spacer(),
Text(
'Stage 1 · Initial Sort',
style: text.labelMedium,
),
],
),
),
child: Column(
children: [
Row(
children: [
Icon(Icons.bookmark_border, size: 18, color: colors.onSurfaceVariant),
const SizedBox(width: 6),
Expanded(
child: Text(
'${sort.sortedCount} of ${sort.totalCount} values sorted',
style: text.labelMedium,
),
),
Text(
'${(sort.sortProgress * 100).round()}%',
style: text.labelMedium?.copyWith(
color: colors.primary,
fontWeight: FontWeight.w700,
),
),
],
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(999),
child: LinearProgressIndicator(
value: sort.sortProgress,
minHeight: 6,
backgroundColor: colors.surfaceContainerHigh,
color: colors.secondary,
),
),
const SizedBox(height: 12),
Expanded(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 280),
child: card == null
? Center(
key: const ValueKey('empty'),
child: Text('All sorted', style: text.headlineMedium),
)
: SingleChildScrollView(
key: ValueKey(card.id),
child: Column(
children: [
ValueCardTile(card: card, elevation: 2),
const SizedBox(height: 14),
Text(
'Tap a pile to sort',
style: text.labelMedium?.copyWith(
color: colors.onSurfaceVariant,
),
),
const SizedBox(height: 8),
...ValuePile.values.map((pile) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: _PileButton(
pile: pile,
count: sort.countInPile(pile),
onTap: () => sort.assignCurrent(pile),
),
);
}),
],
),
),
),
),
],
),
);
}
}
class _PileButton extends StatelessWidget {
const _PileButton({
required this.pile,
required this.count,
required this.onTap,
});
final ValuePile pile;
final int count;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
final text = Theme.of(context).textTheme;
final colors = Theme.of(context).colorScheme;
return Material(
color: colors.surfaceContainerLow,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18),
side: BorderSide(color: pile.accent.withValues(alpha: 0.35)),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(18),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
child: Row(
children: [
Container(
width: 28,
height: 28,
alignment: Alignment.center,
decoration: BoxDecoration(
color: pile.accent.withValues(alpha: 0.15),
shape: BoxShape.circle,
),
child: Text(
'${pile.rank}',
style: text.labelMedium?.copyWith(
color: pile.accent,
fontWeight: FontWeight.w700,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
pile.label,
style: text.titleMedium?.copyWith(
color: colors.onSurface,
),
),
Text(
pile.subtitle,
style: text.bodySmall?.copyWith(
color: colors.onSurfaceVariant,
),
),
],
),
),
Text(
'$count',
style: text.labelMedium?.copyWith(color: pile.accent),
),
],
),
),
),
);
}
}
-212
View File
@@ -1,212 +0,0 @@
/// Home tab: greeting, stats, and a [ListView.builder] of catalog items.
library;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/item_provider.dart';
import '../widgets/empty_state.dart';
import '../widgets/item_card.dart';
import '../widgets/loading_view.dart';
import '../widgets/responsive_body.dart';
import 'add_item_screen.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
final controller = context.watch<ItemController>();
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(title: const Text('Home')),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => const AddItemScreen()),
);
},
icon: const Icon(Icons.add),
label: const Text('Add item'),
),
body: RefreshIndicator(
onRefresh: controller.loadItems,
child: ResponsiveBody(
child: _HomeBody(controller: controller, theme: theme),
),
),
);
}
}
class _HomeBody extends StatelessWidget {
const _HomeBody({
required this.controller,
required this.theme,
});
final ItemController controller;
final ThemeData theme;
@override
Widget build(BuildContext context) {
if (controller.isLoading && controller.items.isEmpty) {
return const LoadingView(message: 'Fetching catalog…');
}
if (controller.error != null && controller.items.isEmpty) {
return ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: [
EmptyState(
icon: Icons.error_outline,
title: 'Something went wrong',
message: controller.error!,
actionLabel: 'Retry',
onAction: controller.loadItems,
),
],
);
}
if (controller.items.isEmpty) {
return ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: const [
EmptyState(
icon: Icons.inbox_outlined,
title: 'No items yet',
message: 'Add your first item with the button below.',
),
],
);
}
// Header occupies the first slot; remaining slots are catalog cards.
return ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.only(top: 8, bottom: 96),
itemCount: controller.items.length + 1,
itemBuilder: (context, index) {
if (index == 0) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_WelcomeBanner(itemCount: controller.count),
const SizedBox(height: 20),
Text('Featured items', style: theme.textTheme.titleMedium),
const SizedBox(height: 12),
],
);
}
final item = controller.items[index - 1];
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: ItemCard(item: item, heroScope: 'home'),
);
},
);
}
}
class _WelcomeBanner extends StatelessWidget {
const _WelcomeBanner({required this.itemCount});
final int itemCount;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colors = theme.colorScheme;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
colors: [colors.primary, colors.tertiary],
),
boxShadow: [
BoxShadow(
color: colors.primary.withValues(alpha: 0.28),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Welcome back',
style: theme.textTheme.labelMedium?.copyWith(
color: colors.onPrimary.withValues(alpha: 0.8),
),
),
const SizedBox(height: 4),
Text(
'Your catalog at a glance',
style: theme.textTheme.headlineMedium?.copyWith(
color: colors.onPrimary,
),
),
const SizedBox(height: 16),
Row(
children: [
_StatPill(
label: 'Items',
value: '$itemCount',
icon: Icons.collections_bookmark_outlined,
),
const SizedBox(width: 10),
const _StatPill(
label: 'Theme',
value: 'M3',
icon: Icons.palette_outlined,
),
],
),
],
),
);
}
}
class _StatPill extends StatelessWidget {
const _StatPill({
required this.label,
required this.value,
required this.icon,
});
final String label;
final String value;
final IconData icon;
@override
Widget build(BuildContext context) {
final onPrimary = Theme.of(context).colorScheme.onPrimary;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: onPrimary.withValues(alpha: 0.14),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Icon(icon, size: 16, color: onPrimary),
const SizedBox(width: 8),
Text(
'$value $label',
style: Theme.of(context).textTheme.labelMedium?.copyWith(
color: onPrimary,
),
),
],
),
);
}
}
-82
View File
@@ -1,82 +0,0 @@
/// Detail route opened from a list card, with a [Hero] matching the list badge.
library;
import 'package:flutter/material.dart';
import '../models/item.dart';
import '../utils/helpers.dart';
import '../widgets/item_hero_badge.dart';
import '../widgets/responsive_body.dart';
class ItemDetailScreen extends StatelessWidget {
const ItemDetailScreen({
super.key,
required this.item,
required this.heroTag,
});
final Item item;
final String heroTag;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colors = theme.colorScheme;
return Scaffold(
appBar: AppBar(title: const Text('Details')),
body: ResponsiveBody(
child: ListView(
children: [
const SizedBox(height: 16),
Center(
child: ItemHeroBadge(item: item, size: 96, heroTag: heroTag),
),
const SizedBox(height: 24),
Text(item.title, style: theme.textTheme.headlineMedium),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
Chip(
avatar: Icon(
item.category.icon,
size: 16,
color: item.category.accent,
),
label: Text(item.category.label),
side: BorderSide(
color: item.category.accent.withValues(alpha: 0.4),
),
),
Chip(
avatar: Icon(
Icons.calendar_today_outlined,
size: 16,
color: colors.onSurfaceVariant,
),
label: Text(formatDate(item.createdAt)),
),
],
),
const SizedBox(height: 20),
Text('Summary', style: theme.textTheme.titleMedium),
const SizedBox(height: 8),
Text(item.subtitle, style: theme.textTheme.bodyLarge),
const SizedBox(height: 20),
Text('Description', style: theme.textTheme.titleMedium),
const SizedBox(height: 8),
Text(item.description, style: theme.textTheme.bodyLarge),
const SizedBox(height: 32),
FilledButton.tonal(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Back to list'),
),
const SizedBox(height: 24),
],
),
),
);
}
}
-64
View File
@@ -1,64 +0,0 @@
/// Host scaffold that owns the bottom navigation bar and its four tabs.
library;
import 'package:flutter/material.dart';
import 'home_screen.dart';
import 'profile_screen.dart';
import 'search_screen.dart';
import 'settings_screen.dart';
/// Switches between Home, Search, Profile, and Settings without stacking routes.
class MainShell extends StatefulWidget {
const MainShell({super.key});
@override
State<MainShell> createState() => _MainShellState();
}
class _MainShellState extends State<MainShell> {
int _index = 0;
static const _pages = [
HomeScreen(),
SearchScreen(),
ProfileScreen(),
SettingsScreen(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _index,
children: _pages,
),
bottomNavigationBar: NavigationBar(
selectedIndex: _index,
onDestinationSelected: (index) => setState(() => _index = index),
destinations: const [
NavigationDestination(
icon: Icon(Icons.home_outlined),
selectedIcon: Icon(Icons.home),
label: 'Home',
),
NavigationDestination(
icon: Icon(Icons.search_outlined),
selectedIcon: Icon(Icons.search),
label: 'Search',
),
NavigationDestination(
icon: Icon(Icons.person_outline),
selectedIcon: Icon(Icons.person),
label: 'Profile',
),
NavigationDestination(
icon: Icon(Icons.settings_outlined),
selectedIcon: Icon(Icons.settings),
label: 'Settings',
),
],
),
);
}
}
+129
View File
@@ -0,0 +1,129 @@
/// Refine a single pile — promote / demote cards (Important Cards pattern).
library;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/value_pile.dart';
import '../providers/values_sort_provider.dart';
import '../widgets/practice_chrome.dart';
import '../widgets/value_card_tile.dart';
import '../utils/app_theme.dart';
class PileReviewScreen extends StatelessWidget {
const PileReviewScreen({super.key});
@override
Widget build(BuildContext context) {
final sort = context.watch<ValuesSortController>();
final pile = sort.reviewPile ?? ValuePile.important;
final cards = sort.cardsInPile(pile);
final text = Theme.of(context).textTheme;
final colors = Theme.of(context).colorScheme;
return PracticeScaffold(
header: Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 12, 0),
child: Row(
children: [
IconButton(
onPressed: sort.goToOverview,
icon: const Icon(Icons.arrow_back),
),
Expanded(
child: Text(
'Reviewing · ${pile.label}',
style: text.labelMedium,
),
),
],
),
),
footer: PrimaryPillButton(
label: 'Back to overview',
onPressed: sort.goToOverview,
),
child: ListView(
children: [
Text(pile.label, style: text.displaySmall),
const SizedBox(height: 6),
Text(
'${cards.length} values in this pile. Promote toward your core or demote when something feels less essential.',
style: text.bodyMedium,
),
const SizedBox(height: 12),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: ValuePile.values.map((p) {
return Padding(
padding: const EdgeInsets.only(right: 8),
child: ActionChip(
avatar: CircleAvatar(
backgroundColor: p.accent.withValues(alpha: 0.2),
radius: 8,
),
label: Text('${p.shortLabel} ${sort.countInPile(p)}'),
onPressed: () => sort.openPileReview(p),
backgroundColor: p == pile
? AppTheme.surfaceContainerHigh
: AppTheme.surfaceContainerLow,
),
);
}).toList(),
),
),
const SizedBox(height: 16),
if (cards.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 40),
child: Text(
'No cards in this pile yet.',
style: text.bodyLarge?.copyWith(color: colors.onSurfaceVariant),
textAlign: TextAlign.center,
),
)
else
...cards.map((card) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: ValueCardTile(
card: card,
compact: true,
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
tooltip: 'Promote',
onPressed: () => _shift(sort, card.id, pile, up: true),
icon: const Icon(Icons.arrow_upward),
),
IconButton(
tooltip: 'Demote',
onPressed: () =>
_shift(sort, card.id, pile, up: false),
icon: const Icon(Icons.arrow_downward),
),
],
),
),
);
}),
],
),
);
}
void _shift(
ValuesSortController sort,
String id,
ValuePile current, {
required bool up,
}) {
final values = ValuePile.values;
final index = values.indexOf(current);
final next = up ? index - 1 : index + 1;
if (next < 0 || next >= values.length) return;
sort.moveCard(id, values[next]);
}
}
+66
View File
@@ -0,0 +1,66 @@
/// Host that swaps practice stages from [ValuesSortController].
library;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/values_sort_provider.dart';
import 'card_sorting_screen.dart';
import 'pile_review_screen.dart';
import 'practice_hub_screen.dart';
import 'rank_order_screen.dart';
import 'reflection_screen.dart';
import 'sorted_overview_screen.dart';
import 'welcome_screen.dart';
class PracticeFlowScreen extends StatelessWidget {
const PracticeFlowScreen({super.key});
@override
Widget build(BuildContext context) {
final sort = context.watch<ValuesSortController>();
if (sort.loading) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
if (sort.error != null) {
return Scaffold(
body: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Could not load the values deck.\n${sort.error}'),
const SizedBox(height: 12),
FilledButton(
onPressed: sort.load,
child: const Text('Retry'),
),
],
),
),
),
);
}
return AnimatedSwitcher(
duration: const Duration(milliseconds: 320),
child: KeyedSubtree(
key: ValueKey(sort.stage),
child: switch (sort.stage) {
SortStage.welcome => const WelcomeScreen(),
SortStage.sorting => const CardSortingScreen(),
SortStage.hub => const PracticeHubScreen(),
SortStage.overview => const SortedOverviewScreen(),
SortStage.pileReview => const PileReviewScreen(),
SortStage.ranking => const RankOrderScreen(),
SortStage.reflection => const ReflectionScreen(),
},
),
);
}
}
+447
View File
@@ -0,0 +1,447 @@
/// Post-sort home — landscape summary and gateways (Stitch Practice Hub).
library;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/value_card.dart';
import '../providers/values_sort_provider.dart';
import '../widgets/practice_chrome.dart';
class PracticeHubScreen extends StatelessWidget {
const PracticeHubScreen({super.key});
@override
Widget build(BuildContext context) {
final sort = context.watch<ValuesSortController>();
final text = Theme.of(context).textTheme;
final colors = Theme.of(context).colorScheme;
final answered = sort.totalAnsweredReflections;
final slots = sort.totalReflectionSlots;
final progress = slots == 0 ? 0.0 : answered / slots;
final core = sort.rankedCards.take(5).toList(growable: false);
return PracticeScaffold(
padding: const EdgeInsets.fromLTRB(20, 4, 20, 8),
footer: FilledButton.tonal(
onPressed: () => _confirmReset(context, sort),
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(52),
backgroundColor: colors.surfaceContainerHigh,
foregroundColor: colors.onSurface,
shape: const StadiumBorder(),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.restart_alt, size: 18, color: colors.outline),
const SizedBox(width: 8),
Text(
'Finish Practice (Reset All)',
style: text.labelLarge?.copyWith(color: colors.onSurface),
),
],
),
),
child: ListView(
children: [
Text(
'Your Values Landscape',
style: text.displaySmall?.copyWith(color: colors.onSurface),
),
const SizedBox(height: 18),
_CoreValuesCard(
values: core,
onValueTap: (card) => sort.goToReflection(valueId: card.id),
),
const SizedBox(height: 20),
_GatewayCard(
icon: Icons.layers_outlined,
iconColor: colors.primary,
title: 'All Sorted Values',
onTap: sort.goToOverview,
),
const SizedBox(height: 12),
_GatewayCard(
icon: Icons.format_list_numbered,
iconColor: colors.secondary,
title: 'Order Top Values',
onTap: sort.goToRanking,
),
const SizedBox(height: 12),
_GatewayCard(
icon: Icons.edit_note,
iconColor: colors.tertiary,
title: 'Reflect & Inquire',
meta: slots == 0
? 'Rank top values first'
: '$answered of $slots Answered (${(progress * 100).round()}%)',
metaBackground: colors.tertiaryContainer.withValues(alpha: 0.45),
metaForeground: colors.onTertiaryContainer,
progress: slots == 0 ? null : progress,
onTap: sort.coreValues.isEmpty ? null : sort.goToReflection,
),
const SizedBox(height: 8),
],
),
);
}
Future<void> _confirmReset(
BuildContext context,
ValuesSortController sort,
) async {
final ok = await showModalBottomSheet<bool>(
context: context,
isScrollControlled: true,
builder: (ctx) {
final text = Theme.of(ctx).textTheme;
final c = Theme.of(ctx).colorScheme;
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(24, 8, 24, 24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
backgroundColor: c.errorContainer.withValues(alpha: 0.5),
child: Icon(Icons.hourglass_empty, color: c.error),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Reset Entire Practice?', style: text.titleLarge),
Text(
'Clean slate contemplation',
style: text.labelSmall,
),
],
),
),
],
),
const SizedBox(height: 14),
Text(
'This deliberate action will clear all 5 categorized piles, '
'your prioritized hierarchy, and stored reflection notes. '
'You will begin this mindful exploration from the initial deck.',
style: text.bodyMedium,
),
const SizedBox(height: 14),
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: c.surfaceContainer,
borderRadius: BorderRadius.circular(16),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.lightbulb_outline,
size: 18, color: c.secondary),
const SizedBox(width: 8),
Expanded(
child: Text(
'Tip: You can pause here and revisit your values whenever you need clarity in daily choices without resetting.',
style: text.bodySmall,
),
),
],
),
),
const SizedBox(height: 18),
PrimaryPillButton(
label: 'Yes, Reset All ${sort.totalCount} Cards',
onPressed: () => Navigator.pop(ctx, true),
),
const SizedBox(height: 8),
GhostPillButton(
label: 'Keep My Current Landscape',
onPressed: () => Navigator.pop(ctx, false),
),
],
),
),
);
},
);
if (ok == true) sort.restart();
}
}
class _CoreValuesCard extends StatelessWidget {
const _CoreValuesCard({
required this.values,
required this.onValueTap,
});
final List<ValueCard> values;
final void Function(ValueCard card) onValueTap;
@override
Widget build(BuildContext context) {
final text = Theme.of(context).textTheme;
final colors = Theme.of(context).colorScheme;
final accent = colors.secondary;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colors.surfaceContainerLow,
borderRadius: BorderRadius.circular(22),
border: Border.all(color: colors.outlineVariant.withValues(alpha: 0.7)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: accent.withValues(alpha: 0.18),
shape: BoxShape.circle,
),
child: Icon(Icons.star, size: 15, color: accent),
),
const SizedBox(width: 10),
Expanded(
child: Text(
'Your Core Values',
style: text.titleLarge?.copyWith(
color: colors.onSurface,
fontWeight: FontWeight.w400,
),
),
),
],
),
const SizedBox(height: 14),
if (values.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text(
'Rank your Most Important values to distill your core set here.',
style: text.bodyMedium?.copyWith(
color: colors.onSurfaceVariant,
),
),
)
else
LayoutBuilder(
builder: (context, constraints) {
final gap = 8.0;
final chipWidth = (constraints.maxWidth - gap) / 2;
return Wrap(
spacing: gap,
runSpacing: gap,
children: [
for (var i = 0; i < values.length; i++)
SizedBox(
width: chipWidth,
child: _CoreValueChip(
rank: i + 1,
title: values[i].title,
accent: accent,
onTap: () => onValueTap(values[i]),
),
),
],
);
},
),
],
),
);
}
}
class _CoreValueChip extends StatelessWidget {
const _CoreValueChip({
required this.rank,
required this.title,
required this.accent,
required this.onTap,
});
final int rank;
final String title;
final Color accent;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final text = Theme.of(context).textTheme;
final colors = Theme.of(context).colorScheme;
return Material(
color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(14),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(14),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
child: Row(
children: [
Container(
width: 22,
height: 22,
alignment: Alignment.center,
decoration: BoxDecoration(
color: accent.withValues(alpha: 0.18),
shape: BoxShape.circle,
),
child: Text(
'$rank',
style: text.labelSmall?.copyWith(
color: accent,
fontWeight: FontWeight.w700,
),
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: text.labelMedium?.copyWith(
color: colors.onSurface,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
),
);
}
}
class _GatewayCard extends StatelessWidget {
const _GatewayCard({
required this.icon,
required this.iconColor,
required this.title,
required this.onTap,
this.meta,
this.metaBackground,
this.metaForeground,
this.progress,
});
final IconData icon;
final Color iconColor;
final String title;
final String? meta;
final Color? metaBackground;
final Color? metaForeground;
final VoidCallback? onTap;
final double? progress;
@override
Widget build(BuildContext context) {
final text = Theme.of(context).textTheme;
final colors = Theme.of(context).colorScheme;
final enabled = onTap != null;
final hasMeta = meta != null && meta!.isNotEmpty;
return Opacity(
opacity: enabled ? 1 : 0.55,
child: Material(
color: colors.surfaceContainerLow,
elevation: 0,
shadowColor: colors.shadow.withValues(alpha: 0.08),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(20),
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: colors.surfaceContainerHigh,
shape: BoxShape.circle,
),
child: Icon(icon, color: iconColor, size: 22),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
title,
style: text.titleLarge?.copyWith(
color: colors.onSurface,
fontWeight: FontWeight.w400,
),
),
if (hasMeta) ...[
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 3,
),
decoration: BoxDecoration(
color: metaBackground ?? colors.surfaceContainer,
borderRadius: BorderRadius.circular(999),
),
child: Text(
meta!,
style: text.labelSmall?.copyWith(
color: metaForeground ?? colors.onSurfaceVariant,
),
),
),
],
],
),
),
Icon(
Icons.arrow_forward,
color: colors.primary,
size: 22,
),
],
),
if (progress != null) ...[
const SizedBox(height: 14),
ClipRRect(
borderRadius: BorderRadius.circular(999),
child: LinearProgressIndicator(
value: progress,
minHeight: 6,
backgroundColor: colors.surfaceContainerHighest,
color: colors.tertiary,
),
),
],
],
),
),
),
),
);
}
}
-142
View File
@@ -1,142 +0,0 @@
/// Profile tab: avatar, stats, and a shortcut to the add-item form.
library;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/item_provider.dart';
import '../providers/theme_provider.dart';
import '../utils/constants.dart';
import '../utils/helpers.dart';
import '../widgets/responsive_body.dart';
import 'add_item_screen.dart';
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
@override
Widget build(BuildContext context) {
final items = context.watch<ItemController>();
final themeController = context.watch<ThemeController>();
final theme = Theme.of(context);
final colors = theme.colorScheme;
return Scaffold(
appBar: AppBar(title: const Text('Profile')),
body: ResponsiveBody(
child: ListView(
children: [
const SizedBox(height: 12),
Center(
child: CircleAvatar(
radius: 44,
backgroundColor: colors.primaryContainer,
child: Icon(
Icons.person,
size: 44,
color: colors.onPrimaryContainer,
),
),
),
const SizedBox(height: 16),
Text(
'Alex Rivera',
textAlign: TextAlign.center,
style: theme.textTheme.headlineMedium,
),
const SizedBox(height: 4),
Text(
'[email protected]',
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: _ProfileStat(
label: 'Items',
value: '${items.count}',
),
),
const SizedBox(width: 12),
Expanded(
child: _ProfileStat(
label: 'Theme',
value: labelForThemeMode(themeController.themeMode),
),
),
const SizedBox(width: 12),
Expanded(
child: _ProfileStat(
label: 'Source',
value: items.useRemoteData ? 'Remote' : 'Local',
),
),
],
),
const SizedBox(height: 24),
Card(
child: Column(
children: [
ListTile(
leading: const Icon(Icons.add_box_outlined),
title: const Text('Add a catalog item'),
subtitle: const Text('Validated form with category picker'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const AddItemScreen(),
),
);
},
),
const Divider(height: 1),
const ListTile(
leading: Icon(Icons.info_outline),
title: Text('About'),
subtitle: Text('$kAppName · $kPackageName'),
),
],
),
),
],
),
),
);
}
}
class _ProfileStat extends StatelessWidget {
const _ProfileStat({required this.label, required this.value});
final String label;
final String value;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: theme.shadowColor.withValues(alpha: 0.06),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Column(
children: [
Text(value, style: theme.textTheme.titleLarge),
const SizedBox(height: 4),
Text(label, style: theme.textTheme.labelMedium),
],
),
);
}
}
+191
View File
@@ -0,0 +1,191 @@
/// Rank-order the Most Important nominees into a personal hierarchy.
library;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/values_sort_provider.dart';
import '../widgets/practice_chrome.dart';
class RankOrderScreen extends StatelessWidget {
const RankOrderScreen({super.key});
@override
Widget build(BuildContext context) {
final sort = context.watch<ValuesSortController>();
final ranked = sort.rankedCards;
final text = Theme.of(context).textTheme;
final colors = Theme.of(context).colorScheme;
return PracticeScaffold(
header: Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 12, 0),
child: Row(
children: [
IconButton(
onPressed: sort.goToHub,
icon: const Icon(Icons.arrow_back),
),
Expanded(
child: Text(
'Stage 3 · Prioritization',
style: text.labelMedium,
),
),
],
),
),
footer: PrimaryPillButton(
label: 'Reflect on top values',
icon: Icons.spa_outlined,
onPressed: ranked.isEmpty ? null : sort.goToReflection,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Order Your Core Values', style: text.displaySmall),
const SizedBox(height: 6),
Text(
'Drag or reorder your finalists. Let #1 represent your non-negotiable value.',
style: text.bodyMedium,
),
const SizedBox(height: 12),
if (ranked.isEmpty)
Expanded(
child: Center(
child: Text(
'Promote a few values into Most Important first.',
style: text.bodyLarge?.copyWith(color: colors.onSurfaceVariant),
textAlign: TextAlign.center,
),
),
)
else
Expanded(
child: ReorderableListView.builder(
itemCount: ranked.length,
onReorderItem: sort.reorderRanked,
proxyDecorator: (child, index, animation) {
return Material(
elevation: 6,
color: Colors.transparent,
shadowColor: const Color(0x332C2825),
child: child,
);
},
itemBuilder: (context, index) {
final card = ranked[index];
final isPrimary = index == 0;
return Container(
key: ValueKey(card.id),
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.fromLTRB(12, 12, 4, 12),
decoration: BoxDecoration(
color: isPrimary
? colors.surfaceContainerLowest
: colors.surfaceContainerLow,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isPrimary
? colors.secondary.withValues(alpha: 0.5)
: colors.outlineVariant,
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 36,
child: Column(
children: [
Text(
'${index + 1}',
style: text.titleLarge?.copyWith(
color: isPrimary
? colors.secondary
: colors.primary,
),
),
if (isPrimary)
Icon(
Icons.star,
size: 14,
color: colors.secondary,
),
],
),
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
card.title,
style: text.titleLarge?.copyWith(
color: colors.onSurface,
),
),
const SizedBox(height: 4),
Text(
card.description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: text.bodyMedium?.copyWith(
color: colors.onSurfaceVariant,
),
),
const SizedBox(height: 4),
Text(
'Pillar: ${card.domain}',
style: text.labelSmall?.copyWith(
color: colors.onSurfaceVariant,
),
),
],
),
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
visualDensity: VisualDensity.compact,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(
minWidth: 32,
minHeight: 28,
),
onPressed: () => sort.moveRank(index, up: true),
icon: const Icon(Icons.keyboard_arrow_up, size: 22),
),
ReorderableDragStartListener(
index: index,
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 2),
child: Icon(Icons.drag_indicator, size: 22),
),
),
IconButton(
visualDensity: VisualDensity.compact,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(
minWidth: 32,
minHeight: 28,
),
onPressed: () => sort.moveRank(index, up: false),
icon: const Icon(Icons.keyboard_arrow_down, size: 22),
),
],
),
],
),
);
},
),
),
],
),
);
}
}
+298
View File
@@ -0,0 +1,298 @@
/// Deep reflection on top values, with optional full-screen journal sheet.
library;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/values_sort_provider.dart';
import '../widgets/practice_chrome.dart';
class ReflectionScreen extends StatelessWidget {
const ReflectionScreen({super.key});
@override
Widget build(BuildContext context) {
final sort = context.watch<ValuesSortController>();
final cores = sort.coreValues;
final activeId = sort.activeReflectionValueId ??
(cores.isEmpty ? null : cores.first.id);
final active = activeId == null ? null : sort.cardById(activeId);
final text = Theme.of(context).textTheme;
final colors = Theme.of(context).colorScheme;
return PracticeScaffold(
header: Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 12, 0),
child: Row(
children: [
IconButton(
onPressed: sort.goToHub,
icon: const Icon(Icons.arrow_back),
),
Expanded(
child: Text(
'Step 5 · Deep Reflection',
style: text.labelMedium,
),
),
],
),
),
child: cores.isEmpty || active == null
? Center(
child: Text(
'Rank at least one core value to reflect.',
style: text.bodyLarge,
),
)
: ListView(
children: [
Text('Your Core Values', style: text.displaySmall),
const SizedBox(height: 4),
Text(
'${cores.length} core anchors · '
'${sort.totalAnsweredReflections} of ${sort.totalReflectionSlots} answered',
style: text.bodyMedium,
),
const SizedBox(height: 14),
SizedBox(
height: 88,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: cores.length,
separatorBuilder: (_, _) => const SizedBox(width: 8),
itemBuilder: (context, index) {
final card = cores[index];
final selected = card.id == active.id;
final answered = sort.answeredPromptCount(card.id);
final rankColor =
selected ? colors.onPrimary : colors.secondary;
final titleColor =
selected ? colors.onPrimary : colors.onSurface;
final metaColor = selected
? colors.onPrimary.withValues(alpha: 0.85)
: colors.onSurfaceVariant;
return InkWell(
onTap: () => sort.selectReflectionValue(card.id),
borderRadius: BorderRadius.circular(16),
child: Container(
width: 120,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: selected
? colors.primary
: colors.surfaceContainerLow,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: selected
? colors.primary
: colors.outlineVariant,
width: selected ? 2 : 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'#${index + 1}',
style: text.labelSmall?.copyWith(
color: rankColor,
fontWeight: FontWeight.w700,
),
),
Text(
card.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: text.titleMedium?.copyWith(
color: titleColor,
fontWeight: selected
? FontWeight.w700
: FontWeight.w500,
),
),
Text(
'$answered/${kReflectionPrompts.length}',
style: text.labelSmall?.copyWith(
color: metaColor,
),
),
],
),
),
);
},
),
),
const SizedBox(height: 18),
Text(active.title, style: text.headlineMedium),
const SizedBox(height: 4),
Text(
'"${active.description}"',
style: text.bodyMedium?.copyWith(fontStyle: FontStyle.italic),
),
const SizedBox(height: 16),
...List.generate(kReflectionPrompts.length, (index) {
final answer = sort.reflectionFor(active.id, index);
final answered = answer.isNotEmpty;
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Material(
color: colors.surfaceContainerLow,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18),
side: BorderSide(color: colors.outlineVariant),
),
child: ListTile(
leading: Icon(
answered ? Icons.check_circle : Icons.circle_outlined,
color: answered
? colors.tertiary
: colors.onSurfaceVariant,
),
title: Text(
kReflectionPrompts[index],
style: text.titleMedium,
),
subtitle: answered
? Text(
answer,
maxLines: 2,
overflow: TextOverflow.ellipsis,
)
: null,
trailing: Icon(
answered ? Icons.edit_outlined : Icons.open_in_full,
),
onTap: () => _openJournal(
context,
sort,
active.id,
index,
),
),
),
);
}),
],
),
);
}
Future<void> _openJournal(
BuildContext context,
ValuesSortController sort,
String valueId,
int promptIndex,
) async {
sort.selectPrompt(promptIndex);
final card = sort.cardById(valueId);
if (card == null) return;
await showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
useSafeArea: true,
builder: (ctx) => _ReflectionJournalSheet(
cardTitle: card.title,
prompt: kReflectionPrompts[promptIndex],
initialText: sort.reflectionFor(valueId, promptIndex),
onSave: (text) {
sort.setReflection(valueId, promptIndex, text);
},
),
);
}
}
class _ReflectionJournalSheet extends StatefulWidget {
const _ReflectionJournalSheet({
required this.cardTitle,
required this.prompt,
required this.initialText,
required this.onSave,
});
final String cardTitle;
final String prompt;
final String initialText;
final ValueChanged<String> onSave;
@override
State<_ReflectionJournalSheet> createState() =>
_ReflectionJournalSheetState();
}
class _ReflectionJournalSheetState extends State<_ReflectionJournalSheet> {
late final TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.initialText);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _saveAndClose() {
widget.onSave(_controller.text);
Navigator.of(context).pop();
}
@override
Widget build(BuildContext context) {
final text = Theme.of(context).textTheme;
final colors = Theme.of(context).colorScheme;
final bottom = MediaQuery.viewInsetsOf(context).bottom;
return Padding(
padding: EdgeInsets.fromLTRB(20, 8, 20, bottom + 20),
child: Column(
children: [
Row(
children: [
Expanded(
child: Text(
'Reflection · ${widget.cardTitle}',
style: text.titleLarge,
),
),
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close),
),
],
),
Align(
alignment: Alignment.centerLeft,
child: Text(
widget.prompt,
style: text.bodyLarge?.copyWith(color: colors.onSurfaceVariant),
),
),
const SizedBox(height: 12),
Expanded(
child: TextField(
controller: _controller,
expands: true,
maxLines: null,
textAlignVertical: TextAlignVertical.top,
decoration: const InputDecoration(
hintText: 'Write freely. This stays on your device.',
),
),
),
const SizedBox(height: 12),
PrimaryPillButton(
label: 'Save reflection',
onPressed: _saveAndClose,
),
],
),
);
}
}
-104
View File
@@ -1,104 +0,0 @@
/// Search tab: query field plus a [ListView.builder] of matching items.
library;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/item.dart';
import '../providers/item_provider.dart';
import '../widgets/empty_state.dart';
import '../widgets/item_card.dart';
import '../widgets/responsive_body.dart';
class SearchScreen extends StatefulWidget {
const SearchScreen({super.key});
@override
State<SearchScreen> createState() => _SearchScreenState();
}
class _SearchScreenState extends State<SearchScreen> {
final _query = TextEditingController();
@override
void dispose() {
_query.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final results = context.watch<ItemController>().search(_query.text);
return Scaffold(
appBar: AppBar(title: const Text('Search')),
body: ResponsiveBody(
child: Column(
children: [
const SizedBox(height: 8),
TextField(
controller: _query,
textInputAction: TextInputAction.search,
onChanged: (_) => setState(() {}),
decoration: InputDecoration(
hintText: 'Title, summary, or category',
prefixIcon: const Icon(Icons.search),
suffixIcon: _query.text.isEmpty
? null
: IconButton(
tooltip: 'Clear',
onPressed: () {
_query.clear();
setState(() {});
},
icon: const Icon(Icons.close),
),
),
),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerLeft,
child: Text(
'${results.length} result${results.length == 1 ? '' : 's'}',
style: Theme.of(context).textTheme.labelMedium,
),
),
const SizedBox(height: 8),
Expanded(child: _ResultsList(results: results, query: _query.text)),
],
),
),
);
}
}
class _ResultsList extends StatelessWidget {
const _ResultsList({required this.results, required this.query});
final List<Item> results;
final String query;
@override
Widget build(BuildContext context) {
if (results.isEmpty) {
return EmptyState(
icon: Icons.search_off,
title: query.trim().isEmpty ? 'Start typing' : 'No matches',
message: query.trim().isEmpty
? 'Search the catalog by title, summary, or category.'
: 'Try a different keyword.',
);
}
return ListView.builder(
padding: const EdgeInsets.only(bottom: 24),
itemCount: results.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: ItemCard(item: results[index], heroScope: 'search'),
);
},
);
}
}
-104
View File
@@ -1,104 +0,0 @@
/// Settings tab: theme mode, data source, and about information.
library;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/item_provider.dart';
import '../providers/theme_provider.dart';
import '../utils/constants.dart';
import '../utils/helpers.dart';
import '../widgets/responsive_body.dart';
class SettingsScreen extends StatelessWidget {
const SettingsScreen({super.key});
@override
Widget build(BuildContext context) {
final themeController = context.watch<ThemeController>();
final items = context.watch<ItemController>();
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(title: const Text('Settings')),
body: ResponsiveBody(
child: ListView(
children: [
const SizedBox(height: 8),
Text('Appearance', style: theme.textTheme.titleMedium),
const SizedBox(height: 8),
Text(
'Theme is saved with shared_preferences and restored on launch.',
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 16),
SegmentedButton<ThemeMode>(
segments: const [
ButtonSegment(
value: ThemeMode.system,
label: Text('System'),
icon: Icon(Icons.brightness_auto),
),
ButtonSegment(
value: ThemeMode.light,
label: Text('Light'),
icon: Icon(Icons.light_mode_outlined),
),
ButtonSegment(
value: ThemeMode.dark,
label: Text('Dark'),
icon: Icon(Icons.dark_mode_outlined),
),
],
selected: {themeController.themeMode},
onSelectionChanged: (selection) {
themeController.setThemeMode(selection.first);
},
),
const SizedBox(height: 28),
Text('Data', style: theme.textTheme.titleMedium),
const SizedBox(height: 8),
Card(
child: SwitchListTile(
title: const Text('Use remote sample API'),
subtitle: const Text(
'When on, items are fetched with the http package from '
'JSONPlaceholder. Failures fall back to local mock data.',
),
value: items.useRemoteData,
onChanged: (value) => items.setUseRemoteData(value),
),
),
const SizedBox(height: 28),
Text('About', style: theme.textTheme.titleMedium),
const SizedBox(height: 8),
Card(
child: Column(
children: [
const ListTile(
leading: Icon(Icons.apps),
title: Text(kAppName),
subtitle: Text('Generic Flutter Android starter'),
),
const Divider(height: 1),
const ListTile(
leading: Icon(Icons.fingerprint),
title: Text('Package'),
subtitle: Text(kPackageName),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.contrast),
title: const Text('Active theme'),
subtitle: Text(labelForThemeMode(themeController.themeMode)),
),
],
),
),
const SizedBox(height: 24),
],
),
),
);
}
}
+289
View File
@@ -0,0 +1,289 @@
/// Bird's-eye overview of all five piles with demote / refine actions.
library;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/value_card.dart';
import '../models/value_pile.dart';
import '../providers/values_sort_provider.dart';
import '../widgets/practice_chrome.dart';
import '../widgets/value_card_tile.dart';
class SortedOverviewScreen extends StatefulWidget {
const SortedOverviewScreen({super.key});
@override
State<SortedOverviewScreen> createState() => _SortedOverviewScreenState();
}
class _SortedOverviewScreenState extends State<SortedOverviewScreen> {
String _query = '';
ValuePile? _filter;
@override
Widget build(BuildContext context) {
final sort = context.watch<ValuesSortController>();
final text = Theme.of(context).textTheme;
final colors = Theme.of(context).colorScheme;
return PracticeScaffold(
header: Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 12, 0),
child: Row(
children: [
IconButton(
onPressed: sort.goToHub,
icon: const Icon(Icons.arrow_back),
),
Expanded(
child: Text(
'All Sorted Values',
style: text.labelMedium,
),
),
],
),
),
child: ListView(
children: [
Text('All Sorted Values', style: text.displaySmall),
const SizedBox(height: 16),
TextField(
onChanged: (v) => setState(() => _query = v.trim().toLowerCase()),
decoration: const InputDecoration(
hintText: 'Find a value (e.g., Creativity)',
prefixIcon: Icon(Icons.search),
),
),
const SizedBox(height: 10),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
_FilterChip(
label: 'All (${sort.totalCount})',
selected: _filter == null,
onTap: () => setState(() => _filter = null),
),
...ValuePile.values.map((pile) {
return _FilterChip(
label: '${pile.shortLabel} (${sort.countInPile(pile)})',
selected: _filter == pile,
accent: pile.accent,
onTap: () => setState(() => _filter = pile),
);
}),
],
),
),
const SizedBox(height: 18),
...ValuePile.values
.where((p) => _filter == null || _filter == p)
.map((pile) {
final cards = sort.cardsInPile(pile).where((c) {
if (_query.isEmpty) return true;
return c.title.toLowerCase().contains(_query) ||
c.description.toLowerCase().contains(_query);
}).toList();
return _PileSection(
pile: pile,
cards: cards,
onDemote: (card) => _showDemoteSheet(context, sort, card),
);
}),
const SizedBox(height: 12),
Text(
'Tip: refine any pile before ranking your core values.',
style: text.bodySmall?.copyWith(color: colors.onSurfaceVariant),
textAlign: TextAlign.center,
),
],
),
);
}
Future<void> _showDemoteSheet(
BuildContext context,
ValuesSortController sort,
ValueCard card,
) async {
await showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
builder: (ctx) {
final text = Theme.of(ctx).textTheme;
final colors = Theme.of(ctx).colorScheme;
final maxHeight = MediaQuery.sizeOf(ctx).height * 0.75;
return SafeArea(
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: maxHeight),
child: SingleChildScrollView(
padding: EdgeInsets.fromLTRB(
24,
8,
24,
MediaQuery.paddingOf(ctx).bottom + 24,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
'Reorder ${card.title}',
style: text.titleLarge,
),
),
IconButton(
onPressed: () => Navigator.pop(ctx),
icon: const Icon(Icons.close),
),
],
),
Text('Move to pile:', style: text.bodyMedium),
const SizedBox(height: 8),
...ValuePile.values.map((pile) {
final current = sort.assignments[card.id] == pile;
return ListTile(
contentPadding: EdgeInsets.zero,
enabled: !current,
leading: CircleAvatar(
backgroundColor: pile.accent.withValues(alpha: 0.2),
child: Text(
'${pile.rank}',
style: text.labelMedium?.copyWith(color: pile.accent),
),
),
title: Text(pile.label),
subtitle: Text(pile.subtitle),
trailing: current
? Icon(Icons.check, color: colors.primary)
: Icon(Icons.arrow_forward, color: colors.onSurfaceVariant),
onTap: current
? null
: () {
sort.moveCard(card.id, pile);
Navigator.pop(ctx);
},
);
}),
],
),
),
),
);
},
);
}
}
class _FilterChip extends StatelessWidget {
const _FilterChip({
required this.label,
required this.selected,
required this.onTap,
this.accent,
});
final String label;
final bool selected;
final VoidCallback onTap;
final Color? accent;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final labelColor = selected ? colors.onPrimary : colors.onSurface;
return Padding(
padding: const EdgeInsets.only(right: 8),
child: ChoiceChip(
label: Text(label),
selected: selected,
onSelected: (_) => onTap(),
showCheckmark: true,
checkmarkColor: colors.onPrimary,
selectedColor: colors.primary,
backgroundColor: colors.surfaceContainer,
labelStyle: Theme.of(context).textTheme.labelMedium?.copyWith(
color: labelColor,
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
),
side: BorderSide(
color: selected
? colors.primary
: (accent ?? colors.outlineVariant),
),
),
);
}
}
class _PileSection extends StatelessWidget {
const _PileSection({
required this.pile,
required this.cards,
required this.onDemote,
});
final ValuePile pile;
final List<ValueCard> cards;
final void Function(ValueCard card) onDemote;
@override
Widget build(BuildContext context) {
final text = Theme.of(context).textTheme;
return Padding(
padding: const EdgeInsets.only(bottom: 22),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.circle, size: 12, color: pile.accent),
const SizedBox(width: 8),
Expanded(
child: Text(
'${pile.label} · ${cards.length} cards',
style: text.titleLarge,
),
),
],
),
const SizedBox(height: 8),
if (pile == ValuePile.mostImportant)
...cards.map(
(card) => Padding(
padding: const EdgeInsets.only(bottom: 10),
child: ValueCardTile(
card: card,
compact: true,
trailing: TextButton(
onPressed: () => onDemote(card),
child: const Text('Reorder'),
),
),
),
)
else
Wrap(
spacing: 8,
runSpacing: 8,
children: cards
.map(
(c) => ValueChip(
title: c.title,
onTap: () => onDemote(c),
),
)
.toList(),
),
],
),
);
}
}
+37 -25
View File
@@ -1,12 +1,12 @@
/// First route shown after launch. Hands off to [MainShell] after a short delay.
/// First route shown after launch. Hands off to the practice flow.
library;
import 'package:flutter/material.dart';
import '../utils/constants.dart';
import 'main_shell.dart';
import 'practice_flow_screen.dart';
/// Branded splash with a fade-in logo. Replaced by the bottom-nav shell.
/// Soft branded splash before the welcome screen.
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@@ -32,17 +32,17 @@ class _SplashScreenState extends State<SplashScreen>
CurvedAnimation(parent: _controller, curve: Curves.easeOutBack),
);
_controller.forward();
_goToShell();
_goToPractice();
}
Future<void> _goToShell() async {
Future<void> _goToPractice() async {
await Future<void>.delayed(kSplashDuration);
if (!mounted) return;
Navigator.of(context).pushReplacement(
PageRouteBuilder<void>(
pageBuilder: (_, animation, _) => FadeTransition(
opacity: animation,
child: const MainShell(),
child: const PracticeFlowScreen(),
),
transitionDuration: const Duration(milliseconds: 400),
),
@@ -57,19 +57,26 @@ class _SplashScreenState extends State<SplashScreen>
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
final colors = Theme.of(context).colorScheme;
final isDark = colors.brightness == Brightness.dark;
return Scaffold(
body: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colors.primary,
colors.primaryContainer,
colors.tertiary,
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: isDark
? [
colors.surface,
colors.surfaceContainerLow,
colors.surfaceContainer,
]
: [
colors.surface,
colors.surfaceContainerLow,
const Color(0xFFE8DFD2),
],
),
),
@@ -82,31 +89,36 @@ class _SplashScreenState extends State<SplashScreen>
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
ClipOval(
child: Image.asset(
'assets/images/emblem.png',
width: 88,
height: 88,
decoration: BoxDecoration(
color: colors.onPrimary.withValues(alpha: 0.16),
borderRadius: BorderRadius.circular(24),
),
child: Icon(
Icons.layers_rounded,
fit: BoxFit.cover,
errorBuilder: (_, _, _) => Container(
width: 88,
height: 88,
color: colors.primaryContainer,
child: const Icon(
Icons.spa,
size: 44,
color: colors.onPrimary,
color: Colors.white,
),
),
),
),
const SizedBox(height: 24),
Text(
kAppName,
style: textTheme.displaySmall?.copyWith(
color: colors.onPrimary,
color: colors.onSurface,
),
),
const SizedBox(height: 8),
Text(
'A Material 3 starter template',
'Clarify what guides you',
style: textTheme.bodyLarge?.copyWith(
color: colors.onPrimary.withValues(alpha: 0.85),
color: colors.onSurfaceVariant,
),
),
const SizedBox(height: 40),
@@ -115,7 +127,7 @@ class _SplashScreenState extends State<SplashScreen>
height: 28,
child: CircularProgressIndicator(
strokeWidth: 3,
color: colors.onPrimary,
color: colors.primary,
),
),
],
+360
View File
@@ -0,0 +1,360 @@
/// 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,
),
],
),
);
}
}
-97
View File
@@ -1,97 +0,0 @@
/// In-memory sample catalog used when the app is offline or in mock mode.
library;
import '../models/item.dart';
/// Static starter data so the UI is useful without a network.
abstract final class FakeItemData {
static final List<Item> items = [
Item(
id: 'local-1',
title: 'Material 3 surfaces',
subtitle: 'Use tonal layers instead of heavy drop shadows.',
description:
'Material 3 prefers surface tints and short, soft shadows. Keep cards '
'on surfaceContainerLow, bump the hero to surfaceContainerHighest, and '
'reserve elevation for sheets and dialogs.',
category: ItemCategory.design,
createdAt: DateTime(2026, 4, 12),
),
Item(
id: 'local-2',
title: 'Provider at the root',
subtitle: 'Lift shared state above the widget that consumes it.',
description:
'Wrap MaterialApp with MultiProvider so ThemeController and '
'ItemController survive route changes. Keep UI widgets dumb: they '
'read context.watch and call controller methods.',
category: ItemCategory.development,
createdAt: DateTime(2026, 5, 3),
),
Item(
id: 'local-3',
title: 'Focus on one task',
subtitle: 'A short list beats an overflowing dashboard.',
description:
'The home screen shows a greeting, a compact stat strip, and a '
'ListView.builder of items. Extra tools live one tap away on Search, '
'Profile, and Settings.',
category: ItemCategory.productivity,
createdAt: DateTime(2026, 6, 18),
),
Item(
id: 'local-4',
title: 'Breathable spacing',
subtitle: '16–24px padding and 12px gaps keep cards from crowding.',
description:
'On compact phones use 16px page padding. From 600px up, step to 24px '
'and cap the content width so lines stay readable on tablets.',
category: ItemCategory.lifestyle,
createdAt: DateTime(2026, 7, 1),
),
Item(
id: 'local-5',
title: 'Hero transitions',
subtitle: 'Match a tag on the list tile and the detail header.',
description:
'Wrap the leading glyph in a Hero with a stable tag derived from the '
'item id. The detail screen reuses the same widget so Flutter can '
'animate size and position.',
category: ItemCategory.design,
createdAt: DateTime(2026, 7, 22),
),
Item(
id: 'local-6',
title: 'Mock first, HTTP later',
subtitle: 'A repository hides whether data is fake or remote.',
description:
'ItemRepository serves FakeItemData by default and can switch to '
'ItemApi (http + JSONPlaceholder) from Settings. Failures fall back '
'to the mock list so the UI never goes empty on a demo device.',
category: ItemCategory.development,
createdAt: DateTime(2026, 8, 4),
),
Item(
id: 'local-7',
title: 'Capture ideas quickly',
subtitle: 'The add-item form validates before it writes.',
description:
'Title, summary, and description each have a length rule. Category is '
'a required dropdown. On success the new item is inserted at the top '
'of ItemController so Home and Search update immediately.',
category: ItemCategory.productivity,
createdAt: DateTime(2026, 8, 15),
),
Item(
id: 'local-8',
title: 'Light and dark equally',
subtitle: 'Seeded ColorSchemes keep contrast in both modes.',
description:
'ThemeController persists ThemeMode in shared_preferences. The '
'settings screen offers System, Light, and Dark. Both schemes come '
'from the same teal seed color.',
category: ItemCategory.lifestyle,
createdAt: DateTime(2026, 8, 20),
),
];
}
-61
View File
@@ -1,61 +0,0 @@
/// HTTP client placeholder that talks to a public sample API.
library;
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/item.dart';
/// Fetches catalog items from JSONPlaceholder.
///
/// This is intentionally a thin, replaceable client. Swap [baseUrl] and
/// [Item.fromJson] when you point the app at a real backend.
class ItemApi {
ItemApi({http.Client? client, this.baseUrl = defaultBaseUrl})
: _client = client ?? http.Client();
static const defaultBaseUrl = 'https://jsonplaceholder.typicode.com';
final http.Client _client;
final String baseUrl;
/// GET `/posts` and map each record to an [Item].
Future<List<Item>> fetchItems({int limit = 12}) async {
final uri = Uri.parse('$baseUrl/posts');
final response = await _client.get(
uri,
headers: const {'Accept': 'application/json'},
);
if (response.statusCode < 200 || response.statusCode >= 300) {
throw ItemApiException(
'Request failed (${response.statusCode})',
statusCode: response.statusCode,
);
}
final decoded = jsonDecode(response.body);
if (decoded is! List) {
throw const ItemApiException('Unexpected response shape');
}
return decoded
.whereType<Map<String, dynamic>>()
.take(limit)
.map(Item.fromJson)
.toList(growable: false);
}
void close() => _client.close();
}
class ItemApiException implements Exception {
const ItemApiException(this.message, {this.statusCode});
final String message;
final int? statusCode;
@override
String toString() => message;
}
-50
View File
@@ -1,50 +0,0 @@
/// Chooses between mock data and the HTTP client, then returns [Item] lists.
library;
import '../models/item.dart';
import 'fake_item_data.dart';
import 'item_api.dart';
/// Single entry point for catalog data. UI code should depend on this type,
/// not on [ItemApi] or [FakeItemData] directly.
class ItemRepository {
ItemRepository({
ItemApi? api,
this.useRemoteData = false,
}) : _api = api ?? ItemApi();
final ItemApi _api;
/// When true, [fetchItems] hits the network; otherwise it returns local fakes.
bool useRemoteData;
Future<List<Item>> fetchItems() async {
if (!useRemoteData) {
return List<Item>.from(FakeItemData.items);
}
try {
return await _api.fetchItems();
} catch (_) {
// Keep the demo usable if the device is offline or the API fails.
return List<Item>.from(FakeItemData.items);
}
}
/// Builds a locally-created item (no network round-trip).
Item createLocal({
required String title,
required String subtitle,
required String description,
required ItemCategory category,
}) {
return Item(
id: 'local-${DateTime.now().microsecondsSinceEpoch}',
title: title.trim(),
subtitle: subtitle.trim(),
description: description.trim(),
category: category,
createdAt: DateTime.now(),
);
}
}
+9 -4
View File
@@ -24,15 +24,20 @@ class StorageService {
}
String get themeModeName =>
_prefs.getString(StorageKeys.themeMode) ?? 'system';
_prefs.getString(StorageKeys.themeMode) ?? 'light';
Future<void> setThemeModeName(String value) {
return _prefs.setString(StorageKeys.themeMode, value);
}
bool get useRemoteData => _prefs.getBool(StorageKeys.useRemoteData) ?? false;
/// Serialized Values Sort session JSON, or null when none saved.
String? get sortSessionJson => _prefs.getString(StorageKeys.sortSession);
Future<void> setUseRemoteData(bool value) {
return _prefs.setBool(StorageKeys.useRemoteData, value);
Future<void> setSortSessionJson(String json) {
return _prefs.setString(StorageKeys.sortSession, json);
}
Future<void> clearSortSession() {
return _prefs.remove(StorageKeys.sortSession);
}
}
+300 -93
View File
@@ -1,75 +1,116 @@
/// Material 3 light and dark themes, including typography and component styles.
/// Mindful Values Architecture theme — warm surfaces with high-contrast type.
library;
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
/// Builds the [ThemeData] used by [MaterialApp] for light and dark mode.
///
/// Colors are generated from a single seed so both schemes stay consistent
/// with Material 3 tonal palettes.
/// Builds [ThemeData] tuned for readable contrast on warm paper surfaces.
abstract final class AppTheme {
static const Color seed = Color(0xFF0F766E);
/// Page canvas — light paper, clearly lighter than elevated cards.
static const Color surface = Color(0xFFFFFBF5);
static ThemeData light() => _build(Brightness.light);
/// Elevated cards / sheets — distinct from the canvas.
static const Color surfaceContainerLow = Color(0xFFFFFFFF);
static const Color surfaceContainer = Color(0xFFF3EDE4);
static const Color surfaceContainerHigh = Color(0xFFE9E2D7);
static const Color surfaceContainerHighest = Color(0xFFDED6C9);
static ThemeData dark() => _build(Brightness.dark);
/// Near-black espresso for primary copy (≈16:1 on surface).
static const Color onSurface = Color(0xFF1A1612);
static ThemeData _build(Brightness brightness) {
final colorScheme = ColorScheme.fromSeed(
seedColor: seed,
brightness: brightness,
/// Secondary copy — still strong on cream (≈9:1).
static const Color onSurfaceVariant = Color(0xFF3F3832);
/// Icons / meta — darker than the old stone grey.
static const Color outline = Color(0xFF5C534B);
static const Color outlineVariant = Color(0xFFC4B8AA);
static const Color primary = Color(0xFF4F4032);
static const Color primaryContainer = Color(0xFF6B5848);
static const Color onPrimary = Color(0xFFFFFBF5);
static const Color secondary = Color(0xFF8A3F1C);
static const Color secondaryContainer = Color(0xFFFFD4BC);
static const Color tertiary = Color(0xFF3A5238);
static const Color inverseSurface = Color(0xFF25211D);
static ThemeData light() {
const colorScheme = ColorScheme(
brightness: Brightness.light,
primary: primary,
onPrimary: onPrimary,
primaryContainer: primaryContainer,
onPrimaryContainer: Color(0xFFFFF3E8),
secondary: secondary,
onSecondary: onPrimary,
secondaryContainer: secondaryContainer,
onSecondaryContainer: Color(0xFF4A1E08),
tertiary: tertiary,
onTertiary: onPrimary,
tertiaryContainer: Color(0xFF5A7358),
onTertiaryContainer: Color(0xFFEAF6E7),
error: Color(0xFFB3261E),
onError: onPrimary,
errorContainer: Color(0xFFFFDAD6),
onErrorContainer: Color(0xFF410002),
surface: surface,
onSurface: onSurface,
surfaceContainerHighest: surfaceContainerHighest,
surfaceContainerHigh: surfaceContainerHigh,
surfaceContainer: surfaceContainer,
surfaceContainerLow: surfaceContainerLow,
surfaceContainerLowest: Color(0xFFFFFFFF),
onSurfaceVariant: onSurfaceVariant,
outline: outline,
outlineVariant: outlineVariant,
inverseSurface: inverseSurface,
onInverseSurface: Color(0xFFF7F1E8),
inversePrimary: Color(0xFFDCC4A8),
shadow: Color(0xFF1A1612),
scrim: Color(0xFF1A1612),
);
final isDark = brightness == Brightness.dark;
final textTheme = _textTheme(colorScheme);
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
textTheme: textTheme,
scaffoldBackgroundColor: colorScheme.surface,
scaffoldBackgroundColor: surface,
appBarTheme: AppBarTheme(
centerTitle: false,
elevation: 0,
scrolledUnderElevation: 1,
backgroundColor: colorScheme.surface,
foregroundColor: colorScheme.onSurface,
scrolledUnderElevation: 0,
backgroundColor: surface,
foregroundColor: onSurface,
titleTextStyle: textTheme.titleLarge,
),
cardTheme: CardThemeData(
elevation: 0,
color: colorScheme.surfaceContainerLow,
color: surfaceContainerLow,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(24),
side: const BorderSide(color: outlineVariant, width: 1.2),
),
clipBehavior: Clip.antiAlias,
margin: EdgeInsets.zero,
),
navigationBarTheme: NavigationBarThemeData(
elevation: 0,
height: 72,
backgroundColor: colorScheme.surfaceContainer,
indicatorColor: colorScheme.secondaryContainer,
labelTextStyle: WidgetStateProperty.resolveWith((states) {
final selected = states.contains(WidgetState.selected);
return textTheme.labelMedium?.copyWith(
fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
);
}),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: colorScheme.surfaceContainerHighest.withValues(alpha: 0.6),
fillColor: Colors.white,
hintStyle: textTheme.bodyMedium?.copyWith(
color: outline.withValues(alpha: 0.85),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: outlineVariant, width: 1.2),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colorScheme.outlineVariant),
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: outlineVariant, width: 1.2),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colorScheme.primary, width: 2),
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: primary, width: 1.8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
@@ -78,93 +119,259 @@ abstract final class AppTheme {
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(48),
minimumSize: const Size.fromHeight(52),
backgroundColor: primary,
foregroundColor: onPrimary,
shape: const StadiumBorder(),
textStyle: textTheme.labelLarge?.copyWith(color: onPrimary),
elevation: 0,
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: onSurface,
textStyle: textTheme.labelMedium?.copyWith(
color: onSurface,
fontWeight: FontWeight.w600,
),
shape: const StadiumBorder(),
),
),
chipTheme: ChipThemeData(
backgroundColor: Colors.white,
selectedColor: surfaceContainerHigh,
side: const BorderSide(color: outlineVariant, width: 1.2),
shape: const StadiumBorder(),
labelStyle: textTheme.labelMedium?.copyWith(color: onSurface),
secondaryLabelStyle: textTheme.labelMedium?.copyWith(color: onSurface),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2),
),
bottomSheetTheme: const BottomSheetThemeData(
backgroundColor: surfaceContainerLow,
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.vertical(top: Radius.circular(32)),
),
textStyle: textTheme.labelLarge,
showDragHandle: true,
),
dividerTheme: const DividerThemeData(
color: outlineVariant,
space: 1,
thickness: 1,
),
listTileTheme: const ListTileThemeData(
textColor: onSurface,
iconColor: onSurfaceVariant,
),
snackBarTheme: SnackBarThemeData(
behavior: SnackBarBehavior.floating,
backgroundColor: inverseSurface,
contentTextStyle: textTheme.bodyMedium?.copyWith(
color: const Color(0xFFF7F1E8),
),
floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
dividerTheme: DividerThemeData(
color: colorScheme.outlineVariant,
space: 1,
),
snackBarTheme: SnackBarThemeData(
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
listTileTheme: ListTileThemeData(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
iconColor: colorScheme.primary,
),
shadowColor: isDark ? Colors.black : const Color(0x1A0F172A),
);
}
/// Slightly tighter, more editorial type scale than the Material defaults.
/// Soft dark mirror — elevated cards stay darker than text for contrast.
static ThemeData dark() {
const darkScheme = ColorScheme(
brightness: Brightness.dark,
primary: Color(0xFFE2CDB6),
onPrimary: Color(0xFF2A2118),
primaryContainer: Color(0xFF6B5848),
onPrimaryContainer: Color(0xFFFFF3E8),
secondary: Color(0xFFFFB691),
onSecondary: Color(0xFF4A1E08),
secondaryContainer: Color(0xFF6B3014),
onSecondaryContainer: Color(0xFFFFD4BC),
tertiary: Color(0xFFB5CDB0),
onTertiary: Color(0xFF1A2A1A),
tertiaryContainer: Color(0xFF3A5238),
onTertiaryContainer: Color(0xFFEAF6E7),
error: Color(0xFFFFB4AB),
onError: Color(0xFF690005),
errorContainer: Color(0xFF93000A),
onErrorContainer: Color(0xFFFFDAD6),
surface: Color(0xFF161310),
onSurface: Color(0xFFF5EFE6),
surfaceContainerHighest: Color(0xFF3A342E),
surfaceContainerHigh: Color(0xFF302B26),
surfaceContainer: Color(0xFF26221E),
surfaceContainerLow: Color(0xFF221E1A),
surfaceContainerLowest: Color(0xFF0E0C0A),
onSurfaceVariant: Color(0xFFD9CEC2),
outline: Color(0xFFB0A397),
outlineVariant: Color(0xFF4A433C),
inverseSurface: Color(0xFFF5EFE6),
onInverseSurface: Color(0xFF1C1916),
inversePrimary: Color(0xFF4F4032),
shadow: Color(0xFF000000),
scrim: Color(0xFF000000),
);
final textTheme = _textTheme(darkScheme);
return ThemeData(
useMaterial3: true,
colorScheme: darkScheme,
textTheme: textTheme,
scaffoldBackgroundColor: darkScheme.surface,
appBarTheme: AppBarTheme(
centerTitle: false,
elevation: 0,
scrolledUnderElevation: 0,
backgroundColor: darkScheme.surface,
foregroundColor: darkScheme.onSurface,
titleTextStyle: textTheme.titleLarge,
),
cardTheme: CardThemeData(
elevation: 0,
color: darkScheme.surfaceContainerLow,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
side: BorderSide(color: darkScheme.outlineVariant, width: 1.2),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: darkScheme.surfaceContainer,
hintStyle: textTheme.bodyMedium?.copyWith(
color: darkScheme.outline.withValues(alpha: 0.9),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(color: darkScheme.outlineVariant, width: 1.2),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(color: darkScheme.outlineVariant, width: 1.2),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(color: darkScheme.primary, width: 1.8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(52),
backgroundColor: darkScheme.primary,
foregroundColor: darkScheme.onPrimary,
shape: const StadiumBorder(),
textStyle: textTheme.labelLarge?.copyWith(color: darkScheme.onPrimary),
elevation: 0,
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: darkScheme.onSurface,
textStyle: textTheme.labelMedium?.copyWith(
color: darkScheme.onSurface,
fontWeight: FontWeight.w600,
),
shape: const StadiumBorder(),
),
),
chipTheme: ChipThemeData(
backgroundColor: darkScheme.surfaceContainer,
selectedColor: darkScheme.surfaceContainerHigh,
side: BorderSide(color: darkScheme.outlineVariant, width: 1.2),
shape: const StadiumBorder(),
labelStyle: textTheme.labelMedium?.copyWith(color: darkScheme.onSurface),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2),
),
bottomSheetTheme: BottomSheetThemeData(
backgroundColor: darkScheme.surfaceContainerLow,
surfaceTintColor: Colors.transparent,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(32)),
),
showDragHandle: true,
),
listTileTheme: ListTileThemeData(
textColor: darkScheme.onSurface,
iconColor: darkScheme.onSurfaceVariant,
),
);
}
static TextTheme _textTheme(ColorScheme colors) {
const family = 'Roboto';
final newsreader = GoogleFonts.newsreaderTextTheme();
final jakarta = GoogleFonts.plusJakartaSansTextTheme();
return TextTheme(
displaySmall: TextStyle(
fontFamily: family,
fontSize: 36,
fontWeight: FontWeight.w700,
letterSpacing: -0.5,
displaySmall: newsreader.displaySmall?.copyWith(
fontSize: 32,
height: 40 / 32,
fontWeight: FontWeight.w500,
letterSpacing: -0.32,
color: colors.onSurface,
),
headlineMedium: TextStyle(
fontFamily: family,
fontSize: 28,
fontWeight: FontWeight.w600,
letterSpacing: -0.3,
headlineMedium: newsreader.headlineMedium?.copyWith(
fontSize: 26,
height: 34 / 26,
fontWeight: FontWeight.w500,
color: colors.onSurface,
),
titleLarge: TextStyle(
fontFamily: family,
headlineSmall: newsreader.headlineSmall?.copyWith(
fontSize: 22,
fontWeight: FontWeight.w600,
height: 30 / 22,
fontWeight: FontWeight.w500,
color: colors.onSurface,
),
titleMedium: TextStyle(
fontFamily: family,
titleLarge: newsreader.titleLarge?.copyWith(
fontSize: 22,
height: 30 / 22,
fontWeight: FontWeight.w500,
color: colors.onSurface,
),
titleMedium: jakarta.titleMedium?.copyWith(
fontSize: 16,
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w700,
color: colors.onSurface,
),
bodyLarge: TextStyle(
fontFamily: family,
bodyLarge: jakarta.bodyLarge?.copyWith(
fontSize: 16,
height: 1.45,
height: 24 / 16,
fontWeight: FontWeight.w500,
color: colors.onSurface,
),
bodyMedium: TextStyle(
fontFamily: family,
bodyMedium: jakarta.bodyMedium?.copyWith(
fontSize: 14,
height: 1.4,
height: 20 / 14,
fontWeight: FontWeight.w500,
color: colors.onSurfaceVariant,
),
labelLarge: TextStyle(
fontFamily: family,
fontSize: 14,
fontWeight: FontWeight.w600,
letterSpacing: 0.1,
bodySmall: jakarta.bodySmall?.copyWith(
fontSize: 12,
height: 16 / 12,
fontWeight: FontWeight.w500,
color: colors.onSurfaceVariant,
),
labelLarge: jakarta.labelLarge?.copyWith(
fontSize: 15,
height: 20 / 15,
fontWeight: FontWeight.w700,
letterSpacing: 0.15,
color: colors.onPrimary,
),
labelMedium: TextStyle(
fontFamily: family,
fontSize: 12,
fontWeight: FontWeight.w500,
labelMedium: jakarta.labelMedium?.copyWith(
fontSize: 13,
height: 18 / 13,
fontWeight: FontWeight.w600,
letterSpacing: 0.2,
color: colors.onSurfaceVariant,
),
labelSmall: jakarta.labelSmall?.copyWith(
fontSize: 11,
height: 16 / 11,
fontWeight: FontWeight.w700,
letterSpacing: 0.4,
color: colors.onSurfaceVariant,
),
+7 -15
View File
@@ -1,27 +1,19 @@
/// Shared string keys, timings, and layout breakpoints used across the app.
library;
/// Human-readable name shown in the splash screen, app bars, and About tile.
const String kAppName = 'MyApp';
/// Human-readable name shown in the splash screen and brand chrome.
const String kAppName = 'Values Sort';
/// Package / Android application id.
const String kPackageName = 'com.example.myapp';
/// How long the splash screen stays visible before navigating to the shell.
const Duration kSplashDuration = Duration(seconds: 2);
/// How long the splash screen stays visible before navigating onward.
const Duration kSplashDuration = Duration(milliseconds: 1400);
/// SharedPreferences keys.
abstract final class StorageKeys {
static const themeMode = 'theme_mode';
static const useRemoteData = 'use_remote_data';
}
/// Named routes registered on [MaterialApp].
abstract final class AppRoutes {
static const splash = '/';
static const shell = '/shell';
static const itemDetail = '/item';
static const addItem = '/add-item';
static const sortSession = 'values_sort_session_v1';
}
/// Width thresholds used to adapt padding and card density.
@@ -30,5 +22,5 @@ abstract final class Breakpoints {
static const medium = 840.0;
}
/// Maximum content width on tablets / large phones so lists stay readable.
const double kMaxContentWidth = 720.0;
/// Maximum content width on tablets so reflective flows stay readable.
const double kMaxContentWidth = 600.0;
-59
View File
@@ -1,59 +0,0 @@
/// Small pure helpers for layout, formatting, and user-facing strings.
library;
import 'package:flutter/material.dart';
import 'constants.dart';
/// Horizontal padding that grows a little on wider screens.
double pagePadding(double width) {
if (width >= Breakpoints.medium) return 32;
if (width >= Breakpoints.compact) return 24;
return 16;
}
/// Formats [date] as `MMM d, y` without pulling in `intl`.
String formatDate(DateTime date) {
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
return '${months[date.month - 1]} ${date.day}, ${date.year}';
}
/// Converts a stored theme key into a [ThemeMode].
ThemeMode themeModeFromName(String name) {
return switch (name) {
'light' => ThemeMode.light,
'dark' => ThemeMode.dark,
_ => ThemeMode.system,
};
}
/// Inverse of [themeModeFromName] for persistence.
String nameFromThemeMode(ThemeMode mode) {
return switch (mode) {
ThemeMode.light => 'light',
ThemeMode.dark => 'dark',
ThemeMode.system => 'system',
};
}
/// Title-cases a [ThemeMode] for settings labels.
String labelForThemeMode(ThemeMode mode) {
return switch (mode) {
ThemeMode.light => 'Light',
ThemeMode.dark => 'Dark',
ThemeMode.system => 'System',
};
}
-44
View File
@@ -1,44 +0,0 @@
/// Labeled text field with a shared decoration used by the add-item form.
library;
import 'package:flutter/material.dart';
class AppTextField extends StatelessWidget {
const AppTextField({
super.key,
required this.controller,
required this.label,
this.hint,
this.maxLines = 1,
this.textInputAction,
this.keyboardType,
this.validator,
this.autofocus = false,
});
final TextEditingController controller;
final String label;
final String? hint;
final int maxLines;
final TextInputAction? textInputAction;
final TextInputType? keyboardType;
final String? Function(String?)? validator;
final bool autofocus;
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
maxLines: maxLines,
autofocus: autofocus,
textInputAction: textInputAction,
keyboardType: keyboardType,
validator: validator,
decoration: InputDecoration(
labelText: label,
hintText: hint,
alignLabelWithHint: maxLines > 1,
),
);
}
}
-51
View File
@@ -1,51 +0,0 @@
/// Placeholder shown when a list or search has nothing to display.
library;
import 'package:flutter/material.dart';
class EmptyState extends StatelessWidget {
const EmptyState({
super.key,
required this.icon,
required this.title,
required this.message,
this.actionLabel,
this.onAction,
});
final IconData icon;
final String title;
final String message;
final String? actionLabel;
final VoidCallback? onAction;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colors = theme.colorScheme;
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 48, color: colors.outline),
const SizedBox(height: 16),
Text(title, style: theme.textTheme.titleMedium),
const SizedBox(height: 8),
Text(
message,
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium,
),
if (actionLabel != null && onAction != null) ...[
const SizedBox(height: 20),
FilledButton(onPressed: onAction, child: Text(actionLabel!)),
],
],
),
),
);
}
}
-127
View File
@@ -1,127 +0,0 @@
/// Tappable card used inside [ListView.builder] on Home and Search.
library;
import 'package:flutter/material.dart';
import '../models/item.dart';
import '../screens/item_detail_screen.dart';
import '../utils/helpers.dart';
import 'item_hero_badge.dart';
/// Displays one [Item] as a rounded, shadowed card that opens the detail route.
class ItemCard extends StatelessWidget {
const ItemCard({
super.key,
required this.item,
this.heroScope = 'home',
});
final Item item;
/// Prefix so Home and Search can show the same item without colliding Heroes.
final String heroScope;
String get heroTag => '$heroScope-${item.heroTag}';
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colors = theme.colorScheme;
return DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: theme.shadowColor.withValues(alpha: 0.08),
blurRadius: 18,
offset: const Offset(0, 8),
),
],
),
child: Material(
color: colors.surfaceContainerLow,
borderRadius: BorderRadius.circular(16),
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => ItemDetailScreen(
item: item,
heroTag: heroTag,
),
),
);
},
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ItemHeroBadge(item: item, heroTag: heroTag),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item.title, style: theme.textTheme.titleMedium),
const SizedBox(height: 4),
Text(
item.subtitle,
style: theme.textTheme.bodyMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 4,
children: [
_MetaChip(
label: item.category.label,
color: item.category.accent,
),
_MetaChip(
label: formatDate(item.createdAt),
color: colors.outline,
),
],
),
],
),
),
Icon(
Icons.chevron_right,
color: colors.outline,
),
],
),
),
),
),
);
}
}
class _MetaChip extends StatelessWidget {
const _MetaChip({required this.label, required this.color});
final String label;
final Color color;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(8),
),
child: Text(
label,
style: Theme.of(context).textTheme.labelMedium?.copyWith(color: color),
),
);
}
}
-54
View File
@@ -1,54 +0,0 @@
/// Colored glyph used as the [Hero] flight target for an [Item].
library;
import 'package:flutter/material.dart';
import '../models/item.dart';
/// Rounded square that carries the category icon. Size is animated by [Hero].
class ItemHeroBadge extends StatelessWidget {
const ItemHeroBadge({
super.key,
required this.item,
required this.heroTag,
this.size = 48,
});
final Item item;
final double size;
/// Unique among on-screen Heroes. Home and Search both list the same items,
/// so callers pass a scoped tag (for example `home-item-hero-1`).
final String heroTag;
@override
Widget build(BuildContext context) {
final radius = size * 0.28;
return Hero(
tag: heroTag,
child: Material(
color: Colors.transparent,
child: Container(
width: size,
height: size,
decoration: BoxDecoration(
color: item.category.accent,
borderRadius: BorderRadius.circular(radius),
boxShadow: [
BoxShadow(
color: item.category.accent.withValues(alpha: 0.35),
blurRadius: size * 0.18,
offset: Offset(0, size * 0.08),
),
],
),
child: Icon(
item.category.icon,
color: Colors.white,
size: size * 0.5,
),
),
),
);
}
}
-24
View File
@@ -1,24 +0,0 @@
/// Centered spinner used while [ItemController] is fetching data.
library;
import 'package:flutter/material.dart';
class LoadingView extends StatelessWidget {
const LoadingView({super.key, this.message = 'Loading…'});
final String message;
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(message, style: Theme.of(context).textTheme.bodyMedium),
],
),
);
}
}
+168
View File
@@ -0,0 +1,168 @@
/// Shared layout chrome for Values Sort screens.
library;
import 'package:flutter/material.dart';
import '../utils/constants.dart';
import 'theme_toggle_button.dart';
/// Centers content and applies mobile margins from the design system.
class PracticeScaffold extends StatelessWidget {
const PracticeScaffold({
super.key,
required this.child,
this.header,
this.footer,
this.padding,
this.showThemeToggle = true,
});
final Widget child;
final Widget? header;
final Widget? footer;
final EdgeInsetsGeometry? padding;
final bool showThemeToggle;
@override
Widget build(BuildContext context) {
final pad = padding ??
const EdgeInsets.fromLTRB(20, 8, 20, 16);
return Scaffold(
body: SafeArea(
child: Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: kMaxContentWidth),
child: Column(
children: [
if (showThemeToggle)
const Padding(
padding: EdgeInsets.fromLTRB(12, 4, 12, 0),
child: Align(
alignment: Alignment.centerRight,
child: ThemeToggleButton(),
),
),
if (header != null) header!,
Expanded(
child: Padding(
padding: pad,
child: child,
),
),
if (footer != null)
Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 12),
child: footer!,
),
],
),
),
),
),
);
}
}
/// Compact brand row used on the welcome screen.
class BrandMark extends StatelessWidget {
const BrandMark({super.key});
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final labels = Theme.of(context).textTheme;
return Row(
children: [
ClipOval(
child: Image.asset(
'assets/images/emblem.png',
width: 32,
height: 32,
fit: BoxFit.cover,
errorBuilder: (_, _, _) => Container(
width: 32,
height: 32,
color: colors.primaryContainer,
child: Icon(Icons.spa, size: 18, color: colors.onPrimary),
),
),
),
const SizedBox(width: 8),
Text(
'VALUES SORT',
style: labels.labelSmall?.copyWith(
letterSpacing: 2.2,
color: colors.onSurfaceVariant,
),
),
],
);
}
}
/// Full-width primary pill CTA.
class PrimaryPillButton extends StatelessWidget {
const PrimaryPillButton({
super.key,
required this.label,
required this.onPressed,
this.icon,
});
final String label;
final VoidCallback? onPressed;
final IconData? icon;
@override
Widget build(BuildContext context) {
return FilledButton(
onPressed: onPressed,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(child: Text(label, overflow: TextOverflow.ellipsis)),
if (icon != null) ...[
const SizedBox(width: 8),
Icon(icon, size: 18),
],
],
),
);
}
}
/// Quiet secondary text action.
class GhostPillButton extends StatelessWidget {
const GhostPillButton({
super.key,
required this.label,
required this.onPressed,
this.icon,
});
final String label;
final VoidCallback? onPressed;
final IconData? icon;
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: onPressed,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(label),
if (icon != null) ...[
const SizedBox(width: 6),
Icon(icon, size: 18),
],
],
),
);
}
}
-34
View File
@@ -1,34 +0,0 @@
/// Shared layout helper that centers content and caps width on large screens.
library;
import 'package:flutter/material.dart';
import '../utils/constants.dart';
import '../utils/helpers.dart';
/// Wraps [child] with responsive horizontal padding and an optional max width.
class ResponsiveBody extends StatelessWidget {
const ResponsiveBody({
super.key,
required this.child,
this.maxWidth = kMaxContentWidth,
});
final Widget child;
final double maxWidth;
@override
Widget build(BuildContext context) {
final width = MediaQuery.sizeOf(context).width;
return Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: pagePadding(width)),
child: child,
),
),
);
}
}
+35
View File
@@ -0,0 +1,35 @@
/// Moon / sun control for switching light and dark themes.
library;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/theme_provider.dart';
class ThemeToggleButton extends StatelessWidget {
const ThemeToggleButton({super.key});
@override
Widget build(BuildContext context) {
final theme = context.watch<ThemeController>();
final colors = Theme.of(context).colorScheme;
final dark = theme.isDark;
return IconButton(
tooltip: dark ? 'Switch to light mode' : 'Switch to dark mode',
onPressed: theme.toggleLightDark,
visualDensity: VisualDensity.compact,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
style: IconButton.styleFrom(
backgroundColor: colors.surfaceContainer,
foregroundColor: colors.onSurfaceVariant,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
icon: Icon(
dark ? Icons.light_mode_outlined : Icons.dark_mode_outlined,
size: 16,
),
);
}
}
+100
View File
@@ -0,0 +1,100 @@
/// Visual tiles for a [ValueCard] in hero, sort, and list contexts.
library;
import 'package:flutter/material.dart';
import '../models/value_card.dart';
/// Elevated paper card used during sorting and as list detail rows.
class ValueCardTile extends StatelessWidget {
const ValueCardTile({
super.key,
required this.card,
this.compact = false,
this.trailing,
this.onTap,
this.elevation = 0,
});
final ValueCard card;
final bool compact;
final Widget? trailing;
final VoidCallback? onTap;
final double elevation;
@override
Widget build(BuildContext context) {
final text = Theme.of(context).textTheme;
final colors = Theme.of(context).colorScheme;
return Material(
color: colors.surfaceContainerLow,
elevation: elevation,
shadowColor: colors.shadow.withValues(alpha: 0.12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
side: BorderSide(color: colors.outlineVariant),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(24),
child: Padding(
padding: EdgeInsets.all(compact ? 14 : 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
card.title,
style: (compact ? text.titleLarge : text.headlineMedium)
?.copyWith(color: colors.onSurface),
),
),
if (trailing != null) ...[
const SizedBox(width: 8),
trailing!,
],
],
),
if (card.description.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
card.description,
style: text.bodyMedium?.copyWith(
color: colors.onSurfaceVariant,
),
maxLines: compact ? 3 : 5,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
),
);
}
}
/// Small chip used in overview grids.
class ValueChip extends StatelessWidget {
const ValueChip({
super.key,
required this.title,
this.onTap,
});
final String title;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return ActionChip(
label: Text(title),
onPressed: onTap,
visualDensity: VisualDensity.compact,
);
}
}
+137 -1
View File
@@ -1,6 +1,14 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
@@ -33,6 +41,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:
@@ -41,6 +57,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
cupertino_icons:
dependency: "direct main"
description:
@@ -96,8 +120,24 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
http:
google_fonts:
dependency: "direct main"
description:
name: google_fonts
sha256: e3cb3ee6b47fd2472c23de6da5744796a4da195137759ddb3fbcc9467b7b3c7d
url: "https://pub.dev"
source: hosted
version: "8.2.1"
hooks:
dependency: transitive
description:
name: hooks
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
http:
dependency: transitive
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
@@ -112,6 +152,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
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: b2310cdd4c18c65c081ab141a41efa94aa26c65431803703ece51996f174f351
url: "https://pub.dev"
source: hosted
version: "1.0.3"
jni_util:
dependency: transitive
description:
name: jni_util
sha256: "1ba86da04a5f2bf18fde2edb235587e70c5b0fc5bd4ba955f46b00942c3fc35f"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
leak_tracker:
dependency: transitive
description:
@@ -144,6 +208,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
@@ -176,6 +248,22 @@ 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:
name: package_config
sha256: ffcf4cf3d6c0b74ac43708d9f56625506e8a68aa935abe9d267a7330f320eb5d
url: "https://pub.dev"
source: hosted
version: "3.0.0"
path:
dependency: transitive
description:
@@ -184,6 +272,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
url: "https://pub.dev"
source: hosted
version: "2.1.6"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
path_provider_linux:
dependency: transitive
description:
@@ -232,6 +344,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "261236774e8b1d69cfc6b9eabbc96c40f25e7a2d6b171f3385d4f65d5734fb24"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
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:
@@ -381,6 +509,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: f67cdd8e07d3c6329146aaef1ba043542b3134c12489f553ca9a7435d1068aea
url: "https://pub.dev"
source: hosted
version: "3.1.4"
sdks:
dart: ">=3.13.2 <4.0.0"
flutter: ">=3.44.0"
+7 -8
View File
@@ -1,5 +1,5 @@
name: myapp
description: Generic Flutter Android starter with Material 3, Provider, and local persistence.
description: Values Sort — a guided card-sort practice for clarifying personal values.
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
@@ -34,9 +34,9 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
provider: ^6.1.5+1 # ChangeNotifier state for theme and catalog
http: ^1.6.0 # Placeholder network client (JSONPlaceholder)
shared_preferences: ^2.5.5 # Persist theme mode and data-source toggle
provider: ^6.1.5+1
shared_preferences: ^2.5.5
google_fonts: ^8.2.1
dev_dependencies:
flutter_test:
@@ -60,10 +60,9 @@ flutter:
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
assets:
- assets/data/values_deck.json
- assets/images/emblem.png
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
+214
View File
@@ -0,0 +1,214 @@
---
name: Mindful Values Architecture
colors:
surface: '#fdf9f2'
surface-dim: '#dddad3'
surface-bright: '#fdf9f2'
surface-container-lowest: '#ffffff'
surface-container-low: '#f7f3ec'
surface-container: '#f1ede6'
surface-container-high: '#ece8e1'
surface-container-highest: '#e6e2db'
on-surface: '#1c1c18'
on-surface-variant: '#4d453e'
inverse-surface: '#31302c'
inverse-on-surface: '#f4f0e9'
outline: '#7f756d'
outline-variant: '#d0c4ba'
surface-tint: '#6c5b49'
primary: '#635341'
on-primary: '#ffffff'
primary-container: '#7d6b58'
on-primary-container: '#ffefe1'
inverse-primary: '#d9c3ac'
secondary: '#8e4d2a'
on-secondary: '#ffffff'
secondary-container: '#feaa7f'
on-secondary-container: '#783d1a'
tertiary: '#465b44'
on-tertiary: '#ffffff'
tertiary-container: '#5e745c'
on-tertiary-container: '#e1f9db'
error: '#ba1a1a'
on-error: '#ffffff'
error-container: '#ffdad6'
on-error-container: '#93000a'
primary-fixed: '#f6dfc7'
primary-fixed-dim: '#d9c3ac'
on-primary-fixed: '#25190b'
on-primary-fixed-variant: '#544433'
secondary-fixed: '#ffdbcb'
secondary-fixed-dim: '#ffb691'
on-secondary-fixed: '#341100'
on-secondary-fixed-variant: '#713714'
tertiary-fixed: '#d1e9cc'
tertiary-fixed-dim: '#b5cdb0'
on-tertiary-fixed: '#0d200d'
on-tertiary-fixed-variant: '#374c36'
background: '#fdf9f2'
on-background: '#1c1c18'
surface-variant: '#e6e2db'
typography:
headline-xl:
fontFamily: Newsreader
fontSize: 40px
fontWeight: '400'
lineHeight: 48px
letterSpacing: -0.02em
headline-xl-mobile:
fontFamily: Newsreader
fontSize: 32px
fontWeight: '400'
lineHeight: 40px
letterSpacing: -0.01em
headline-lg:
fontFamily: Newsreader
fontSize: 30px
fontWeight: '400'
lineHeight: 38px
letterSpacing: -0.01em
headline-lg-mobile:
fontFamily: Newsreader
fontSize: 26px
fontWeight: '400'
lineHeight: 34px
letterSpacing: 0em
headline-md:
fontFamily: Newsreader
fontSize: 22px
fontWeight: '400'
lineHeight: 30px
body-lg:
fontFamily: Plus Jakarta Sans
fontSize: 18px
fontWeight: '400'
lineHeight: 28px
body-md:
fontFamily: Plus Jakarta Sans
fontSize: 16px
fontWeight: '400'
lineHeight: 24px
body-sm:
fontFamily: Plus Jakarta Sans
fontSize: 14px
fontWeight: '400'
lineHeight: 20px
label-lg:
fontFamily: Plus Jakarta Sans
fontSize: 15px
fontWeight: '600'
lineHeight: 20px
letterSpacing: 0.01em
label-md:
fontFamily: Plus Jakarta Sans
fontSize: 13px
fontWeight: '500'
lineHeight: 18px
letterSpacing: 0.02em
label-sm:
fontFamily: Plus Jakarta Sans
fontSize: 11px
fontWeight: '600'
lineHeight: 16px
letterSpacing: 0.04em
rounded:
sm: 0.5rem
DEFAULT: 1rem
md: 1.5rem
lg: 2rem
xl: 3rem
full: 9999px
spacing:
gutter: 1rem
gutter-mobile: 0.75rem
margin: 1.5rem
margin-mobile: 1.25rem
space-xs: 0.25rem
space-sm: 0.5rem
space-md: 1rem
space-lg: 1.5rem
space-xl: 2.5rem
---
## Brand & Style
This design system is crafted for deep introspection, intentional decision-making, and personal grounding. It serves individuals navigating life transitions, therapeutic exercises, and values-clarification practices. The experience is designed to feel unhurried, quietly reverent, and deeply personal—eschewing addictive loops, loud notifications, and superficial gamification in favor of sustained focus and calm contemplation.
The visual style blends **warm editorial minimalism** with **tactile paper-like depth**. It draws inspiration from artisanal bookbinding, linen stationery, and modern contemplative journals:
- **Atmosphere:** Serene, grounded, deliberate, and emotionally secure.
- **Rhythm:** Generous breathing room with relaxed vertical pacing, inviting the eye to linger and pause.
- **Materiality:** Soft stone surfaces, delicate warm contours, and nuanced diffuse shadows that ground elements without feeling synthetic or rigid.
## Colors
The palette is rooted in the natural world: mineral sands, quiet clay, sun-bleached stone, and muted foliage. The dynamic range avoids stark pure whites and abrasive digital blacks, fostering an environment free of eye strain and cognitive fatigue.
### Color Tiers & Intent
- **Canvas Base (`#FBF9F5`):** The primary view background; evokes raw, unbleached cotton paper.
- **Surface Elevation (`#F5F1EA`):** Neutral surface for raised cards, value tiles, and interactive sheets.
- **Subtle Surface & Dividers (`#E8E2D8`):** Restful borders, inset surfaces, and non-intrusive separators.
- **Primary / Earth Ground (`#7D6B58`):** A balanced warm taupe-stone that grounds primary interactive states and contemplative highlights.
- **Secondary / Terracotta Accent (`#C87D55`):** A soft, sun-dried clay tone used selectively for active selections, progress markers, and reflective milestones.
- **Tertiary / Sage Accent (`#6B8168`):** A muted eucalyptus green designated for affirmation states, grounded confirmations, and calm categorization.
- **Text & Contrast (`#2C2825` Primary, `#4A443F` Secondary):** Deep espresso and soft slate hues that preserve crisp readability with none of the harshness of pure black.
## Typography
The typographic hierarchy balances literary gravitas with clean, approachable utility.
- **Display & Headlines (`Newsreader`):** An editorial, high-character serif that confers an intellectual, gentle, book-like presence. Headlines use standard weight and slightly negative tracking to evoke printed literary works.
- **Body & Labels (`Plus Jakarta Sans`):** A soft, humanist geometric sans-serif that balances modern legibility with rounded, friendly terminal strokes. It delivers effortless readability across reflection prompts, explanatory texts, and compact mobile UI controls.
- **Rhythm & Formatting:** Line heights remain generous across all tiers to safeguard vertical breathing room and prevent visual crowding during dense contemplative sessions.
## Layout & Spacing
The layout is built around spacious, open single-column and modular card streams suited for portrait handheld usage. It prioritizes focus by presenting a limited number of elements on screen at any given time.
### Grid & Form Factors
- **Mobile (<640px):** Single-column layout with fluid full-width cards anchored by `1.25rem` outer canvas padding. Component stacks maintain `space-md` gaps.
- **Tablet (640px–1024px):** Centered max-width canvas (600px for reflective sort flows, 768px for summary balance views) with `1.5rem` outer margins.
- **Sorting Viewports:** Sorting columns or buckets utilize vertical flex stacks with `space-sm` or `space-md` separation to maintain clear tactile drop zones and distinct groupings.
## Elevation & Depth
Visual depth is achieved through layered warm surfaces paired with whisper-soft, colored ambient shadows rather than synthetic gray blurs.
- **Flat / Ground Level:** Page background (`#FBF9F5`) with no shadow.
- **Elevated Tiles & Cards:** Surface color `#F5F1EA` wrapped in a 1px border of `#E8E2D8`, elevated by a warm, diffuse ambient shadow: `box-shadow: 0 4px 16px -2px rgba(44, 40, 37, 0.04), 0 1px 4px 0 rgba(44, 40, 37, 0.02)`.
- **Floating Controls & Active Drags:** While dragging or floating above other content, cards subtly lift: `box-shadow: 0 12px 28px -4px rgba(44, 40, 37, 0.08), 0 2px 8px 0 rgba(44, 40, 37, 0.03)`.
- **Borders as Tone:** In place of strong contrast lines, borders act as quiet edge definitions using `#E8E2D8` to cleanly separate panels without fragmenting the canvas.
## Shapes
The design system embraces a high-radius, organic shape language (`roundedness: 3`).
- **Pill Geometry:** Interactive buttons, category filters, and selection pills utilize complete capsule geometry (`rounded-full`), reinforcing physical softness and comfort in the hand.
- **Cards & Value Containers:** Larger surfaces utilize relaxed, generous border radii (`1.5rem` to `2rem` / `rounded-xl` to `rounded-2xl`), eliminating aggressive edges and providing an organic, pebble-like quality to draggable cards.
- **Inset Wells & Modals:** Droppable sort regions adopt matching rounded corners to establish a natural visual container for nested items.
## Components
### Buttons
- **Primary:** Full capsule (`rounded-full`) pill button with a `#2C2825` or `#7D6B58` background, `#FBF9F5` typography, and generous horizontal padding (`1.75rem`). No heavy drop shadows; tactile feedback relies on soft tonal lightening on press.
- **Secondary / Gentle:** Subtle stone pill surface (`#F5F1EA`) with a 1px `#E8E2D8` border and `#2C2825` text.
- **Ghost / Reflective:** Borderless with `#4A443F` text, emphasizing deliberate, low-urgency actions (e.g., "Skip for now", "Pause reflection").
### Value Cards (Sorting Unit)
- **Resting:** Background `#F5F1EA`, border 1px solid `#E8E2D8`, padding `space-lg`, radius `1.5rem`. Title set in `Newsreader` (`headline-md`) with an optional single-sentence descriptor below in `Plus Jakarta Sans` (`body-sm`, `#4A443F`).
- **Dragging / Active:** Slight scale increase (1.02x) with ambient shadow lift and an accent border transition to `#C87D55`.
### Chips & Sort Buckets
- **Chips:** Capsule pills with `0.5rem` vertical and `1rem` horizontal padding. Selected states transition from background `#F5F1EA` to `#E8E2D8` with text shifting to `#2C2825`.
- **Sort Buckets (Drop Zones):** Spacious containers framed in dashed or soft solid lines of `#E8E2D8` with background `#FBF9F5`. Labelled with serene, muted headers (e.g., *Always True for Me*, *Important to Me*, *Rarely Considered*).
### Lists & Prompts
- Framed in full-width soft-edged tiles or separated by subtle `#E8E2D8` hairline dividers. Touch targets remain large (`min-height: 52px`), accompanied by relaxed vertical spacing.
### Checkboxes & Radio Buttons
- Rounded indicators (circular radios, `6px` radius checkboxes) using `#E8E2D8` borders that fill with muted sage (`#6B8168`) or terracotta (`#C87D55`) when selected, accompanied by a soft white icon check.
### Input Fields
- Understated input panels with background `#F5F1EA`, bordered in `#E8E2D8`, rounding to `1rem`. Active focus shifts border tone smoothly to `#7D6B58` with no high-contrast glow rings.
### Specialized Component: Contemplative Reflection Sheet
- A bottom sheet modal featuring a soft drag handle, `2rem` top border radii, linen-stone background (`#F5F1EA`), and generous whitespace designed specifically for writing personal rationales during deep values prioritization.
File diff suppressed because one or more lines are too long
+110
View File
@@ -0,0 +1,110 @@
[
{
"id": "e3940f39a073450299cb3024914064c6",
"slug": "welcome",
"title": "Values Sort \u2014 Welcome (with Guide Drawer)",
"width": "780",
"height": "1768",
"deviceType": "MOBILE",
"htmlUrl": "https://contribution.usercontent.google.com/download?c=CgthaWRhX2NvZGVmeBJ7Eh1hcHBfY29tcGFuaW9uX2dlbmVyYXRlZF9maWxlcxpaCiVodG1sXzAwMDY1YmIyNTQwYmQ5N2IwMjNiZTk5NjE2MDEzZDgzEgsSBxD_iY311x0YAZIBIwoKcHJvamVjdF9pZBIVQhM5MzQzMzg3NTEyMzcwNzMzOTk0&filename=&opi=89354086",
"screenshotUrl": "https://lh3.googleusercontent.com/aida/AEtjO1XxoE8CbYmmotxXpSsZdC3wUE-E_xSas8nAxp_8GIUsaVeT2TjBeYVAwZM64nKp2KiGgcv4OGIq2noOJzxbWuzc-L2mitY9iU_fmiwGqgff0u7RpyjXlC0eRZlhvcRybK_E_dFQ_b8Q3GBOP06crg0MlaWtIPMS9WT-WIwmui7yJkqcEDsjlJlU8reI8HinfJtzvxK1gfOfEWbLDV5gPc7T_ypo6WR-fM3lZ35FSrkH4NQoX2e-d1-DlO0",
"htmlFile": "welcome.html",
"htmlBytes": 17353,
"screenshotFile": "welcome.png",
"screenshotBytes": 35118
},
{
"id": "07a9fc8b113c479db7ece5398a017a9e",
"slug": "rank-order",
"title": "Values Sort \u2014 Rank Order Top Values",
"width": "780",
"height": "4408",
"deviceType": "MOBILE",
"htmlUrl": "https://contribution.usercontent.google.com/download?c=CgthaWRhX2NvZGVmeBJ7Eh1hcHBfY29tcGFuaW9uX2dlbmVyYXRlZF9maWxlcxpaCiVodG1sXzAwMDY1YmIyY2VlNWY5MzgwMjNiZWU1MjMxMjVjMzFhEgsSBxD_iY311x0YAZIBIwoKcHJvamVjdF9pZBIVQhM5MzQzMzg3NTEyMzcwNzMzOTk0&filename=&opi=89354086",
"screenshotUrl": "https://lh3.googleusercontent.com/aida/AEtjO1WpFA576TA5wf1HnZIoIBm8m5-S0M9LlUvAz8ujVYmbkVydsAdUAmtOpQKsY9nixUIwZEd5bqrDTe6ITlJfFFRhDRxkehFVmgcdAZksW9L89pME6e2gTYuO-qqFQKwD-XAXg_PDunjOg8XZeYs5p1dIVenclfxkXwSQ4Y0IIDTHtIMONRE4S07sHY7skqnnL-f2vmbEBg3ah1WyjcLXIUFIL4AINmYK-vTMCxwwsQ_ev3qhq909m7CdoQ",
"htmlFile": "rank-order.html",
"htmlBytes": 35244,
"screenshotFile": "rank-order.png",
"screenshotBytes": 2676
},
{
"id": "73272c6039864978bcec444c7df2973f",
"slug": "most-important",
"title": "Values Sort \u2014 Most Important (with Demote Drawer)",
"width": "780",
"height": "6564",
"deviceType": "MOBILE",
"htmlUrl": "https://contribution.usercontent.google.com/download?c=CgthaWRhX2NvZGVmeBJ7Eh1hcHBfY29tcGFuaW9uX2dlbmVyYXRlZF9maWxlcxpaCiVodG1sXzAwMDY1YmIyNzZkYjg4ZjgwNWYxMzVmZTYyMDcyNzBkEgsSBxD_iY311x0YAZIBIwoKcHJvamVjdF9pZBIVQhM5MzQzMzg3NTEyMzcwNzMzOTk0&filename=&opi=89354086",
"screenshotUrl": "https://lh3.googleusercontent.com/aida/AEtjO1Xhu39G6wcAy9suH2BL28jVCSboZ_ydBY69b4vlY9-gtnbu66RyRiHaRDF8MkiSVfcjYFtlIVfpFgyN856OBgA4qenmijh6Uii1vIm22lmUNWElTOI1GSSUyOF_NMNLN7N3HKNwLY6pc0PlrUBoB--DVbW6-RcnqfBG-CIAFWWzuOn0YEXUIbLzFLfecIwxJbbHEblBtC8m7q0RR140TUxcbtxtzHRX_km3es8xnlabSjh1DMxb3K1Efg",
"htmlFile": "most-important.html",
"htmlBytes": 55201,
"screenshotFile": "most-important.png",
"screenshotBytes": 24782
},
{
"id": "8ca48b5fb47f46efb62882151cf3966d",
"slug": "card-sorting",
"title": "Values Sort \u2014 Card Sorting (5 Piles)",
"width": "780",
"height": "1768",
"deviceType": "MOBILE",
"htmlUrl": "https://contribution.usercontent.google.com/download?c=CgthaWRhX2NvZGVmeBJ7Eh1hcHBfY29tcGFuaW9uX2dlbmVyYXRlZF9maWxlcxpaCiVodG1sXzAwMDY1YmIyNTNlZWY1YjEwNGU3NTljNGQwMzgyZWUzEgsSBxD_iY311x0YAZIBIwoKcHJvamVjdF9pZBIVQhM5MzQzMzg3NTEyMzcwNzMzOTk0&filename=&opi=89354086",
"screenshotUrl": "https://lh3.googleusercontent.com/aida/AEtjO1UKPd2AvYz_Ucd2ae0AXKkL1mf0yGN6F1_AxDfBFGuduAa3Glzo-QZSGEM9bnNThdbJBwaLp6-YZKG96EL79UjeMqDwutbANo2j2Xxdxyo3S-P2kEfS5WsFqdk99nTzvphFypQtiFa5uF_CftoMwJRKKFm1D8Xo3YaGT07BEPrl09YGNSZR0wytLyEJUkl9Y2epmCJMJ_NiudNMLR-JxcsXqk5XndAVRz09mkUI3A8JitbLiv6Z3QlZDSU",
"htmlFile": "card-sorting.html",
"htmlBytes": 18377,
"screenshotFile": "card-sorting.png",
"screenshotBytes": 75318
},
{
"id": "12809277486904866316",
"slug": "important-cards",
"title": "Values Sort \u2014 Important Cards (Pile 3)",
"width": "780",
"height": "2766",
"deviceType": "MOBILE",
"htmlUrl": "https://contribution.usercontent.google.com/download?c=CgthaWRhX2NvZGVmeBJ7Eh1hcHBfY29tcGFuaW9uX2dlbmVyYXRlZF9maWxlcxpaCiVodG1sXzAwMDY1YmIyODM5NjZmMWUwNGU3NThiMjE2MDFkYWQxEgsSBxD_iY311x0YAZIBIwoKcHJvamVjdF9pZBIVQhM5MzQzMzg3NTEyMzcwNzMzOTk0&filename=&opi=89354086",
"screenshotUrl": "https://lh3.googleusercontent.com/aida/AEtjO1XQVoRrEK9mxF3gFjm9RD-lkFg-pwyvf_kJsLOREyZzFD3UWWWPbcivFX2aIvhiewCkv3Rw4SWGxq3AtN-TbfKPD2L4R0QKA3XPc_RImmva7z66yTUJo8bvozFBpwKdHXo5vPaxqB2-UyjVDLrfiCY75WYAkp6P0BF4W3xqoRy8mWUqe5ljCtjlIwbyo2GyR66wkPSOAeUqFl6s0jLycyB7MAaiBbBPOejz27uo2xYdbppL0Bzc7XVrpeE",
"htmlFile": "important-cards.html",
"htmlBytes": 30492,
"screenshotFile": "important-cards.png",
"screenshotBytes": 47590
},
{
"id": "7bb00b6022b14dd9a687b9a31ec6b1f5",
"slug": "reflection-journal",
"title": "Values Sort \u2014 Reflection (Full-Screen Journal Drawer)",
"width": "780",
"height": "3596",
"deviceType": "MOBILE",
"htmlUrl": "https://contribution.usercontent.google.com/download?c=CgthaWRhX2NvZGVmeBJ7Eh1hcHBfY29tcGFuaW9uX2dlbmVyYXRlZF9maWxlcxpaCiVodG1sXzAwMDY1YmIyYWE0ZDI3M2IwMjNiZWU1MjMxMjVjMzFhEgsSBxD_iY311x0YAZIBIwoKcHJvamVjdF9pZBIVQhM5MzQzMzg3NTEyMzcwNzMzOTk0&filename=&opi=89354086",
"screenshotUrl": "https://lh3.googleusercontent.com/aida/AEtjO1UAeYCnBIII639-PoE0tFfV3LHW2yIsQ2XGm0L6YPETkrpzkMplK_QGvg-8F8PSLt_oJCS-3Uajl851y6H1IkywclVOp_OG34ANFKKEmKIyGFxXYg-XXlWWlanu_22zhpb_lhiO4auBOIFSxwwFc2NbU3b6qlsAIlBGONtInErxesAiJILt6j1yKKM1OkGJIrh4sWabbDoAfBNaJlu_hDywInGjs4j-d6SZOEvzgunjmwSjLStHmjuQoVk",
"htmlFile": "reflection-journal.html",
"htmlBytes": 27117,
"screenshotFile": "reflection-journal.png",
"screenshotBytes": 4968
},
{
"id": "3064045350829608185",
"slug": "reflect-top-values",
"title": "Values Sort \u2014 Reflect on Top Values",
"width": "780",
"height": "3384",
"deviceType": "MOBILE",
"htmlUrl": "https://contribution.usercontent.google.com/download?c=CgthaWRhX2NvZGVmeBJ7Eh1hcHBfY29tcGFuaW9uX2dlbmVyYXRlZF9maWxlcxpaCiVodG1sXzAwMDY1YmIyOWJmZTJmY2UwOTY4YTAzNjNmMGQ1ODhmEgsSBxD_iY311x0YAZIBIwoKcHJvamVjdF9pZBIVQhM5MzQzMzg3NTEyMzcwNzMzOTk0&filename=&opi=89354086",
"screenshotUrl": "https://lh3.googleusercontent.com/aida/AEtjO1Uwhwbp12i_K453L9y11FLMvbRC4yDVlY0fdRpaqI4rBUKKwEFBEzO2fbvVyKER5fnk5X11ceImXKtx6h37TpNJIYr6CZNuNhJc1LNKbOGiV_CO3ee7AVc0nOez48-19Q7vf8_H6Q1DlfjZj3B3v2X23in3GCgyxo5rAa-NsMkS9_G4oCvnWSIioTblp-viQRuJhyq3jIlEldpfZPkU5FQh7hr6lH9RMVXv2Pdy_sviQJK_4R2KOQJxAUc",
"htmlFile": "reflect-top-values.html",
"htmlBytes": 20710,
"screenshotFile": "reflect-top-values.png",
"screenshotBytes": 4038
},
{
"id": "7247b606c08a44ef906836e5ab250fd1",
"slug": "practice-hub",
"title": "Values Sort \u2014 Practice Hub",
"width": "780",
"height": "3234",
"deviceType": "MOBILE",
"htmlFile": "practice-hub.html",
"screenshotFile": "practice-hub.png"
}
]
+303
View File
File diff suppressed because one or more lines are too long
+391
View File
@@ -0,0 +1,391 @@
<!DOCTYPE html><html lang="en" style=""><head><meta charset="utf-8"><meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" name="viewport"><link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200" rel="stylesheet"><link href="https://fonts.googleapis.com" rel="preconnect"><link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"><link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,200..800;1,6..72,200..800&amp;family=Plus+Jakarta+Sans:ital,wght@0,300..800;1,300..800&amp;display=swap" rel="stylesheet"><script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script><script id="tailwind-config">tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
"inverse-on-surface": "#f4f0e9",
"on-error": "#ffffff",
"on-tertiary": "#ffffff",
"primary-fixed": "#f6dfc7",
"secondary-fixed-dim": "#ffb691",
"inverse-primary": "#d9c3ac",
"error": "#ba1a1a",
"on-tertiary-fixed-variant": "#374c36",
"error-container": "#ffdad6",
"secondary": "#8e4d2a",
"on-primary-fixed": "#25190b",
"on-background": "#1c1c18",
"on-secondary-container": "#783d1a",
"surface-dim": "#dddad3",
"primary": "#635341",
"on-surface": "#1c1c18",
"background": "#fdf9f2",
"surface-container-high": "#ece8e1",
"on-surface-variant": "#4d453e",
"on-primary-container": "#ffefe1",
"surface-container": "#f1ede6",
"on-tertiary-fixed": "#0d200d",
"outline": "#7f756d",
"tertiary-container": "#5e745c",
"surface": "#fdf9f2",
"on-error-container": "#93000a",
"on-primary-fixed-variant": "#544433",
"primary-container": "#7d6b58",
"on-secondary-fixed": "#341100",
"outline-variant": "#d0c4ba",
"on-tertiary-container": "#e1f9db",
"surface-container-highest": "#e6e2db",
"secondary-container": "#feaa7f",
"surface-container-lowest": "#ffffff",
"on-primary": "#ffffff",
"surface-container-low": "#f7f3ec",
"on-secondary": "#ffffff",
"surface-variant": "#e6e2db",
"tertiary": "#465b44",
"on-secondary-fixed-variant": "#713714",
"tertiary-fixed-dim": "#b5cdb0",
"surface-tint": "#6c5b49",
"primary-fixed-dim": "#d9c3ac",
"tertiary-fixed": "#d1e9cc",
"secondary-fixed": "#ffdbcb",
"inverse-surface": "#31302c",
"surface-bright": "#fdf9f2"
},
borderRadius: {
"DEFAULT": "1rem",
"lg": "1.75rem",
"xl": "2.25rem",
"full": "9999px"
},
spacing: {
"margin-mobile": "1.25rem",
"space-lg": "1.5rem",
"space-md": "1rem",
"space-xl": "2.5rem",
"gutter-mobile": "0.75rem",
"space-xs": "0.25rem",
"gutter": "1rem",
"space-sm": "0.5rem",
"margin": "1.5rem"
},
fontFamily: {
"body-sm": ["Plus Jakarta Sans", "sans-serif"],
"headline-md": ["Newsreader", "serif"],
"headline-xl": ["Newsreader", "serif"],
"label-md": ["Plus Jakarta Sans", "sans-serif"],
"headline-lg-mobile": ["Newsreader", "serif"],
"headline-lg": ["Newsreader", "serif"],
"label-sm": ["Plus Jakarta Sans", "sans-serif"],
"body-lg": ["Plus Jakarta Sans", "sans-serif"],
"body-md": ["Plus Jakarta Sans", "sans-serif"],
"headline-xl-mobile": ["Newsreader", "serif"],
"label-lg": ["Plus Jakarta Sans", "sans-serif"]
},
fontSize: {
"body-sm": ["14px", { lineHeight: "20px", fontWeight: "400" }],
"headline-md": ["22px", { lineHeight: "30px", fontWeight: "400" }],
"headline-xl": ["40px", { lineHeight: "48px", letterSpacing: "-0.02em", fontWeight: "400" }],
"label-md": ["13px", { lineHeight: "18px", letterSpacing: "0.02em", fontWeight: "500" }],
"headline-lg-mobile": ["26px", { lineHeight: "34px", letterSpacing: "0em", fontWeight: "400" }],
"headline-lg": ["30px", { lineHeight: "38px", letterSpacing: "-0.01em", fontWeight: "400" }],
"label-sm": ["11px", { lineHeight: "16px", letterSpacing: "0.04em", fontWeight: "600" }],
"body-lg": ["18px", { lineHeight: "28px", fontWeight: "400" }],
"body-md": ["16px", { lineHeight: "24px", fontWeight: "400" }],
"headline-xl-mobile": ["32px", { lineHeight: "40px", letterSpacing: "-0.01em", fontWeight: "400" }],
"label-lg": ["15px", { lineHeight: "20px", letterSpacing: "0.01em", fontWeight: "600" }]
}
}
}
};</script><style>
@layer base {
html, body {
width: 100vw;
margin: 0;
padding: 0;
background-color: #fdf9f2;
}
body {
overscroll-behavior: none;
}
.pb-safe {
padding-bottom: env(safe-area-inset-bottom, 16px);
}
.pt-safe {
padding-top: env(safe-area-inset-top, 16px);
}
}
::-webkit-scrollbar {
display: none;
}
.card-enter {
animation: gentleFadeUp 0.32s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
.card-exit {
animation: gentleFadeOut 0.22s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@keyframes gentleFadeUp {
0% {
opacity: 0;
transform: translateY(14px) scale(0.985);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes gentleFadeOut {
0% {
opacity: 1;
transform: translateY(0) scale(1);
}
100% {
opacity: 0;
transform: translateY(-10px) scale(0.98);
}
}
</style></head><body class="bg-surface font-body-md text-body-md text-on-surface min-h-screen flex flex-col justify-between pt-safe pb-safe selection:bg-secondary/15"><main class="flex-1 flex flex-col w-full max-w-md mx-auto px-margin-mobile justify-between"><header class="pt-2 pb-1 flex flex-col gap-2">
<div class="flex items-center justify-between">
<button aria-label="Undo previous value" class="inline-flex items-center gap-1.5 py-1.5 px-3 rounded-full bg-surface-container text-on-surface-variant font-label-md text-label-md transition-all active:scale-95 disabled:opacity-40 disabled:pointer-events-none hover:bg-surface-container-high" id="undo-btn" onclick="undoLastSort()">
<span class="material-symbols-outlined text-[17px]">undo</span>
<span class="">Previous</span>
</button>
<div class="flex items-center gap-1.5 px-3 py-1 rounded-full bg-surface-container-low border border-outline-variant/40">
<span class="w-1.5 h-1.5 rounded-full bg-tertiary"></span>
<span class="font-label-sm text-label-sm text-primary uppercase tracking-wider">Stage 1 · Initial Sort</span>
</div>
<button aria-label="Save and exit" class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-surface-container text-on-surface-variant transition-colors hover:bg-surface-container-high active:scale-95" onclick="saveAndExit()" title="Save &amp; pause">
<span class="material-symbols-outlined text-[18px]">bookmark_border</span>
</button>
</div>
<!-- Progress indicator -->
<div class="mt-1.5 flex flex-col gap-1.5">
<div class="flex items-baseline justify-between text-on-surface-variant">
<span class="font-label-md text-label-md tracking-tight text-on-surface font-medium" id="counter-label">104 of 100 values sorted</span>
<span class="font-label-sm text-label-sm text-outline" id="percent-label">100%</span>
</div>
<div class="w-full h-1 bg-surface-container-high rounded-full overflow-hidden">
<div class="h-full bg-primary-container rounded-full transition-all duration-300 ease-out" id="progress-bar" style="width: 100%;"></div>
</div>
</div>
</header>
<!-- Center Hero: Single Focused Value Card -->
<section class="my-auto py-2 flex flex-col items-center">
<div class="w-full relative card-enter" id="card-container">
<!-- Shadow stack aesthetic cue underneath -->
<div class="absolute inset-x-3 -bottom-1.5 h-6 rounded-xl bg-surface-container-high/60 -z-10 transform scale-[0.98]"></div>
<div class="absolute inset-x-6 -bottom-3 h-5 rounded-xl bg-surface-container-highest/40 -z-20 transform scale-[0.95]"></div>
<article class="relative bg-surface-container-lowest border border-outline-variant/50 rounded-xl p-6 shadow-sm flex flex-col transition-all duration-200">
<!-- Card Category Tag -->
<div class="flex items-center justify-between mb-4">
</div>
<!-- Value Title -->
<h2 class="font-headline-xl text-3xl sm:text-headline-xl text-on-surface tracking-normal leading-tight mb-3" id="card-title">Compassion</h2>
<!-- Definition Essence -->
<p class="font-body-md text-body-md text-on-surface-variant leading-relaxed mb-6 font-normal" id="card-definition">Meeting human vulnerability, struggle, and limitation with open warmth and kindness.</p>
<!-- Contemplative Question -->
</article>
</div>
</section>
<!-- Bottom: 5 Reflective Piles Rating Scale & Pile Distribution -->
<footer class="pb-1 pt-1 flex flex-col gap-3">
<!-- Prompt guidance text -->
<div class="flex items-center justify-between px-1">
<span class="font-label-sm text-label-sm text-on-surface-variant tracking-wide uppercase flex items-center gap-1">
<span class="material-symbols-outlined text-[15px] text-primary">layers</span>
Assign to reflective pile
</span>
<span class="font-label-sm text-label-sm text-outline" id="pile-helper">Tap to sort</span>
</div>
<!-- 5 Rating Buttons / Tier Rows -->
<div aria-label="Sort into piles 1 through 5" class="flex flex-col gap-1.5" role="group">
<!-- Pile 5 -->
<button class="group relative flex items-center justify-between w-full py-2.5 px-3.5 rounded-lg bg-surface-container hover:bg-surface-container-high active:scale-[0.98] border border-outline-variant/40 transition-all text-left" onclick="sortCurrentCard(5)" type="button">
<div class="flex items-center gap-3">
<span class="w-6 h-6 rounded-full bg-tertiary text-on-tertiary flex items-center justify-center font-label-md text-label-md font-semibold shrink-0">5</span>
<span class="font-label-lg text-label-lg text-on-surface font-semibold group-hover:text-primary transition-colors">Most Important</span>
</div>
<span class="font-label-sm text-label-sm text-on-surface-variant/80">Non-negotiable core</span>
</button>
<!-- Pile 4 -->
<button class="group relative flex items-center justify-between w-full py-2.5 px-3.5 rounded-lg bg-surface-container hover:bg-surface-container-high active:scale-[0.98] border border-outline-variant/40 transition-all text-left" onclick="sortCurrentCard(4)" type="button">
<div class="flex items-center gap-3">
<span class="w-6 h-6 rounded-full bg-surface-container-highest text-on-surface flex items-center justify-center font-label-md text-label-md font-semibold shrink-0">4</span>
<span class="font-label-lg text-label-lg text-on-surface font-medium group-hover:text-primary transition-colors">Very Important</span>
</div>
<span class="font-label-sm text-label-sm text-on-surface-variant/80">Deeply held guide</span>
</button>
<!-- Pile 3 -->
<button class="group relative flex items-center justify-between w-full py-2.5 px-3.5 rounded-lg bg-surface-container hover:bg-surface-container-high active:scale-[0.98] border border-outline-variant/40 transition-all text-left" onclick="sortCurrentCard(3)" type="button">
<div class="flex items-center gap-3">
<span class="w-6 h-6 rounded-full bg-surface-container-highest text-on-surface flex items-center justify-center font-label-md text-label-md font-semibold shrink-0">3</span>
<span class="font-label-lg text-label-lg text-on-surface font-medium group-hover:text-primary transition-colors">Important</span>
</div>
<span class="font-label-sm text-label-sm text-on-surface-variant/80">Valued aspect</span>
</button>
<!-- Pile 2 -->
<button class="group relative flex items-center justify-between w-full py-2.5 px-3.5 rounded-lg bg-surface-container-low hover:bg-surface-container active:scale-[0.98] border border-outline-variant/30 transition-all text-left" onclick="sortCurrentCard(2)" type="button">
<div class="flex items-center gap-3">
<span class="w-6 h-6 rounded-full bg-surface-container-high text-on-surface-variant flex items-center justify-center font-label-md text-label-md shrink-0">2</span>
<span class="font-label-md text-label-md text-on-surface font-medium">Somewhat Important</span>
</div>
<span class="font-label-sm text-label-sm text-outline">Contextual</span>
</button>
<!-- Pile 1 -->
<button class="group relative flex items-center justify-between w-full py-2.5 px-3.5 rounded-lg bg-surface-container-low hover:bg-surface-container active:scale-[0.98] border border-outline-variant/30 transition-all text-left" onclick="sortCurrentCard(1)" type="button">
<div class="flex items-center gap-3">
<span class="w-6 h-6 rounded-full bg-surface-container-high text-on-surface-variant flex items-center justify-center font-label-md text-label-md shrink-0">1</span>
<span class="font-label-md text-label-md text-on-surface font-medium">Not Important</span>
</div>
<span class="font-label-sm text-label-sm text-outline">Not a current compass</span>
</button>
</div>
<!-- Live Pile Distribution Strip -->
</footer>
</main>
<script>
// Contemplative Values Deck Data
const valuesDeck = [
{
title: "Courage",
domain: "Core Ground",
icon: "nature",
definition: "Moving forward in fidelity to truth and conviction despite fear, doubt, or external friction.",
prompt: "How essential is this fidelity to who you choose to be each day?"
},
{
title: "Authenticity",
domain: "Inward Presence",
icon: "self_improvement",
definition: "Living in honest congruence with who you genuinely are, without defensive performance.",
prompt: "Where does being fully yourself create peace and power in your life?"
},
{
title: "Compassion",
domain: "Relational",
icon: "favorite",
definition: "Meeting human vulnerability, struggle, and limitation with open warmth and kindness.",
prompt: "How central is gentle empathy when things are difficult?"
},
{
title: "Wisdom",
domain: "Inquiry",
icon: "menu_book",
definition: "Discerning what is enduring, balanced, and essential through patient observation.",
prompt: "Do you look to patient discernment to steer your major choices?"
},
{
title: "Freedom",
domain: "Sovereignty",
icon: "all_inclusive",
definition: "Spaciousness to author your own rhythm, personal boundaries, and inner cadence.",
prompt: "How much do autonomy and breathing room shape your daily vitality?"
},
{
title: "Integrity",
domain: "Core Ground",
icon: "shield",
definition: "Honoring your word, ethical compass, and silent internal agreements without compromise.",
prompt: "Does living in harmony with your conscience anchor your peace of mind?"
},
{
title: "Creativity",
domain: "Expression",
icon: "palette",
definition: "Generative synthesis, playfulness, and bringing novel perspective or art into form.",
prompt: "Is making, imagining, or expressing vital to your well-being?"
},
{
title: "Peace",
domain: "Inward Presence",
icon: "spa",
definition: "Cultivating unshakeable interior stillness, gentle calm, and non-reactivity.",
prompt: "How essential is quiet inner tranquility amidst worldly commotion?"
}
];
let currentIndex = 0;
let sortedCount = 14;
const totalCards = 100;
const pileCounts = { 1: 2, 2: 3, 3: 5, 4: 3, 5: 1 };
const historyStack = [];
function updateDisplay() {
const cardData = valuesDeck[currentIndex % valuesDeck.length];
const container = document.getElementById('card-container');
// Retain animation smoothness
container.classList.remove('card-enter');
void container.offsetWidth; // trigger reflow
container.classList.add('card-enter');
document.getElementById('card-title').textContent = cardData.title;
document.getElementById('card-domain').textContent = cardData.domain;
document.getElementById('card-icon').textContent = cardData.icon;
document.getElementById('card-definition').textContent = cardData.definition;
document.getElementById('card-prompt').textContent = cardData.prompt;
document.getElementById('counter-label').textContent = `${sortedCount} of ${totalCards} values sorted`;
const percentage = Math.min(100, Math.round((sortedCount / totalCards) * 100));
document.getElementById('percent-label').textContent = `${percentage}%`;
document.getElementById('progress-bar').style.width = `${percentage}%`;
// Update pile pill numbers
for (let i = 1; i <= 5; i++) {
const el = document.getElementById(`count-${i}`);
if (el) el.textContent = pileCounts[i];
}
// Undo button state
document.getElementById('undo-btn').disabled = historyStack.length === 0;
}
function sortCurrentCard(pileNumber) {
// Record to history for smooth undo
historyStack.push({
cardIndex: currentIndex,
assignedPile: pileNumber
});
pileCounts[pileNumber] = (pileCounts[pileNumber] || 0) + 1;
sortedCount++;
currentIndex = (currentIndex + 1) % valuesDeck.length;
const container = document.getElementById('card-container');
container.classList.add('card-exit');
setTimeout(() => {
container.classList.remove('card-exit');
updateDisplay();
}, 120);
}
function undoLastSort() {
if (historyStack.length === 0) return;
const lastAction = historyStack.pop();
if (pileCounts[lastAction.assignedPile] > 0) {
pileCounts[lastAction.assignedPile]--;
}
sortedCount = Math.max(0, sortedCount - 1);
currentIndex = lastAction.cardIndex;
updateDisplay();
}
function saveAndExit() {
const btn = document.getElementById('pile-helper');
btn.textContent = "Progress saved";
btn.classList.add('text-tertiary', 'font-semibold');
setTimeout(() => {
btn.textContent = "Tap to sort";
btn.classList.remove('text-tertiary', 'font-semibold');
}, 2000);
}
document.addEventListener('DOMContentLoaded', () => {
updateDisplay();
});
</script>
</body></html>
+16
View File
@@ -0,0 +1,16 @@
{
"name": "projects/9343387512370733994/screens/8ca48b5fb47f46efb62882151cf3966d",
"title": "Values Sort \u2014 Card Sorting (5 Piles)",
"screenshot": {
"name": "projects/9343387512370733994/files/15029646364846437108",
"downloadUrl": "https://lh3.googleusercontent.com/aida/AEtjO1UKPd2AvYz_Ucd2ae0AXKkL1mf0yGN6F1_AxDfBFGuduAa3Glzo-QZSGEM9bnNThdbJBwaLp6-YZKG96EL79UjeMqDwutbANo2j2Xxdxyo3S-P2kEfS5WsFqdk99nTzvphFypQtiFa5uF_CftoMwJRKKFm1D8Xo3YaGT07BEPrl09YGNSZR0wytLyEJUkl9Y2epmCJMJ_NiudNMLR-JxcsXqk5XndAVRz09mkUI3A8JitbLiv6Z3QlZDSU"
},
"htmlCode": {
"name": "projects/9343387512370733994/files/10948046668312136526",
"downloadUrl": "https://contribution.usercontent.google.com/download?c=CgthaWRhX2NvZGVmeBJ7Eh1hcHBfY29tcGFuaW9uX2dlbmVyYXRlZF9maWxlcxpaCiVodG1sXzAwMDY1YmIyNTNlZWY1YjEwNGU3NTljNGQwMzgyZWUzEgsSBxD_iY311x0YAZIBIwoKcHJvamVjdF9pZBIVQhM5MzQzMzg3NTEyMzcwNzMzOTk0&filename=&opi=89354086",
"mimeType": "text/html"
},
"width": "780",
"height": "1768",
"deviceType": "MOBILE"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

+467
View File
@@ -0,0 +1,467 @@
<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8"/><meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" name="viewport"/><link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200" rel="stylesheet"/><link href="https://fonts.googleapis.com" rel="preconnect"/><link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"/><link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,200..800;1,6..72,200..800&amp;family=Plus+Jakarta+Sans:ital,wght@0,300..800;1,300..800&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,[email protected],0..1&amp;display=swap" rel="stylesheet"/><script src="https://cdn.tailwindcss.com"></script><script id="tailwind-config">tailwind.config = { darkMode: 'class', theme: { extend: { colors: { 'on-tertiary-container': '#e1f9db', 'surface-container': '#f1ede6', 'on-primary-container': '#ffefe1', 'primary-fixed-dim': '#d9c3ac', 'tertiary-fixed-dim': '#b5cdb0', 'surface': '#fdf9f2', 'on-secondary-fixed-variant': '#713714', 'surface-container-lowest': '#ffffff', 'error-container': '#ffdad6', 'primary': '#635341', 'outline-variant': '#d0c4ba', 'tertiary': '#465b44', 'surface-dim': '#dddad3', 'primary-container': '#7d6b58', 'outline': '#7f756d', 'on-error-container': '#93000a', 'surface-bright': '#fdf9f2', 'background': '#fdf9f2', 'secondary-container': '#feaa7f', 'tertiary-container': '#5e745c', 'surface-tint': '#6c5b49', 'on-secondary-container': '#783d1a', 'on-primary-fixed-variant': '#544433', 'on-primary-fixed': '#25190b', 'tertiary-fixed': '#d1e9cc', 'on-error': '#ffffff', 'on-secondary': '#ffffff', 'on-tertiary-fixed-variant': '#374c36', 'on-surface': '#1c1c18', 'surface-container-high': '#ece8e1', 'secondary': '#8e4d2a', 'secondary-fixed-dim': '#ffb691', 'secondary-fixed': '#ffdbcb', 'inverse-surface': '#31302c', 'inverse-on-surface': '#f4f0e9', 'on-primary': '#ffffff', 'primary-fixed': '#f6dfc7', 'on-surface-variant': '#4d453e', 'surface-container-highest': '#e6e2db', 'on-tertiary-fixed': '#0d200d', 'on-secondary-fixed': '#341100', 'on-background': '#1c1c18', 'surface-variant': '#e6e2db', 'on-tertiary': '#ffffff', 'error': '#ba1a1a', 'inverse-primary': '#d9c3ac', 'surface-container-low': '#f7f3ec' }, borderRadius: { DEFAULT: '1rem', lg: '2rem', xl: '3rem', full: '9999px' }, spacing: { 'space-xs': '0.25rem', 'margin': '1.5rem', 'gutter': '1rem', 'space-xl': '2.5rem', 'margin-mobile': '1.25rem', 'gutter-mobile': '0.75rem', 'space-sm': '0.5rem', 'space-lg': '1.5rem', 'space-md': '1rem' }, fontFamily: { 'label-lg': ['Plus Jakarta Sans'], 'label-sm': ['Plus Jakarta Sans'], 'headline-xl-mobile': ['Newsreader'], 'body-lg': ['Plus Jakarta Sans'], 'body-sm': ['Plus Jakarta Sans'], 'headline-lg-mobile': ['Newsreader'], 'headline-md': ['Newsreader'], 'headline-lg': ['Newsreader'], 'body-md': ['Plus Jakarta Sans'], 'headline-xl': ['Newsreader'], 'label-md': ['Plus Jakarta Sans'] }, fontSize: { 'label-lg': ['15px', { lineHeight: '20px', letterSpacing: '0.01em', fontWeight: '600' }], 'label-sm': ['11px', { lineHeight: '16px', letterSpacing: '0.04em', fontWeight: '600' }], 'headline-xl-mobile': ['32px', { lineHeight: '40px', letterSpacing: '-0.01em', fontWeight: '400' }], 'body-lg': ['18px', { lineHeight: '28px', fontWeight: '400' }], 'body-sm': ['14px', { lineHeight: '20px', fontWeight: '400' }], 'headline-lg-mobile': ['26px', { lineHeight: '34px', letterSpacing: '0em', fontWeight: '400' }], 'headline-md': ['22px', { lineHeight: '30px', fontWeight: '400' }], 'headline-lg': ['30px', { lineHeight: '38px', letterSpacing: '-0.01em', fontWeight: '400' }], 'body-md': ['16px', { lineHeight: '24px', fontWeight: '400' }], 'headline-xl': ['40px', { lineHeight: '48px', letterSpacing: '-0.02em', fontWeight: '400' }], 'label-md': ['13px', { lineHeight: '18px', letterSpacing: '0.02em', fontWeight: '500' }] } } } };</script><style>@layer base { html, body { width: 100vw; margin: 0; padding: 0; } body { overscroll-behavior: none; } .pb-safe { padding-bottom: env(safe-area-inset-bottom, 0px); } .pt-safe { padding-top: env(safe-area-inset-top, 0px); } main > :first-child { margin-top: 0 !important; } main > :last-child { margin-bottom: 0 !important; } } ::-webkit-scrollbar { display: none; }</style><style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head><body class="bg-surface font-body-md text-body-md text-on-surface min-h-screen flex flex-col pt-safe pb-safe"><main class="flex-1 flex flex-col relative w-full bg-surface"><div class="flex flex-col w-full px-margin-mobile sm:px-margin pb-space-xl">
<!-- Header Intro -->
<header class="pt-space-sm pb-space-md">
<div class="flex items-center justify-between gap-gutter">
<div class="inline-flex items-center gap-space-xs px-space-sm py-0.5 rounded-full bg-surface-container text-on-surface-variant font-label-sm text-label-sm">
<span>Step 3 of 5</span>
<span class="w-1 h-1 rounded-full bg-outline"></span>
<span>Reviewing Group</span>
</div>
<button aria-label="Values guide" class="w-8 h-8 rounded-full bg-surface-container flex items-center justify-center text-on-surface-variant hover:bg-surface-container-high transition-colors" type="button">
<span class="material-symbols-outlined text-[18px]">info</span>
</button>
</div>
<h1 class="font-headline-lg-mobile sm:font-headline-lg text-headline-lg-mobile sm:text-headline-lg text-on-surface mt-space-sm mb-space-xs">
Values Sort — Important Cards (Pile 3)
</h1>
<p class="font-body-sm text-body-sm text-on-surface-variant max-w-xl">
32 values considered meaningful, though secondary to your core anchors.
</p>
</header>
<!-- Pile Navigation Pill Track -->
<nav aria-label="Value Piles" class="w-full overflow-x-auto -mx-margin-mobile sm:-mx-margin px-margin-mobile sm:px-margin pb-space-sm pt-space-xs flex items-center gap-space-xs">
<button class="whitespace-nowrap px-space-md py-space-xs rounded-full bg-surface-container text-on-surface-variant hover:bg-surface-container-high transition-all flex items-center gap-1.5 font-label-md text-label-md" type="button">
<span>Most</span>
<span class="px-1.5 py-0.5 rounded-full bg-surface-container-high text-[11px] font-label-sm">8</span>
</button>
<button class="whitespace-nowrap px-space-md py-space-xs rounded-full bg-surface-container text-on-surface-variant hover:bg-surface-container-high transition-all flex items-center gap-1.5 font-label-md text-label-md" type="button">
<span>Very</span>
<span class="px-1.5 py-0.5 rounded-full bg-surface-container-high text-[11px] font-label-sm">18</span>
</button>
<button aria-current="page" class="whitespace-nowrap px-space-md py-space-xs rounded-full bg-primary text-on-primary shadow-sm flex items-center gap-1.5 font-label-md text-label-md" type="button">
<span>Important</span>
<span class="px-1.5 py-0.5 rounded-full bg-surface/20 text-on-primary text-[11px] font-label-sm">32</span>
</button>
<button class="whitespace-nowrap px-space-md py-space-xs rounded-full bg-surface-container text-on-surface-variant hover:bg-surface-container-high transition-all flex items-center gap-1.5 font-label-md text-label-md" type="button">
<span>Somewhat</span>
<span class="px-1.5 py-0.5 rounded-full bg-surface-container-high text-[11px] font-label-sm">24</span>
</button>
<button class="whitespace-nowrap px-space-md py-space-xs rounded-full bg-surface-container text-on-surface-variant hover:bg-surface-container-high transition-all flex items-center gap-1.5 font-label-md text-label-md" type="button">
<span>Not</span>
<span class="px-1.5 py-0.5 rounded-full bg-surface-container-high text-[11px] font-label-sm">18</span>
</button>
</nav>
<!-- Search & Domain Filter Bar -->
<section aria-label="Search and Filter" class="mt-space-sm mb-space-md flex flex-col gap-space-sm">
<div class="relative w-full">
<span class="material-symbols-outlined absolute left-space-md top-1/2 -translate-y-1/2 text-outline text-[20px] pointer-events-none">search</span>
<input class="w-full pl-11 pr-space-md py-2.5 rounded-full bg-surface-container text-on-surface placeholder:text-outline font-body-sm text-body-sm focus:outline-none focus:bg-surface-container-high transition-colors" id="value-search-input" placeholder="Search within 32 values..." type="search"/>
</div>
<!-- Domain Chips -->
<div class="flex items-center gap-space-xs overflow-x-auto pb-1" id="domain-filters">
<button class="domain-btn active whitespace-nowrap px-space-sm py-1 rounded-full bg-primary-container text-on-primary-container font-label-sm text-label-sm transition-colors" data-domain="all" type="button">
All Domains
</button>
<button class="domain-btn whitespace-nowrap px-space-sm py-1 rounded-full bg-surface-container text-on-surface-variant hover:bg-surface-container-high font-label-sm text-label-sm transition-colors" data-domain="Presence" type="button">
Presence (7)
</button>
<button class="domain-btn whitespace-nowrap px-space-sm py-1 rounded-full bg-surface-container text-on-surface-variant hover:bg-surface-container-high font-label-sm text-label-sm transition-colors" data-domain="Growth" type="button">
Growth (9)
</button>
<button class="domain-btn whitespace-nowrap px-space-sm py-1 rounded-full bg-surface-container text-on-surface-variant hover:bg-surface-container-high font-label-sm text-label-sm transition-colors" data-domain="Connection" type="button">
Connection (6)
</button>
<button class="domain-btn whitespace-nowrap px-space-sm py-1 rounded-full bg-surface-container text-on-surface-variant hover:bg-surface-container-high font-label-sm text-label-sm transition-colors" data-domain="Agency" type="button">
Agency (5)
</button>
<button class="domain-btn whitespace-nowrap px-space-sm py-1 rounded-full bg-surface-container text-on-surface-variant hover:bg-surface-container-high font-label-sm text-label-sm transition-colors" data-domain="Community" type="button">
Community (5)
</button>
</div>
</section>
<!-- Active Card List -->
<section aria-label="Important Values List" class="flex flex-col gap-space-sm" id="values-stream">
<!-- Card 1 -->
<article class="value-card bg-surface-container-low p-space-md rounded-lg shadow-sm flex flex-col gap-space-sm transition-all" data-domain="Presence" data-value-name="Equanimity">
<div class="flex items-start justify-between gap-gutter">
<div class="flex flex-col gap-1 min-w-0">
<span class="inline-block self-start px-2 py-0.5 rounded-full bg-tertiary-fixed text-on-tertiary-fixed font-label-sm text-label-sm">
Presence
</span>
<h2 class="font-headline-md text-headline-md text-on-surface leading-tight">
Equanimity
</h2>
</div>
<div class="flex items-center gap-space-xs shrink-0 pt-0.5">
<button class="px-space-sm py-1.5 rounded-full bg-surface-container hover:bg-surface-container-high text-on-surface font-label-md text-label-md flex items-center gap-1 transition-colors" onclick="openPromoteDialog('Equanimity')" title="Promote to a higher priority pile" type="button">
<span class="material-symbols-outlined text-[16px] text-primary">arrow_upward</span>
<span>Promote</span>
</button>
<button class="px-space-sm py-1.5 rounded-full text-secondary hover:bg-secondary-fixed/40 font-label-md text-label-md flex items-center gap-1 transition-colors" onclick="openDemoteDrawer('Equanimity', 'Presence')" title="Demote to lower priority pile" type="button">
<span class="material-symbols-outlined text-[16px]">arrow_downward</span>
<span>Demote</span>
</button>
</div>
</div>
<p class="font-body-sm text-body-sm text-on-surface-variant">
Maintaining inner balance and composure regardless of external turmoil or unexpected storms.
</p>
</article>
<!-- Card 2 -->
<article class="value-card bg-surface-container-low p-space-md rounded-lg shadow-sm flex flex-col gap-space-sm transition-all" data-domain="Growth" data-value-name="Curiosity">
<div class="flex items-start justify-between gap-gutter">
<div class="flex flex-col gap-1 min-w-0">
<span class="inline-block self-start px-2 py-0.5 rounded-full bg-secondary-fixed text-on-secondary-fixed font-label-sm text-label-sm">
Growth
</span>
<h2 class="font-headline-md text-headline-md text-on-surface leading-tight">
Curiosity
</h2>
</div>
<div class="flex items-center gap-space-xs shrink-0 pt-0.5">
<button class="px-space-sm py-1.5 rounded-full bg-surface-container hover:bg-surface-container-high text-on-surface font-label-md text-label-md flex items-center gap-1 transition-colors" onclick="openPromoteDialog('Curiosity')" title="Promote to a higher priority pile" type="button">
<span class="material-symbols-outlined text-[16px] text-primary">arrow_upward</span>
<span>Promote</span>
</button>
<button class="px-space-sm py-1.5 rounded-full text-secondary hover:bg-secondary-fixed/40 font-label-md text-label-md flex items-center gap-1 transition-colors" onclick="openDemoteDrawer('Curiosity', 'Growth')" title="Demote to lower priority pile" type="button">
<span class="material-symbols-outlined text-[16px]">arrow_downward</span>
<span>Demote</span>
</button>
</div>
</div>
<p class="font-body-sm text-body-sm text-on-surface-variant">
Approaching unknown frontiers with open wonder and eager inquiry rather than hasty assumptions.
</p>
</article>
<!-- Card 3 -->
<article class="value-card bg-surface-container-low p-space-md rounded-lg shadow-sm flex flex-col gap-space-sm transition-all" data-domain="Connection" data-value-name="Sincerity">
<div class="flex items-start justify-between gap-gutter">
<div class="flex flex-col gap-1 min-w-0">
<span class="inline-block self-start px-2 py-0.5 rounded-full bg-primary-fixed text-on-primary-fixed font-label-sm text-label-sm">
Connection
</span>
<h2 class="font-headline-md text-headline-md text-on-surface leading-tight">
Sincerity
</h2>
</div>
<div class="flex items-center gap-space-xs shrink-0 pt-0.5">
<button class="px-space-sm py-1.5 rounded-full bg-surface-container hover:bg-surface-container-high text-on-surface font-label-md text-label-md flex items-center gap-1 transition-colors" onclick="openPromoteDialog('Sincerity')" title="Promote to a higher priority pile" type="button">
<span class="material-symbols-outlined text-[16px] text-primary">arrow_upward</span>
<span>Promote</span>
</button>
<button class="px-space-sm py-1.5 rounded-full text-secondary hover:bg-secondary-fixed/40 font-label-md text-label-md flex items-center gap-1 transition-colors" onclick="openDemoteDrawer('Sincerity', 'Connection')" title="Demote to lower priority pile" type="button">
<span class="material-symbols-outlined text-[16px]">arrow_downward</span>
<span>Demote</span>
</button>
</div>
</div>
<p class="font-body-sm text-body-sm text-on-surface-variant">
Living and communicating without pretense, honoring what is genuine between ourselves and others.
</p>
</article>
<!-- Card 4 -->
<article class="value-card bg-surface-container-low p-space-md rounded-lg shadow-sm flex flex-col gap-space-sm transition-all" data-domain="Agency" data-value-name="Autonomy">
<div class="flex items-start justify-between gap-gutter">
<div class="flex flex-col gap-1 min-w-0">
<span class="inline-block self-start px-2 py-0.5 rounded-full bg-surface-container-high text-on-surface-variant font-label-sm text-label-sm">
Agency
</span>
<h2 class="font-headline-md text-headline-md text-on-surface leading-tight">
Autonomy
</h2>
</div>
<div class="flex items-center gap-space-xs shrink-0 pt-0.5">
<button class="px-space-sm py-1.5 rounded-full bg-surface-container hover:bg-surface-container-high text-on-surface font-label-md text-label-md flex items-center gap-1 transition-colors" onclick="openPromoteDialog('Autonomy')" title="Promote to a higher priority pile" type="button">
<span class="material-symbols-outlined text-[16px] text-primary">arrow_upward</span>
<span>Promote</span>
</button>
<button class="px-space-sm py-1.5 rounded-full text-secondary hover:bg-secondary-fixed/40 font-label-md text-label-md flex items-center gap-1 transition-colors" onclick="openDemoteDrawer('Autonomy', 'Agency')" title="Demote to lower priority pile" type="button">
<span class="material-symbols-outlined text-[16px]">arrow_downward</span>
<span>Demote</span>
</button>
</div>
</div>
<p class="font-body-sm text-body-sm text-on-surface-variant">
The freedom to direct one’s own destiny, making choices grounded in authentic personal conviction.
</p>
</article>
<!-- Card 5 -->
<article class="value-card bg-surface-container-low p-space-md rounded-lg shadow-sm flex flex-col gap-space-sm transition-all" data-domain="Community" data-value-name="Hospitality">
<div class="flex items-start justify-between gap-gutter">
<div class="flex flex-col gap-1 min-w-0">
<span class="inline-block self-start px-2 py-0.5 rounded-full bg-tertiary-fixed text-on-tertiary-fixed font-label-sm text-label-sm">
Community
</span>
<h2 class="font-headline-md text-headline-md text-on-surface leading-tight">
Hospitality
</h2>
</div>
<div class="flex items-center gap-space-xs shrink-0 pt-0.5">
<button class="px-space-sm py-1.5 rounded-full bg-surface-container hover:bg-surface-container-high text-on-surface font-label-md text-label-md flex items-center gap-1 transition-colors" onclick="openPromoteDialog('Hospitality')" title="Promote to a higher priority pile" type="button">
<span class="material-symbols-outlined text-[16px] text-primary">arrow_upward</span>
<span>Promote</span>
</button>
<button class="px-space-sm py-1.5 rounded-full text-secondary hover:bg-secondary-fixed/40 font-label-md text-label-md flex items-center gap-1 transition-colors" onclick="openDemoteDrawer('Hospitality', 'Community')" title="Demote to lower priority pile" type="button">
<span class="material-symbols-outlined text-[16px]">arrow_downward</span>
<span>Demote</span>
</button>
</div>
</div>
<p class="font-body-sm text-body-sm text-on-surface-variant">
Extending warm physical and emotional sanctuary to neighbors, wanderers, and dear companions.
</p>
</article>
<!-- Card 6 -->
<article class="value-card bg-surface-container-low p-space-md rounded-lg shadow-sm flex flex-col gap-space-sm transition-all" data-domain="Growth" data-value-name="Craftsmanship">
<div class="flex items-start justify-between gap-gutter">
<div class="flex flex-col gap-1 min-w-0">
<span class="inline-block self-start px-2 py-0.5 rounded-full bg-secondary-fixed text-on-secondary-fixed font-label-sm text-label-sm">
Growth
</span>
<h2 class="font-headline-md text-headline-md text-on-surface leading-tight">
Craftsmanship
</h2>
</div>
<div class="flex items-center gap-space-xs shrink-0 pt-0.5">
<button class="px-space-sm py-1.5 rounded-full bg-surface-container hover:bg-surface-container-high text-on-surface font-label-md text-label-md flex items-center gap-1 transition-colors" onclick="openPromoteDialog('Craftsmanship')" title="Promote to a higher priority pile" type="button">
<span class="material-symbols-outlined text-[16px] text-primary">arrow_upward</span>
<span>Promote</span>
</button>
<button class="px-space-sm py-1.5 rounded-full text-secondary hover:bg-secondary-fixed/40 font-label-md text-label-md flex items-center gap-1 transition-colors" onclick="openDemoteDrawer('Craftsmanship', 'Growth')" title="Demote to lower priority pile" type="button">
<span class="material-symbols-outlined text-[16px]">arrow_downward</span>
<span>Demote</span>
</button>
</div>
</div>
<p class="font-body-sm text-body-sm text-on-surface-variant">
Devoting unhurried patience to shaping work with precision, care, and silent dedication.
</p>
</article>
</section>
<!-- Empty state for search filter (hidden by default) -->
<div class="hidden flex-col items-center justify-center text-center p-space-xl bg-surface-container-low rounded-lg mt-space-sm" id="no-results-state">
<span class="material-symbols-outlined text-outline text-3xl mb-space-xs">filter_vintage</span>
<h3 class="font-headline-md text-headline-md text-on-surface">No values matched</h3>
<p class="font-body-sm text-body-sm text-on-surface-variant mt-1">Try another keyword or select All Domains above.</p>
</div>
<!-- Contemplative Sticky Bottom Summary Bar -->
<div class="sticky bottom-3 mt-space-lg w-full z-10">
<div class="w-full bg-surface-container-highest/95 backdrop-blur-md p-space-sm rounded-full shadow-md flex items-center justify-between gap-space-xs">
<button class="px-space-md py-2.5 rounded-full bg-surface-container text-on-surface hover:bg-surface-container-high font-label-md text-label-md flex items-center gap-1.5 transition-colors" onclick="reviewOtherPiles()" type="button">
<span class="material-symbols-outlined text-[18px]">view_carousel</span>
<span>Review Other Piles</span>
</button>
<button class="px-space-lg py-2.5 rounded-full bg-primary text-on-primary hover:bg-primary-container font-label-md text-label-md shadow-sm flex items-center gap-1.5 transition-colors ml-auto" onclick="returnToRankFlow()" type="button">
<span>Return to Rank Flow</span>
<span class="material-symbols-outlined text-[18px]">arrow_forward</span>
</button>
</div>
</div>
<!-- Demote Bottom Sheet Drawer (Hidden initially) -->
<div class="fixed inset-0 bg-inverse-surface/40 backdrop-blur-xs opacity-0 pointer-events-none transition-opacity duration-300 z-40" id="demote-sheet-backdrop" onclick="closeDemoteDrawer()"></div>
<aside aria-labelledby="demote-sheet-title" aria-modal="true" class="fixed bottom-0 left-0 right-0 max-w-lg mx-auto bg-surface-container-low rounded-t-xl shadow-xl transform translate-y-full transition-transform duration-300 ease-out z-50 p-margin-mobile sm:p-margin flex flex-col gap-space-md" id="demote-sheet" role="dialog">
<!-- Drag Handle -->
<div class="w-12 h-1 rounded-full bg-outline-variant mx-auto -mt-1 cursor-grab" onclick="closeDemoteDrawer()"></div>
<div class="flex items-start justify-between gap-gutter">
<div>
<span class="font-label-sm text-label-sm text-secondary uppercase tracking-wider">Demote Value</span>
<h3 class="font-headline-md text-headline-md text-on-surface mt-0.5" id="demote-sheet-title">
Move “<span id="drawer-value-name">Value</span>”
</h3>
<p class="font-body-sm text-body-sm text-on-surface-variant mt-1">
Select a lower pile to place this value in gently. You can adjust this at any stage.
</p>
</div>
<button aria-label="Close sheet" class="w-9 h-9 rounded-full bg-surface-container flex items-center justify-center text-on-surface-variant hover:bg-surface-container-high transition-colors" onclick="closeDemoteDrawer()" type="button">
<span class="material-symbols-outlined text-[20px]">close</span>
</button>
</div>
<!-- Move Destinations -->
<div class="flex flex-col gap-space-xs pt-space-xs">
<!-- Pile 2 -->
<button class="w-full text-left p-space-md rounded-DEFAULT bg-surface hover:bg-surface-container transition-colors flex items-center justify-between" onclick="executeDemote('Pile 2: Somewhat Important')" type="button">
<div class="flex flex-col">
<div class="flex items-center gap-space-xs">
<span class="w-2 h-2 rounded-full bg-secondary"></span>
<span class="font-label-lg text-label-lg text-on-surface">Pile 2: Somewhat Important</span>
</div>
<span class="font-body-sm text-body-sm text-on-surface-variant mt-0.5 pl-3">Holds worth, but not actively prioritized right now (24 values)</span>
</div>
<span class="material-symbols-outlined text-outline">south</span>
</button>
<!-- Pile 1 -->
<button class="w-full text-left p-space-md rounded-DEFAULT bg-surface hover:bg-surface-container transition-colors flex items-center justify-between" onclick="executeDemote('Pile 1: Not Important')" type="button">
<div class="flex flex-col">
<div class="flex items-center gap-space-xs">
<span class="w-2 h-2 rounded-full bg-outline"></span>
<span class="font-label-lg text-label-lg text-on-surface">Pile 1: Not Important</span>
</div>
<span class="font-body-sm text-body-sm text-on-surface-variant mt-0.5 pl-3">Values that rarely drive your current life choices (18 values)</span>
</div>
<span class="material-symbols-outlined text-outline">keyboard_double_arrow_down</span>
</button>
<!-- Move to custom pile -->
<button class="w-full text-center py-space-sm font-label-md text-label-md text-on-surface-variant hover:text-on-surface transition-colors mt-space-xs" onclick="executeDemote('Custom Pile Selection')" type="button">
Move to another pile…
</button>
</div>
</aside>
<!-- Notification Toast -->
<div class="fixed top-4 left-1/2 -translate-x-1/2 bg-inverse-surface text-inverse-on-surface px-space-md py-2 rounded-full shadow-lg font-body-sm text-body-sm opacity-0 pointer-events-none transition-all duration-300 z-50 flex items-center gap-2" id="toast-msg">
<span class="material-symbols-outlined text-[18px] text-tertiary-fixed">check_circle</span>
<span id="toast-text">Value moved</span>
</div>
</div>
<script>
let activeValueForDemote = null;
function openDemoteDrawer(valueName, domain) {
activeValueForDemote = valueName;
const nameHolder = document.getElementById('drawer-value-name');
if (nameHolder) nameHolder.textContent = valueName;
const sheet = document.getElementById('demote-sheet');
const backdrop = document.getElementById('demote-sheet-backdrop');
if (sheet && backdrop) {
backdrop.classList.remove('opacity-0', 'pointer-events-none');
backdrop.classList.add('opacity-100', 'pointer-events-auto');
sheet.classList.remove('translate-y-full');
sheet.classList.add('translate-y-0');
}
}
function closeDemoteDrawer() {
const sheet = document.getElementById('demote-sheet');
const backdrop = document.getElementById('demote-sheet-backdrop');
if (sheet && backdrop) {
backdrop.classList.remove('opacity-100', 'pointer-events-auto');
backdrop.classList.add('opacity-0', 'pointer-events-none');
sheet.classList.remove('translate-y-0');
sheet.classList.add('translate-y-full');
}
}
function executeDemote(targetPile) {
if (!activeValueForDemote) return;
const valName = activeValueForDemote;
closeDemoteDrawer();
// Softly remove card from UI list with animation
const cards = document.querySelectorAll('.value-card');
cards.forEach(card => {
if (card.getAttribute('data-value-name') === valName) {
card.style.opacity = '0';
card.style.transform = 'translateY(12px)';
setTimeout(() => {
card.remove();
checkRemainingCards();
}, 250);
}
});
showToast(`Moved "${valName}" to ${targetPile}`);
}
function openPromoteDialog(valName) {
showToast(`Promoted "${valName}" to Pile 4: Very Important`);
// Animate removal from Important
const cards = document.querySelectorAll('.value-card');
cards.forEach(card => {
if (card.getAttribute('data-value-name') === valName) {
card.style.opacity = '0';
card.style.transform = 'translateY(-12px)';
setTimeout(() => {
card.remove();
checkRemainingCards();
}, 250);
}
});
}
function showToast(message) {
const toast = document.getElementById('toast-msg');
const toastText = document.getElementById('toast-text');
if (!toast || !toastText) return;
toastText.textContent = message;
toast.classList.remove('opacity-0', 'pointer-events-none');
toast.classList.add('opacity-100', 'pointer-events-auto');
setTimeout(() => {
toast.classList.remove('opacity-100', 'pointer-events-auto');
toast.classList.add('opacity-0', 'pointer-events-none');
}, 2800);
}
function checkRemainingCards() {
const visibleCards = document.querySelectorAll('.value-card');
const noResults = document.getElementById('no-results-state');
if (visibleCards.length === 0 && noResults) {
noResults.classList.remove('hidden');
noResults.classList.add('flex');
}
}
function reviewOtherPiles() {
showToast("Opening pile overview overview...");
}
function returnToRankFlow() {
showToast("Returning to Values Prioritization Rank flow...");
}
// Client-side quick search & domain filtering
const searchInput = document.getElementById('value-search-input');
const domainButtons = document.querySelectorAll('.domain-btn');
let selectedDomain = 'all';
function filterCards() {
const query = (searchInput?.value || '').toLowerCase().trim();
const cards = document.querySelectorAll('.value-card');
let visibleCount = 0;
cards.forEach(card => {
const name = (card.getAttribute('data-value-name') || '').toLowerCase();
const domain = card.getAttribute('data-domain') || '';
const text = card.textContent?.toLowerCase() || '';
const matchesQuery = !query || name.includes(query) || text.includes(query);
const matchesDomain = (selectedDomain === 'all') || (domain === selectedDomain);
if (matchesQuery && matchesDomain) {
card.style.display = 'flex';
visibleCount++;
} else {
card.style.display = 'none';
}
});
const noResults = document.getElementById('no-results-state');
if (noResults) {
if (visibleCount === 0) {
noResults.classList.remove('hidden');
noResults.classList.add('flex');
} else {
noResults.classList.add('hidden');
noResults.classList.remove('flex');
}
}
}
if (searchInput) {
searchInput.addEventListener('input', filterCards);
}
domainButtons.forEach(btn => {
btn.addEventListener('click', () => {
domainButtons.forEach(b => {
b.classList.remove('bg-primary-container', 'text-on-primary-container');
b.classList.add('bg-surface-container', 'text-on-surface-variant');
});
btn.classList.add('bg-primary-container', 'text-on-primary-container');
btn.classList.remove('bg-surface-container', 'text-on-surface-variant');
selectedDomain = btn.getAttribute('data-domain') || 'all';
filterCards();
});
});
</script></main></body></html>
+16
View File
@@ -0,0 +1,16 @@
{
"name": "projects/9343387512370733994/screens/12809277486904866316",
"title": "Values Sort \u2014 Important Cards (Pile 3)",
"screenshot": {
"name": "projects/9343387512370733994/files/12809277486904866801",
"downloadUrl": "https://lh3.googleusercontent.com/aida/AEtjO1XQVoRrEK9mxF3gFjm9RD-lkFg-pwyvf_kJsLOREyZzFD3UWWWPbcivFX2aIvhiewCkv3Rw4SWGxq3AtN-TbfKPD2L4R0QKA3XPc_RImmva7z66yTUJo8bvozFBpwKdHXo5vPaxqB2-UyjVDLrfiCY75WYAkp6P0BF4W3xqoRy8mWUqe5ljCtjlIwbyo2GyR66wkPSOAeUqFl6s0jLycyB7MAaiBbBPOejz27uo2xYdbppL0Bzc7XVrpeE"
},
"htmlCode": {
"name": "projects/9343387512370733994/files/12809277486904867286",
"downloadUrl": "https://contribution.usercontent.google.com/download?c=CgthaWRhX2NvZGVmeBJ7Eh1hcHBfY29tcGFuaW9uX2dlbmVyYXRlZF9maWxlcxpaCiVodG1sXzAwMDY1YmIyODM5NjZmMWUwNGU3NThiMjE2MDFkYWQxEgsSBxD_iY311x0YAZIBIwoKcHJvamVjdF9pZBIVQhM5MzQzMzg3NTEyMzcwNzMzOTk0&filename=&opi=89354086",
"mimeType": "text/html"
},
"width": "780",
"height": "2766",
"deviceType": "MOBILE"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

+722
View File
@@ -0,0 +1,722 @@
<!DOCTYPE html>
<html lang="en" style=""><head><meta charset="utf-8"/><meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" name="viewport"/><link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200" rel="stylesheet"/><link href="https://fonts.googleapis.com" rel="preconnect"/><link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"/><link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,200..800;1,6..72,200..800&amp;family=Plus+Jakarta+Sans:ital,wght@0,300..800;1,300..800&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,[email protected],0..1&amp;display=swap" rel="stylesheet"/><script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script><script id="tailwind-config">tailwind.config = { darkMode: 'class', theme: { extend: { colors: { 'on-tertiary-container': '#e1f9db', 'surface-container': '#f1ede6', 'on-primary-container': '#ffefe1', 'primary-fixed-dim': '#d9c3ac', 'tertiary-fixed-dim': '#b5cdb0', 'surface': '#fdf9f2', 'on-secondary-fixed-variant': '#713714', 'surface-container-lowest': '#ffffff', 'error-container': '#ffdad6', 'primary': '#635341', 'outline-variant': '#d0c4ba', 'tertiary': '#465b44', 'surface-dim': '#dddad3', 'primary-container': '#7d6b58', 'outline': '#7f756d', 'on-error-container': '#93000a', 'surface-bright': '#fdf9f2', 'background': '#fdf9f2', 'secondary-container': '#feaa7f', 'tertiary-container': '#5e745c', 'surface-tint': '#6c5b49', 'on-secondary-container': '#783d1a', 'on-primary-fixed-variant': '#544433', 'on-primary-fixed': '#25190b', 'tertiary-fixed': '#d1e9cc', 'on-error': '#ffffff', 'on-secondary': '#ffffff', 'on-tertiary-fixed-variant': '#374c36', 'on-surface': '#1c1c18', 'surface-container-high': '#ece8e1', 'secondary': '#8e4d2a', 'secondary-fixed-dim': '#ffb691', 'secondary-fixed': '#ffdbcb', 'inverse-surface': '#31302c', 'inverse-on-surface': '#f4f0e9', 'on-primary': '#ffffff', 'primary-fixed': '#f6dfc7', 'on-surface-variant': '#4d453e', 'surface-container-highest': '#e6e2db', 'on-tertiary-fixed': '#0d200d', 'on-secondary-fixed': '#341100', 'on-background': '#1c1c18', 'surface-variant': '#e6e2db', 'on-tertiary': '#ffffff', 'error': '#ba1a1a', 'inverse-primary': '#d9c3ac', 'surface-container-low': '#f7f3ec' }, borderRadius: { DEFAULT: '1rem', lg: '2rem', xl: '3rem', full: '9999px' }, spacing: { 'space-xs': '0.25rem', 'margin': '1.5rem', 'gutter': '1rem', 'space-xl': '2.5rem', 'margin-mobile': '1.25rem', 'gutter-mobile': '0.75rem', 'space-sm': '0.5rem', 'space-lg': '1.5rem', 'space-md': '1rem' }, fontFamily: { 'label-lg': ['Plus Jakarta Sans'], 'label-sm': ['Plus Jakarta Sans'], 'headline-xl-mobile': ['Newsreader'], 'body-lg': ['Plus Jakarta Sans'], 'body-sm': ['Plus Jakarta Sans'], 'headline-lg-mobile': ['Newsreader'], 'headline-md': ['Newsreader'], 'headline-lg': ['Newsreader'], 'body-md': ['Plus Jakarta Sans'], 'headline-xl': ['Newsreader'], 'label-md': ['Plus Jakarta Sans'] }, fontSize: { 'label-lg': ['15px', { lineHeight: '20px', letterSpacing: '0.01em', fontWeight: '600' }], 'label-sm': ['11px', { lineHeight: '16px', letterSpacing: '0.04em', fontWeight: '600' }], 'headline-xl-mobile': ['32px', { lineHeight: '40px', letterSpacing: '-0.01em', fontWeight: '400' }], 'body-lg': ['18px', { lineHeight: '28px', fontWeight: '400' }], 'body-sm': ['14px', { lineHeight: '20px', fontWeight: '400' }], 'headline-lg-mobile': ['26px', { lineHeight: '34px', letterSpacing: '0em', fontWeight: '400' }], 'headline-md': ['22px', { lineHeight: '30px', fontWeight: '400' }], 'headline-lg': ['30px', { lineHeight: '38px', letterSpacing: '-0.01em', fontWeight: '400' }], 'body-md': ['16px', { lineHeight: '24px', fontWeight: '400' }], 'headline-xl': ['40px', { lineHeight: '48px', letterSpacing: '-0.02em', fontWeight: '400' }], 'label-md': ['13px', { lineHeight: '18px', letterSpacing: '0.02em', fontWeight: '500' }] } } } };</script><style>@layer base { html, body { width: 100vw; margin: 0; padding: 0; } body { overscroll-behavior: none; } .pb-safe { padding-bottom: env(safe-area-inset-bottom, 0px); } .pt-safe { padding-top: env(safe-area-inset-top, 0px); } main > :first-child { margin-top: 0 !important; } main > :last-child { margin-bottom: 0 !important; } } ::-webkit-scrollbar { display: none; }</style></head><body class="bg-surface font-body-md text-body-md text-on-surface min-h-screen flex flex-col pt-safe pb-safe"><main class="flex-1 flex flex-col relative w-full bg-surface"><div class="flex flex-col w-full pb-32">
<!-- Top Bar & Contextual Back Navigation -->
<div class="px-5 pt-3 pb-4 flex items-center justify-between">
<button class="inline-flex items-center gap-1.5 text-primary py-1 px-2.5 rounded-full bg-surface-container active:scale-95 transition-transform duration-150" type="button">
<span class="material-symbols-outlined text-[18px]">arrow_back</span>
<span class="font-label-md text-label-md">Back to Ranking</span>
</button>
<div class="flex items-center gap-1 text-on-surface-variant bg-surface-container px-3 py-1 rounded-full">
</div>
</div>
<!-- Editorial Header & Summary -->
<header class="px-5 pt-2 pb-5 flex flex-col gap-2">
<div class="flex items-baseline justify-between">
<h1 class="font-headline-lg-mobile text-headline-lg-mobile text-on-surface">All Sorted Values</h1>
<span class="font-label-sm text-label-sm text-on-surface-variant uppercase tracking-widest bg-surface-container-high px-2.5 py-1 rounded-full">100 Cards</span>
</div>
<p class="font-body-md text-body-md text-on-surface-variant leading-relaxed">
A bird’s-eye view of your 100 cards distributed across the 5 reflective piles. Review your intuition or gently rebalance.
</p>
<!-- Visual Density Indicator Metric Row -->
<div class="mt-3 p-4 rounded-2xl bg-surface-container flex flex-col gap-3">
<div class="flex items-center justify-between font-label-md text-label-md">
<span class="text-on-surface font-semibold flex items-center gap-1.5">
<span class="w-2 h-2 rounded-full bg-secondary"></span>
Distribution Profile
</span>
<span class="text-on-surface-variant" id="metric-summary">8 Core Nominees</span>
</div>
<!-- Tactile Proportional Pill Bar -->
<div class="flex h-2.5 w-full rounded-full overflow-hidden bg-surface-container-highest gap-0.5">
<div class="h-full bg-secondary transition-all" style="width: 8%" title="Most: 8%"></div>
<div class="h-full bg-secondary-container transition-all" style="width: 18%" title="Very: 18%"></div>
<div class="h-full bg-tertiary-container transition-all" style="width: 32%" title="Important: 32%"></div>
<div class="h-full bg-surface-dim transition-all" style="width: 24%" title="Somewhat: 24%"></div>
<div class="h-full bg-outline-variant transition-all" style="width: 18%" title="Not: 18%"></div>
</div>
<div class="flex justify-between items-center text-on-surface-variant font-label-sm text-label-sm pt-0.5">
<span class="">P5: 8</span>
<span class="">P4: 18</span>
<span class="">P3: 32</span>
<span class="">P2: 24</span>
<span class="">P1: 18</span>
</div>
</div>
</header>
<!-- Serene Search / In-situ Query -->
<div class="px-5 mb-5">
<div class="relative flex items-center bg-surface-container rounded-2xl px-3.5 py-2.5 shadow-sm transition-all focus-within:bg-surface-container-lowest">
<span class="material-symbols-outlined text-outline text-[20px] mr-2.5">search</span>
<input class="bg-transparent text-on-surface placeholder:text-outline text-body-sm font-body-sm w-full outline-none" id="value-search" placeholder="Find where 'Creativity' or 'Peace' rests..." type="text"/>
<button class="hidden text-outline hover:text-on-surface ml-1 p-0.5" id="clear-search" type="button">
<span class="material-symbols-outlined text-[18px]">close</span>
</button>
</div>
</div>
<!-- Interactive Horizontal Sticky Pile Scroller -->
<nav aria-label="Pile quick jump" class="sticky top-0 z-20 bg-surface/95 backdrop-blur-md px-5 py-2.5 mb-4 flex items-center gap-2 overflow-x-auto no-scrollbar">
<a class="pile-tab active flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-secondary text-on-secondary whitespace-nowrap font-label-md text-label-md shadow-sm transition-all" href="#pile-5">
<span class="">Most</span>
<span class="px-1.5 py-0.2 rounded-full bg-on-secondary/20 text-on-secondary font-label-sm text-label-sm" id="tab-p5-count">8</span>
</a>
<a class="pile-tab flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-surface-container text-on-surface whitespace-nowrap font-label-md text-label-md transition-all" href="#pile-4">
<span class="">Very</span>
<span class="px-1.5 py-0.2 rounded-full bg-surface-container-high text-on-surface-variant font-label-sm text-label-sm">18</span>
</a>
<a class="pile-tab flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-surface-container text-on-surface whitespace-nowrap font-label-md text-label-md transition-all" href="#pile-3">
<span class="">Important</span>
<span class="px-1.5 py-0.2 rounded-full bg-surface-container-high text-on-surface-variant font-label-sm text-label-sm">32</span>
</a>
<a class="pile-tab flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-surface-container text-on-surface whitespace-nowrap font-label-md text-label-md transition-all" href="#pile-2">
<span class="">Somewhat</span>
<span class="px-1.5 py-0.2 rounded-full bg-surface-container-high text-on-surface-variant font-label-sm text-label-sm">24</span>
</a>
<a class="pile-tab flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-surface-container text-on-surface whitespace-nowrap font-label-md text-label-md transition-all" href="#pile-1">
<span class="">Not</span>
<span class="px-1.5 py-0.2 rounded-full bg-surface-container-high text-on-surface-variant font-label-sm text-label-sm">18</span>
</a>
</nav>
<!-- Piles Container -->
<div class="px-5 flex flex-col gap-8" id="piles-list">
<!-- PILE 5: MOST IMPORTANT (CORE NOMINEES) -->
<section class="pile-group flex flex-col gap-3 scroll-mt-16" id="pile-5">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="flex h-6 w-6 rounded-full bg-secondary text-on-secondary font-label-sm text-label-sm items-center justify-center font-bold">5</span>
<div>
<h2 class="font-headline-md text-headline-md text-on-surface leading-tight">Most Important</h2>
</div>
</div>
<span class="bg-secondary-fixed text-on-secondary-fixed px-2.5 py-0.5 rounded-full font-label-sm text-label-sm font-semibold" id="p5-badge-count">8 cards</span>
</div>
<!-- Pile 5 Rich Cards -->
<div class="grid grid-cols-1 gap-3 mt-1" id="pile-5-items">
<!-- Authenticity -->
<article class="value-item bg-surface-container-low p-4 rounded-2xl shadow-sm flex flex-col gap-2 relative transition-all" data-title="Authenticity">
<div class="flex items-center justify-between">
<h3 class="font-headline-md text-headline-md text-on-surface">Authenticity</h3>
<button class="demote-btn inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-secondary font-label-md text-label-md hover:bg-secondary/10 active:bg-secondary/20 transition-colors" data-value="Authenticity" type="button">
<span>Demote</span>
<span class="material-symbols-outlined text-[16px]">expand_more</span>
</button>
</div>
<p class="font-body-sm text-body-sm text-on-surface-variant">
Living in genuine alignment with inner truth and speaking without pretense or performance.
</p>
</article>
<!-- Courage -->
<article class="value-item bg-surface-container-low p-4 rounded-2xl shadow-sm flex flex-col gap-2 relative transition-all" data-title="Courage">
<div class="flex items-center justify-between">
<h3 class="font-headline-md text-headline-md text-on-surface">Courage</h3>
<button class="demote-btn inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-secondary font-label-md text-label-md hover:bg-secondary/10 active:bg-secondary/20 transition-colors" data-value="Courage" type="button">
<span>Demote</span>
<span class="material-symbols-outlined text-[16px]">expand_more</span>
</button>
</div>
<p class="font-body-sm text-body-sm text-on-surface-variant">
Stepping directly into the unknown and acting in alignment with what is right, despite palpable fear.
</p>
</article>
<!-- Compassion -->
<article class="value-item bg-surface-container-low p-4 rounded-2xl shadow-sm flex flex-col gap-2 relative transition-all" data-title="Compassion">
<div class="flex items-center justify-between">
<h3 class="font-headline-md text-headline-md text-on-surface">Compassion</h3>
<button class="demote-btn inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-secondary font-label-md text-label-md hover:bg-secondary/10 active:bg-secondary/20 transition-colors" data-value="Compassion" type="button">
<span>Demote</span>
<span class="material-symbols-outlined text-[16px]">expand_more</span>
</button>
</div>
<p class="font-body-sm text-body-sm text-on-surface-variant">
Meeting suffering—in myself and others—with a gentle, unwavering posture of mercy and assistance.
</p>
</article>
<!-- Freedom -->
<article class="value-item bg-surface-container-low p-4 rounded-2xl shadow-sm flex flex-col gap-2 relative transition-all" data-title="Freedom">
<div class="flex items-center justify-between">
<h3 class="font-headline-md text-headline-md text-on-surface">Freedom</h3>
<button class="demote-btn inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-secondary font-label-md text-label-md hover:bg-secondary/10 active:bg-secondary/20 transition-colors" data-value="Freedom" type="button">
<span>Demote</span>
<span class="material-symbols-outlined text-[16px]">expand_more</span>
</button>
</div>
<p class="font-body-sm text-body-sm text-on-surface-variant">
Possessing agency over my time, movement, and life path without undue coercion or arbitrary constraints.
</p>
</article>
<!-- Curiosity -->
<article class="value-item bg-surface-container-low p-4 rounded-2xl shadow-sm flex flex-col gap-2 relative transition-all" data-title="Curiosity">
<div class="flex items-center justify-between">
<h3 class="font-headline-md text-headline-md text-on-surface">Curiosity</h3>
<button class="demote-btn inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-secondary font-label-md text-label-md hover:bg-secondary/10 active:bg-secondary/20 transition-colors" data-value="Curiosity" type="button">
<span>Demote</span>
<span class="material-symbols-outlined text-[16px]">expand_more</span>
</button>
</div>
<p class="font-body-sm text-body-sm text-on-surface-variant">
Approaching life, people, and puzzles with wonder, humility, and an open thirst for understanding.
</p>
</article>
<!-- Integrity -->
<article class="value-item bg-surface-container-low p-4 rounded-2xl shadow-sm flex flex-col gap-2 relative transition-all" data-title="Integrity">
<div class="flex items-center justify-between">
<h3 class="font-headline-md text-headline-md text-on-surface">Integrity</h3>
<button class="demote-btn inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-secondary font-label-md text-label-md hover:bg-secondary/10 active:bg-secondary/20 transition-colors" data-value="Integrity" type="button">
<span>Demote</span>
<span class="material-symbols-outlined text-[16px]">expand_more</span>
</button>
</div>
<p class="font-body-sm text-body-sm text-on-surface-variant">
Upholding moral wholeness and consistency in character when nobody is watching.
</p>
</article>
<!-- Connection -->
<article class="value-item bg-surface-container-low p-4 rounded-2xl shadow-sm flex flex-col gap-2 relative transition-all" data-title="Connection">
<div class="flex items-center justify-between">
<h3 class="font-headline-md text-headline-md text-on-surface">Connection</h3>
<button class="demote-btn inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-secondary font-label-md text-label-md hover:bg-secondary/10 active:bg-secondary/20 transition-colors" data-value="Connection" type="button">
<span>Demote</span>
<span class="material-symbols-outlined text-[16px]">expand_more</span>
</button>
</div>
<p class="font-body-sm text-body-sm text-on-surface-variant">
Fostering deep mutual bonds and empathy where souls are seen, understood, and cherished.
</p>
</article>
<!-- Wisdom -->
<article class="value-item bg-surface-container-low p-4 rounded-2xl shadow-sm flex flex-col gap-2 relative transition-all" data-title="Wisdom">
<div class="flex items-center justify-between">
<h3 class="font-headline-md text-headline-md text-on-surface">Wisdom</h3>
<button class="demote-btn inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-secondary font-label-md text-label-md hover:bg-secondary/10 active:bg-secondary/20 transition-colors" data-value="Wisdom" type="button">
<span>Demote</span>
<span class="material-symbols-outlined text-[16px]">expand_more</span>
</button>
</div>
<p class="font-body-sm text-body-sm text-on-surface-variant">
Synthesizing deep experience and discernment into mindful, centered action.
</p>
</article>
</div>
</section>
<!-- PILE 4: VERY IMPORTANT -->
<section class="pile-group flex flex-col gap-3 scroll-mt-16" id="pile-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="flex h-6 w-6 rounded-full bg-surface-container-highest text-on-surface font-label-sm text-label-sm items-center justify-center font-bold">4</span>
<div>
<h2 class="font-headline-md text-headline-md text-on-surface leading-tight">Very Important</h2>
<p class="font-label-sm text-label-sm text-on-surface-variant">Strong priorities shaping everyday rhythms</p>
</div>
</div>
<span class="bg-surface-container-high text-on-surface-variant px-2.5 py-0.5 rounded-full font-label-sm text-label-sm" id="p4-badge-count">18 cards</span>
</div>
<div class="p-4 rounded-2xl bg-surface-container flex flex-col gap-3">
<div class="flex flex-wrap gap-2" id="pile-4-chips">
<!-- Values Chips with quick promote/demote indicators -->
<button class="value-item group flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-surface-container-lowest text-on-surface font-label-md text-label-md shadow-sm active:scale-95 transition-all" data-title="Creativity" type="button">
<span class="">Creativity</span>
<span class="material-symbols-outlined text-outline group-hover:text-secondary text-[14px]">north</span>
</button>
<button class="value-item group flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-surface-container-lowest text-on-surface font-label-md text-label-md shadow-sm active:scale-95 transition-all" data-title="Peace" type="button">
<span class="">Peace</span>
<span class="material-symbols-outlined text-outline group-hover:text-secondary text-[14px]">north</span>
</button>
<button class="value-item group flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-surface-container-lowest text-on-surface font-label-md text-label-md shadow-sm active:scale-95 transition-all" data-title="Autonomy" type="button">
<span class="">Autonomy</span>
<span class="material-symbols-outlined text-outline group-hover:text-secondary text-[14px]">north</span>
</button>
<button class="value-item group flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-surface-container-lowest text-on-surface font-label-md text-label-md shadow-sm active:scale-95 transition-all" data-title="Growth" type="button">
<span class="">Growth</span>
<span class="material-symbols-outlined text-outline group-hover:text-secondary text-[14px]">north</span>
</button>
<button class="value-item group flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-surface-container-lowest text-on-surface font-label-md text-label-md shadow-sm active:scale-95 transition-all" data-title="Kindness" type="button">
<span class="">Kindness</span>
<span class="material-symbols-outlined text-outline group-hover:text-secondary text-[14px]">north</span>
</button>
<button class="value-item group flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-surface-container-lowest text-on-surface font-label-md text-label-md shadow-sm active:scale-95 transition-all" data-title="Health" type="button">
<span class="">Health</span>
<span class="material-symbols-outlined text-outline group-hover:text-secondary text-[14px]">north</span>
</button>
<button class="value-item group flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-surface-container-lowest text-on-surface font-label-md text-label-md shadow-sm active:scale-95 transition-all" data-title="Gratitude" type="button">
<span class="">Gratitude</span>
<span class="material-symbols-outlined text-outline group-hover:text-secondary text-[14px]">north</span>
</button>
<button class="value-item group flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-surface-container-lowest text-on-surface font-label-md text-label-md shadow-sm active:scale-95 transition-all" data-title="Purpose" type="button">
<span class="">Purpose</span>
<span class="material-symbols-outlined text-outline group-hover:text-secondary text-[14px]">north</span>
</button>
<button class="value-item group flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-surface-container-lowest text-on-surface font-label-md text-label-md shadow-sm active:scale-95 transition-all" data-title="Joy" type="button">
<span class="">Joy</span>
<span class="material-symbols-outlined text-outline group-hover:text-secondary text-[14px]">north</span>
</button>
<button class="value-item group flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-surface-container-lowest text-on-surface font-label-md text-label-md shadow-sm active:scale-95 transition-all" data-title="Humor" type="button">
<span class="">Humor</span>
<span class="material-symbols-outlined text-outline group-hover:text-secondary text-[14px]">north</span>
</button>
<button class="value-item group flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-surface-container-lowest text-on-surface font-label-md text-label-md shadow-sm active:scale-95 transition-all" data-title="Resilience" type="button">
<span class="">Resilience</span>
<span class="material-symbols-outlined text-outline group-hover:text-secondary text-[14px]">north</span>
</button>
<button class="value-item group flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-surface-container-lowest text-on-surface font-label-md text-label-md shadow-sm active:scale-95 transition-all" data-title="Empathy" type="button">
<span class="">Empathy</span>
<span class="material-symbols-outlined text-outline group-hover:text-secondary text-[14px]">north</span>
</button>
<button class="value-item group flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-surface-container-lowest text-on-surface font-label-md text-label-md shadow-sm active:scale-95 transition-all" data-title="Self-Respect" type="button">
<span class="">Self-Respect</span>
<span class="material-symbols-outlined text-outline group-hover:text-secondary text-[14px]">north</span>
</button>
<button class="value-item group flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-surface-container-lowest text-on-surface font-label-md text-label-md shadow-sm active:scale-95 transition-all" data-title="Justice" type="button">
<span class="">Justice</span>
<span class="material-symbols-outlined text-outline group-hover:text-secondary text-[14px]">north</span>
</button>
<button class="value-item group flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-surface-container-lowest text-on-surface font-label-md text-label-md shadow-sm active:scale-95 transition-all" data-title="Generosity" type="button">
<span class="">Generosity</span>
<span class="material-symbols-outlined text-outline group-hover:text-secondary text-[14px]">north</span>
</button>
<button class="value-item group flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-surface-container-lowest text-on-surface font-label-md text-label-md shadow-sm active:scale-95 transition-all" data-title="Harmony" type="button">
<span class="">Harmony</span>
<span class="material-symbols-outlined text-outline group-hover:text-secondary text-[14px]">north</span>
</button>
<button class="value-item group flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-surface-container-lowest text-on-surface font-label-md text-label-md shadow-sm active:scale-95 transition-all" data-title="Nature" type="button">
<span class="">Nature</span>
<span class="material-symbols-outlined text-outline group-hover:text-secondary text-[14px]">north</span>
</button>
<button class="value-item group flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-surface-container-lowest text-on-surface font-label-md text-label-md shadow-sm active:scale-95 transition-all" data-title="Simplicity" type="button">
<span class="">Simplicity</span>
<span class="material-symbols-outlined text-outline group-hover:text-secondary text-[14px]">north</span>
</button>
</div>
<div class="text-right">
<span class="font-label-sm text-label-sm text-outline">Tap chip arrow to promote to Most Important</span>
</div>
</div>
</section>
<!-- PILE 3: IMPORTANT -->
<section class="pile-group flex flex-col gap-3 scroll-mt-16" id="pile-3">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="flex h-6 w-6 rounded-full bg-surface-container-highest text-on-surface font-label-sm text-label-sm items-center justify-center font-bold">3</span>
<div>
<h2 class="font-headline-md text-headline-md text-on-surface leading-tight">Important</h2>
<p class="font-label-sm text-label-sm text-on-surface-variant">Meaningful aspects kept in calm balance</p>
</div>
</div>
<span class="bg-surface-container-high text-on-surface-variant px-2.5 py-0.5 rounded-full font-label-sm text-label-sm" id="p3-badge-count">32 cards</span>
</div>
<div class="p-4 rounded-2xl bg-surface-container flex flex-col gap-3">
<div class="flex flex-wrap gap-2" id="pile-3-chips">
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Security">Security</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Order">Order</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Adventure">Adventure</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Discipline">Discipline</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Patience">Patience</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Loyalty">Loyalty</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Knowledge">Knowledge</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Beauty">Beauty</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Playfulness">Playfulness</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Solitude">Solitude</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Reliability">Reliability</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Honesty">Honesty</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Openness">Openness</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Mastery">Mastery</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Intimacy">Intimacy</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Balance">Balance</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Presence">Presence</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Acceptance">Acceptance</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Clarity">Clarity</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Modesty">Modesty</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Vitality">Vitality</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Insight">Insight</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Contribution">Contribution</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Depth">Depth</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Cooperation">Cooperation</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Resourcefulness">Resourcefulness</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Adaptability">Adaptability</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Spontaneity">Spontaneity</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Family">Family</span>
<span class="value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md" data-title="Community">Community</span>
</div>
</div>
</section>
<!-- PILE 2: SOMEWHAT IMPORTANT -->
<section class="pile-group flex flex-col gap-3 scroll-mt-16" id="pile-2">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="flex h-6 w-6 rounded-full bg-surface-container-highest text-on-surface font-label-sm text-label-sm items-center justify-center font-bold">2</span>
<div>
<h2 class="font-headline-md text-headline-md text-on-surface leading-tight">Somewhat Important</h2>
<p class="font-label-sm text-label-sm text-on-surface-variant">Present, but rarely leading choices</p>
</div>
</div>
<span class="bg-surface-container-high text-on-surface-variant px-2.5 py-0.5 rounded-full font-label-sm text-label-sm">24 cards</span>
</div>
<div class="p-4 rounded-2xl bg-surface-container flex flex-col gap-2">
<div class="flex flex-wrap gap-1.5 opacity-90" id="pile-2-chips">
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-high text-on-surface-variant font-label-md text-label-md" data-title="Comfort">Comfort</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-high text-on-surface-variant font-label-md text-label-md" data-title="Tradition">Tradition</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-high text-on-surface-variant font-label-md text-label-md" data-title="Efficiency">Efficiency</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-high text-on-surface-variant font-label-md text-label-md" data-title="Leisure">Leisure</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-high text-on-surface-variant font-label-md text-label-md" data-title="Predictability">Predictability</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-high text-on-surface-variant font-label-md text-label-md" data-title="Novelty">Novelty</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-high text-on-surface-variant font-label-md text-label-md" data-title="Ambition">Ambition</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-high text-on-surface-variant font-label-md text-label-md" data-title="Status">Status</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-high text-on-surface-variant font-label-md text-label-md" data-title="Precision">Precision</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-high text-on-surface-variant font-label-md text-label-md" data-title="Competition">Competition</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-high text-on-surface-variant font-label-md text-label-md" data-title="Thrift">Thrift</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-high text-on-surface-variant font-label-md text-label-md" data-title="Privacy">Privacy</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-high text-on-surface-variant font-label-md text-label-md" data-title="Recognition">Recognition</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-high text-on-surface-variant font-label-md text-label-md" data-title="Power">Power</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-high text-on-surface-variant font-label-md text-label-md" data-title="Popularity">Popularity</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-high text-on-surface-variant font-label-md text-label-md" data-title="Conformity">Conformity</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-high text-on-surface-variant font-label-md text-label-md" data-title="Charm">Charm</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-high text-on-surface-variant font-label-md text-label-md" data-title="Intensity">Intensity</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-high text-on-surface-variant font-label-md text-label-md" data-title="Pleasure">Pleasure</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-high text-on-surface-variant font-label-md text-label-md" data-title="Influence">Influence</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-high text-on-surface-variant font-label-md text-label-md" data-title="Formality">Formality</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-high text-on-surface-variant font-label-md text-label-md" data-title="Thrill">Thrill</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-high text-on-surface-variant font-label-md text-label-md" data-title="Reputation">Reputation</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-high text-on-surface-variant font-label-md text-label-md" data-title="Speed">Speed</span>
</div>
</div>
</section>
<!-- PILE 1: NOT IMPORTANT -->
<section class="pile-group flex flex-col gap-3 scroll-mt-16" id="pile-1">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="flex h-6 w-6 rounded-full bg-surface-container-highest text-on-surface font-label-sm text-label-sm items-center justify-center font-bold">1</span>
<div>
<h2 class="font-headline-md text-headline-md text-on-surface leading-tight">Not Important</h2>
<p class="font-label-sm text-label-sm text-on-surface-variant">Consciously released from my life compass</p>
</div>
</div>
<span class="bg-surface-container-high text-on-surface-variant px-2.5 py-0.5 rounded-full font-label-sm text-label-sm">18 cards</span>
</div>
<div class="p-4 rounded-2xl bg-surface-container flex flex-col gap-2">
<div class="flex flex-wrap gap-1.5 opacity-60" id="pile-1-chips">
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-highest text-on-surface-variant font-label-md text-label-md line-through decoration-outline" data-title="Wealth">Wealth</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-highest text-on-surface-variant font-label-md text-label-md line-through decoration-outline" data-title="Fame">Fame</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-highest text-on-surface-variant font-label-md text-label-md line-through decoration-outline" data-title="Authority">Authority</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-highest text-on-surface-variant font-label-md text-label-md line-through decoration-outline" data-title="Luxury">Luxury</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-highest text-on-surface-variant font-label-md text-label-md line-through decoration-outline" data-title="Winning">Winning</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-highest text-on-surface-variant font-label-md text-label-md line-through decoration-outline" data-title="Glamour">Glamour</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-highest text-on-surface-variant font-label-md text-label-md line-through decoration-outline" data-title="Perfection">Perfection</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-highest text-on-surface-variant font-label-md text-label-md line-through decoration-outline" data-title="Control">Control</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-highest text-on-surface-variant font-label-md text-label-md line-through decoration-outline" data-title="Hierarchy">Hierarchy</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-highest text-on-surface-variant font-label-md text-label-md line-through decoration-outline" data-title="Ritual">Ritual</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-highest text-on-surface-variant font-label-md text-label-md line-through decoration-outline" data-title="Dominance">Dominance</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-highest text-on-surface-variant font-label-md text-label-md line-through decoration-outline" data-title="Appearance">Appearance</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-highest text-on-surface-variant font-label-md text-label-md line-through decoration-outline" data-title="Acquisition">Acquisition</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-highest text-on-surface-variant font-label-md text-label-md line-through decoration-outline" data-title="Strictness">Strictness</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-highest text-on-surface-variant font-label-md text-label-md line-through decoration-outline" data-title="Exclusivity">Exclusivity</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-highest text-on-surface-variant font-label-md text-label-md line-through decoration-outline" data-title="Superiority">Superiority</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-highest text-on-surface-variant font-label-md text-label-md line-through decoration-outline" data-title="Pride">Pride</span>
<span class="value-item px-2.5 py-1 rounded-lg bg-surface-container-highest text-on-surface-variant font-label-md text-label-md line-through decoration-outline" data-title="Publicity">Publicity</span>
</div>
</div>
</section>
<!-- Empty Search State Container (hidden by default) -->
<div class="hidden flex-col items-center justify-center py-12 px-4 text-center bg-surface-container rounded-2xl" id="empty-state">
<span class="material-symbols-outlined text-[36px] text-outline mb-2">sentiment_content</span>
<p class="font-headline-md text-headline-md text-on-surface">No values found</p>
<p class="font-body-sm text-body-sm text-on-surface-variant mt-1 max-w-xs">
Try searching for another keyword or clear the search to view all 100 cards.
</p>
<button class="mt-4 px-4 py-2 rounded-full bg-primary text-on-primary font-label-md text-label-md" id="reset-search-btn" type="button">
Show All Piles
</button>
</div>
</div>
<!-- Tactile Sticky Bottom Bar -->
<aside class="fixed bottom-0 left-0 right-0 z-30 bg-surface/95 backdrop-blur-md px-5 pt-3 pb-safe shadow-xl">
<div class="max-w-md mx-auto flex flex-col gap-2 pb-3">
<button class="w-full py-3.5 px-6 rounded-full bg-primary text-on-primary font-label-lg text-label-lg flex items-center justify-center gap-2 shadow-md active:scale-[0.98] transition-transform duration-150" type="button">
<span class="">Return to Ranking</span>
<span class="material-symbols-outlined text-[18px]">arrow_forward</span>
</button>
</div>
</aside>
</div>
<!-- Demote Bottom Sheet Drawer -->
<div aria-hidden="true" class="fixed inset-0 z-50 pointer-events-none transition-opacity duration-300 opacity-0" id="demote-sheet-container">
<!-- Backdrop overlay -->
<div class="absolute inset-0 bg-inverse-surface/40 backdrop-blur-sm transition-opacity" id="demote-backdrop"></div>
<!-- Drawer Content -->
<div class="absolute bottom-0 left-0 right-0 bg-surface rounded-t-3xl shadow-2xl border-t border-outline-variant/30 flex flex-col max-h-[85vh] transform translate-y-full transition-transform duration-300 ease-out pb-safe" id="demote-sheet-panel">
<!-- Handle bar -->
<div class="w-full flex justify-center pt-3 pb-1">
<span class="w-10 h-1 rounded-full bg-outline-variant/60"></span>
</div>
<!-- Sheet Header -->
<div class="px-6 pt-2 pb-4 flex items-start justify-between border-b border-surface-container">
<div>
<span class="font-label-sm text-label-sm uppercase tracking-wider text-secondary font-semibold">Demote Value</span>
<h2 class="font-headline-md text-headline-md text-on-surface mt-0.5" id="demote-sheet-title">Move to pile:</h2>
</div>
<button aria-label="Close sheet" class="text-outline hover:text-on-surface p-1 rounded-full bg-surface-container active:scale-95 transition-transform" id="close-demote-sheet" type="button">
<span class="material-symbols-outlined text-[20px]">close</span>
</button>
</div>
<!-- Piles Selection List -->
<div class="px-6 py-4 flex flex-col gap-2.5 overflow-y-auto">
<!-- Destination Pile 4 -->
<button class="dest-pile-btn flex items-center justify-between p-3.5 rounded-2xl bg-surface-container-low hover:bg-surface-container active:scale-[0.99] border border-transparent hover:border-secondary/20 transition-all text-left group" data-pile="4" data-pile-name="Very Important" type="button">
<div class="flex items-center gap-3">
<span class="flex h-7 w-7 rounded-full bg-surface-container-highest text-on-surface font-label-md text-label-md items-center justify-center font-bold">4</span>
<div>
<p class="font-label-lg text-label-lg text-on-surface font-semibold group-hover:text-secondary transition-colors">Pile 4: Very Important</p>
<p class="font-body-sm text-body-sm text-on-surface-variant">Deeply held guide</p>
</div>
</div>
<span class="material-symbols-outlined text-outline group-hover:text-secondary text-[20px]">south</span>
</button>
<!-- Destination Pile 3 -->
<button class="dest-pile-btn flex items-center justify-between p-3.5 rounded-2xl bg-surface-container-low hover:bg-surface-container active:scale-[0.99] border border-transparent hover:border-secondary/20 transition-all text-left group" data-pile="3" data-pile-name="Important" type="button">
<div class="flex items-center gap-3">
<span class="flex h-7 w-7 rounded-full bg-surface-container-highest text-on-surface font-label-md text-label-md items-center justify-center font-bold">3</span>
<div>
<p class="font-label-lg text-label-lg text-on-surface font-semibold group-hover:text-secondary transition-colors">Pile 3: Important</p>
<p class="font-body-sm text-body-sm text-on-surface-variant">Valued aspect</p>
</div>
</div>
<span class="material-symbols-outlined text-outline group-hover:text-secondary text-[20px]">south</span>
</button>
<!-- Destination Pile 2 -->
<button class="dest-pile-btn flex items-center justify-between p-3.5 rounded-2xl bg-surface-container-low hover:bg-surface-container active:scale-[0.99] border border-transparent hover:border-secondary/20 transition-all text-left group" data-pile="2" data-pile-name="Somewhat Important" type="button">
<div class="flex items-center gap-3">
<span class="flex h-7 w-7 rounded-full bg-surface-container-highest text-on-surface font-label-md text-label-md items-center justify-center font-bold">2</span>
<div>
<p class="font-label-lg text-label-lg text-on-surface font-semibold group-hover:text-secondary transition-colors">Pile 2: Somewhat Important</p>
<p class="font-body-sm text-body-sm text-on-surface-variant">Contextual value</p>
</div>
</div>
<span class="material-symbols-outlined text-outline group-hover:text-secondary text-[20px]">south</span>
</button>
<!-- Destination Pile 1 -->
<button class="dest-pile-btn flex items-center justify-between p-3.5 rounded-2xl bg-surface-container-low hover:bg-surface-container active:scale-[0.99] border border-transparent hover:border-secondary/20 transition-all text-left group" data-pile="1" data-pile-name="Not Important" type="button">
<div class="flex items-center gap-3">
<span class="flex h-7 w-7 rounded-full bg-surface-container-highest text-on-surface font-label-sm text-label-sm items-center justify-center font-bold">1</span>
<div>
<p class="font-label-lg text-label-lg text-on-surface font-semibold group-hover:text-secondary transition-colors">Pile 1: Not Important</p>
<p class="font-body-sm text-body-sm text-on-surface-variant">Not a current compass</p>
</div>
</div>
<span class="material-symbols-outlined text-outline group-hover:text-secondary text-[20px]">south</span>
</button>
</div>
<div class="px-6 pt-1 pb-4 text-center">
<span class="font-label-sm text-label-sm text-outline">You can reorder or bring cards back anytime</span>
</div>
</div>
</div>
<!-- Notification Toast -->
<div class="fixed top-5 left-1/2 -translate-x-1/2 z-50 pointer-events-none transition-all duration-300 opacity-0 -translate-y-3" id="toast-message">
<div class="flex items-center gap-2 bg-inverse-surface text-inverse-on-surface px-4 py-2.5 rounded-full shadow-lg font-label-md text-label-md">
<span class="material-symbols-outlined text-secondary-fixed text-[18px]">check_circle</span>
<span id="toast-text">Card moved</span>
</div>
</div>
<script>
(function() {
const searchInput = document.getElementById('value-search');
const clearBtn = document.getElementById('clear-search');
const resetBtn = document.getElementById('reset-search-btn');
const valueItems = document.querySelectorAll('.value-item');
const pileGroups = document.querySelectorAll('.pile-group');
const emptyState = document.getElementById('empty-state');
const tabs = document.querySelectorAll('.pile-tab');
// Demote Drawer Elements
const sheetContainer = document.getElementById('demote-sheet-container');
const sheetPanel = document.getElementById('demote-sheet-panel');
const sheetBackdrop = document.getElementById('demote-backdrop');
const closeSheetBtn = document.getElementById('close-demote-sheet');
const sheetTitle = document.getElementById('demote-sheet-title');
const destPileBtns = document.querySelectorAll('.dest-pile-btn');
const toast = document.getElementById('toast-message');
const toastText = document.getElementById('toast-text');
let activeCardTitle = '';
let activeCardElement = null;
function openDemoteSheet(cardTitle, cardEl) {
activeCardTitle = cardTitle;
activeCardElement = cardEl;
sheetTitle.textContent = `Move '${cardTitle}' to:`;
sheetContainer.classList.remove('pointer-events-none', 'opacity-0');
sheetContainer.classList.add('opacity-100');
sheetPanel.classList.remove('translate-y-full');
sheetPanel.classList.add('translate-y-0');
}
function closeDemoteSheet() {
sheetPanel.classList.remove('translate-y-0');
sheetPanel.classList.add('translate-y-full');
sheetContainer.classList.remove('opacity-100');
sheetContainer.classList.add('opacity-0');
setTimeout(() => {
sheetContainer.classList.add('pointer-events-none');
}, 300);
}
function showToast(msg) {
toastText.textContent = msg;
toast.classList.remove('opacity-0', '-translate-y-3');
toast.classList.add('opacity-100', 'translate-y-0');
setTimeout(() => {
toast.classList.remove('opacity-100', 'translate-y-0');
toast.classList.add('opacity-0', '-translate-y-3');
}, 2500);
}
// Attach Demote click handlers
document.querySelectorAll('.demote-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const card = btn.closest('.value-item');
const valName = btn.getAttribute('data-value') || card.getAttribute('data-title');
openDemoteSheet(valName, card);
});
});
// Destination Pile Selection
destPileBtns.forEach(btn => {
btn.addEventListener('click', () => {
const destPileNum = btn.getAttribute('data-pile');
const destPileName = btn.getAttribute('data-pile-name');
if (activeCardElement) {
// Animate removal from pile 5
activeCardElement.style.transition = 'opacity 0.25s, transform 0.25s';
activeCardElement.style.opacity = '0';
activeCardElement.style.transform = 'scale(0.95)';
setTimeout(() => {
activeCardElement.remove();
// Append chip to destination pile container
const destContainer = document.getElementById(`pile-${destPileNum}-chips`);
if (destContainer) {
const newChip = document.createElement('span');
newChip.className = destPileNum === '1'
? 'value-item px-2.5 py-1 rounded-lg bg-surface-container-highest text-on-surface-variant font-label-md text-label-md line-through decoration-outline transition-all'
: destPileNum === '2'
? 'value-item px-2.5 py-1 rounded-lg bg-surface-container-high text-on-surface-variant font-label-md text-label-md transition-all'
: destPileNum === '3'
? 'value-item px-3 py-1 rounded-full bg-surface-container-low text-on-surface-variant font-label-md text-label-md transition-all'
: 'value-item group flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-surface-container-lowest text-on-surface font-label-md text-label-md shadow-sm active:scale-95 transition-all';
newChip.setAttribute('data-title', activeCardTitle);
newChip.textContent = activeCardTitle;
destContainer.prepend(newChip);
}
// Update P5 counts
const remainingP5 = document.querySelectorAll('#pile-5-items .value-item').length;
const badge = document.getElementById('p5-badge-count');
const tabBadge = document.getElementById('tab-p5-count');
const metric = document.getElementById('metric-summary');
if (badge) badge.textContent = `${remainingP5} cards`;
if (tabBadge) tabBadge.textContent = remainingP5;
if (metric) metric.textContent = `${remainingP5} Core Nominees`;
}, 250);
}
closeDemoteSheet();
showToast(`'${activeCardTitle}' moved to Pile ${destPileNum}: ${destPileName}`);
});
});
if (sheetBackdrop) sheetBackdrop.addEventListener('click', closeDemoteSheet);
if (closeSheetBtn) closeSheetBtn.addEventListener('click', closeDemoteSheet);
function filterValues(query) {
const term = query.trim().toLowerCase();
let matchCount = 0;
if (!term) {
clearBtn.classList.add('hidden');
document.querySelectorAll('.value-item').forEach(item => item.classList.remove('hidden'));
pileGroups.forEach(group => group.classList.remove('hidden'));
emptyState.classList.add('hidden');
return;
}
clearBtn.classList.remove('hidden');
pileGroups.forEach(group => {
const itemsInGroup = group.querySelectorAll('.value-item');
let visibleInGroup = 0;
itemsInGroup.forEach(item => {
const title = (item.getAttribute('data-title') || '').toLowerCase();
const text = item.textContent.toLowerCase();
if (title.includes(term) || text.includes(term)) {
item.classList.remove('hidden');
visibleInGroup++;
matchCount++;
} else {
item.classList.add('hidden');
}
});
if (visibleInGroup === 0) {
group.classList.add('hidden');
} else {
group.classList.remove('hidden');
}
});
if (matchCount === 0) {
emptyState.classList.remove('hidden');
emptyState.classList.add('flex');
} else {
emptyState.classList.add('hidden');
emptyState.classList.remove('flex');
}
}
if (searchInput) {
searchInput.addEventListener('input', (e) => filterValues(e.target.value));
}
if (clearBtn) {
clearBtn.addEventListener('click', () => {
searchInput.value = '';
filterValues('');
searchInput.focus();
});
}
if (resetBtn) {
resetBtn.addEventListener('click', () => {
searchInput.value = '';
filterValues('');
});
}
// Tab active styling updates on click
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => {
t.classList.remove('bg-secondary', 'text-on-secondary');
t.classList.add('bg-surface-container', 'text-on-surface');
const badge = t.querySelector('span:last-child');
if (badge) {
badge.classList.remove('bg-on-secondary/20', 'text-on-secondary');
badge.classList.add('bg-surface-container-high', 'text-on-surface-variant');
}
});
tab.classList.add('bg-secondary', 'text-on-secondary');
tab.classList.remove('bg-surface-container', 'text-on-surface');
const activeBadge = tab.querySelector('span:last-child');
if (activeBadge) {
activeBadge.classList.add('bg-on-secondary/20', 'text-on-secondary');
activeBadge.classList.remove('bg-surface-container-high', 'text-on-surface-variant');
}
});
});
})();
</script></main></body></html>
+16
View File
@@ -0,0 +1,16 @@
{
"name": "projects/9343387512370733994/screens/73272c6039864978bcec444c7df2973f",
"title": "Values Sort \u2014 Most Important (with Demote Drawer)",
"screenshot": {
"name": "projects/9343387512370733994/screens/73272c6039864978bcec444c7df2973f/fileEntries/screenshot",
"downloadUrl": "https://lh3.googleusercontent.com/aida/AEtjO1Xhu39G6wcAy9suH2BL28jVCSboZ_ydBY69b4vlY9-gtnbu66RyRiHaRDF8MkiSVfcjYFtlIVfpFgyN856OBgA4qenmijh6Uii1vIm22lmUNWElTOI1GSSUyOF_NMNLN7N3HKNwLY6pc0PlrUBoB--DVbW6-RcnqfBG-CIAFWWzuOn0YEXUIbLzFLfecIwxJbbHEblBtC8m7q0RR140TUxcbtxtzHRX_km3es8xnlabSjh1DMxb3K1Efg"
},
"htmlCode": {
"name": "projects/9343387512370733994/screens/73272c6039864978bcec444c7df2973f/fileEntries/html",
"downloadUrl": "https://contribution.usercontent.google.com/download?c=CgthaWRhX2NvZGVmeBJ7Eh1hcHBfY29tcGFuaW9uX2dlbmVyYXRlZF9maWxlcxpaCiVodG1sXzAwMDY1YmIyNzZkYjg4ZjgwNWYxMzVmZTYyMDcyNzBkEgsSBxD_iY311x0YAZIBIwoKcHJvamVjdF9pZBIVQhM5MzQzMzg3NTEyMzcwNzMzOTk0&filename=&opi=89354086",
"mimeType": "text/html"
},
"width": "780",
"height": "6564",
"deviceType": "MOBILE"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+191
View File
@@ -0,0 +1,191 @@
<!DOCTYPE html><html lang="en" style=""><head><meta charset="utf-8"><meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" name="viewport"><link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200" rel="stylesheet"><link href="https://fonts.googleapis.com" rel="preconnect"><link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"><link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,200..800;1,6..72,200..800&amp;family=Plus+Jakarta+Sans:ital,wght@0,300..800;1,300..800&amp;display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,[email protected],0..1&amp;display=swap" rel="stylesheet"><script src="https://cdn.tailwindcss.com"></script><script id="tailwind-config">tailwind.config = { darkMode: 'class', theme: { extend: { colors: { 'on-tertiary-container': '#e1f9db', 'surface-container': '#f1ede6', 'on-primary-container': '#ffefe1', 'primary-fixed-dim': '#d9c3ac', 'tertiary-fixed-dim': '#b5cdb0', 'surface': '#fdf9f2', 'on-secondary-fixed-variant': '#713714', 'surface-container-lowest': '#ffffff', 'error-container': '#ffdad6', 'primary': '#635341', 'outline-variant': '#d0c4ba', 'tertiary': '#465b44', 'surface-dim': '#dddad3', 'primary-container': '#7d6b58', 'outline': '#7f756d', 'on-error-container': '#93000a', 'surface-bright': '#fdf9f2', 'background': '#fdf9f2', 'secondary-container': '#feaa7f', 'tertiary-container': '#5e745c', 'surface-tint': '#6c5b49', 'on-secondary-container': '#783d1a', 'on-primary-fixed-variant': '#544433', 'on-primary-fixed': '#25190b', 'tertiary-fixed': '#d1e9cc', 'on-error': '#ffffff', 'on-secondary': '#ffffff', 'on-tertiary-fixed-variant': '#374c36', 'on-surface': '#1c1c18', 'surface-container-high': '#ece8e1', 'secondary': '#8e4d2a', 'secondary-fixed-dim': '#ffb691', 'secondary-fixed': '#ffdbcb', 'inverse-surface': '#31302c', 'inverse-on-surface': '#f4f0e9', 'on-primary': '#ffffff', 'primary-fixed': '#f6dfc7', 'on-surface-variant': '#4d453e', 'surface-container-highest': '#e6e2db', 'on-tertiary-fixed': '#0d200d', 'on-secondary-fixed': '#341100', 'on-background': '#1c1c18', 'surface-variant': '#e6e2db', 'on-tertiary': '#ffffff', 'error': '#ba1a1a', 'inverse-primary': '#d9c3ac', 'surface-container-low': '#f7f3ec' }, borderRadius: { DEFAULT: '1rem', lg: '2rem', xl: '3rem', full: '9999px' }, spacing: { 'space-xs': '0.25rem', 'margin': '1.5rem', 'gutter': '1rem', 'space-xl': '2.5rem', 'margin-mobile': '1.25rem', 'gutter-mobile': '0.75rem', 'space-sm': '0.5rem', 'space-lg': '1.5rem', 'space-md': '1rem' }, fontFamily: { 'label-lg': ['Plus Jakarta Sans'], 'label-sm': ['Plus Jakarta Sans'], 'headline-xl-mobile': ['Newsreader'], 'body-lg': ['Plus Jakarta Sans'], 'body-sm': ['Plus Jakarta Sans'], 'headline-lg-mobile': ['Newsreader'], 'headline-md': ['Newsreader'], 'headline-lg': ['Newsreader'], 'body-md': ['Plus Jakarta Sans'], 'headline-xl': ['Newsreader'], 'label-md': ['Plus Jakarta Sans'] }, fontSize: { 'label-lg': ['15px', { lineHeight: '20px', letterSpacing: '0.01em', fontWeight: '600' }], 'label-sm': ['11px', { lineHeight: '16px', letterSpacing: '0.04em', fontWeight: '600' }], 'headline-xl-mobile': ['32px', { lineHeight: '40px', letterSpacing: '-0.01em', fontWeight: '400' }], 'body-lg': ['18px', { lineHeight: '28px', fontWeight: '400' }], 'body-sm': ['14px', { lineHeight: '20px', fontWeight: '400' }], 'headline-lg-mobile': ['26px', { lineHeight: '34px', letterSpacing: '0em', fontWeight: '400' }], 'headline-md': ['22px', { lineHeight: '30px', fontWeight: '400' }], 'headline-lg': ['30px', { lineHeight: '38px', letterSpacing: '-0.01em', fontWeight: '400' }], 'body-md': ['16px', { lineHeight: '24px', fontWeight: '400' }], 'headline-xl': ['40px', { lineHeight: '48px', letterSpacing: '-0.02em', fontWeight: '400' }], 'label-md': ['13px', { lineHeight: '18px', letterSpacing: '0.02em', fontWeight: '500' }] } } } };</script><style>@layer base { html, body { width: 100vw; margin: 0; padding: 0; } body { overscroll-behavior: none; } .pb-safe { padding-bottom: env(safe-area-inset-bottom, 0px); } .pt-safe { padding-top: env(safe-area-inset-top, 0px); } main > :first-child { margin-top: 0 !important; } main > :last-child { margin-bottom: 0 !important; } } ::-webkit-scrollbar { display: none; }</style></head><body class="bg-surface font-body-md text-body-md text-on-surface min-h-screen flex flex-col pt-safe pb-safe"><main class="flex-1 flex flex-col relative w-full bg-surface"><div class="flex flex-col w-full pb-10">
<!-- Status Strip / Ambient Top Bar -->
<div class="px-margin-mobile pt-3 pb-2 flex items-center justify-between">
<div class="flex items-center gap-space-xs">
<span class="w-2 h-2 rounded-full bg-tertiary animate-pulse"></span>
<span class="font-label-sm text-label-sm uppercase tracking-wider text-on-surface-variant">Values Sort Hub</span>
</div>
</div>
<!-- Introspective Hero Greeting -->
<div class="px-margin-mobile mt-4 mb-5">
<div class="flex items-center gap-2 mb-1.5">
<span class="material-symbols-outlined text-[18px] text-primary" style="font-variation-settings: &quot;FILL&quot; 1;">spa</span>
</div>
<h1 class="font-headline-xl-mobile text-headline-xl-mobile text-on-surface tracking-tight">Your values landscape</h1>
</div>
<!-- Central Compass Status Card -->
<div class="px-margin-mobile mb-6"><div class="bg-surface-container-low rounded-lg p-5 shadow-sm border border-outline-variant/30 flex flex-col gap-3.5"><div class="flex items-center justify-between"><div class="flex items-center gap-2"><span class="material-symbols-outlined text-[18px] text-secondary" style="font-variation-settings: &quot;FILL&quot; 1;">stars</span><h2 class="font-headline-md text-headline-md text-on-surface font-normal">Your Core Values</h2></div></div><p class="font-body-sm text-body-sm text-on-surface-variant">The foundational principles selected as your highest inner compass:</p><div class="flex flex-wrap gap-2 pt-1"><span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-surface-container-high text-on-surface font-label-md text-label-md shadow-sm"><span class="w-4 h-4 rounded-full bg-secondary text-on-secondary flex items-center justify-center font-label-sm text-[10px]">1</span> Authenticity</span><span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-surface-container-high text-on-surface font-label-md text-label-md shadow-sm"><span class="w-4 h-4 rounded-full bg-secondary text-on-secondary flex items-center justify-center font-label-sm text-[10px]">2</span> Courage</span><span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-surface-container-high text-on-surface font-label-md text-label-md shadow-sm"><span class="w-4 h-4 rounded-full bg-secondary text-on-secondary flex items-center justify-center font-label-sm text-[10px]">3</span> Compassion</span><span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-surface-container-high text-on-surface font-label-md text-label-md shadow-sm"><span class="w-4 h-4 rounded-full bg-secondary text-on-secondary flex items-center justify-center font-label-sm text-[10px]">4</span> Freedom</span><span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-surface-container-high text-on-surface font-label-md text-label-md shadow-sm"><span class="w-4 h-4 rounded-full bg-secondary text-on-secondary flex items-center justify-center font-label-sm text-[10px]">5</span> Curiosity</span><span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-surface-container-high text-on-surface font-label-md text-label-md shadow-sm"><span class="w-4 h-4 rounded-full bg-secondary text-on-secondary flex items-center justify-center font-label-sm text-[10px]">6</span> Integrity</span><span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-surface-container-high text-on-surface font-label-md text-label-md shadow-sm"><span class="w-4 h-4 rounded-full bg-secondary text-on-secondary flex items-center justify-center font-label-sm text-[10px]">7</span> Connection</span><span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-surface-container-high text-on-surface font-label-md text-label-md shadow-sm"><span class="w-4 h-4 rounded-full bg-secondary text-on-secondary flex items-center justify-center font-label-sm text-[10px]">8</span> Wisdom</span></div></div></div>
<!-- Gateway Cards Section -->
<div class="px-margin-mobile flex flex-col gap-4">
<div class="flex items-center justify-between px-1">
</div>
<!-- Gateway 1: All Sorted Values -->
<button class="w-full text-left bg-surface-container-low active:bg-surface-container rounded-lg p-5 shadow-sm transition-all flex flex-col gap-3 group" type="button">
<div class="flex items-start justify-between w-full">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-surface-container-high flex items-center justify-center text-primary group-hover:scale-105 transition-transform">
<span class="material-symbols-outlined text-[22px]">layers</span>
</div>
<div>
<div class="flex items-center gap-2">
<h2 class="font-headline-md text-headline-md text-on-surface font-normal">All Sorted Values</h2>
</div>
</div>
</div>
<span class="material-symbols-outlined text-primary group-hover:translate-x-1 transition-transform mt-1">arrow_forward</span>
</div>
<div class="pt-2 flex items-center justify-between">
</div>
</button>
<!-- Gateway 2: Order Top Values -->
<button class="w-full text-left bg-surface-container-low active:bg-surface-container rounded-lg p-5 shadow-sm transition-all flex flex-col gap-3 group" type="button">
<div class="flex items-start justify-between w-full">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-surface-container-high flex items-center justify-center text-secondary group-hover:scale-105 transition-transform">
<span class="material-symbols-outlined text-[22px]">format_list_numbered</span>
</div>
<div>
<h2 class="font-headline-md text-headline-md text-on-surface font-normal">Order Top Values</h2>
<span class="inline-block mt-0.5 bg-secondary-fixed/50 px-2.5 py-0.5 rounded-full font-label-sm text-label-sm text-on-secondary-fixed-variant">
8 Nominees · #1 Authenticity
</span>
</div>
</div>
<span class="material-symbols-outlined text-primary group-hover:translate-x-1 transition-transform mt-1">arrow_forward</span>
</div>
</button>
<!-- Gateway 3: Reflect (Journal) -->
<button class="w-full text-left bg-surface-container-low active:bg-surface-container rounded-lg p-5 shadow-sm transition-all flex flex-col gap-3 group" type="button">
<div class="flex items-start justify-between w-full">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-surface-container-high flex items-center justify-center text-tertiary group-hover:scale-105 transition-transform">
<span class="material-symbols-outlined text-[22px]" style="font-variation-settings: &quot;FILL&quot; 1;">edit_note</span>
</div>
<div>
<h2 class="font-headline-md text-headline-md text-on-surface font-normal">Reflect &amp; Inquire</h2>
<span class="inline-block mt-0.5 bg-tertiary-fixed/60 px-2.5 py-0.5 rounded-full font-label-sm text-label-sm text-on-tertiary-fixed-variant">
14 of 20 Answered (70%)
</span>
</div>
</div>
<span class="material-symbols-outlined text-primary group-hover:translate-x-1 transition-transform mt-1">arrow_forward</span>
</div>
<!-- Mini Progress bar inside card -->
<div class="w-full bg-surface-container-highest rounded-full h-1.5 mt-1 overflow-hidden">
<div class="bg-tertiary h-full rounded-full" style="width: 70%;"></div>
</div>
<div class="pt-1 flex items-center justify-between">
</div>
</button>
</div>
<!-- Contemplative Breathing Quote / Ambient Breakpoint -->
<div class="px-margin-mobile mt-8 mb-4">
</div>
<!-- Practice Lifecycle Management Section -->
<div class="px-margin-mobile mt-4 flex flex-col items-center">
<div class="w-full pt-4 flex flex-col items-center text-center">
<button class="w-full py-3.5 px-6 rounded-full bg-surface-container-high hover:bg-surface-dim active:scale-[0.99] text-on-surface font-label-lg text-label-lg transition-all flex items-center justify-center gap-2 shadow-sm" id="openResetBtn" type="button">
<span class="material-symbols-outlined text-[18px] text-outline">restart_alt</span>
Finish Practice (Reset All)
</button>
</div>
</div>
<!-- Mindful Confirmation Drawer Modal -->
<div class="fixed inset-0 bg-inverse-surface/40 backdrop-blur-sm z-40 transition-opacity duration-300 flex items-end justify-center opacity-0 pointer-events-none" id="resetDrawerBackdrop">
<div class="w-full max-w-md bg-surface-container-low rounded-t-xl p-6 shadow-xl transform transition-transform duration-300 ease-out z-50 flex flex-col translate-y-full" id="resetDrawerSheet">
<!-- Soft drag bar pill -->
<div class="w-12 h-1.5 rounded-full bg-surface-variant mx-auto mb-4"></div>
<div class="flex items-center gap-3 mb-2">
<div class="w-10 h-10 rounded-full bg-error-container/50 flex items-center justify-center text-error">
<span class="material-symbols-outlined text-[22px]">hourglass_empty</span>
</div>
<div>
<h3 class="font-headline-md text-headline-md text-on-surface">Reset Entire Practice?</h3>
<span class="font-label-sm text-label-sm text-on-surface-variant">Clean slate contemplation</span>
</div>
</div>
<p class="font-body-md text-body-md text-on-surface-variant my-3 leading-relaxed">
This deliberate action will clear all 5 categorized piles, your prioritized 1–8 hierarchy, and stored reflection notes. You will begin this mindful exploration from the initial deck.
</p>
<div class="bg-surface-container p-3.5 rounded-lg mb-6 flex items-start gap-2.5">
<span class="material-symbols-outlined text-[18px] text-secondary mt-0.5">lightbulb</span>
<span class="font-body-sm text-body-sm text-on-surface-variant">
Tip: You can pause here and revisit your values whenever you need clarity in daily choices without resetting.
</span>
</div>
<!-- Action Buttons -->
<div class="flex flex-col gap-2.5">
<button class="w-full py-3.5 rounded-full bg-primary text-on-primary font-label-lg text-label-lg active:bg-primary-container transition-colors shadow-sm" id="confirmResetBtn" type="button">
Yes, Reset All 100 Cards
</button>
<button class="w-full py-3 rounded-full bg-surface-container hover:bg-surface-container-high text-on-surface font-label-md text-label-md transition-colors" id="cancelResetBtn" type="button">
Keep My Current Landscape
</button>
</div>
</div>
</div>
</div>
<script>
(function() {
const openBtn = document.getElementById('openResetBtn');
const closeBtn = document.getElementById('cancelResetBtn');
const confirmBtn = document.getElementById('confirmResetBtn');
const backdrop = document.getElementById('resetDrawerBackdrop');
const sheet = document.getElementById('resetDrawerSheet');
function openDrawer() {
backdrop.classList.remove('pointer-events-none');
backdrop.classList.remove('opacity-0');
backdrop.classList.add('opacity-100');
sheet.classList.remove('translate-y-full');
sheet.classList.add('translate-y-0');
}
function closeDrawer() {
backdrop.classList.add('opacity-0');
backdrop.classList.remove('opacity-100');
sheet.classList.remove('translate-y-0');
sheet.classList.add('translate-y-full');
setTimeout(() => {
backdrop.classList.add('pointer-events-none');
}, 300);
}
if (openBtn) openBtn.addEventListener('click', openDrawer);
if (closeBtn) closeBtn.addEventListener('click', closeDrawer);
if (backdrop) {
backdrop.addEventListener('click', (e) => {
if (e.target === backdrop) closeDrawer();
});
}
if (confirmBtn) {
confirmBtn.addEventListener('click', () => {
confirmBtn.innerHTML = '<span class="material-symbols-outlined text-[18px] animate-spin">refresh</span> Resetting Sanctuary...';
setTimeout(() => {
closeDrawer();
confirmBtn.textContent = 'Yes, Reset All 100 Cards';
}, 900);
});
}
})();
</script></main>
</body></html>
+16
View File
@@ -0,0 +1,16 @@
{
"name": "projects/9343387512370733994/screens/7247b606c08a44ef906836e5ab250fd1",
"title": "Values Sort \u2014 Practice Hub",
"screenshot": {
"name": "projects/9343387512370733994/files/7801972050289016175",
"downloadUrl": "https://lh3.googleusercontent.com/aida/AEtjO1UHEdAYHIX_yrZnosUKgBf2xJF0wQL3zTavsKvxg0OSr8wi6CA62NhoBaFMmJGpz7onY3fJBqEfn0FLm1DARm7R4FJ1Kvh-yLyufytn_LW9IJhT_HgxFUtYHE2lLfLUw0BK3O39haKwmhTXdWNkTAdlWoXmJZNRYACXJ6uXbfbXPvQsLtpT5LaQgBu30F_KmlrUqjUtsxxtzNzTEUF-GZhO8Hi-B5weq6pQ0aAppHrH13K3uxb0j6VwSiE"
},
"htmlCode": {
"name": "projects/9343387512370733994/files/3785570347874661052",
"downloadUrl": "https://contribution.usercontent.google.com/download?c=CgthaWRhX2NvZGVmeBJ7Eh1hcHBfY29tcGFuaW9uX2dlbmVyYXRlZF9maWxlcxpaCiVodG1sXzAwMDY1YmI0MDNhMzc1YWIwNWYxMjlkYjE3MDRmY2VkEgsSBxD_iY311x0YAZIBIwoKcHJvamVjdF9pZBIVQhM5MzQzMzg3NTEyMzcwNzMzOTk0&filename=&opi=89354086",
"mimeType": "text/html"
},
"width": "780",
"height": "3234",
"deviceType": "MOBILE"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

+547
View File
@@ -0,0 +1,547 @@
<!DOCTYPE html><html lang="en" style=""><head><meta charset="utf-8"><meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" name="viewport"><link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200" rel="stylesheet"><link href="https://fonts.googleapis.com" rel="preconnect"><link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"><link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,200..800;1,6..72,200..800&amp;family=Plus+Jakarta+Sans:ital,wght@0,300..800;1,300..800&amp;display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,[email protected],0..1&amp;display=swap" rel="stylesheet"><script src="https://cdn.tailwindcss.com"></script><script id="tailwind-config">tailwind.config = { darkMode: 'class', theme: { extend: { colors: { 'on-tertiary-container': '#e1f9db', 'surface-container': '#f1ede6', 'on-primary-container': '#ffefe1', 'primary-fixed-dim': '#d9c3ac', 'tertiary-fixed-dim': '#b5cdb0', 'surface': '#fdf9f2', 'on-secondary-fixed-variant': '#713714', 'surface-container-lowest': '#ffffff', 'error-container': '#ffdad6', 'primary': '#635341', 'outline-variant': '#d0c4ba', 'tertiary': '#465b44', 'surface-dim': '#dddad3', 'primary-container': '#7d6b58', 'outline': '#7f756d', 'on-error-container': '#93000a', 'surface-bright': '#fdf9f2', 'background': '#fdf9f2', 'secondary-container': '#feaa7f', 'tertiary-container': '#5e745c', 'surface-tint': '#6c5b49', 'on-secondary-container': '#783d1a', 'on-primary-fixed-variant': '#544433', 'on-primary-fixed': '#25190b', 'tertiary-fixed': '#d1e9cc', 'on-error': '#ffffff', 'on-secondary': '#ffffff', 'on-tertiary-fixed-variant': '#374c36', 'on-surface': '#1c1c18', 'surface-container-high': '#ece8e1', 'secondary': '#8e4d2a', 'secondary-fixed-dim': '#ffb691', 'secondary-fixed': '#ffdbcb', 'inverse-surface': '#31302c', 'inverse-on-surface': '#f4f0e9', 'on-primary': '#ffffff', 'primary-fixed': '#f6dfc7', 'on-surface-variant': '#4d453e', 'surface-container-highest': '#e6e2db', 'on-tertiary-fixed': '#0d200d', 'on-secondary-fixed': '#341100', 'on-background': '#1c1c18', 'surface-variant': '#e6e2db', 'on-tertiary': '#ffffff', 'error': '#ba1a1a', 'inverse-primary': '#d9c3ac', 'surface-container-low': '#f7f3ec' }, borderRadius: { DEFAULT: '1rem', lg: '2rem', xl: '3rem', full: '9999px' }, spacing: { 'space-xs': '0.25rem', 'margin': '1.5rem', 'gutter': '1rem', 'space-xl': '2.5rem', 'margin-mobile': '1.25rem', 'gutter-mobile': '0.75rem', 'space-sm': '0.5rem', 'space-lg': '1.5rem', 'space-md': '1rem' }, fontFamily: { 'label-lg': ['Plus Jakarta Sans'], 'label-sm': ['Plus Jakarta Sans'], 'headline-xl-mobile': ['Newsreader'], 'body-lg': ['Plus Jakarta Sans'], 'body-sm': ['Plus Jakarta Sans'], 'headline-lg-mobile': ['Newsreader'], 'headline-md': ['Newsreader'], 'headline-lg': ['Newsreader'], 'body-md': ['Plus Jakarta Sans'], 'headline-xl': ['Newsreader'], 'label-md': ['Plus Jakarta Sans'] }, fontSize: { 'label-lg': ['15px', { lineHeight: '20px', letterSpacing: '0.01em', fontWeight: '600' }], 'label-sm': ['11px', { lineHeight: '16px', letterSpacing: '0.04em', fontWeight: '600' }], 'headline-xl-mobile': ['32px', { lineHeight: '40px', letterSpacing: '-0.01em', fontWeight: '400' }], 'body-lg': ['18px', { lineHeight: '28px', fontWeight: '400' }], 'body-sm': ['14px', { lineHeight: '20px', fontWeight: '400' }], 'headline-lg-mobile': ['26px', { lineHeight: '34px', letterSpacing: '0em', fontWeight: '400' }], 'headline-md': ['22px', { lineHeight: '30px', fontWeight: '400' }], 'headline-lg': ['30px', { lineHeight: '38px', letterSpacing: '-0.01em', fontWeight: '400' }], 'body-md': ['16px', { lineHeight: '24px', fontWeight: '400' }], 'headline-xl': ['40px', { lineHeight: '48px', letterSpacing: '-0.02em', fontWeight: '400' }], 'label-md': ['13px', { lineHeight: '18px', letterSpacing: '0.02em', fontWeight: '500' }] } } } };</script><style>@layer base { html, body { width: 100vw; margin: 0; padding: 0; } body { overscroll-behavior: none; } .pb-safe { padding-bottom: env(safe-area-inset-bottom, 0px); } .pt-safe { padding-top: env(safe-area-inset-top, 0px); } main > :first-child { margin-top: 0 !important; } main > :last-child { margin-bottom: 0 !important; } } ::-webkit-scrollbar { display: none; }</style></head><body class="bg-surface font-body-md text-body-md text-on-surface min-h-screen flex flex-col pt-safe pb-safe"><main class="flex-1 flex flex-col relative w-full bg-surface"><div class="flex flex-col w-full px-margin-mobile pb-12">
<!-- Progress Header & Stage Cue -->
<header class="flex flex-col space-y-space-sm pt-2">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-1.5">
<span class="w-2 h-2 rounded-full bg-secondary"></span>
<span class="font-label-sm text-label-sm uppercase tracking-wider text-secondary">Stage 3 of 4 · Prioritization</span>
</div>
<button class="inline-flex items-center space-x-1.5 px-3 py-1.5 rounded-full bg-surface-container text-on-surface-variant text-label-md font-label-md shadow-sm transition-transform active:scale-95" onclick="togglePileSheet()" type="button">
<span class="material-symbols-outlined text-[16px]">layers</span>
<span class="">View Piles (100)</span>
</button>
</div>
<!-- Stepper Graphic Inline Bar -->
<div class="w-full bg-surface-container-high h-1 rounded-full overflow-hidden flex">
<div class="bg-surface-variant w-1/4 h-full"></div>
<div class="bg-surface-variant w-1/4 h-full"></div>
<div class="bg-secondary w-1/4 h-full"></div>
<div class="bg-surface-container-high w-1/4 h-full"></div>
</div>
<div class="pt-2">
<h1 class="font-headline-lg-mobile text-headline-lg-mobile text-on-surface tracking-tight">Order Your Core Values</h1>
<p class="font-body-sm text-body-sm text-on-surface-variant mt-1">
Drag or reorder your finalists. Let <span class="font-semibold text-secondary">#1</span> represent your non-negotiable value.</p>
</div>
</header>
<!-- Editorial Visual Inspiration Vignette -->
<!-- Reorderable List Container -->
<div class="flex flex-col space-y-space-sm mt-5" id="rankList">
<!-- Item 1 (North Star Highlight) -->
<article class="rank-card relative rounded-lg p-4 bg-surface-container-lowest shadow-sm flex flex-col space-y-2 transition-all duration-200" data-id="1" draggable="true">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2.5">
<div class="w-7 h-7 rounded-full bg-secondary-container text-on-secondary-container flex items-center justify-center font-label-md text-label-md font-bold rank-num">1</div>
<div class="north-star-tag inline-flex items-center space-x-1 px-2.5 py-0.5 rounded-full bg-secondary/10 text-secondary text-label-sm font-label-sm"><span class="material-symbols-outlined text-[12px]" style="font-variation-settings: 'FILL' 1;">star</span><span class="">Primary North Star</span></div></div>
<div class="flex items-center space-x-1 text-on-surface-variant">
<button aria-label="Move Down" class="w-8 h-8 rounded-full flex items-center justify-center bg-surface-container-low active:bg-surface-container-high transition-colors" onclick="moveRank(this, 1)" type="button">
<span class="material-symbols-outlined text-[18px]">keyboard_arrow_down</span>
</button>
<div class="drag-handle w-8 h-8 flex items-center justify-center cursor-grab text-outline touch-none">
<span class="material-symbols-outlined text-[20px]">drag_indicator</span>
</div>
</div>
</div>
<div>
<h2 class="font-headline-md text-headline-md text-on-surface">Authenticity</h2>
<p class="font-body-sm text-body-sm text-on-surface-variant mt-0.5">
Living in honest alignment with who you are, without continuous performance or self-betrayal.
</p>
</div>
<div class="flex items-center justify-between pt-1">
<span class="font-label-sm text-label-sm text-outline">Pillar: Self-Alignment</span>
<button class="font-label-sm text-label-sm text-secondary underline" onclick="openRationaleModal('Authenticity', '1')" type="button">Add personal rationale</button>
</div>
</article>
<!-- Item 2 -->
<!-- Item 3 -->
<article class="rank-card rounded-lg p-4 bg-surface-container shadow-sm flex flex-col space-y-2 transition-all duration-200" data-id="3" draggable="true">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2.5">
<div class="w-7 h-7 rounded-full bg-surface-container-high text-on-surface flex items-center justify-center font-label-md text-label-md rank-num">2</div>
</div>
<div class="flex items-center space-x-1 text-on-surface-variant">
<button aria-label="Move Up" class="w-8 h-8 rounded-full flex items-center justify-center bg-surface-container-lowest active:bg-surface-container-high transition-colors" onclick="moveRank(this, -1)" type="button">
<span class="material-symbols-outlined text-[18px]">keyboard_arrow_up</span>
</button>
<button aria-label="Move Down" class="w-8 h-8 rounded-full flex items-center justify-center bg-surface-container-lowest active:bg-surface-container-high transition-colors" onclick="moveRank(this, 1)" type="button">
<span class="material-symbols-outlined text-[18px]">keyboard_arrow_down</span>
</button>
<div class="drag-handle w-8 h-8 flex items-center justify-center cursor-grab text-outline touch-none">
<span class="material-symbols-outlined text-[20px]">drag_indicator</span>
</div>
</div>
</div>
<div>
<h2 class="font-headline-md text-headline-md text-on-surface">Compassion</h2>
<p class="font-body-sm text-body-sm text-on-surface-variant mt-0.5">
Meeting human frailty with gentleness, extending warmth before judgment.
</p>
</div>
<div class="flex items-center justify-between pt-1">
<span class="font-label-sm text-label-sm text-outline">Pillar: Relational</span>
<button class="font-label-sm text-label-sm text-on-surface-variant underline" onclick="openRationaleModal('Compassion', '3')" type="button">Add personal rationale</button>
</div>
</article>
<!-- Item 4 -->
<article class="rank-card rounded-lg p-4 bg-surface-container shadow-sm flex flex-col space-y-2 transition-all duration-200" data-id="4" draggable="true">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2.5">
<div class="w-7 h-7 rounded-full bg-surface-container-high text-on-surface flex items-center justify-center font-label-md text-label-md rank-num">3</div>
</div>
<div class="flex items-center space-x-1 text-on-surface-variant">
<button aria-label="Move Up" class="w-8 h-8 rounded-full flex items-center justify-center bg-surface-container-lowest active:bg-surface-container-high transition-colors" onclick="moveRank(this, -1)" type="button">
<span class="material-symbols-outlined text-[18px]">keyboard_arrow_up</span>
</button>
<button aria-label="Move Down" class="w-8 h-8 rounded-full flex items-center justify-center bg-surface-container-lowest active:bg-surface-container-high transition-colors" onclick="moveRank(this, 1)" type="button">
<span class="material-symbols-outlined text-[18px]">keyboard_arrow_down</span>
</button>
<div class="drag-handle w-8 h-8 flex items-center justify-center cursor-grab text-outline touch-none">
<span class="material-symbols-outlined text-[20px]">drag_indicator</span>
</div>
</div>
</div>
<div>
<h2 class="font-headline-md text-headline-md text-on-surface">Freedom</h2>
<p class="font-body-sm text-body-sm text-on-surface-variant mt-0.5">
Sovereignty over one’s calendar, bodily presence, and mental headspace.
</p>
</div>
<div class="flex items-center justify-between pt-1">
<span class="font-label-sm text-label-sm text-outline">Pillar: Independence</span>
<button class="font-label-sm text-label-sm text-on-surface-variant underline" onclick="openRationaleModal('Freedom', '4')" type="button">Add personal rationale</button>
</div>
</article>
<!-- Item 5 -->
<article class="rank-card rounded-lg p-4 bg-surface-container shadow-sm flex flex-col space-y-2 transition-all duration-200" data-id="5" draggable="true">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2.5">
<div class="w-7 h-7 rounded-full bg-surface-container-high text-on-surface flex items-center justify-center font-label-md text-label-md rank-num">4</div>
</div>
<div class="flex items-center space-x-1 text-on-surface-variant">
<button aria-label="Move Up" class="w-8 h-8 rounded-full flex items-center justify-center bg-surface-container-lowest active:bg-surface-container-high transition-colors" onclick="moveRank(this, -1)" type="button">
<span class="material-symbols-outlined text-[18px]">keyboard_arrow_up</span>
</button>
<button aria-label="Move Down" class="w-8 h-8 rounded-full flex items-center justify-center bg-surface-container-lowest active:bg-surface-container-high transition-colors" onclick="moveRank(this, 1)" type="button">
<span class="material-symbols-outlined text-[18px]">keyboard_arrow_down</span>
</button>
<div class="drag-handle w-8 h-8 flex items-center justify-center cursor-grab text-outline touch-none">
<span class="material-symbols-outlined text-[20px]">drag_indicator</span>
</div>
</div>
</div>
<div>
<h2 class="font-headline-md text-headline-md text-on-surface">Curiosity</h2>
<p class="font-body-sm text-body-sm text-on-surface-variant mt-0.5">
An open question kept alive without the desperate hurry to conclude.
</p>
</div>
<div class="flex items-center justify-between pt-1">
<span class="font-label-sm text-label-sm text-outline">Pillar: Growth</span>
<button class="font-label-sm text-label-sm text-on-surface-variant underline" onclick="openRationaleModal('Curiosity', '5')" type="button">Add personal rationale</button>
</div>
</article>
<!-- Item 6 -->
<article class="rank-card rounded-lg p-4 bg-surface-container shadow-sm flex flex-col space-y-2 transition-all duration-200" data-id="6" draggable="true">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2.5">
<div class="w-7 h-7 rounded-full bg-surface-container-high text-on-surface flex items-center justify-center font-label-md text-label-md rank-num">5</div>
</div>
<div class="flex items-center space-x-1 text-on-surface-variant">
<button aria-label="Move Up" class="w-8 h-8 rounded-full flex items-center justify-center bg-surface-container-lowest active:bg-surface-container-high transition-colors" onclick="moveRank(this, -1)" type="button">
<span class="material-symbols-outlined text-[18px]">keyboard_arrow_up</span>
</button>
<button aria-label="Move Down" class="w-8 h-8 rounded-full flex items-center justify-center bg-surface-container-lowest active:bg-surface-container-high transition-colors" onclick="moveRank(this, 1)" type="button">
<span class="material-symbols-outlined text-[18px]">keyboard_arrow_down</span>
</button>
<div class="drag-handle w-8 h-8 flex items-center justify-center cursor-grab text-outline touch-none">
<span class="material-symbols-outlined text-[20px]">drag_indicator</span>
</div>
</div>
</div>
<div>
<h2 class="font-headline-md text-headline-md text-on-surface">Integrity</h2>
<p class="font-body-sm text-body-sm text-on-surface-variant mt-0.5">
Wholeness in private conduct; matching private conviction with public stance.
</p>
</div>
<div class="flex items-center justify-between pt-1">
<span class="font-label-sm text-label-sm text-outline">Pillar: Alignment</span>
<button class="font-label-sm text-label-sm text-on-surface-variant underline" onclick="openRationaleModal('Integrity', '6')" type="button">Add personal rationale</button>
</div>
</article><article class="rank-card rounded-lg p-4 bg-surface-container shadow-sm flex flex-col space-y-2 transition-all duration-200" data-id="2" draggable="true">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2.5">
<div class="w-7 h-7 rounded-full bg-surface-container-high text-on-surface flex items-center justify-center font-label-md text-label-md rank-num">6</div>
<span class="font-label-sm text-label-sm text-outline font-medium">Secondary Anchor</span>
</div>
<div class="flex items-center space-x-1 text-on-surface-variant">
<button aria-label="Move Up" class="w-8 h-8 rounded-full flex items-center justify-center bg-surface-container-lowest active:bg-surface-container-high transition-colors" onclick="moveRank(this, -1)" type="button">
<span class="material-symbols-outlined text-[18px]">keyboard_arrow_up</span>
</button>
<button aria-label="Move Down" class="w-8 h-8 rounded-full flex items-center justify-center bg-surface-container-lowest active:bg-surface-container-high transition-colors" onclick="moveRank(this, 1)" type="button">
<span class="material-symbols-outlined text-[18px]">keyboard_arrow_down</span>
</button>
<div class="drag-handle w-8 h-8 flex items-center justify-center cursor-grab text-outline touch-none">
<span class="material-symbols-outlined text-[20px]">drag_indicator</span>
</div>
</div>
</div>
<div>
<h2 class="font-headline-md text-headline-md text-on-surface">Courage</h2>
<p class="font-body-sm text-body-sm text-on-surface-variant mt-0.5">
Entering the uncertain room, speaking the tender truth despite physical trembling.
</p>
</div>
<div class="flex items-center justify-between pt-1">
<span class="font-label-sm text-label-sm text-outline">Pillar: Action</span>
<button class="font-label-sm text-label-sm text-on-surface-variant underline" onclick="openRationaleModal('Courage', '2')" type="button">Add personal rationale</button>
</div>
</article>
<!-- Item 7 -->
<article class="rank-card rounded-lg p-4 bg-surface-container shadow-sm flex flex-col space-y-2 transition-all duration-200" data-id="7" draggable="true">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2.5">
<div class="w-7 h-7 rounded-full bg-surface-container-high text-on-surface flex items-center justify-center font-label-md text-label-md rank-num">7</div>
</div>
<div class="flex items-center space-x-1 text-on-surface-variant">
<button aria-label="Move Up" class="w-8 h-8 rounded-full flex items-center justify-center bg-surface-container-lowest active:bg-surface-container-high transition-colors" onclick="moveRank(this, -1)" type="button">
<span class="material-symbols-outlined text-[18px]">keyboard_arrow_up</span>
</button>
<button aria-label="Move Down" class="w-8 h-8 rounded-full flex items-center justify-center bg-surface-container-lowest active:bg-surface-container-high transition-colors" onclick="moveRank(this, 1)" type="button">
<span class="material-symbols-outlined text-[18px]">keyboard_arrow_down</span>
</button>
<div class="drag-handle w-8 h-8 flex items-center justify-center cursor-grab text-outline touch-none">
<span class="material-symbols-outlined text-[20px]">drag_indicator</span>
</div>
</div>
</div>
<div>
<h2 class="font-headline-md text-headline-md text-on-surface">Connection</h2>
<p class="font-body-sm text-body-sm text-on-surface-variant mt-0.5">
Deep, unhurried presence that allows another soul to feel seen and held.
</p>
</div>
<div class="flex items-center justify-between pt-1">
<span class="font-label-sm text-label-sm text-outline">Pillar: Relational</span>
<button class="font-label-sm text-label-sm text-on-surface-variant underline" onclick="openRationaleModal('Connection', '7')" type="button">Add personal rationale</button>
</div>
</article>
<!-- Item 8 -->
<article class="rank-card rounded-lg p-4 bg-surface-container shadow-sm flex flex-col space-y-2 transition-all duration-200" data-id="8" draggable="true">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2.5">
<div class="w-7 h-7 rounded-full bg-surface-container-high text-on-surface flex items-center justify-center font-label-md text-label-md rank-num">8</div>
</div>
<div class="flex items-center space-x-1 text-on-surface-variant">
<button aria-label="Move Up" class="w-8 h-8 rounded-full flex items-center justify-center bg-surface-container-lowest active:bg-surface-container-high transition-colors" onclick="moveRank(this, -1)" type="button">
<span class="material-symbols-outlined text-[18px]">keyboard_arrow_up</span>
</button>
<button aria-label="Move Down" class="w-8 h-8 rounded-full flex items-center justify-center bg-surface-container-lowest active:bg-surface-container-high transition-colors" onclick="moveRank(this, 1)" type="button">
<span class="material-symbols-outlined text-[18px]">keyboard_arrow_down</span>
</button>
<div class="drag-handle w-8 h-8 flex items-center justify-center cursor-grab text-outline touch-none">
<span class="material-symbols-outlined text-[20px]">drag_indicator</span>
</div>
</div>
</div>
<div>
<h2 class="font-headline-md text-headline-md text-on-surface">Creativity</h2>
<p class="font-body-sm text-body-sm text-on-surface-variant mt-0.5">
The quiet joy of shaping raw experience into form, word, or tangible craft.
</p>
</div>
<div class="flex items-center justify-between pt-1">
<span class="font-label-sm text-label-sm text-outline">Pillar: Self-Expression</span>
<button class="font-label-sm text-label-sm text-on-surface-variant underline" onclick="openRationaleModal('Creativity', '8')" type="button">Add personal rationale</button>
</div>
</article>
</div>
<!-- Add from sorted piles action -->
<button class="mt-4 w-full py-3.5 px-4 rounded-lg bg-surface-container-low text-on-surface-variant flex items-center justify-center space-x-2 font-label-md text-label-md transition-colors active:bg-surface-container-high" onclick="togglePileSheet()" type="button">
<span class="material-symbols-outlined text-[18px]">add_circle</span>
<span class="">Pull another value from 'Important' pile</span>
</button>
<!-- Reflection Callout Box -->
<div class="mt-6 rounded-lg bg-surface-container p-4 flex space-x-3.5 shadow-sm">
<div class="w-8 h-8 rounded-full bg-secondary/15 text-secondary flex items-center justify-center flex-shrink-0 mt-0.5">
<span class="material-symbols-outlined text-[18px]">psychology_alt</span>
</div>
<div class="flex-1">
<h3 class="font-headline-md text-[17px] text-on-surface">Gentle Inquiry</h3>
<p class="font-body-sm text-body-sm text-on-surface-variant mt-1">
Notice where inner resistance surfaces. Are you ranking what you <em>truly</em> value, or what you feel you <em>should</em> value for others?
</p>
</div>
</div>
<!-- Secondary Reflective Imagery Accent -->
<div class="mt-5 rounded-lg overflow-hidden bg-surface-container-low p-4 flex items-center space-x-4 shadow-sm">
<div class="flex-1">
<p class="font-label-sm text-label-sm text-tertiary uppercase tracking-wider font-semibold">Stage Insight</p>
<p class="font-headline-md text-[16px] text-on-surface mt-1 leading-snug">
"When values conflict under pressure, #1 determines your immediate direction."
</p>
</div>
<div class="w-16 h-16 rounded-lg overflow-hidden flex-shrink-0 bg-surface-container">
<img class="w-full h-full object-cover" data-alt="Warm ceramic vessel resting beside dry meadow grass on handcrafted linen background with contemplative gentle sunlight in muted terracotta tones" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAIibaS4hwL-2jGWWTqf_pQJzU-KHkI-0vmbMMYQ11vuGGKomTPwa3vX1M4ozxfJwuTDpCGJpxyvYwrE-yLV1HOzHdQEKL_iWpaeNnPK73O6DOoD-28XHwmzQpJ0dmwu0qfzBTCRngNOqNLG6jADnkDi4PUKSw83cpPmU3HKyRC-ZEc_QzSzyUUR0SrR9y60_IllXLFkw3fkI5c_R_XV9FqTzhjiV7b5CdPcBoopqcT6bJ5xcdhQ168">
</div>
</div>
<!-- Bottom Floating CTA Container -->
<div class="sticky bottom-3 mt-8 bg-surface/90 backdrop-blur-md p-2 rounded-full shadow-xl flex items-center justify-between space-x-2">
<button aria-label="How sorting works" class="w-12 h-12 rounded-full bg-surface-container flex items-center justify-center text-on-surface-variant active:scale-95 transition-transform" onclick="showHelpSnackbar()" type="button">
<span class="material-symbols-outlined text-[20px]">help_outline</span>
</button>
<button class="flex-1 h-12 px-6 rounded-full bg-primary text-on-primary font-label-lg text-label-lg flex items-center justify-center space-x-2 shadow-md active:opacity-90 transition-opacity" onclick="continueToReflection()" type="button">
<span class="">Lock &amp; Continue</span>
<span class="material-symbols-outlined text-[18px]">arrow_forward</span>
</button>
</div>
<!-- Modals and Drawers (Hidden by default) -->
<!-- 1. Reflection / Rationale Modal Sheet -->
<div class="fixed inset-0 z-50 bg-inverse-surface/40 backdrop-blur-sm hidden flex items-end justify-center" id="rationaleModal">
<div class="w-full max-w-lg bg-surface-container-lowest rounded-t-xl p-margin-mobile shadow-2xl flex flex-col space-y-4 max-h-[85vh] overflow-y-auto">
<div class="w-10 h-1 rounded-full bg-outline-variant mx-auto self-center mb-1"></div>
<div class="flex justify-between items-start">
<div>
<span class="font-label-sm text-label-sm text-secondary uppercase font-semibold" id="modalRankBadge">Value #1</span>
<h2 class="font-headline-lg-mobile text-headline-lg-mobile text-on-surface" id="modalValueName">Authenticity</h2>
</div>
<button class="w-8 h-8 rounded-full bg-surface-container flex items-center justify-center text-on-surface-variant" onclick="closeRationaleModal()" type="button">
<span class="material-symbols-outlined text-[18px]">close</span>
</button>
</div>
<p class="font-body-sm text-body-sm text-on-surface-variant">
Why does this value occupy this exact rank? When was the last time you honored or compromised it?
</p>
<textarea class="w-full p-3.5 rounded-lg bg-surface-container-low text-on-surface font-body-sm text-body-sm outline-none resize-none placeholder-outline" placeholder="Write your personal reflections or lived example here..." rows="4"></textarea>
<div class="flex items-center justify-end space-x-2 pt-2">
<button class="px-4 py-2.5 rounded-full bg-surface-container text-on-surface font-label-md text-label-md" onclick="closeRationaleModal()" type="button">Cancel</button>
<button class="px-5 py-2.5 rounded-full bg-primary text-on-primary font-label-md text-label-md" onclick="saveRationale()" type="button">Save Note</button>
</div>
</div>
</div>
<!-- 2. View All Piles Drawer -->
<div class="fixed inset-0 z-50 bg-inverse-surface/40 backdrop-blur-sm flex items-end justify-center hidden" id="pilesSheet">
<div class="w-full max-w-lg bg-surface-container rounded-t-xl p-margin-mobile shadow-2xl flex flex-col space-y-4 max-h-[88vh] overflow-y-auto">
<div class="w-10 h-1 rounded-full bg-outline-variant mx-auto self-center mb-1"></div>
<div class="flex justify-between items-center">
<div>
<h2 class="font-headline-md text-headline-md text-on-surface">All 5 Sorting Piles</h2>
<p class="font-body-sm text-body-sm text-on-surface-variant">100 Original Values Distributed</p>
</div>
<button class="w-8 h-8 rounded-full bg-surface-container-high flex items-center justify-center text-on-surface-variant" onclick="togglePileSheet()" type="button">
<span class="material-symbols-outlined text-[18px]">close</span>
</button>
</div>
<!-- Pile mini summaries -->
<div class="space-y-2.5">
<div class="p-3 bg-surface-container-lowest rounded-lg flex items-center justify-between shadow-sm">
<div class="flex items-center space-x-2.5">
<span class="w-3 h-3 rounded-full bg-secondary"></span>
<div>
<p class="font-headline-md text-[17px] text-on-surface">Always True for Me (Top Tier)</p>
<p class="font-label-sm text-label-sm text-outline">8 cards selected (Active in rank order)</p>
</div>
</div>
<span class="font-label-md text-label-md text-secondary font-semibold">8</span>
</div>
<div class="p-3 bg-surface-container-lowest rounded-lg flex items-center justify-between shadow-sm">
<div class="flex items-center space-x-2.5">
<span class="w-3 h-3 rounded-full bg-tertiary"></span>
<div>
<p class="font-headline-md text-[17px] text-on-surface">Very Important</p>
<p class="font-label-sm text-label-sm text-outline">Wisdom, Resilience, Humility, Patience...</p>
</div>
</div>
<span class="font-label-md text-label-md text-on-surface-variant font-semibold">18</span>
</div>
<div class="p-3 bg-surface-container-lowest rounded-lg flex items-center justify-between shadow-sm">
<div class="flex items-center space-x-2.5">
<span class="w-3 h-3 rounded-full bg-outline-variant"></span>
<div>
<p class="font-headline-md text-[17px] text-on-surface">Important to Me</p>
<p class="font-label-sm text-label-sm text-outline">Craft, Adventure, Security, Precision...</p>
</div>
</div>
<span class="font-label-md text-label-md text-on-surface-variant font-semibold">32</span>
</div>
<div class="p-3 bg-surface-container-lowest rounded-lg flex items-center justify-between shadow-sm">
<div class="flex items-center space-x-2.5">
<span class="w-3 h-3 rounded-full bg-surface-variant"></span>
<div>
<p class="font-headline-md text-[17px] text-on-surface">Somewhat Important</p>
<p class="font-label-sm text-label-sm text-outline">Tradition, Recognition, Predictability...</p>
</div>
</div>
<span class="font-label-md text-label-md text-on-surface-variant font-semibold">24</span>
</div>
<div class="p-3 bg-surface-container-lowest rounded-lg flex items-center justify-between shadow-sm">
<div class="flex items-center space-x-2.5">
<span class="w-3 h-3 rounded-full bg-surface-dim"></span>
<div>
<p class="font-headline-md text-[17px] text-on-surface">Not Central to My Life</p>
<p class="font-label-sm text-label-sm text-outline">Authority, Fame, Conformity, Competition...</p>
</div>
</div>
<span class="font-label-md text-label-md text-on-surface-variant font-semibold">18</span>
</div>
</div>
<button class="w-full py-3 rounded-full bg-primary text-on-primary font-label-lg text-label-lg mt-2" onclick="togglePileSheet()" type="button">
Return to Ordering
</button>
</div>
</div>
<!-- Toast / Feedback message container -->
<div class="fixed top-5 left-1/2 -translate-x-1/2 z-50 bg-inverse-surface text-inverse-on-surface font-label-md text-label-md px-4 py-2 rounded-full shadow-lg transition-opacity duration-300 flex items-center space-x-2 opacity-0 pointer-events-none" id="toastMessage">
<span class="material-symbols-outlined text-[16px] text-secondary-fixed">check_circle</span>
<span id="toastText" class="">New hierarchy saved</span>
</div>
</div>
<script>
// Dynamic Reordering Functions
function moveRank(button, direction) {
const card = button.closest('.rank-card');
const parent = card.parentElement;
if (direction === -1) {
const prev = card.previousElementSibling;
if (prev) {
parent.insertBefore(card, prev);
triggerHapticVisual(card);
}
} else if (direction === 1) {
const next = card.nextElementSibling;
if (next) {
parent.insertBefore(next, card);
triggerHapticVisual(card);
}
}
recalculateRanks();
}
function recalculateRanks() {
const cards = document.querySelectorAll('#rankList .rank-card');
cards.forEach((card, idx) => {
const rankNum = idx + 1;
const badge = card.querySelector('.rank-num');
if (badge) {
badge.textContent = rankNum;
}
// Rank 1 gets the highlighted North Star treatment
if (rankNum === 1) {
card.classList.remove('bg-surface-container');
card.classList.add('bg-surface-container-lowest');
badge.className = 'w-7 h-7 rounded-full bg-secondary-container text-on-secondary-container flex items-center justify-center font-label-md text-label-md font-bold rank-num';
// Ensure North Star Tag
if (!card.querySelector('.north-star-tag')) {
const headerLeft = card.querySelector('.flex.items-center.space-x-2\\.5');
const tag = document.createElement('div');
tag.className = 'north-star-tag inline-flex items-center space-x-1 px-2.5 py-0.5 rounded-full bg-secondary/10 text-secondary text-label-sm font-label-sm';
tag.innerHTML = '<span class="material-symbols-outlined text-[12px]" style="font-variation-settings: \'FILL\' 1;">star</span><span>Primary North Star</span>';
headerLeft.appendChild(tag);
}
} else {
card.classList.add('bg-surface-container');
card.classList.remove('bg-surface-container-lowest');
badge.className = 'w-7 h-7 rounded-full bg-surface-container-high text-on-surface flex items-center justify-center font-label-md text-label-md rank-num';
const existingTag = card.querySelector('.north-star-tag');
if (existingTag) {
existingTag.remove();
}
}
});
}
function triggerHapticVisual(element) {
element.classList.add('scale-[1.01]', 'shadow-md');
setTimeout(() => {
element.classList.remove('scale-[1.01]', 'shadow-md');
}, 200);
}
// Modals & Bottom Sheets
function togglePileSheet() {
const sheet = document.getElementById('pilesSheet');
sheet.classList.toggle('hidden');
}
function openRationaleModal(valueName, rank) {
document.getElementById('modalValueName').textContent = valueName;
document.getElementById('modalRankBadge').textContent = 'Value #' + rank;
document.getElementById('rationaleModal').classList.remove('hidden');
}
function closeRationaleModal() {
document.getElementById('rationaleModal').classList.add('hidden');
}
function saveRationale() {
closeRationaleModal();
showToast('Reflection note preserved');
}
function continueToReflection() {
showToast('Values hierarchy locked. Proceeding...');
}
function showHelpSnackbar() {
showToast('Drag handles or tap arrows to sort');
}
function showToast(text) {
const toast = document.getElementById('toastMessage');
const toastText = document.getElementById('toastText');
toastText.textContent = text;
toast.classList.remove('opacity-0', 'pointer-events-none');
setTimeout(() => {
toast.classList.add('opacity-0', 'pointer-events-none');
}, 2200);
}
// Touch Drag Emulation / Simple drag sorting setup
let draggedCard = null;
const rankList = document.getElementById('rankList');
rankList.addEventListener('dragstart', (e) => {
draggedCard = e.target.closest('.rank-card');
if (draggedCard) {
e.dataTransfer.effectAllowed = 'move';
draggedCard.classList.add('opacity-50');
}
});
rankList.addEventListener('dragover', (e) => {
e.preventDefault();
const targetCard = e.target.closest('.rank-card');
if (targetCard && targetCard !== draggedCard) {
const rect = targetCard.getBoundingClientRect();
const next = (e.clientY - rect.top) / (rect.bottom - rect.top) > 0.5;
rankList.insertBefore(draggedCard, next ? targetCard.nextSibling : targetCard);
recalculateRanks();
}
});
rankList.addEventListener('dragend', () => {
if (draggedCard) {
draggedCard.classList.remove('opacity-50');
draggedCard = null;
recalculateRanks();
showToast('New hierarchy saved');
}
});
// Enable drag attribute on cards
document.querySelectorAll('.rank-card').forEach(card => {
card.setAttribute('draggable', 'true');
});
</script></main>
</body></html>
+16
View File
@@ -0,0 +1,16 @@
{
"name": "projects/9343387512370733994/screens/07a9fc8b113c479db7ece5398a017a9e",
"title": "Values Sort \u2014 Rank Order Top Values",
"screenshot": {
"name": "projects/9343387512370733994/files/1597732408047210864",
"downloadUrl": "https://lh3.googleusercontent.com/aida/AEtjO1WpFA576TA5wf1HnZIoIBm8m5-S0M9LlUvAz8ujVYmbkVydsAdUAmtOpQKsY9nixUIwZEd5bqrDTe6ITlJfFFRhDRxkehFVmgcdAZksW9L89pME6e2gTYuO-qqFQKwD-XAXg_PDunjOg8XZeYs5p1dIVenclfxkXwSQ4Y0IIDTHtIMONRE4S07sHY7skqnnL-f2vmbEBg3ah1WyjcLXIUFIL4AINmYK-vTMCxwwsQ_ev3qhq909m7CdoQ"
},
"htmlCode": {
"name": "projects/9343387512370733994/files/15103438974169067770",
"downloadUrl": "https://contribution.usercontent.google.com/download?c=CgthaWRhX2NvZGVmeBJ7Eh1hcHBfY29tcGFuaW9uX2dlbmVyYXRlZF9maWxlcxpaCiVodG1sXzAwMDY1YmIyY2VlNWY5MzgwMjNiZWU1MjMxMjVjMzFhEgsSBxD_iY311x0YAZIBIwoKcHJvamVjdF9pZBIVQhM5MzQzMzg3NTEyMzcwNzMzOTk0&filename=&opi=89354086",
"mimeType": "text/html"
},
"width": "780",
"height": "4408",
"deviceType": "MOBILE"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

+299
View File
@@ -0,0 +1,299 @@
<!DOCTYPE html><html lang="en" style=""><head><meta charset="utf-8"><meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" name="viewport"><link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200" rel="stylesheet"><link href="https://fonts.googleapis.com" rel="preconnect"><link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"><link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,200..800;1,6..72,200..800&amp;family=Plus+Jakarta+Sans:ital,wght@0,300..800;1,300..800&amp;display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,[email protected],0..1&amp;display=swap" rel="stylesheet"><script src="https://cdn.tailwindcss.com"></script><script id="tailwind-config">tailwind.config = { darkMode: 'class', theme: { extend: { colors: { 'on-tertiary-container': '#e1f9db', 'surface-container': '#f1ede6', 'on-primary-container': '#ffefe1', 'primary-fixed-dim': '#d9c3ac', 'tertiary-fixed-dim': '#b5cdb0', 'surface': '#fdf9f2', 'on-secondary-fixed-variant': '#713714', 'surface-container-lowest': '#ffffff', 'error-container': '#ffdad6', 'primary': '#635341', 'outline-variant': '#d0c4ba', 'tertiary': '#465b44', 'surface-dim': '#dddad3', 'primary-container': '#7d6b58', 'outline': '#7f756d', 'on-error-container': '#93000a', 'surface-bright': '#fdf9f2', 'background': '#fdf9f2', 'secondary-container': '#feaa7f', 'tertiary-container': '#5e745c', 'surface-tint': '#6c5b49', 'on-secondary-container': '#783d1a', 'on-primary-fixed-variant': '#544433', 'on-primary-fixed': '#25190b', 'tertiary-fixed': '#d1e9cc', 'on-error': '#ffffff', 'on-secondary': '#ffffff', 'on-tertiary-fixed-variant': '#374c36', 'on-surface': '#1c1c18', 'surface-container-high': '#ece8e1', 'secondary': '#8e4d2a', 'secondary-fixed-dim': '#ffb691', 'secondary-fixed': '#ffdbcb', 'inverse-surface': '#31302c', 'inverse-on-surface': '#f4f0e9', 'on-primary': '#ffffff', 'primary-fixed': '#f6dfc7', 'on-surface-variant': '#4d453e', 'surface-container-highest': '#e6e2db', 'on-tertiary-fixed': '#0d200d', 'on-secondary-fixed': '#341100', 'on-background': '#1c1c18', 'surface-variant': '#e6e2db', 'on-tertiary': '#ffffff', 'error': '#ba1a1a', 'inverse-primary': '#d9c3ac', 'surface-container-low': '#f7f3ec' }, borderRadius: { DEFAULT: '1rem', lg: '2rem', xl: '3rem', full: '9999px' }, spacing: { 'space-xs': '0.25rem', 'margin': '1.5rem', 'gutter': '1rem', 'space-xl': '2.5rem', 'margin-mobile': '1.25rem', 'gutter-mobile': '0.75rem', 'space-sm': '0.5rem', 'space-lg': '1.5rem', 'space-md': '1rem' }, fontFamily: { 'label-lg': ['Plus Jakarta Sans'], 'label-sm': ['Plus Jakarta Sans'], 'headline-xl-mobile': ['Newsreader'], 'body-lg': ['Plus Jakarta Sans'], 'body-sm': ['Plus Jakarta Sans'], 'headline-lg-mobile': ['Newsreader'], 'headline-md': ['Newsreader'], 'headline-lg': ['Newsreader'], 'body-md': ['Plus Jakarta Sans'], 'headline-xl': ['Newsreader'], 'label-md': ['Plus Jakarta Sans'] }, fontSize: { 'label-lg': ['15px', { lineHeight: '20px', letterSpacing: '0.01em', fontWeight: '600' }], 'label-sm': ['11px', { lineHeight: '16px', letterSpacing: '0.04em', fontWeight: '600' }], 'headline-xl-mobile': ['32px', { lineHeight: '40px', letterSpacing: '-0.01em', fontWeight: '400' }], 'body-lg': ['18px', { lineHeight: '28px', fontWeight: '400' }], 'body-sm': ['14px', { lineHeight: '20px', fontWeight: '400' }], 'headline-lg-mobile': ['26px', { lineHeight: '34px', letterSpacing: '0em', fontWeight: '400' }], 'headline-md': ['22px', { lineHeight: '30px', fontWeight: '400' }], 'headline-lg': ['30px', { lineHeight: '38px', letterSpacing: '-0.01em', fontWeight: '400' }], 'body-md': ['16px', { lineHeight: '24px', fontWeight: '400' }], 'headline-xl': ['40px', { lineHeight: '48px', letterSpacing: '-0.02em', fontWeight: '400' }], 'label-md': ['13px', { lineHeight: '18px', letterSpacing: '0.02em', fontWeight: '500' }] } } } };</script><style>@layer base { html, body { width: 100vw; margin: 0; padding: 0; } body { overscroll-behavior: none; } .pb-safe { padding-bottom: env(safe-area-inset-bottom, 0px); } .pt-safe { padding-top: env(safe-area-inset-top, 0px); } main > :first-child { margin-top: 0 !important; } main > :last-child { margin-bottom: 0 !important; } } ::-webkit-scrollbar { display: none; }</style></head><body class="bg-surface font-body-md text-body-md text-on-surface min-h-screen flex flex-col pt-safe pb-safe"><main class="flex-1 flex flex-col relative w-full bg-surface"><div class="flex flex-col w-full px-margin-mobile sm:px-margin space-y-space-lg pb-12">
<!-- Eyebrow & Stage Header -->
<header class="flex flex-col space-y-space-xs text-left pt-2">
<div class="flex items-center justify-between">
<span class="font-label-sm text-label-sm text-secondary tracking-widest uppercase bg-surface-container-high px-3 py-1 rounded-full">
Step 5 of 5 · Deep Reflection
</span>
<span class="font-label-sm text-label-sm text-on-surface-variant flex items-center gap-1">
<span class="material-symbols-outlined text-[16px] text-tertiary">spa</span>
Unrushed Practice
</span>
</div>
<h1 class="font-headline-xl-mobile sm:font-headline-xl text-headline-xl-mobile sm:text-headline-xl text-on-surface mt-1">Your Core Values</h1>
</header>
<!-- Value Selector Carousel -->
<section class="flex flex-col space-y-2">
<div class="flex items-center justify-between px-1">
<span class="font-label-sm text-label-sm text-on-surface-variant uppercase tracking-wider">Top 5 Core Anchors</span>
<span class="font-label-sm text-label-sm text-tertiary font-semibold" id="carousel-status">3 of 4 answered</span>
</div>
<div class="flex gap-2 overflow-x-auto pb-2 scrollbar-none snap-x" id="value-tabs">
<button class="snap-start flex-shrink-0 flex items-center gap-2 px-4 py-2 rounded-full bg-primary text-on-primary font-label-md text-label-md shadow-sm transition-all">
<span class="w-2 h-2 rounded-full bg-secondary-fixed"></span>
#1 Authenticity
<span class="font-label-sm text-label-sm px-1.5 py-0.5 rounded-full bg-primary-container text-on-primary-container">3/4</span>
</button>
<button class="snap-start flex-shrink-0 flex items-center gap-2 px-4 py-2 rounded-full bg-surface-container-high text-on-surface-variant hover:bg-surface-container-highest font-label-md text-label-md transition-colors">
<span class="w-2 h-2 rounded-full bg-tertiary"></span>
#2 Courage
<span class="font-label-sm text-label-sm px-1.5 py-0.5 rounded-full bg-surface-container text-on-surface-variant">4/4</span>
</button>
<button class="snap-start flex-shrink-0 flex items-center gap-2 px-4 py-2 rounded-full bg-surface-container-high text-on-surface-variant hover:bg-surface-container-highest font-label-md text-label-md transition-colors">
<span class="w-2 h-2 rounded-full bg-outline-variant"></span>
#3 Compassion
<span class="font-label-sm text-label-sm px-1.5 py-0.5 rounded-full bg-surface-container text-on-surface-variant">2/4</span>
</button>
<button class="snap-start flex-shrink-0 flex items-center gap-2 px-4 py-2 rounded-full bg-surface-container-high text-on-surface-variant hover:bg-surface-container-highest font-label-md text-label-md transition-colors">
<span class="w-2 h-2 rounded-full bg-outline-variant"></span>
#4 Freedom
<span class="font-label-sm text-label-sm px-1.5 py-0.5 rounded-full bg-surface-container text-on-surface-variant">2/4</span>
</button>
<button class="snap-start flex-shrink-0 flex items-center gap-2 px-4 py-2 rounded-full bg-surface-container-high text-on-surface-variant hover:bg-surface-container-highest font-label-md text-label-md transition-colors">
<span class="w-2 h-2 rounded-full bg-outline-variant"></span>
#5 Curiosity
<span class="font-label-sm text-label-sm px-1.5 py-0.5 rounded-full bg-surface-container text-on-surface-variant">3/4</span>
</button>
</div>
</section>
<!-- Core Value Spotlight Card -->
<section class="bg-surface-container-low rounded-lg p-space-lg shadow-sm flex flex-col gap-4 relative overflow-hidden">
<div class="flex items-start justify-between">
<div class="flex flex-col">
<h2 class="font-headline-lg-mobile sm:font-headline-lg text-headline-lg-mobile sm:text-headline-lg text-on-surface mt-0.5">
Authenticity
</h2>
</div>
</div>
<p class="font-body-md text-body-md text-on-surface-variant italic">
"Living in genuine alignment with inner truth and speaking without pretense or performance."
</p>
</section>
<!-- Reflection Questions List (Accordions) -->
<section class="flex flex-col space-y-3" id="questions-container">
<!-- Question 1: Answered & Collapsible -->
<article class="bg-surface-container-low rounded-lg shadow-sm transition-all overflow-hidden" id="card-q1">
<button class="w-full text-left p-space-lg flex items-center justify-between gap-3 focus:outline-none" onclick="toggleCard('q1')">
<div class="flex items-start gap-3 min-w-0">
<span class="w-6 h-6 rounded-full bg-tertiary-fixed flex items-center justify-center text-on-tertiary-fixed shrink-0 mt-0.5">
<span class="material-symbols-outlined text-[15px] font-bold">check</span>
</span>
<div class="flex flex-col min-w-0">
<div class="flex items-center gap-2">
</div>
<h3 class="font-headline-md text-headline-md text-on-surface truncate sm:whitespace-normal">
What does this value mean to you?
</h3>
</div>
</div>
<span class="material-symbols-outlined text-outline transition-transform duration-200 rotate-180" id="icon-q1">expand_more</span>
</button>
<div class="px-space-lg pb-space-lg pt-0 flex-col gap-3 flex" id="content-q1">
<div class="bg-surface-container p-space-md rounded-DEFAULT">
<p class="font-body-md text-body-md text-on-surface leading-relaxed" id="text-q1">
To me, authenticity is the quiet permission to stop curating who I am for other people's approval. It means letting my outward actions and words match what is truly happening inside, even when that makes me feel vulnerable or imperfect.
</p>
</div>
<div class="flex justify-end gap-2">
<button class="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-full bg-surface-container-high text-on-surface font-label-sm text-label-sm hover:bg-surface-container-highest transition-colors" onclick="editAnswer('q1')">
<span class="material-symbols-outlined text-[14px]">edit</span>
Edit reflection
</button>
</div>
</div>
</article>
<!-- Question 2: Answered & Collapsed default -->
<article class="bg-surface-container-low rounded-lg shadow-sm transition-all overflow-hidden" id="card-q2">
<button class="w-full text-left p-space-lg flex items-center justify-between gap-3 focus:outline-none" onclick="toggleCard('q2')">
<div class="flex items-start gap-3 min-w-0">
<span class="w-6 h-6 rounded-full bg-tertiary-fixed flex items-center justify-center text-on-tertiary-fixed shrink-0 mt-0.5">
<span class="material-symbols-outlined text-[15px] font-bold">check</span>
</span>
<div class="flex flex-col min-w-0">
<div class="flex items-center gap-2">
</div>
<h3 class="font-headline-md text-headline-md text-on-surface truncate sm:whitespace-normal">
Why did you choose this as important?
</h3>
</div>
</div>
<span class="material-symbols-outlined text-outline transition-transform duration-200" id="icon-q2">expand_more</span>
</button>
<div class="px-space-lg pb-space-lg pt-0 flex-col gap-3 hidden" id="content-q2">
<div class="bg-surface-container p-space-md rounded-DEFAULT">
<p class="font-body-md text-body-md text-on-surface leading-relaxed" id="text-q2">
Whenever I have betrayed my own intuition to keep the peace or satisfy someone else, an inner resentment always took root. Choosing authenticity has been my most reliable antidote to regret and burnout.
</p>
</div>
<div class="flex justify-end gap-2">
<button class="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-full bg-surface-container-high text-on-surface font-label-sm text-label-sm hover:bg-surface-container-highest transition-colors" onclick="editAnswer('q2')">
<span class="material-symbols-outlined text-[14px]">edit</span>
Edit reflection
</button>
</div>
</div>
</article>
<!-- Question 3: Active / Expanded Input Field -->
<article class="bg-surface-container-low rounded-lg shadow-md transition-all overflow-hidden" id="card-q3">
<button class="w-full text-left p-space-lg flex items-center justify-between gap-3 focus:outline-none" onclick="toggleCard('q3')">
<div class="flex items-start gap-3 min-w-0">
<span class="w-6 h-6 rounded-full bg-secondary-fixed flex items-center justify-center text-on-secondary-fixed-variant shrink-0 mt-0.5">
<span class="material-symbols-outlined text-[15px]">edit_note</span>
</span>
<div class="flex flex-col min-w-0">
<div class="flex items-center gap-2">
<span class="font-label-sm text-label-sm text-secondary font-semibold uppercase tracking-wider">In Progress</span>
<span class="font-label-sm text-label-sm text-outline">· Prompt 3</span>
</div>
<h3 class="font-headline-md text-headline-md text-on-surface">
How have you shown this value in your daily life?
</h3>
</div>
</div>
<span class="material-symbols-outlined text-outline transition-transform duration-200 rotate-180" id="icon-q3">expand_more</span>
</button>
<div class="px-space-lg pb-space-lg pt-0 flex flex-col gap-3" id="content-q3">
<div class="flex flex-col gap-2">
<textarea class="w-full p-space-md bg-surface rounded-DEFAULT text-on-surface font-body-md text-body-md focus:outline-none shadow-inner resize-none transition-colors" id="textarea-q3" oninput="handleInput(this)" placeholder="Reflect gently on recent moments where this was present..." rows="4">Recently, during a project retrospective, I spoke honestly about feeling overwhelmed instead of nodding along like everything was fine. It opened the door for my colleagues to share their true workload too.</textarea>
</div>
<div class="flex items-center justify-end gap-2 pt-1">
<button class="px-5 py-2 rounded-full bg-primary text-on-primary font-label-md text-label-md hover:bg-primary-container transition-all shadow-sm" onclick="saveReflection('q3')">
Save Reflection
</button>
</div>
</div>
</article>
<!-- Question 4: Unanswered / Inviting Call to Reflect -->
<article class="bg-surface-container-low rounded-lg shadow-sm transition-all overflow-hidden" id="card-q4">
<button class="w-full text-left p-space-lg flex items-center justify-between gap-3 focus:outline-none" onclick="toggleCard('q4')">
<div class="flex items-start gap-3 min-w-0">
<span class="w-6 h-6 rounded-full bg-surface-container-highest flex items-center justify-center text-outline shrink-0 mt-0.5">
<span class="material-symbols-outlined text-[16px]">add</span>
</span>
<div class="flex flex-col min-w-0">
<div class="flex items-center gap-2">
<span class="font-label-sm text-label-sm text-outline font-semibold uppercase tracking-wider">Unanswered</span>
<span class="font-label-sm text-label-sm text-outline">· Prompt 4</span>
</div>
<h3 class="font-headline-md text-headline-md text-on-surface">
In what ways could you be even more true to it?
</h3>
</div>
</div>
<span class="material-symbols-outlined text-outline transition-transform duration-200 rotate-180" id="icon-q4">expand_more</span>
</button>
<div class="px-space-lg pb-space-lg pt-0 flex-col gap-3 flex" id="content-q4">
<div class="flex flex-col gap-2">
<textarea class="w-full p-space-md bg-surface rounded-DEFAULT text-on-surface font-body-md text-body-md focus:outline-none shadow-inner resize-none transition-colors" id="textarea-q4" oninput="handleInput(this)" placeholder="Notice the areas where you still mask or hold back. Write your reflections here..." rows="3"></textarea>
<div class="flex items-center justify-between px-1">
<span class="font-label-sm text-label-sm text-on-surface-variant" id="counter-q4">
0 words
</span>
</div>
</div>
<div class="flex items-center justify-end gap-2 pt-1">
<button class="px-5 py-2 rounded-full bg-primary text-on-primary font-label-md text-label-md hover:bg-primary-container transition-all shadow-sm" onclick="saveReflection('q4')">
Save Reflection
</button>
</div>
</div>
</article>
</section>
<!-- Mindful Life Compass Quick-peek Card -->
<section class="bg-surface-container p-space-md rounded-lg flex items-center justify-between gap-4">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-tertiary-container text-on-tertiary-container flex items-center justify-center">
<span class="material-symbols-outlined text-[20px]">explore</span>
</div>
<div class="flex flex-col">
<span class="font-label-md text-label-md text-on-surface font-semibold">Your Life Compass is Taking Shape</span>
<span class="font-body-sm text-body-sm text-on-surface-variant">14 of 20 total reflections recorded</span>
</div>
</div>
<button class="px-3 py-1.5 rounded-full bg-surface-container-high text-on-surface font-label-sm text-label-sm hover:bg-surface-container-highest transition-colors shrink-0">
Preview
</button>
</section>
<!-- Bottom Navigation / Floating Action Bar -->
<nav class="sticky bottom-2 z-10 bg-surface-container-lowest/90 backdrop-blur-md p-space-md rounded-lg shadow-xl flex flex-col gap-3">
<!-- Progress Indicator Bar -->
<!-- Navigation Buttons -->
<div class="flex items-center gap-3">
<button class="flex-1 py-3 px-4 rounded-full bg-surface-container-high text-on-surface font-label-lg text-label-lg hover:bg-surface-container-highest transition-colors flex items-center justify-center gap-1">
<span class="material-symbols-outlined text-[18px]">arrow_back</span>
Previous
</button>
<button class="flex-[2] py-3 px-5 rounded-full bg-primary text-on-primary font-label-lg text-label-lg hover:bg-primary-container transition-all shadow-md flex items-center justify-center gap-2">
<span class="">Next: #2 Courage</span>
<span class="material-symbols-outlined text-[18px]">arrow_forward</span>
</button>
</div>
</nav>
<!-- Interactive Client-side micro-interactions -->
<script>
function toggleCard(id) {
const content = document.getElementById('content-' + id);
const icon = document.getElementById('icon-' + id);
if (!content) return;
const isHidden = content.classList.contains('hidden');
if (isHidden) {
content.classList.remove('hidden');
content.classList.add('flex');
icon.classList.add('rotate-180');
} else {
content.classList.add('hidden');
content.classList.remove('flex');
icon.classList.remove('rotate-180');
}
}
function handleInput(textarea) {
const words = textarea.value.trim().split(/\s+/).filter(Boolean).length;
const id = textarea.id.replace('textarea-', '');
const counter = document.getElementById('counter-' + id);
if (counter) {
counter.textContent = `${words} words · Editing`;
}
}
function saveReflection(id) {
const textarea = document.getElementById('textarea-' + id);
const counter = document.getElementById('counter-' + id);
if (textarea && counter) {
const words = textarea.value.trim().split(/\s+/).filter(Boolean).length;
counter.textContent = `${words} words · Saved just now`;
}
}
function editAnswer(id) {
const content = document.getElementById('content-' + id);
const textElem = document.getElementById('text-' + id);
if (!content || !textElem) return;
const currentText = textElem.textContent.trim();
content.innerHTML = `
<div class="flex flex-col gap-2">
<textarea
id="textarea-${id}"
rows="3"
class="w-full p-space-md bg-surface rounded-DEFAULT text-on-surface font-body-md text-body-md focus:outline-none shadow-inner resize-none"
oninput="handleInput(this)"
>${currentText}</textarea>
<div class="flex items-center justify-between px-1">
<span class="font-label-sm text-label-sm text-on-surface-variant" id="counter-${id}">Editing</span>
</div>
</div>
<div class="flex justify-end gap-2 pt-1">
<button onclick="location.reload()" class="px-5 py-2 rounded-full bg-primary text-on-primary font-label-md text-label-md shadow-sm">
Update Reflection
</button>
</div>
`;
}
</script>
</div></main>
</body></html>
+16
View File
@@ -0,0 +1,16 @@
{
"name": "projects/9343387512370733994/screens/3064045350829608185",
"title": "Values Sort \u2014 Reflect on Top Values",
"screenshot": {
"name": "projects/9343387512370733994/files/3064045350829606144",
"downloadUrl": "https://lh3.googleusercontent.com/aida/AEtjO1Uwhwbp12i_K453L9y11FLMvbRC4yDVlY0fdRpaqI4rBUKKwEFBEzO2fbvVyKER5fnk5X11ceImXKtx6h37TpNJIYr6CZNuNhJc1LNKbOGiV_CO3ee7AVc0nOez48-19Q7vf8_H6Q1DlfjZj3B3v2X23in3GCgyxo5rAa-NsMkS9_G4oCvnWSIioTblp-viQRuJhyq3jIlEldpfZPkU5FQh7hr6lH9RMVXv2Pdy_sviQJK_4R2KOQJxAUc"
},
"htmlCode": {
"name": "projects/9343387512370733994/files/3064045350829608199",
"downloadUrl": "https://contribution.usercontent.google.com/download?c=CgthaWRhX2NvZGVmeBJ7Eh1hcHBfY29tcGFuaW9uX2dlbmVyYXRlZF9maWxlcxpaCiVodG1sXzAwMDY1YmIyOWJmZTJmY2UwOTY4YTAzNjNmMGQ1ODhmEgsSBxD_iY311x0YAZIBIwoKcHJvamVjdF9pZBIVQhM5MzQzMzg3NTEyMzcwNzMzOTk0&filename=&opi=89354086",
"mimeType": "text/html"
},
"width": "780",
"height": "3384",
"deviceType": "MOBILE"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

+391
View File
@@ -0,0 +1,391 @@
<!DOCTYPE html><html lang="en" style=""><head><meta charset="utf-8"><meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" name="viewport"><link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200" rel="stylesheet"><link href="https://fonts.googleapis.com" rel="preconnect"><link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"><link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,200..800;1,6..72,200..800&amp;family=Plus+Jakarta+Sans:ital,wght@0,300..800;1,300..800&amp;display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,[email protected],0..1&amp;display=swap" rel="stylesheet"><script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script><script id="tailwind-config">tailwind.config = { darkMode: 'class', theme: { extend: { colors: { 'on-tertiary-container': '#e1f9db', 'surface-container': '#f1ede6', 'on-primary-container': '#ffefe1', 'primary-fixed-dim': '#d9c3ac', 'tertiary-fixed-dim': '#b5cdb0', 'surface': '#fdf9f2', 'on-secondary-fixed-variant': '#713714', 'surface-container-lowest': '#ffffff', 'error-container': '#ffdad6', 'primary': '#635341', 'outline-variant': '#d0c4ba', 'tertiary': '#465b44', 'surface-dim': '#dddad3', 'primary-container': '#7d6b58', 'outline': '#7f756d', 'on-error-container': '#93000a', 'surface-bright': '#fdf9f2', 'background': '#fdf9f2', 'secondary-container': '#feaa7f', 'tertiary-container': '#5e745c', 'surface-tint': '#6c5b49', 'on-secondary-container': '#783d1a', 'on-primary-fixed-variant': '#544433', 'on-primary-fixed': '#25190b', 'tertiary-fixed': '#d1e9cc', 'on-error': '#ffffff', 'on-secondary': '#ffffff', 'on-tertiary-fixed-variant': '#374c36', 'on-surface': '#1c1c18', 'surface-container-high': '#ece8e1', 'secondary': '#8e4d2a', 'secondary-fixed-dim': '#ffb691', 'secondary-fixed': '#ffdbcb', 'inverse-surface': '#31302c', 'inverse-on-surface': '#f4f0e9', 'on-primary': '#ffffff', 'primary-fixed': '#f6dfc7', 'on-surface-variant': '#4d453e', 'surface-container-highest': '#e6e2db', 'on-tertiary-fixed': '#0d200d', 'on-secondary-fixed': '#341100', 'on-background': '#1c1c18', 'surface-variant': '#e6e2db', 'on-tertiary': '#ffffff', 'error': '#ba1a1a', 'inverse-primary': '#d9c3ac', 'surface-container-low': '#f7f3ec' }, borderRadius: { DEFAULT: '1rem', lg: '2rem', xl: '3rem', full: '9999px' }, spacing: { 'space-xs': '0.25rem', 'margin': '1.5rem', 'gutter': '1rem', 'space-xl': '2.5rem', 'margin-mobile': '1.25rem', 'gutter-mobile': '0.75rem', 'space-sm': '0.5rem', 'space-lg': '1.5rem', 'space-md': '1rem' }, fontFamily: { 'label-lg': ['Plus Jakarta Sans'], 'label-sm': ['Plus Jakarta Sans'], 'headline-xl-mobile': ['Newsreader'], 'body-lg': ['Plus Jakarta Sans'], 'body-sm': ['Plus Jakarta Sans'], 'headline-lg-mobile': ['Newsreader'], 'headline-md': ['Newsreader'], 'headline-lg': ['Newsreader'], 'body-md': ['Plus Jakarta Sans'], 'headline-xl': ['Newsreader'], 'label-md': ['Plus Jakarta Sans'] }, fontSize: { 'label-lg': ['15px', { lineHeight: '20px', letterSpacing: '0.01em', fontWeight: '600' }], 'label-sm': ['11px', { lineHeight: '16px', letterSpacing: '0.04em', fontWeight: '600' }], 'headline-xl-mobile': ['32px', { lineHeight: '40px', letterSpacing: '-0.01em', fontWeight: '400' }], 'body-lg': ['18px', { lineHeight: '28px', fontWeight: '400' }], 'body-sm': ['14px', { lineHeight: '20px', fontWeight: '400' }], 'headline-lg-mobile': ['26px', { lineHeight: '34px', letterSpacing: '0em', fontWeight: '400' }], 'headline-md': ['22px', { lineHeight: '30px', fontWeight: '400' }], 'headline-lg': ['30px', { lineHeight: '38px', letterSpacing: '-0.01em', fontWeight: '400' }], 'body-md': ['16px', { lineHeight: '24px', fontWeight: '400' }], 'headline-xl': ['40px', { lineHeight: '48px', letterSpacing: '-0.02em', fontWeight: '400' }], 'label-md': ['13px', { lineHeight: '18px', letterSpacing: '0.02em', fontWeight: '500' }] } } } };</script><style>@layer base { html, body { width: 100vw; margin: 0; padding: 0; } body { overscroll-behavior: none; } .pb-safe { padding-bottom: env(safe-area-inset-bottom, 0px); } .pt-safe { padding-top: env(safe-area-inset-top, 0px); } main > :first-child { margin-top: 0 !important; } main > :last-child { margin-bottom: 0 !important; } } ::-webkit-scrollbar { display: none; }</style></head><body class="bg-surface font-body-md text-body-md text-on-surface min-h-screen flex flex-col pt-safe pb-safe"><main class="flex-1 flex flex-col relative w-full bg-surface"><div class="flex flex-col w-full px-margin-mobile sm:px-margin space-y-space-lg pb-12">
<!-- Eyebrow & Stage Header -->
<header class="flex flex-col space-y-space-xs text-left pt-2">
<div class="flex items-center justify-between">
<span class="font-label-sm text-label-sm text-secondary tracking-widest uppercase bg-surface-container-high px-3 py-1 rounded-full">
Step 5 of 5 · Deep Reflection
</span>
<span class="font-label-sm text-label-sm text-on-surface-variant flex items-center gap-1">
<span class="material-symbols-outlined text-[16px] text-tertiary">spa</span>
Unrushed Practice
</span>
</div>
<h1 class="font-headline-xl-mobile sm:font-headline-xl text-headline-xl-mobile sm:text-headline-xl text-on-surface mt-1">Your Core Values</h1>
</header>
<!-- Value Selector Carousel -->
<section class="flex flex-col space-y-2">
<div class="flex items-center justify-between px-1">
<span class="font-label-sm text-label-sm text-on-surface-variant uppercase tracking-wider">Top 5 Core Anchors</span>
<span class="font-label-sm text-label-sm text-tertiary font-semibold" id="carousel-status">3 of 4 answered</span>
</div>
<div class="flex gap-2 overflow-x-auto pb-2 scrollbar-none snap-x" id="value-tabs">
<button class="snap-start flex-shrink-0 flex items-center gap-2 px-4 py-2 rounded-full bg-primary text-on-primary font-label-md text-label-md shadow-sm transition-all">
<span class="w-2 h-2 rounded-full bg-secondary-fixed"></span>
#1 Authenticity
<span class="font-label-sm text-label-sm px-1.5 py-0.5 rounded-full bg-primary-container text-on-primary-container">3/4</span>
</button>
<button class="snap-start flex-shrink-0 flex items-center gap-2 px-4 py-2 rounded-full bg-surface-container-high text-on-surface-variant hover:bg-surface-container-highest font-label-md text-label-md transition-colors">
<span class="w-2 h-2 rounded-full bg-tertiary"></span>
#2 Courage
<span class="font-label-sm text-label-sm px-1.5 py-0.5 rounded-full bg-surface-container text-on-surface-variant">4/4</span>
</button>
<button class="snap-start flex-shrink-0 flex items-center gap-2 px-4 py-2 rounded-full bg-surface-container-high text-on-surface-variant hover:bg-surface-container-highest font-label-md text-label-md transition-colors">
<span class="w-2 h-2 rounded-full bg-outline-variant"></span>
#3 Compassion
<span class="font-label-sm text-label-sm px-1.5 py-0.5 rounded-full bg-surface-container text-on-surface-variant">2/4</span>
</button>
<button class="snap-start flex-shrink-0 flex items-center gap-2 px-4 py-2 rounded-full bg-surface-container-high text-on-surface-variant hover:bg-surface-container-highest font-label-md text-label-md transition-colors">
<span class="w-2 h-2 rounded-full bg-outline-variant"></span>
#4 Freedom
<span class="font-label-sm text-label-sm px-1.5 py-0.5 rounded-full bg-surface-container text-on-surface-variant">2/4</span>
</button>
<button class="snap-start flex-shrink-0 flex items-center gap-2 px-4 py-2 rounded-full bg-surface-container-high text-on-surface-variant hover:bg-surface-container-highest font-label-md text-label-md transition-colors">
<span class="w-2 h-2 rounded-full bg-outline-variant"></span>
#5 Curiosity
<span class="font-label-sm text-label-sm px-1.5 py-0.5 rounded-full bg-surface-container text-on-surface-variant">3/4</span>
</button>
</div>
</section>
<!-- Core Value Spotlight Card -->
<section class="bg-surface-container-low rounded-lg p-space-lg shadow-sm flex flex-col gap-4 relative overflow-hidden">
<div class="flex items-start justify-between">
<div class="flex flex-col">
<h2 class="font-headline-lg-mobile sm:font-headline-lg text-headline-lg-mobile sm:text-headline-lg text-on-surface mt-0.5">
Authenticity
</h2>
</div>
</div>
<p class="font-body-md text-body-md text-on-surface-variant italic">
"Living in genuine alignment with inner truth and speaking without pretense or performance."
</p>
</section>
<!-- Reflection Questions List (Accordions) -->
<section class="flex flex-col space-y-3" id="questions-container">
<!-- Question 1: Answered & Collapsible -->
<article class="bg-surface-container-low rounded-lg shadow-sm transition-all overflow-hidden cursor-pointer hover:bg-surface-container" id="card-q1">
<div class="w-full text-left p-space-lg flex items-center justify-between gap-3" onclick="openDrawer('q1')">
<div class="flex items-start gap-3 min-w-0">
<span class="w-6 h-6 rounded-full bg-tertiary-fixed flex items-center justify-center text-on-tertiary-fixed shrink-0 mt-0.5">
<span class="material-symbols-outlined text-[15px] font-bold">check</span>
</span>
<div class="flex flex-col min-w-0">
<div class="flex items-center gap-2">
<span class="font-label-sm text-label-sm text-tertiary font-semibold uppercase tracking-wider">Answered</span>
<span class="font-label-sm text-label-sm text-outline">· Prompt 1</span>
</div>
<h3 class="font-headline-md text-headline-md text-on-surface truncate sm:whitespace-normal">
What does this value mean to you?
</h3>
</div>
</div>
<span class="material-symbols-outlined text-outline">open_in_full</span>
</div>
<div class="px-space-lg pb-space-lg pt-0 flex-col gap-3 flex" id="content-q1">
<div class="bg-surface-container p-space-md rounded-DEFAULT">
<p class="font-body-md text-body-md text-on-surface leading-relaxed" id="text-q1">
To me, authenticity is the quiet permission to stop curating who I am for other people's approval. It means letting my outward actions and words match what is truly happening inside, even when that makes me feel vulnerable or imperfect.
</p>
</div>
<div class="flex justify-end gap-2">
<button class="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-full bg-surface-container-high text-on-surface font-label-sm text-label-sm hover:bg-surface-container-highest transition-colors" onclick="event.stopPropagation(); openDrawer('q1')">
<span class="material-symbols-outlined text-[14px]">edit</span>
Open Reflection
</button>
</div>
</div>
</article>
<!-- Question 2: Answered & Collapsed default -->
<article class="bg-surface-container-low rounded-lg shadow-sm transition-all overflow-hidden cursor-pointer hover:bg-surface-container" id="card-q2">
<div class="w-full text-left p-space-lg flex items-center justify-between gap-3" onclick="openDrawer('q2')">
<div class="flex items-start gap-3 min-w-0">
<span class="w-6 h-6 rounded-full bg-tertiary-fixed flex items-center justify-center text-on-tertiary-fixed shrink-0 mt-0.5">
<span class="material-symbols-outlined text-[15px] font-bold">check</span>
</span>
<div class="flex flex-col min-w-0">
<div class="flex items-center gap-2">
<span class="font-label-sm text-label-sm text-tertiary font-semibold uppercase tracking-wider">Answered</span>
<span class="font-label-sm text-label-sm text-outline">· Prompt 2</span>
</div>
<h3 class="font-headline-md text-headline-md text-on-surface truncate sm:whitespace-normal">
Why did you choose this as important?
</h3>
</div>
</div>
<span class="material-symbols-outlined text-outline">open_in_full</span>
</div>
<div class="px-space-lg pb-space-lg pt-0 flex-col gap-3 hidden" id="content-q2">
<div class="bg-surface-container p-space-md rounded-DEFAULT">
<p class="font-body-md text-body-md text-on-surface leading-relaxed" id="text-q2">
Whenever I have betrayed my own intuition to keep the peace or satisfy someone else, an inner resentment always took root. Choosing authenticity has been my most reliable antidote to regret and burnout.
</p>
</div>
<div class="flex justify-end gap-2">
<button class="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-full bg-surface-container-high text-on-surface font-label-sm text-label-sm hover:bg-surface-container-highest transition-colors" onclick="event.stopPropagation(); openDrawer('q2')">
<span class="material-symbols-outlined text-[14px]">edit</span>
Open Reflection
</button>
</div>
</div>
</article>
<!-- Question 3: Active / In Progress Card -->
<article class="bg-surface-container-low rounded-lg shadow-md transition-all overflow-hidden border border-outline-variant/50 cursor-pointer hover:bg-surface-container" id="card-q3" onclick="openDrawer('q3')">
<div class="w-full text-left p-space-lg flex items-center justify-between gap-3">
<div class="flex items-start gap-3 min-w-0">
<span class="w-6 h-6 rounded-full bg-secondary-fixed flex items-center justify-center text-on-secondary-fixed-variant shrink-0 mt-0.5">
<span class="material-symbols-outlined text-[15px]">edit_note</span>
</span>
<div class="flex flex-col min-w-0">
<div class="flex items-center gap-2">
<span class="font-label-sm text-label-sm text-secondary font-semibold uppercase tracking-wider">In Progress</span>
<span class="font-label-sm text-label-sm text-outline">· Prompt 3</span>
</div>
<h3 class="font-headline-md text-headline-md text-on-surface">
How have you shown this value in your daily life?
</h3>
</div>
</div>
<span class="material-symbols-outlined text-secondary">open_in_full</span>
</div>
<div class="px-space-lg pb-space-lg pt-0 flex flex-col gap-3" id="content-q3">
<div class="p-space-md bg-surface rounded-DEFAULT border border-outline-variant/30">
<p class="text-on-surface font-body-md text-body-md line-clamp-2 italic text-on-surface-variant" id="text-q3">"Recently, during a project retrospective, I spoke honestly about feeling overwhelmed instead of nodding along like everything was fine..."</p>
</div>
<div class="flex items-center justify-between pt-1">
<span class="font-label-sm text-label-sm text-secondary flex items-center gap-1">
<span class="material-symbols-outlined text-[15px]">mode_edit</span> Tap to expand &amp; journal
</span>
<button class="px-4 py-1.5 rounded-full bg-primary text-on-primary font-label-sm text-label-sm hover:bg-primary-container transition-all shadow-sm flex items-center gap-1">
Continue writing
</button>
</div>
</div>
</article>
<!-- Question 4: Unanswered / Inviting Call to Reflect -->
<article class="bg-surface-container-low rounded-lg shadow-sm transition-all overflow-hidden border border-dashed border-outline-variant cursor-pointer hover:bg-surface-container" id="card-q4" onclick="openDrawer('q4')">
<div class="w-full text-left p-space-lg flex items-center justify-between gap-3">
<div class="flex items-start gap-3 min-w-0">
<span class="w-6 h-6 rounded-full bg-surface-container-highest flex items-center justify-center text-outline shrink-0 mt-0.5">
<span class="material-symbols-outlined text-[16px]">add</span>
</span>
<div class="flex flex-col min-w-0">
<div class="flex items-center gap-2">
<span class="font-label-sm text-label-sm text-outline font-semibold uppercase tracking-wider">Unanswered</span>
<span class="font-label-sm text-label-sm text-outline">· Prompt 4</span>
</div>
<h3 class="font-headline-md text-headline-md text-on-surface">
In what ways could you be even more true to it?
</h3>
</div>
</div>
<span class="material-symbols-outlined text-outline">edit_square</span>
</div>
<div class="px-space-lg pb-space-lg pt-0 flex-col gap-3 flex" id="content-q4">
<p class="font-body-sm text-body-sm text-on-surface-variant">Notice where you still mask, hold back, or seek quiet convenience over your core alignment.</p>
<div class="flex items-center justify-end gap-2 pt-1">
<button class="px-5 py-2 rounded-full bg-surface-container-highest text-on-surface font-label-md text-label-md hover:bg-surface-container-high transition-all flex items-center gap-1">
<span class="material-symbols-outlined text-[16px]">edit</span>
Start Prompt 4
</button>
</div>
</div>
</article>
</section>
<!-- Mindful Life Compass Quick-peek Card -->
<section class="bg-surface-container p-space-md rounded-lg flex items-center justify-between gap-4">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-tertiary-container text-on-tertiary-container flex items-center justify-center">
<span class="material-symbols-outlined text-[20px]">explore</span>
</div>
<div class="flex flex-col">
<span class="font-label-md text-label-md text-on-surface font-semibold">Your Life Compass is Taking Shape</span>
<span class="font-body-sm text-body-sm text-on-surface-variant">14 of 20 total reflections recorded</span>
</div>
</div>
<button class="px-3 py-1.5 rounded-full bg-surface-container-high text-on-surface font-label-sm text-label-sm hover:bg-surface-container-highest transition-colors shrink-0">
Preview
</button>
</section>
<!-- Bottom Navigation / Floating Action Bar -->
<nav class="sticky bottom-2 z-10 bg-surface-container-lowest/90 backdrop-blur-md p-space-md rounded-lg shadow-xl flex flex-col gap-3">
<!-- Navigation Buttons -->
<div class="flex items-center gap-3">
<button class="flex-1 py-3 px-4 rounded-full bg-surface-container-high text-on-surface font-label-lg text-label-lg hover:bg-surface-container-highest transition-colors flex items-center justify-center gap-1">
<span class="material-symbols-outlined text-[18px]">arrow_back</span>
Previous
</button>
<button class="flex-[2] py-3 px-5 rounded-full bg-primary text-on-primary font-label-lg text-label-lg hover:bg-primary-container transition-all shadow-md flex items-center justify-center gap-2">
<span class="">Next: #2 Courage</span>
<span class="material-symbols-outlined text-[18px]">arrow_forward</span>
</button>
</div>
</nav>
<!-- FULL-SCREEN REFLECTION DRAWER / MODAL (Occupies whole viewport: z-50 inset-0) -->
<div class="fixed inset-0 z-50 flex flex-col bg-surface text-on-surface overflow-hidden transition-all duration-300" id="reflection-drawer">
<!-- Top Navigation & Action Bar -->
<header class="w-full bg-surface/95 backdrop-blur-md border-b border-outline-variant/40 px-margin-mobile sm:px-margin pt-safe">
<div class="h-16 flex items-center justify-between">
<button class="inline-flex items-center gap-1 px-3 py-2 -ml-2 rounded-full text-on-surface-variant hover:bg-surface-container-high transition-colors font-label-md text-label-md" onclick="closeDrawer()">
<span class="material-symbols-outlined text-[22px]">close</span>
<span class="">Close</span>
</button>
<div class="flex items-center gap-1.5 text-center">
<span class="w-2 h-2 rounded-full bg-secondary"></span>
<span class="font-label-sm text-label-sm text-secondary font-semibold uppercase tracking-wider" id="drawer-step-badge">Prompt 3 of 4</span>
</div>
<button class="px-4 py-2 rounded-full bg-primary text-on-primary font-label-md text-label-md hover:bg-primary-container transition-all shadow-sm flex items-center gap-1" onclick="saveAndCloseDrawer()">
<span class="material-symbols-outlined text-[16px]">check</span>
<span class="">Done</span>
</button>
</div>
</header>
<!-- Spacious Distraction-Free Reflection Writing Area -->
<div class="flex-1 flex flex-col overflow-y-auto px-margin-mobile sm:px-margin py-space-lg space-y- space-y-4">
<!-- Pinned Question Header in Elegant Serif Typography -->
<div class="flex flex-col space-y-2 border-b border-outline-variant/30 pb-4">
<div class="flex items-center gap-2">
<span class="font-label-sm text-label-sm text-secondary bg-surface-container-high px-2.5 py-0.5 rounded-full font-medium" id="drawer-value-tag">
Reflecting on #1 Authenticity
</span>
</div>
<h2 class="font-headline-xl-mobile sm:font-headline-xl text-headline-xl-mobile sm:text-headline-xl text-on-surface leading-tight font-normal" id="drawer-question-title">
How have you shown this value in your daily life?
</h2>
<!-- Guiding Thought / Prompt Hint -->
</div>
<!-- Spacious Writing Textarea Field -->
<div class="flex-1 flex flex-col min-h-[280px]">
<textarea class="w-full flex-1 p-space-lg bg-surface-container-lowest/80 rounded-lg text-on-surface font-body-lg text-body-lg focus:outline-none focus:ring-1 focus:ring-primary-container shadow-inner resize-none transition-all leading-relaxed placeholder:text-outline/60 border border-outline-variant/30" id="drawer-textarea" oninput="handleDrawerInput(this)" placeholder="There are no wrong answers here. Write freely without curating or judging your thoughts...">Recently, during a project retrospective, I spoke honestly about feeling overwhelmed instead of nodding along like everything was fine. It opened the door for my colleagues to share their true workload too.</textarea>
<div class="flex items-center justify-between px-2 pt-2 text-on-surface-variant">
<span class="font-label-sm text-label-sm" id="drawer-word-counter">
33 words · 214 characters
</span>
<span class="font-label-sm text-label-sm text-outline flex items-center gap-1">
<span class="material-symbols-outlined text-[14px]">spa</span> Mindful focus mode
</span>
</div>
</div>
</div>
<!-- Bottom Drawer Floating Action Bar -->
<footer class="w-full bg-surface-container-lowest/95 backdrop-blur-md border-t border-outline-variant/40 px-margin-mobile sm:px-margin py-3 pb-safe">
<div class="flex items-center justify-between gap-3">
<button class="flex-1 py-3 px-4 rounded-full bg-surface-container-high text-on-surface font-label-md text-label-md hover:bg-surface-container-highest transition-colors flex items-center justify-center gap-1.5" onclick="closeDrawer()">
<span class="material-symbols-outlined text-[18px]">close</span>
<span class="">Cancel</span>
</button>
<button class="flex-[2] py-3 px-5 rounded-full bg-primary text-on-primary font-label-md text-label-md hover:bg-primary-container transition-all shadow-md flex items-center justify-center gap-2" onclick="saveAndContinue()">
<span class="">Save &amp; Continue</span>
<span class="material-symbols-outlined text-[18px]">arrow_forward</span>
</button>
</div>
</footer>
</div>
<!-- Interactive Client-side micro-interactions -->
<script>
const questionData = {
q1: {
number: "Prompt 1 of 4",
question: "What does this value mean to you?",
hint: "Define authenticity in your own rhythm and voice. What sensation or truth does it evoke when you are undisturbed?",
text: "To me, authenticity is the quiet permission to stop curating who I am for other people's approval. It means letting my outward actions and words match what is truly happening inside, even when that makes me feel vulnerable or imperfect."
},
q2: {
number: "Prompt 2 of 4",
question: "Why did you choose this as important?",
hint: "Reflect on past moments where keeping quiet or playing a role cost you peace of mind.",
text: "Whenever I have betrayed my own intuition to keep the peace or satisfy someone else, an inner resentment always took root. Choosing authenticity has been my most reliable antidote to regret and burnout."
},
q3: {
number: "Prompt 3 of 4",
question: "How have you shown this value in your daily life?",
hint: "Reflect gently on recent moments where you stayed aligned with your truth, even when convenience or approval tempted you otherwise.",
text: "Recently, during a project retrospective, I spoke honestly about feeling overwhelmed instead of nodding along like everything was fine. It opened the door for my colleagues to share their true workload too."
},
q4: {
number: "Prompt 4 of 4",
question: "In what ways could you be even more true to it?",
hint: "Notice the specific relationships, environments, or habits where you still find yourself putting on a mask.",
text: ""
}
};
let currentPromptId = 'q3';
function openDrawer(id) {
currentPromptId = id;
const data = questionData[id] || questionData.q3;
document.getElementById('drawer-step-badge').textContent = data.number;
document.getElementById('drawer-question-title').textContent = data.question;
document.getElementById('drawer-guiding-hint').textContent = `"${data.hint}"`;
const textarea = document.getElementById('drawer-textarea');
textarea.value = data.text;
handleDrawerInput(textarea);
const drawer = document.getElementById('reflection-drawer');
drawer.classList.remove('hidden');
drawer.classList.add('flex');
document.body.style.overflow = 'hidden';
}
function closeDrawer() {
const drawer = document.getElementById('reflection-drawer');
drawer.classList.add('hidden');
drawer.classList.remove('flex');
document.body.style.overflow = '';
}
function handleDrawerInput(textarea) {
const text = textarea.value.trim();
const words = text ? text.split(/\s+/).filter(Boolean).length : 0;
const chars = textarea.value.length;
const counter = document.getElementById('drawer-word-counter');
if (counter) {
counter.textContent = `${words} words · ${chars} characters`;
}
const indicator = document.getElementById('drawer-save-indicator');
if (indicator) {
indicator.innerHTML = '<span class="material-symbols-outlined text-[13px] text-secondary">sync</span> Saving draft...';
clearTimeout(window.saveTimer);
window.saveTimer = setTimeout(() => {
indicator.innerHTML = '<span class="material-symbols-outlined text-[13px] text-tertiary">cloud_done</span> Autosaved';
}, 800);
}
}
function saveAndCloseDrawer() {
const textarea = document.getElementById('drawer-textarea');
if (questionData[currentPromptId]) {
questionData[currentPromptId].text = textarea.value;
const cardText = document.getElementById('text-' + currentPromptId);
if (cardText) {
cardText.textContent = textarea.value;
}
}
closeDrawer();
}
function saveAndContinue() {
const textarea = document.getElementById('drawer-textarea');
if (questionData[currentPromptId]) {
questionData[currentPromptId].text = textarea.value;
const cardText = document.getElementById('text-' + currentPromptId);
if (cardText) {
cardText.textContent = textarea.value;
}
}
const keys = ['q1', 'q2', 'q3', 'q4'];
const nextIdx = (keys.indexOf(currentPromptId) + 1) % keys.length;
openDrawer(keys[nextIdx]);
}
</script>
</div></main>
</body></html>
+16
View File
@@ -0,0 +1,16 @@
{
"name": "projects/9343387512370733994/screens/7bb00b6022b14dd9a687b9a31ec6b1f5",
"title": "Values Sort \u2014 Reflection (Full-Screen Journal Drawer)",
"screenshot": {
"name": "projects/9343387512370733994/files/15389870381633265971",
"downloadUrl": "https://lh3.googleusercontent.com/aida/AEtjO1UAeYCnBIII639-PoE0tFfV3LHW2yIsQ2XGm0L6YPETkrpzkMplK_QGvg-8F8PSLt_oJCS-3Uajl851y6H1IkywclVOp_OG34ANFKKEmKIyGFxXYg-XXlWWlanu_22zhpb_lhiO4auBOIFSxwwFc2NbU3b6qlsAIlBGONtInErxesAiJILt6j1yKKM1OkGJIrh4sWabbDoAfBNaJlu_hDywInGjs4j-d6SZOEvzgunjmwSjLStHmjuQoVk"
},
"htmlCode": {
"name": "projects/9343387512370733994/files/2406486854586640137",
"downloadUrl": "https://contribution.usercontent.google.com/download?c=CgthaWRhX2NvZGVmeBJ7Eh1hcHBfY29tcGFuaW9uX2dlbmVyYXRlZF9maWxlcxpaCiVodG1sXzAwMDY1YmIyYWE0ZDI3M2IwMjNiZWU1MjMxMjVjMzFhEgsSBxD_iY311x0YAZIBIwoKcHJvamVjdF9pZBIVQhM5MzQzMzg3NTEyMzcwNzMzOTk0&filename=&opi=89354086",
"mimeType": "text/html"
},
"width": "780",
"height": "3596",
"deviceType": "MOBILE"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

+223
View File
@@ -0,0 +1,223 @@
<!DOCTYPE html><html lang="en" style=""><head><meta charset="utf-8"><meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" name="viewport"><link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200" rel="stylesheet"><link href="https://fonts.googleapis.com" rel="preconnect"><link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"><link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,200..800;1,6..72,200..800&amp;family=Plus+Jakarta+Sans:ital,wght@0,300..800;1,300..800&amp;display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,[email protected],0..1&amp;display=swap" rel="stylesheet"><script src="https://cdn.tailwindcss.com"></script><script id="tailwind-config">tailwind.config = { darkMode: 'class', theme: { extend: { colors: { 'on-tertiary-container': '#e1f9db', 'surface-container': '#f1ede6', 'on-primary-container': '#ffefe1', 'primary-fixed-dim': '#d9c3ac', 'tertiary-fixed-dim': '#b5cdb0', 'surface': '#fdf9f2', 'on-secondary-fixed-variant': '#713714', 'surface-container-lowest': '#ffffff', 'error-container': '#ffdad6', 'primary': '#635341', 'outline-variant': '#d0c4ba', 'tertiary': '#465b44', 'surface-dim': '#dddad3', 'primary-container': '#7d6b58', 'outline': '#7f756d', 'on-error-container': '#93000a', 'surface-bright': '#fdf9f2', 'background': '#fdf9f2', 'secondary-container': '#feaa7f', 'tertiary-container': '#5e745c', 'surface-tint': '#6c5b49', 'on-secondary-container': '#783d1a', 'on-primary-fixed-variant': '#544433', 'on-primary-fixed': '#25190b', 'tertiary-fixed': '#d1e9cc', 'on-error': '#ffffff', 'on-secondary': '#ffffff', 'on-tertiary-fixed-variant': '#374c36', 'on-surface': '#1c1c18', 'surface-container-high': '#ece8e1', 'secondary': '#8e4d2a', 'secondary-fixed-dim': '#ffb691', 'secondary-fixed': '#ffdbcb', 'inverse-surface': '#31302c', 'inverse-on-surface': '#f4f0e9', 'on-primary': '#ffffff', 'primary-fixed': '#f6dfc7', 'on-surface-variant': '#4d453e', 'surface-container-highest': '#e6e2db', 'on-tertiary-fixed': '#0d200d', 'on-secondary-fixed': '#341100', 'on-background': '#1c1c18', 'surface-variant': '#e6e2db', 'on-tertiary': '#ffffff', 'error': '#ba1a1a', 'inverse-primary': '#d9c3ac', 'surface-container-low': '#f7f3ec' }, borderRadius: { DEFAULT: '1rem', lg: '2rem', xl: '3rem', full: '9999px' }, spacing: { 'space-xs': '0.25rem', 'margin': '1.5rem', 'gutter': '1rem', 'space-xl': '2.5rem', 'margin-mobile': '1.25rem', 'gutter-mobile': '0.75rem', 'space-sm': '0.5rem', 'space-lg': '1.5rem', 'space-md': '1rem' }, fontFamily: { 'label-lg': ['Plus Jakarta Sans'], 'label-sm': ['Plus Jakarta Sans'], 'headline-xl-mobile': ['Newsreader'], 'body-lg': ['Plus Jakarta Sans'], 'body-sm': ['Plus Jakarta Sans'], 'headline-lg-mobile': ['Newsreader'], 'headline-md': ['Newsreader'], 'headline-lg': ['Newsreader'], 'body-md': ['Plus Jakarta Sans'], 'headline-xl': ['Newsreader'], 'label-md': ['Plus Jakarta Sans'] }, fontSize: { 'label-lg': ['15px', { lineHeight: '20px', letterSpacing: '0.01em', fontWeight: '600' }], 'label-sm': ['11px', { lineHeight: '16px', letterSpacing: '0.04em', fontWeight: '600' }], 'headline-xl-mobile': ['32px', { lineHeight: '40px', letterSpacing: '-0.01em', fontWeight: '400' }], 'body-lg': ['18px', { lineHeight: '28px', fontWeight: '400' }], 'body-sm': ['14px', { lineHeight: '20px', fontWeight: '400' }], 'headline-lg-mobile': ['26px', { lineHeight: '34px', letterSpacing: '0em', fontWeight: '400' }], 'headline-md': ['22px', { lineHeight: '30px', fontWeight: '400' }], 'headline-lg': ['30px', { lineHeight: '38px', letterSpacing: '-0.01em', fontWeight: '400' }], 'body-md': ['16px', { lineHeight: '24px', fontWeight: '400' }], 'headline-xl': ['40px', { lineHeight: '48px', letterSpacing: '-0.02em', fontWeight: '400' }], 'label-md': ['13px', { lineHeight: '18px', letterSpacing: '0.02em', fontWeight: '500' }] } } } };</script><style>@layer base { html, body { width: 100vw; margin: 0; padding: 0; } body { overscroll-behavior: none; } .pb-safe { padding-bottom: env(safe-area-inset-bottom, 0px); } .pt-safe { padding-top: env(safe-area-inset-top, 0px); } main > :first-child { margin-top: 0 !important; } main > :last-child { margin-bottom: 0 !important; } } ::-webkit-scrollbar { display: none; }</style></head><body class="bg-surface font-body-md text-body-md text-on-surface min-h-screen flex flex-col pt-safe pb-safe"><main class="flex-1 flex flex-col relative w-full bg-surface"><div class="flex flex-col w-full px-margin-mobile pb-space-lg select-none">
<!-- Top Subtle Brand Navigation -->
<header class="flex items-center justify-between w-full pt-space-xs pb-space-md">
<div class="flex items-center gap-space-sm">
<img alt="Values Sort Emblem" class="w-8 h-8 rounded-full shadow-sm object-cover" src="https://lh3.googleusercontent.com/aida/AEtjO1VxhUd_UEQbNaJg7xz-AiOrtZWLiZw7oiFfgI1iGc4Ag2ZSYEnTYF5NR-_fX5fUGLeepg5mkonGjOR6YHa8M8yyNFuATrjW9mPC6x2-vKmbEJcHjBBLGkpOS7K6xhYyywXBgsdSreZcpJKeqZnLlO4wl5TMdHrEjkAUmiOLwouqlqtRx9PwJhZ26tf1an8bI8c8QO6wdhCvMD1L757xSkIFnXkAyvRbwt-YkTQcykj8FJaDytqPf0zYxA">
<span class="font-label-sm text-label-sm tracking-widest uppercase text-on-surface-variant">VALUES SORT</span>
</div>
</header>
<!-- Central Tactile Cards Composition -->
<section class="relative w-full py-space-sm flex items-center justify-center overflow-hidden my-space-xs">
<!-- Ambient organic gradient glow -->
<div class="absolute inset-0 bg-gradient-to-b from-primary/5 via-tertiary/5 to-transparent rounded-lg blur-2xl pointer-events-none"></div>
<div class="relative w-full max-w-[320px] h-[210px] flex items-center justify-center">
<!-- Background Card 1 (Left Tilt: Curiosity) -->
<div class="absolute w-[180px] h-[135px] rounded-lg p-space-md bg-surface-container shadow-sm -rotate-12 -translate-x-12 translate-y-3 transition-transform duration-500 ease-out hover:-rotate-14 hover:-translate-y-1">
<span class="font-label-sm text-label-sm text-outline uppercase tracking-wider block mb-1">Inquiry</span>
<h4 class="font-headline-md text-headline-md text-on-surface mb-1 leading-none">Curiosity</h4>
<p class="font-body-sm text-body-sm text-on-surface-variant text-[11px] leading-tight line-clamp-2">Seeking wonder, exploring questions without immediate answers.</p>
</div>
<!-- Background Card 2 (Right Tilt: Connection) -->
<div class="absolute w-[180px] h-[135px] rounded-lg p-space-md bg-surface-container shadow-sm rotate-12 translate-x-12 translate-y-2 transition-transform duration-500 ease-out hover:rotate-14 hover:-translate-y-1">
<span class="font-label-sm text-label-sm text-outline uppercase tracking-wider block mb-1">Belonging</span>
<h4 class="font-headline-md text-headline-md text-on-surface mb-1 leading-none">Connection</h4>
<p class="font-body-sm text-body-sm text-on-surface-variant text-[11px] leading-tight line-clamp-2">Nurturing deep kinship and seeing others as they truly are.</p>
</div>
<!-- Foreground Card 3 (Elevated Hero Card: Authenticity) -->
<div class="relative z-10 w-[210px] h-[155px] rounded-lg p-space-md bg-surface-container-lowest shadow-md rotate-1 translate-y-1 transition-transform duration-500 hover:rotate-0 hover:scale-105 cursor-pointer">
<div class="flex items-center justify-between mb-2">
<span class="font-label-sm text-label-sm text-secondary tracking-widest uppercase">Core Ground</span>
<span class="material-symbols-outlined text-[16px] text-primary" style="font-variation-settings: &quot;FILL&quot; 1;">spa</span>
</div>
<h3 class="font-headline-lg-mobile text-headline-lg-mobile text-on-surface mb-1">Authenticity</h3>
<p class="font-body-sm text-body-sm text-on-surface-variant leading-snug">Living in honest alignment with who you are, without performance.</p>
<div class="mt-3 flex items-center gap-1.5">
<span class="h-1 w-6 rounded-full bg-secondary"></span>
<span class="h-1 w-1.5 rounded-full bg-surface-container-highest"></span>
<span class="h-1 w-1.5 rounded-full bg-surface-container-highest"></span>
</div>
</div>
</div>
</section>
<!-- Editorial Headline & Thoughtful Copy -->
<section class="flex flex-col text-center px-1 mt-space-sm mb-space-md">
<h1 class="font-headline-xl-mobile text-headline-xl-mobile text-on-surface tracking-tight mb-space-xs">
Discover what matters most to you
</h1>
<p class="font-body-md text-body-md text-on-surface-variant leading-relaxed px-1">
Through a gentle, guided card-sort practice, clarify your intrinsic values and illuminate the principles that guide your everyday decisions.
</p>
<!-- Introspective Trust & Rhythm Metadata -->
<div class="flex items-center justify-center flex-wrap gap-x-2 gap-y-1 mt-space-md text-on-surface-variant font-label-md text-label-md">
<div class="flex items-center gap-1">
<span class="material-symbols-outlined text-[15px] text-tertiary">schedule</span>
<span class="">10–15 min</span>
</div>
<span class="text-outline-variant">•</span>
<div class="flex items-center gap-1">
<span class="material-symbols-outlined text-[15px] text-tertiary">pace</span>
<span class="">Self-paced</span>
</div>
<span class="text-outline-variant">•</span>
<div class="flex items-center gap-1">
<span class="material-symbols-outlined text-[15px] text-tertiary">lock</span>
<span class="">Private &amp; reflective</span>
</div>
</div>
</section>
<!-- Primary Actions / Grounded CTAs -->
<footer class="mt-auto flex flex-col gap-space-xs pt-space-xs">
<button class="w-full py-2.5 px-4 rounded-full bg-transparent text-on-surface-variant font-label-md text-label-md active:bg-surface-container transition-colors flex items-center justify-center gap-1.5" id="open-sheet-btn" type="button">
<span class="">How this practice works</span>
<span class="material-symbols-outlined text-[18px]">info</span>
</button><button class="w-full py-3.5 px-6 rounded-full bg-primary text-on-primary font-label-lg text-label-lg shadow-sm active:scale-[0.985] active:bg-primary-container transition-all flex items-center justify-center gap-2" id="start-btn" style="opacity: 1;" type="button">
<span class="">Begin Reflection</span>
<span class="material-symbols-outlined text-[18px]">arrow_forward</span>
</button>
<!-- Anchored quiet psychological micro-copy -->
</footer>
</div>
<!-- Serene Bottom Sheet / Explanation Drawer Modal -->
<div aria-labelledby="sheet-title" aria-modal="true" class="fixed inset-0 z-50 pointer-events-none flex flex-col justify-end transition-opacity duration-300 opacity-0" id="explanation-sheet-container" role="dialog">
<!-- Backdrop -->
<div class="absolute inset-0 bg-inverse-surface/40 backdrop-blur-[2px] transition-opacity duration-300 cursor-pointer opacity-0" id="sheet-backdrop"></div>
<!-- Sheet Panel -->
<div class="relative w-full max-w-lg mx-auto bg-surface rounded-t-[2rem] p-space-lg shadow-2xl transition-transform duration-300 ease-out transform translate-y-full flex flex-col max-h-[85vh] overflow-y-auto border-t border-outline-variant/30" id="sheet-panel">
<!-- Drag Handle Indicator -->
<div class="w-10 h-1 rounded-full bg-outline-variant/60 mx-auto mb-space-md shrink-0"></div>
<!-- Sheet Header -->
<div class="flex items-center justify-between pb-space-sm border-b border-outline-variant/20 mb-space-md">
<div class="flex items-center gap-2">
<span class="material-symbols-outlined text-[20px] text-tertiary" style="font-variation-settings: 'FILL' 1;">nature_people</span>
<h3 class="font-headline-md text-headline-md text-on-surface" id="sheet-title">The Practice of Sorting</h3>
</div>
<button aria-label="Close details" class="w-8 h-8 rounded-full flex items-center justify-center text-on-surface-variant hover:bg-surface-container active:scale-95 transition-all" id="close-sheet-btn" type="button">
<span class="material-symbols-outlined text-[20px]">close</span>
</button>
</div>
<!-- Explanation Steps -->
<div class="flex flex-col gap-space-md">
<p class="font-body-md text-body-md text-on-surface-variant leading-relaxed">
Grounded in contemplative psychology, this sorting practice helps clarify the core tenets that give your life meaning and direction.
</p>
<ol class="space-y-space-md my-space-xs">
<!-- Step 1 -->
<li class="flex items-start gap-space-sm">
<div class="w-6 h-6 rounded-full bg-surface-container-high text-primary font-label-md text-label-md flex items-center justify-center shrink-0 mt-0.5 font-semibold">
1
</div>
<div class="flex flex-col">
<h4 class="font-label-lg text-label-lg text-on-surface font-semibold">One card at a time</h4>
<p class="font-body-sm text-body-sm text-on-surface-variant leading-relaxed mt-0.5">
Review cards with diverse values one at a time, checking in with your intuition without overthinking.
</p>
</div>
</li>
<!-- Step 2 -->
<li class="flex items-start gap-space-sm">
<div class="w-6 h-6 rounded-full bg-surface-container-high text-secondary font-label-md text-label-md flex items-center justify-center shrink-0 mt-0.5 font-semibold">
2
</div>
<div class="flex flex-col">
<h4 class="font-label-lg text-label-lg text-on-surface font-semibold">Sort into intuitive piles</h4>
<p class="font-body-sm text-body-sm text-on-surface-variant leading-relaxed mt-0.5">
Sort each into: <strong class="font-medium text-on-surface">‘Always Important’</strong>, <strong class="font-medium text-on-surface">‘Sometimes Important’</strong>, and <strong class="font-medium text-on-surface">‘Not as Important’</strong>.
</p>
</div>
</li>
<!-- Step 3 -->
<li class="flex items-start gap-space-sm">
<div class="w-6 h-6 rounded-full bg-surface-container-high text-tertiary font-label-md text-label-md flex items-center justify-center shrink-0 mt-0.5 font-semibold">
3
</div>
<div class="flex flex-col">
<h4 class="font-label-lg text-label-lg text-on-surface font-semibold">Distill your top 5</h4>
<p class="font-body-sm text-body-sm text-on-surface-variant leading-relaxed mt-0.5">
Narrow down to your core top 5 values to illuminate and guide your daily decisions with clarity.
</p>
</div>
</li>
</ol>
<div class="bg-surface-container-low rounded-lg p-space-sm px-space-md border border-outline-variant/30 flex items-center gap-space-sm mt-space-xs">
<span class="material-symbols-outlined text-[18px] text-tertiary shrink-0">spa</span>
<p class="font-body-sm text-body-sm text-on-surface-variant italic">
There are no right or wrong answers—only your authentic presence.
</p>
</div>
<!-- Action inside sheet -->
<button class="mt-space-sm w-full py-3 px-6 rounded-full bg-primary text-on-primary font-label-lg text-label-lg shadow-sm active:bg-primary-container transition-all flex items-center justify-center gap-2" id="sheet-start-btn" type="button">
<span>Got it, let's begin</span>
<span class="material-symbols-outlined text-[18px]">arrow_forward</span>
</button>
</div>
</div>
</div>
<script>
(function() {
const openBtn = document.getElementById('open-sheet-btn');
const closeBtn = document.getElementById('close-sheet-btn');
const sheetContainer = document.getElementById('explanation-sheet-container');
const sheetBackdrop = document.getElementById('sheet-backdrop');
const sheetPanel = document.getElementById('sheet-panel');
const sheetStartBtn = document.getElementById('sheet-start-btn');
const startBtn = document.getElementById('start-btn');
function openSheet() {
sheetContainer.classList.remove('pointer-events-none');
sheetContainer.classList.add('pointer-events-auto', 'opacity-100');
sheetContainer.classList.remove('opacity-0');
sheetBackdrop.classList.remove('opacity-0');
sheetBackdrop.classList.add('opacity-100');
sheetPanel.classList.remove('translate-y-full');
sheetPanel.classList.add('translate-y-0');
document.body.style.overflow = 'hidden';
}
function closeSheet() {
sheetBackdrop.classList.remove('opacity-100');
sheetBackdrop.classList.add('opacity-0');
sheetPanel.classList.remove('translate-y-0');
sheetPanel.classList.add('translate-y-full');
setTimeout(() => {
sheetContainer.classList.remove('pointer-events-auto', 'opacity-100');
sheetContainer.classList.add('pointer-events-none', 'opacity-0');
document.body.style.overflow = '';
}, 300);
}
if (openBtn) openBtn.addEventListener('click', openSheet);
if (closeBtn) closeBtn.addEventListener('click', closeSheet);
if (sheetBackdrop) sheetBackdrop.addEventListener('click', closeSheet);
if (sheetStartBtn) {
sheetStartBtn.addEventListener('click', () => {
closeSheet();
if (startBtn) {
startBtn.click();
}
});
}
if (startBtn) {
startBtn.addEventListener('click', () => {
startBtn.style.opacity = '0.85';
setTimeout(() => {
startBtn.style.opacity = '1';
}, 150);
});
}
// Keyboard support (Escape to close)
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !sheetContainer.classList.contains('pointer-events-none')) {
closeSheet();
}
});
})();
</script></main>
</body></html>
+16
View File
@@ -0,0 +1,16 @@
{
"name": "projects/9343387512370733994/screens/e3940f39a073450299cb3024914064c6",
"title": "Values Sort \u2014 Welcome (with Guide Drawer)",
"screenshot": {
"name": "projects/9343387512370733994/files/13321085336226512739",
"downloadUrl": "https://lh3.googleusercontent.com/aida/AEtjO1XxoE8CbYmmotxXpSsZdC3wUE-E_xSas8nAxp_8GIUsaVeT2TjBeYVAwZM64nKp2KiGgcv4OGIq2noOJzxbWuzc-L2mitY9iU_fmiwGqgff0u7RpyjXlC0eRZlhvcRybK_E_dFQ_b8Q3GBOP06crg0MlaWtIPMS9WT-WIwmui7yJkqcEDsjlJlU8reI8HinfJtzvxK1gfOfEWbLDV5gPc7T_ypo6WR-fM3lZ35FSrkH4NQoX2e-d1-DlO0"
},
"htmlCode": {
"name": "projects/9343387512370733994/files/24632818105687442",
"downloadUrl": "https://contribution.usercontent.google.com/download?c=CgthaWRhX2NvZGVmeBJ7Eh1hcHBfY29tcGFuaW9uX2dlbmVyYXRlZF9maWxlcxpaCiVodG1sXzAwMDY1YmIyNTQwYmQ5N2IwMjNiZTk5NjE2MDEzZDgzEgsSBxD_iY311x0YAZIBIwoKcHJvamVjdF9pZBIVQhM5MzQzMzg3NTEyMzcwNzMzOTk0&filename=&opi=89354086",
"mimeType": "text/html"
},
"width": "780",
"height": "1768",
"deviceType": "MOBILE"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+53 -40
View File
@@ -1,66 +1,79 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:myapp/main.dart';
import 'package:myapp/models/item.dart';
import 'package:myapp/services/item_repository.dart';
import 'package:myapp/models/value_card.dart';
import 'package:myapp/models/value_pile.dart';
import 'package:myapp/providers/values_sort_provider.dart';
import 'package:myapp/services/storage_service.dart';
import 'package:myapp/utils/constants.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
GoogleFonts.config.allowRuntimeFetching = false;
Future<void> pumpApp(WidgetTester tester) async {
testWidgets('splash shows Values Sort then opens welcome', (tester) async {
SharedPreferences.setMockInitialValues({});
final storage = StorageService();
await storage.init();
await tester.pumpWidget(
MyApp(
storage: storage,
repository: ItemRepository(),
),
);
}
testWidgets('splash shows the app name then opens Home', (tester) async {
await pumpApp(tester);
await tester.pumpWidget(MyApp(storage: storage));
expect(find.text(kAppName), findsWidgets);
await tester.pump(kSplashDuration);
await tester.pump(const Duration(milliseconds: 450));
await tester.pump(const Duration(milliseconds: 350));
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 200));
expect(find.text('Home'), findsWidgets);
expect(find.text('Featured items'), findsOneWidget);
expect(find.text('Search'), findsOneWidget);
expect(find.text('Profile'), findsOneWidget);
expect(find.text('Settings'), findsOneWidget);
expect(find.text('Begin Reflection'), findsOneWidget);
expect(find.textContaining('Discover what matters'), findsOneWidget);
});
testWidgets('tapping a list card opens the detail screen', (tester) async {
await pumpApp(tester);
await tester.pump(kSplashDuration);
await tester.pump(const Duration(milliseconds: 450));
await tester.pump(const Duration(milliseconds: 350));
test('ValueCard and ValuePile helpers', () {
final card = ValueCard.fromJson({
'id': 'v1',
'title': 'Courage',
'description': 'Acting despite fear.',
'domain': 'Agency',
});
expect(card.title, 'Courage');
expect(ValuePile.mostImportant.rank, 5);
expect(ValuePile.notImportant.shortLabel, 'Not');
});
await tester.tap(find.text('Material 3 surfaces'));
testWidgets('sort progress persists across controller reloads', (tester) async {
SharedPreferences.setMockInitialValues({});
final storage = StorageService();
await storage.init();
const seed = [
ValueCard(
id: 'v001',
title: 'Acceptance',
description: 'To be accepted as I am',
),
ValueCard(
id: 'v002',
title: 'Accuracy',
description: 'To be accurate in my opinions and beliefs',
),
];
final first = ValuesSortController(storage: storage);
await first.load(seedDeck: seed);
first.beginPractice();
final cardId = first.currentCard!.id;
first.assignCurrent(ValuePile.mostImportant);
await tester.pump();
await tester.pump(const Duration(milliseconds: 400));
expect(find.text('Details'), findsOneWidget);
expect(find.text('Back to list'), findsOneWidget);
});
expect(storage.sortSessionJson, isNotNull);
test('Item.fromJson maps a JSONPlaceholder post', () {
final item = Item.fromJson({
'id': 3,
'title': 'hello world',
'body': 'First sentence. Second sentence.',
});
first.dispose();
expect(item.id, 'remote-3');
expect(item.title, 'Hello world');
expect(item.subtitle, 'First sentence.');
expect(item.category, ItemCategory.lifestyle);
final second = ValuesSortController(storage: storage);
await second.load(seedDeck: seed);
expect(second.stage, SortStage.sorting);
expect(second.assignments[cardId], ValuePile.mostImportant);
expect(second.sortedCount, 1);
second.dispose();
});
}