docs: document focus flow flutter app

This commit is contained in:
Ashley Venn 2026-06-29 21:27:34 -07:00
parent fc1d100b1f
commit f985b3ee84
28 changed files with 410 additions and 0 deletions

View file

@ -21,6 +21,7 @@ linter:
# `// ignore_for_file: name_of_lint` syntax on the line or in the file # `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint. # producing the lint.
rules: rules:
public_member_api_docs: true
# avoid_print: false # Uncomment to disable the `avoid_print` rule # avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule

View file

@ -7,6 +7,7 @@ import '../controllers/scheduler_command_controller.dart';
import '../controllers/scheduler_read_controller.dart'; import '../controllers/scheduler_read_controller.dart';
import '../controllers/today_screen_controller.dart'; import '../controllers/today_screen_controller.dart';
/// Wires the Flutter demo UI to in-memory scheduler application use cases.
class DemoSchedulerComposition { class DemoSchedulerComposition {
DemoSchedulerComposition._({ DemoSchedulerComposition._({
required this.store, required this.store,
@ -17,19 +18,37 @@ class DemoSchedulerComposition {
required this.readAt, required this.readAt,
}); });
/// Fixed owner used by the seeded demo data.
static const ownerId = 'owner-1'; static const ownerId = 'owner-1';
/// Fixed owner time zone used by the seeded demo data.
static const timeZoneId = 'UTC'; static const timeZoneId = 'UTC';
/// Fixed project id used by the seeded demo data.
static const projectId = 'home'; static const projectId = 'home';
/// In-memory application store backing the demo.
final InMemoryApplicationUnitOfWork store; final InMemoryApplicationUnitOfWork store;
/// Query object used to read the Today state.
final GetTodayStateQuery todayQuery; final GetTodayStateQuery todayQuery;
/// Management use cases used by backlog-facing UI surfaces.
final V1ApplicationManagementUseCases managementUseCases; final V1ApplicationManagementUseCases managementUseCases;
/// Command use cases used by interactive UI controls.
final V1ApplicationCommandUseCases commandUseCases; final V1ApplicationCommandUseCases commandUseCases;
/// Local date represented by the seeded demo.
final CivilDate date; final CivilDate date;
/// Stable read clock used for deterministic demo operations.
final DateTime readAt; final DateTime readAt;
/// Human-readable label for [date].
String get dateLabel => formatDateLabel(date); String get dateLabel => formatDateLabel(date);
/// Creates a deterministic seeded composition for the demo app and tests.
factory DemoSchedulerComposition.seeded({ factory DemoSchedulerComposition.seeded({
Clock? clock, Clock? clock,
CivilDate? selectedDate, CivilDate? selectedDate,
@ -87,6 +106,7 @@ class DemoSchedulerComposition {
); );
} }
/// Creates the controller used by the compact Today screen mockup.
TodayScreenController createTodayScreenController() { TodayScreenController createTodayScreenController() {
return TodayScreenController( return TodayScreenController(
read: () { read: () {
@ -101,6 +121,7 @@ class DemoSchedulerComposition {
); );
} }
/// Creates a generic read controller for Today-state panes.
UiReadController<TodayState> createTodayController() { UiReadController<TodayState> createTodayController() {
return ApplicationReadController<TodayState>( return ApplicationReadController<TodayState>(
read: () { read: () {
@ -118,6 +139,7 @@ class DemoSchedulerComposition {
); );
} }
/// Creates a generic read controller for backlog panes.
UiReadController<BacklogQueryResult> createBacklogController() { UiReadController<BacklogQueryResult> createBacklogController() {
return ApplicationReadController<BacklogQueryResult>( return ApplicationReadController<BacklogQueryResult>(
read: () { read: () {
@ -132,6 +154,7 @@ class DemoSchedulerComposition {
); );
} }
/// Creates the command controller used by interactive scheduler controls.
SchedulerCommandController createCommandController({ SchedulerCommandController createCommandController({
required ReadRefresh refreshReads, required ReadRefresh refreshReads,
}) { }) {
@ -143,6 +166,7 @@ class DemoSchedulerComposition {
); );
} }
/// Creates an application operation context for the supplied [operationId].
ApplicationOperationContext context(String operationId) { ApplicationOperationContext context(String operationId) {
return ApplicationOperationContext.start( return ApplicationOperationContext.start(
operationId: operationId, operationId: operationId,
@ -155,6 +179,7 @@ class DemoSchedulerComposition {
); );
} }
/// Formats a civil date as a long month/day/year label.
static String formatDateLabel(CivilDate date) { static String formatDateLabel(CivilDate date) {
const months = [ const months = [
'January', 'January',

View file

@ -15,9 +15,12 @@ import '../widgets/timeline/timeline_view.dart';
import '../widgets/top_bar.dart'; import '../widgets/top_bar.dart';
import 'demo_scheduler_composition.dart'; import 'demo_scheduler_composition.dart';
/// Root Material app for the FocusFlow desktop UI.
class FocusFlowApp extends StatelessWidget { class FocusFlowApp extends StatelessWidget {
/// Creates a FocusFlow app from an application composition.
const FocusFlowApp({required this.composition, super.key}); const FocusFlowApp({required this.composition, super.key});
/// Factory and controller bundle used by the app tree.
final DemoSchedulerComposition composition; final DemoSchedulerComposition composition;
@override @override
@ -31,9 +34,12 @@ class FocusFlowApp extends StatelessWidget {
} }
} }
/// Stateful home screen that owns the Today controller lifecycle.
class FocusFlowHome extends StatefulWidget { class FocusFlowHome extends StatefulWidget {
/// Creates the FocusFlow home screen from an application composition.
const FocusFlowHome({required this.composition, super.key}); const FocusFlowHome({required this.composition, super.key});
/// Factory and controller bundle used by the home screen.
final DemoSchedulerComposition composition; final DemoSchedulerComposition composition;
@override @override

View file

@ -4,37 +4,55 @@ library;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:scheduler_core/scheduler_core.dart'; import 'package:scheduler_core/scheduler_core.dart';
/// Builds an application operation context for a UI command operation.
typedef OperationContextFactory = typedef OperationContextFactory =
ApplicationOperationContext Function(String operationId); ApplicationOperationContext Function(String operationId);
/// Refreshes any reads that should reflect a completed command.
typedef ReadRefresh = Future<void> Function(); typedef ReadRefresh = Future<void> Function();
/// Base state for scheduler command execution in the UI.
sealed class SchedulerCommandState { sealed class SchedulerCommandState {
/// Creates a scheduler command state.
const SchedulerCommandState(); const SchedulerCommandState();
} }
/// Indicates that no scheduler command is currently running.
class SchedulerCommandIdle extends SchedulerCommandState { class SchedulerCommandIdle extends SchedulerCommandState {
/// Creates an idle command state.
const SchedulerCommandIdle(); const SchedulerCommandIdle();
} }
/// Indicates that a scheduler command is currently running.
class SchedulerCommandRunning extends SchedulerCommandState { class SchedulerCommandRunning extends SchedulerCommandState {
/// Creates a running command state with a user-facing [label].
const SchedulerCommandRunning(this.label); const SchedulerCommandRunning(this.label);
/// User-facing description of the command in progress.
final String label; final String label;
} }
/// Indicates that a scheduler command completed successfully.
class SchedulerCommandSuccess extends SchedulerCommandState { class SchedulerCommandSuccess extends SchedulerCommandState {
/// Creates a success command state with a user-facing [message].
const SchedulerCommandSuccess(this.message); const SchedulerCommandSuccess(this.message);
/// User-facing success message.
final String message; final String message;
} }
/// Indicates that a scheduler command failed.
class SchedulerCommandFailure extends SchedulerCommandState { class SchedulerCommandFailure extends SchedulerCommandState {
/// Creates a failure state from an application [failure] or unexpected [error].
const SchedulerCommandFailure({this.failure, this.error}); const SchedulerCommandFailure({this.failure, this.error});
/// Typed application failure returned by the scheduler core.
final ApplicationFailure? failure; final ApplicationFailure? failure;
/// Unexpected error thrown while running the command.
final Object? error; final Object? error;
/// Stable label for display and diagnostics.
String get codeLabel { String get codeLabel {
final typed = failure; final typed = failure;
if (typed != null) { if (typed != null) {
@ -44,7 +62,9 @@ class SchedulerCommandFailure extends SchedulerCommandState {
} }
} }
/// Runs scheduler write commands and exposes command state to widgets.
class SchedulerCommandController extends ChangeNotifier { class SchedulerCommandController extends ChangeNotifier {
/// Creates a command controller from scheduler use cases and UI callbacks.
SchedulerCommandController({ SchedulerCommandController({
required this.commands, required this.commands,
required this.contextFor, required this.contextFor,
@ -52,15 +72,24 @@ class SchedulerCommandController extends ChangeNotifier {
required this.refreshReads, required this.refreshReads,
}); });
/// Scheduler write use cases invoked by this controller.
final V1ApplicationCommandUseCases commands; final V1ApplicationCommandUseCases commands;
/// Factory for command-scoped application operation contexts.
final OperationContextFactory contextFor; final OperationContextFactory contextFor;
/// Local date commands should operate against.
final CivilDate localDate; final CivilDate localDate;
/// Refresh callback invoked after commands that mutate read state.
final ReadRefresh refreshReads; final ReadRefresh refreshReads;
var _sequence = 0; var _sequence = 0;
SchedulerCommandState _state = const SchedulerCommandIdle(); SchedulerCommandState _state = const SchedulerCommandIdle();
/// Current command execution state.
SchedulerCommandState get state => _state; SchedulerCommandState get state => _state;
/// Captures [title] as a backlog task.
Future<void> quickCaptureToBacklog(String title) async { Future<void> quickCaptureToBacklog(String title) async {
await _run( await _run(
label: 'Capturing task', label: 'Capturing task',
@ -76,6 +105,7 @@ class SchedulerCommandController extends ChangeNotifier {
); );
} }
/// Schedules a backlog task into the next available slot.
Future<void> scheduleBacklogItem({ Future<void> scheduleBacklogItem({
required String taskId, required String taskId,
required int durationMinutes, required int durationMinutes,
@ -97,6 +127,7 @@ class SchedulerCommandController extends ChangeNotifier {
); );
} }
/// Completes a planned flexible task from the Today view.
Future<void> completeFlexibleTask({ Future<void> completeFlexibleTask({
required String taskId, required String taskId,
DateTime? expectedUpdatedAt, DateTime? expectedUpdatedAt,

View file

@ -4,43 +4,66 @@ library;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:scheduler_core/scheduler_core.dart'; import 'package:scheduler_core/scheduler_core.dart';
/// Reads an application value for a UI surface.
typedef ApplicationReader<T> = Future<ApplicationResult<T>> Function(); typedef ApplicationReader<T> = Future<ApplicationResult<T>> Function();
/// Determines whether a successful read should be rendered as empty state.
typedef EmptyPredicate<T> = bool Function(T value); typedef EmptyPredicate<T> = bool Function(T value);
/// Shared interface for read controllers consumed by UI widgets.
abstract class UiReadController<T> extends ChangeNotifier { abstract class UiReadController<T> extends ChangeNotifier {
/// Current read state.
SchedulerReadState<T> get state; SchedulerReadState<T> get state;
/// Loads the backing value.
Future<void> load(); Future<void> load();
/// Retries the latest read.
Future<void> retry(); Future<void> retry();
} }
/// Base state for scheduler read operations.
sealed class SchedulerReadState<T> { sealed class SchedulerReadState<T> {
/// Creates a scheduler read state.
const SchedulerReadState(); const SchedulerReadState();
} }
/// Indicates that a scheduler read is in progress.
class SchedulerReadLoading<T> extends SchedulerReadState<T> { class SchedulerReadLoading<T> extends SchedulerReadState<T> {
/// Creates a loading read state.
const SchedulerReadLoading(); const SchedulerReadLoading();
} }
/// Indicates that a scheduler read returned renderable data.
class SchedulerReadData<T> extends SchedulerReadState<T> { class SchedulerReadData<T> extends SchedulerReadState<T> {
/// Creates a data state from [value].
const SchedulerReadData(this.value); const SchedulerReadData(this.value);
/// Value returned by the read operation.
final T value; final T value;
} }
/// Indicates that a scheduler read succeeded but has no primary content.
class SchedulerReadEmpty<T> extends SchedulerReadState<T> { class SchedulerReadEmpty<T> extends SchedulerReadState<T> {
/// Creates an empty state that still carries the read [value].
const SchedulerReadEmpty(this.value); const SchedulerReadEmpty(this.value);
/// Value returned by the read operation.
final T value; final T value;
} }
/// Indicates that a scheduler read failed.
class SchedulerReadFailure<T> extends SchedulerReadState<T> { class SchedulerReadFailure<T> extends SchedulerReadState<T> {
/// Creates a failure state from an application [failure] or unexpected [error].
const SchedulerReadFailure({this.failure, this.error}); const SchedulerReadFailure({this.failure, this.error});
/// Typed application failure returned by the scheduler core.
final ApplicationFailure? failure; final ApplicationFailure? failure;
/// Unexpected error thrown while running the read.
final Object? error; final Object? error;
/// Stable label for display and diagnostics.
String get codeLabel { String get codeLabel {
final typed = failure; final typed = failure;
if (typed != null) { if (typed != null) {
@ -50,17 +73,24 @@ class SchedulerReadFailure<T> extends SchedulerReadState<T> {
} }
} }
/// Default read controller backed by an [ApplicationReader].
class ApplicationReadController<T> extends UiReadController<T> { class ApplicationReadController<T> extends UiReadController<T> {
/// Creates a controller from a read callback and empty-state predicate.
ApplicationReadController({required this.read, required this.isEmpty}); ApplicationReadController({required this.read, required this.isEmpty});
/// Callback used to load the application value.
final ApplicationReader<T> read; final ApplicationReader<T> read;
/// Predicate used to classify a successful value as empty or populated.
final EmptyPredicate<T> isEmpty; final EmptyPredicate<T> isEmpty;
SchedulerReadState<T> _state = SchedulerReadLoading<T>(); SchedulerReadState<T> _state = SchedulerReadLoading<T>();
/// Current read state.
@override @override
SchedulerReadState<T> get state => _state; SchedulerReadState<T> get state => _state;
/// Loads the current value and updates [state].
@override @override
Future<void> load() async { Future<void> load() async {
_setState(SchedulerReadLoading<T>()); _setState(SchedulerReadLoading<T>());
@ -82,6 +112,7 @@ class ApplicationReadController<T> extends UiReadController<T> {
} }
} }
/// Retries by running [load] again.
@override @override
Future<void> retry() => load(); Future<void> retry() => load();

View file

@ -6,44 +6,63 @@ import 'package:scheduler_core/scheduler_core.dart';
import '../models/today_screen_models.dart'; import '../models/today_screen_models.dart';
/// Reads Today state from the scheduler application layer.
typedef TodayStateReader = Future<ApplicationResult<TodayState>> Function(); typedef TodayStateReader = Future<ApplicationResult<TodayState>> Function();
/// Base state for the compact Today screen.
sealed class TodayScreenState { sealed class TodayScreenState {
/// Creates a Today screen state.
const TodayScreenState(); const TodayScreenState();
} }
/// Indicates that the Today screen is loading.
class TodayScreenLoading extends TodayScreenState { class TodayScreenLoading extends TodayScreenState {
/// Creates a loading Today screen state.
const TodayScreenLoading(); const TodayScreenLoading();
} }
/// Indicates that the Today screen has renderable timeline data.
class TodayScreenReady extends TodayScreenState { class TodayScreenReady extends TodayScreenState {
/// Creates a ready state with mapped screen [data].
const TodayScreenReady(this.data); const TodayScreenReady(this.data);
/// Presentation model for the Today screen.
final TodayScreenData data; final TodayScreenData data;
} }
/// Indicates that the Today screen loaded successfully with no cards.
class TodayScreenEmpty extends TodayScreenState { class TodayScreenEmpty extends TodayScreenState {
/// Creates an empty Today screen state.
const TodayScreenEmpty(); const TodayScreenEmpty();
} }
/// Indicates that the Today screen failed to load.
class TodayScreenFailure extends TodayScreenState { class TodayScreenFailure extends TodayScreenState {
/// Creates a failure state with a displayable [message].
const TodayScreenFailure(this.message); const TodayScreenFailure(this.message);
/// User-facing failure message or code.
final String message; final String message;
} }
/// Loads Today screen data and tracks selected timeline cards.
class TodayScreenController extends ChangeNotifier { class TodayScreenController extends ChangeNotifier {
/// Creates a Today screen controller from a read callback.
TodayScreenController({required this.read}); TodayScreenController({required this.read});
/// Callback used to read Today state.
final TodayStateReader read; final TodayStateReader read;
TodayScreenState _state = const TodayScreenLoading(); TodayScreenState _state = const TodayScreenLoading();
TimelineCardModel? _selectedCard; TimelineCardModel? _selectedCard;
/// Current Today screen loading/data/failure state.
TodayScreenState get state => _state; TodayScreenState get state => _state;
/// Currently selected timeline card, if a modal should be visible.
TimelineCardModel? get selectedCard => _selectedCard; TimelineCardModel? get selectedCard => _selectedCard;
/// Loads Today state and maps it into presentation data.
Future<void> load() async { Future<void> load() async {
_state = const TodayScreenLoading(); _state = const TodayScreenLoading();
notifyListeners(); notifyListeners();
@ -67,6 +86,7 @@ class TodayScreenController extends ChangeNotifier {
} }
} }
/// Selects [card] when the card allows selection.
void selectCard(TimelineCardModel card) { void selectCard(TimelineCardModel card) {
if (!card.isSelectable) { if (!card.isSelectable) {
return; return;
@ -75,6 +95,7 @@ class TodayScreenController extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
/// Clears the selected timeline card.
void clearSelection() { void clearSelection() {
if (_selectedCard == null) { if (_selectedCard == null) {
return; return;

View file

@ -9,6 +9,7 @@ import 'app/focus_flow_app.dart';
export 'app/demo_scheduler_composition.dart'; export 'app/demo_scheduler_composition.dart';
export 'app/focus_flow_app.dart'; export 'app/focus_flow_app.dart';
/// Starts the FocusFlow app with the seeded demo scheduler composition.
void main() { void main() {
runApp(FocusFlowApp(composition: DemoSchedulerComposition.seeded())); runApp(FocusFlowApp(composition: DemoSchedulerComposition.seeded()));
} }

View file

@ -3,32 +3,54 @@ library;
import 'package:scheduler_core/scheduler_core.dart'; import 'package:scheduler_core/scheduler_core.dart';
/// Presentation model for the next-required-task banner.
class RequiredBannerModel { class RequiredBannerModel {
/// Creates a required banner model.
const RequiredBannerModel({required this.title, required this.timeText}); const RequiredBannerModel({required this.title, required this.timeText});
/// Title of the next required task.
final String title; final String title;
/// Display-ready time for the next required task.
final String timeText; final String timeText;
} }
/// Visible timeline range expressed as minutes since midnight.
class TimelineRangeModel { class TimelineRangeModel {
/// Creates a timeline range model.
const TimelineRangeModel({ const TimelineRangeModel({
required this.startMinutes, required this.startMinutes,
required this.endMinutes, required this.endMinutes,
}); });
/// First visible minute from midnight.
final int startMinutes; final int startMinutes;
/// Last visible minute from midnight.
final int endMinutes; final int endMinutes;
} }
/// Visual category used to style timeline cards.
enum TaskVisualKind { enum TaskVisualKind {
/// Flexible planned work.
flexible, flexible,
/// Critical required task.
required, required,
/// Inflexible appointment-style task.
appointment, appointment,
/// Intentional free time.
freeSlot, freeSlot,
/// Completed surprise task.
completedSurprise, completedSurprise,
} }
/// Presentation model for the compact Today screen.
class TodayScreenData { class TodayScreenData {
/// Creates Today screen data.
const TodayScreenData({ const TodayScreenData({
required this.dateLabel, required this.dateLabel,
required this.timelineRange, required this.timelineRange,
@ -36,6 +58,7 @@ class TodayScreenData {
this.requiredBanner, this.requiredBanner,
}); });
/// Creates an empty Today screen model for [dateLabel].
factory TodayScreenData.empty({required String dateLabel}) { factory TodayScreenData.empty({required String dateLabel}) {
return TodayScreenData( return TodayScreenData(
dateLabel: dateLabel, dateLabel: dateLabel,
@ -44,6 +67,7 @@ class TodayScreenData {
); );
} }
/// Maps scheduler core [TodayState] into UI presentation data.
factory TodayScreenData.fromTodayState(TodayState state) { factory TodayScreenData.fromTodayState(TodayState state) {
final cards = [ final cards = [
for (final item in state.timelineItems) TimelineCardModel.fromItem(item), for (final item in state.timelineItems) TimelineCardModel.fromItem(item),
@ -62,18 +86,28 @@ class TodayScreenData {
); );
} }
/// Default visible range for the compact timeline.
static const defaultRange = TimelineRangeModel( static const defaultRange = TimelineRangeModel(
startMinutes: 16 * 60, startMinutes: 16 * 60,
endMinutes: 20 * 60 + 45, endMinutes: 20 * 60 + 45,
); );
/// Display-ready date label.
final String dateLabel; final String dateLabel;
/// Optional next-required-task banner model.
final RequiredBannerModel? requiredBanner; final RequiredBannerModel? requiredBanner;
/// Visible timeline range.
final TimelineRangeModel timelineRange; final TimelineRangeModel timelineRange;
/// Timeline cards to render.
final List<TimelineCardModel> cards; final List<TimelineCardModel> cards;
} }
/// Presentation model for a single compact timeline card.
class TimelineCardModel { class TimelineCardModel {
/// Creates a timeline card model.
const TimelineCardModel({ const TimelineCardModel({
required this.id, required this.id,
required this.title, required this.title,
@ -91,6 +125,7 @@ class TimelineCardModel {
this.durationMinutes, this.durationMinutes,
}); });
/// Maps a scheduler timeline item into card presentation data.
factory TimelineCardModel.fromItem(TodayTimelineItem item) { factory TimelineCardModel.fromItem(TodayTimelineItem item) {
final start = item.start; final start = item.start;
final end = item.end; final end = item.end;
@ -121,21 +156,49 @@ class TimelineCardModel {
); );
} }
/// Stable card identifier.
final String id; final String id;
/// Primary task title shown on the card.
final String title; final String title;
/// Short task type label.
final String typeLabel; final String typeLabel;
/// Secondary line shown under the title.
final String subtitle; final String subtitle;
/// Display-ready time or duration text.
final String timeText; final String timeText;
/// Card start position in minutes since midnight.
final int startMinutes; final int startMinutes;
/// Card end position in minutes since midnight.
final int endMinutes; final int endMinutes;
/// Optional task duration in minutes.
final int? durationMinutes; final int? durationMinutes;
/// Visual category used for styling.
final TaskVisualKind visualKind; final TaskVisualKind visualKind;
/// Reward icon token from the scheduler core.
final TimelineRewardIconToken rewardIconToken; final TimelineRewardIconToken rewardIconToken;
/// Difficulty icon token from the scheduler core.
final TimelineDifficultyIconToken difficultyIconToken; final TimelineDifficultyIconToken difficultyIconToken;
/// Whether the card represents completed work.
final bool isCompleted; final bool isCompleted;
/// Whether tapping the card should open its selection modal.
final bool isSelectable; final bool isSelectable;
/// Whether the card should show compact quick-action controls.
final bool showsQuickActions; final bool showsQuickActions;
/// Accessibility and modal label for the reward icon.
String get rewardLabel { String get rewardLabel {
return switch (rewardIconToken) { return switch (rewardIconToken) {
TimelineRewardIconToken.notSet => 'Reward not set', TimelineRewardIconToken.notSet => 'Reward not set',
@ -147,6 +210,7 @@ class TimelineCardModel {
}; };
} }
/// Accessibility and modal label for the effort icon.
String get effortLabel { String get effortLabel {
return switch (difficultyIconToken) { return switch (difficultyIconToken) {
TimelineDifficultyIconToken.notSet => 'Effort not set', TimelineDifficultyIconToken.notSet => 'Effort not set',

View file

@ -5,7 +5,9 @@ import 'package:flutter/material.dart';
import 'focus_flow_tokens.dart'; import 'focus_flow_tokens.dart';
/// Theme factory for the FocusFlow Flutter app.
abstract final class FocusFlowTheme { abstract final class FocusFlowTheme {
/// Builds the dark Material theme used by the desktop app.
static ThemeData dark() { static ThemeData dark() {
final scheme = ColorScheme.fromSeed( final scheme = ColorScheme.fromSeed(
seedColor: FocusFlowTokens.accentMagenta, seedColor: FocusFlowTokens.accentMagenta,

View file

@ -3,48 +3,119 @@ library;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// Shared visual constants for the FocusFlow Flutter app.
abstract final class FocusFlowTokens { abstract final class FocusFlowTokens {
/// Root application background color.
static const appBackground = Color(0xFF070811); static const appBackground = Color(0xFF070811);
/// Border color for the app frame.
static const appFrameBorder = Color(0xFF3B3D4E); static const appFrameBorder = Color(0xFF3B3D4E);
/// Sidebar background color.
static const sidebarBackground = Color(0xFF090A14); static const sidebarBackground = Color(0xFF090A14);
/// Standard panel background color.
static const panelBackground = Color(0xFF11121D); static const panelBackground = Color(0xFF11121D);
/// Elevated panel background color.
static const elevatedPanel = Color(0xFF181724); static const elevatedPanel = Color(0xFF181724);
/// Translucent panel color for soft surfaces.
static const glassPanel = Color(0x662A1025); static const glassPanel = Color(0x662A1025);
/// Stronger translucent panel color for selected surfaces.
static const glassPanelStrong = Color(0x992A1025); static const glassPanelStrong = Color(0x992A1025);
/// Timeline grid line color.
static const gridLine = Color(0x243B3D4E); static const gridLine = Color(0x243B3D4E);
/// Timeline rail color.
static const rail = Color(0xFF7D7F91); static const rail = Color(0xFF7D7F91);
/// Subtle border color for controls and panels.
static const subtleBorder = Color(0xFF3A3A4A); static const subtleBorder = Color(0xFF3A3A4A);
/// Active navigation border color.
static const navBorder = Color(0xFFB72D82); static const navBorder = Color(0xFFB72D82);
/// Primary accent color.
static const accentMagenta = Color(0xFFFF5BA8); static const accentMagenta = Color(0xFFFF5BA8);
/// Primary text color.
static const textPrimary = Color(0xFFF7F2FA); static const textPrimary = Color(0xFFF7F2FA);
/// Muted text color.
static const textMuted = Color(0xFFBBB5C8); static const textMuted = Color(0xFFBBB5C8);
/// Faint text color.
static const textFaint = Color(0xFF8E889D); static const textFaint = Color(0xFF8E889D);
/// Text color for completed task labels.
static const completedText = Color(0xFF8F8A99); static const completedText = Color(0xFF8F8A99);
/// Accent color for flexible tasks.
static const flexibleGreen = Color(0xFF4BE064); static const flexibleGreen = Color(0xFF4BE064);
/// Accent color for required tasks.
static const requiredPink = Color(0xFFFF4E7D); static const requiredPink = Color(0xFFFF4E7D);
/// Accent color for appointment tasks.
static const appointmentBlue = Color(0xFF38A8FF); static const appointmentBlue = Color(0xFF38A8FF);
/// Accent color for rest and free-slot surfaces.
static const restPurple = Color(0xFFB45CFF); static const restPurple = Color(0xFFB45CFF);
/// Accent color for completed tasks.
static const completedGray = Color(0xFF777482); static const completedGray = Color(0xFF777482);
/// Warning accent color.
static const warningYellow = Color(0xFFFFCC66); static const warningYellow = Color(0xFFFFCC66);
/// Fixed sidebar width.
static const sidebarWidth = 250.0; static const sidebarWidth = 250.0;
/// Horizontal gutter for main app content.
static const mainGutter = 32.0; static const mainGutter = 32.0;
/// Width of the timeline time rail.
static const timelineRailWidth = 120.0; static const timelineRailWidth = 120.0;
/// Horizontal padding inside timeline cards.
static const cardHorizontalPadding = 18.0; static const cardHorizontalPadding = 18.0;
/// Border radius for the outer app frame.
static const appFrameRadius = 8.0; static const appFrameRadius = 8.0;
/// Border radius for navigation items.
static const navRadius = 8.0; static const navRadius = 8.0;
/// Border radius for required-task banners.
static const bannerRadius = 8.0; static const bannerRadius = 8.0;
/// Border radius for timeline cards.
static const cardRadius = 8.0; static const cardRadius = 8.0;
/// Border radius for task selection modals.
static const modalRadius = 8.0; static const modalRadius = 8.0;
/// Border radius for compact controls.
static const smallButtonRadius = 8.0; static const smallButtonRadius = 8.0;
/// Font size for page titles.
static const pageTitleSize = 40.0; static const pageTitleSize = 40.0;
/// Font size for timeline card titles.
static const cardTitleSize = 24.0; static const cardTitleSize = 24.0;
/// Font size for timeline card metadata.
static const cardMetaSize = 16.0; static const cardMetaSize = 16.0;
/// Font size for sidebar navigation labels.
static const sidebarNavSize = 18.0; static const sidebarNavSize = 18.0;
/// Font size for modal titles.
static const modalTitleSize = 26.0; static const modalTitleSize = 26.0;
/// Font size for modal action buttons.
static const modalButtonSize = 18.0; static const modalButtonSize = 18.0;
} }

View file

@ -4,7 +4,9 @@ library;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:scheduler_core/scheduler_core.dart'; import 'package:scheduler_core/scheduler_core.dart';
/// Maps scheduler domain tokens to Flutter colors and icons.
abstract final class SchedulerVisualTokens { abstract final class SchedulerVisualTokens {
/// Returns the color associated with a project color [token].
static Color projectColor(String token) { static Color projectColor(String token) {
return switch (token) { return switch (token) {
'home' => const Color(0xFF68B0AB), 'home' => const Color(0xFF68B0AB),
@ -13,6 +15,7 @@ abstract final class SchedulerVisualTokens {
}; };
} }
/// Returns the background color associated with a timeline background token.
static Color backgroundColor( static Color backgroundColor(
TimelineBackgroundToken token, TimelineBackgroundToken token,
ColorScheme scheme, ColorScheme scheme,
@ -27,6 +30,7 @@ abstract final class SchedulerVisualTokens {
}; };
} }
/// Returns the icon associated with a task [type].
static IconData taskIcon(TaskType type) { static IconData taskIcon(TaskType type) {
return switch (type) { return switch (type) {
TaskType.flexible => Icons.task_alt, TaskType.flexible => Icons.task_alt,
@ -38,6 +42,7 @@ abstract final class SchedulerVisualTokens {
}; };
} }
/// Returns the icon associated with a reward token.
static IconData rewardIcon(TimelineRewardIconToken token) { static IconData rewardIcon(TimelineRewardIconToken token) {
return switch (token) { return switch (token) {
TimelineRewardIconToken.notSet => Icons.remove_circle_outline, TimelineRewardIconToken.notSet => Icons.remove_circle_outline,
@ -49,6 +54,7 @@ abstract final class SchedulerVisualTokens {
}; };
} }
/// Returns the icon associated with a difficulty token.
static IconData difficultyIcon(TimelineDifficultyIconToken token) { static IconData difficultyIcon(TimelineDifficultyIconToken token) {
return switch (token) { return switch (token) {
TimelineDifficultyIconToken.notSet => Icons.remove_circle_outline, TimelineDifficultyIconToken.notSet => Icons.remove_circle_outline,
@ -60,6 +66,7 @@ abstract final class SchedulerVisualTokens {
}; };
} }
/// Returns the icon associated with a backlog staleness marker.
static IconData stalenessIcon(BacklogStalenessMarker marker) { static IconData stalenessIcon(BacklogStalenessMarker marker) {
return switch (marker) { return switch (marker) {
BacklogStalenessMarker.green => Icons.circle, BacklogStalenessMarker.green => Icons.circle,
@ -68,6 +75,7 @@ abstract final class SchedulerVisualTokens {
}; };
} }
/// Returns the color associated with a backlog staleness marker.
static Color stalenessColor( static Color stalenessColor(
BacklogStalenessMarker marker, BacklogStalenessMarker marker,
ColorScheme scheme, ColorScheme scheme,

View file

@ -5,10 +5,15 @@ import 'package:flutter/material.dart';
import '../theme/focus_flow_tokens.dart'; import '../theme/focus_flow_tokens.dart';
/// Top-level shell that frames the sidebar and main content area.
class AppShell extends StatelessWidget { class AppShell extends StatelessWidget {
/// Creates an app shell with a [sidebar] and main [child].
const AppShell({required this.sidebar, required this.child, super.key}); const AppShell({required this.sidebar, required this.child, super.key});
/// Sidebar widget displayed at the left edge of the app.
final Widget sidebar; final Widget sidebar;
/// Primary content widget displayed beside the sidebar.
final Widget child; final Widget child;
@override @override

View file

@ -9,14 +9,19 @@ import '../controllers/scheduler_read_controller.dart';
import 'read_state_view.dart'; import 'read_state_view.dart';
import 'schedule_components.dart'; import 'schedule_components.dart';
/// Read-driven pane that renders backlog content and command controls.
class BacklogPane extends StatelessWidget { class BacklogPane extends StatelessWidget {
/// Creates a backlog pane from a read [controller].
const BacklogPane({ const BacklogPane({
required this.controller, required this.controller,
this.commandController, this.commandController,
super.key, super.key,
}); });
/// Controller that loads backlog data.
final UiReadController<BacklogQueryResult> controller; final UiReadController<BacklogQueryResult> controller;
/// Optional command controller for mutating backlog items.
final SchedulerCommandController? commandController; final SchedulerCommandController? commandController;
@override @override
@ -32,14 +37,19 @@ class BacklogPane extends StatelessWidget {
} }
} }
/// Populated backlog view for a loaded backlog query result.
class BacklogContent extends StatelessWidget { class BacklogContent extends StatelessWidget {
/// Creates backlog content for [result].
const BacklogContent({ const BacklogContent({
required this.result, required this.result,
this.commandController, this.commandController,
super.key, super.key,
}); });
/// Loaded backlog result to display.
final BacklogQueryResult result; final BacklogQueryResult result;
/// Optional command controller for capture and scheduling actions.
final SchedulerCommandController? commandController; final SchedulerCommandController? commandController;
@override @override
@ -73,9 +83,12 @@ class BacklogContent extends StatelessWidget {
} }
} }
/// Small form for quickly capturing a task into the backlog.
class QuickCaptureForm extends StatefulWidget { class QuickCaptureForm extends StatefulWidget {
/// Creates a quick capture form.
const QuickCaptureForm({required this.commandController, super.key}); const QuickCaptureForm({required this.commandController, super.key});
/// Command controller used to submit captured task titles.
final SchedulerCommandController commandController; final SchedulerCommandController commandController;
@override @override
@ -121,9 +134,12 @@ class _QuickCaptureFormState extends State<QuickCaptureForm> {
} }
} }
/// Displays the latest command state for backlog interactions.
class CommandStatusBanner extends StatelessWidget { class CommandStatusBanner extends StatelessWidget {
/// Creates a command status banner.
const CommandStatusBanner({required this.controller, super.key}); const CommandStatusBanner({required this.controller, super.key});
/// Command controller whose state should be rendered.
final SchedulerCommandController controller; final SchedulerCommandController controller;
@override @override

View file

@ -5,7 +5,9 @@ import 'package:flutter/material.dart';
import '../controllers/scheduler_read_controller.dart'; import '../controllers/scheduler_read_controller.dart';
/// Renders loading, empty, failure, and data states from a read controller.
class ReadStateView<T> extends StatelessWidget { class ReadStateView<T> extends StatelessWidget {
/// Creates a read-state view.
const ReadStateView({ const ReadStateView({
required this.controller, required this.controller,
required this.title, required this.title,
@ -15,10 +17,19 @@ class ReadStateView<T> extends StatelessWidget {
super.key, super.key,
}); });
/// Controller that owns the current read state.
final UiReadController<T> controller; final UiReadController<T> controller;
/// Name of the surface being loaded, used in failure copy.
final String title; final String title;
/// Title shown when the read succeeds with empty content.
final String emptyTitle; final String emptyTitle;
/// Message shown when the read succeeds with empty content.
final String emptyMessage; final String emptyMessage;
/// Builds the populated content for a read value.
final Widget Function(BuildContext context, T value) dataBuilder; final Widget Function(BuildContext context, T value) dataBuilder;
@override @override
@ -52,7 +63,9 @@ class ReadStateView<T> extends StatelessWidget {
} }
} }
/// Standard empty-state panel with optional surrounding content.
class EmptyStatePanel extends StatelessWidget { class EmptyStatePanel extends StatelessWidget {
/// Creates an empty-state panel.
const EmptyStatePanel({ const EmptyStatePanel({
required this.title, required this.title,
required this.message, required this.message,
@ -60,8 +73,13 @@ class EmptyStatePanel extends StatelessWidget {
super.key, super.key,
}); });
/// Primary empty-state title.
final String title; final String title;
/// Supporting empty-state message.
final String message; final String message;
/// Optional content shown alongside the empty-state panel.
final Widget? child; final Widget? child;
@override @override
@ -97,7 +115,9 @@ class EmptyStatePanel extends StatelessWidget {
} }
} }
/// Standard failure panel with a retry action.
class FailureStatePanel extends StatelessWidget { class FailureStatePanel extends StatelessWidget {
/// Creates a failure-state panel.
const FailureStatePanel({ const FailureStatePanel({
required this.title, required this.title,
required this.code, required this.code,
@ -106,9 +126,16 @@ class FailureStatePanel extends StatelessWidget {
super.key, super.key,
}); });
/// Primary failure title.
final String title; final String title;
/// Displayable failure code.
final String code; final String code;
/// Optional technical detail for unexpected errors.
final String? technicalDetail; final String? technicalDetail;
/// Callback invoked when the user retries the read.
final Future<void> Function() onRetry; final Future<void> Function() onRetry;
@override @override

View file

@ -6,9 +6,12 @@ import 'package:flutter/material.dart';
import '../models/today_screen_models.dart'; import '../models/today_screen_models.dart';
import '../theme/focus_flow_tokens.dart'; import '../theme/focus_flow_tokens.dart';
/// Banner that highlights the next required task, if one exists.
class RequiredBanner extends StatelessWidget { class RequiredBanner extends StatelessWidget {
/// Creates a required-task banner.
const RequiredBanner({this.model, super.key}); const RequiredBanner({this.model, super.key});
/// Optional required task model to render.
final RequiredBannerModel? model; final RequiredBannerModel? model;
@override @override

View file

@ -6,9 +6,12 @@ import 'package:scheduler_core/scheduler_core.dart';
import '../theme/scheduler_visual_tokens.dart'; import '../theme/scheduler_visual_tokens.dart';
/// Compact summary panel for timeline items.
class CompactPanel extends StatelessWidget { class CompactPanel extends StatelessWidget {
/// Creates a compact panel for [items].
const CompactPanel({required this.items, super.key}); const CompactPanel({required this.items, super.key});
/// Timeline items to display in compact form.
final List<TodayTimelineItem> items; final List<TodayTimelineItem> items;
@override @override
@ -28,10 +31,15 @@ class CompactPanel extends StatelessWidget {
} }
} }
/// Card row for a single Today timeline item.
class TimelineRow extends StatelessWidget { class TimelineRow extends StatelessWidget {
/// Creates a timeline row.
const TimelineRow({required this.item, this.onDone, super.key}); const TimelineRow({required this.item, this.onDone, super.key});
/// Timeline item to render.
final TodayTimelineItem item; final TodayTimelineItem item;
/// Optional completion callback for eligible tasks.
final Future<void> Function()? onDone; final Future<void> Function()? onDone;
@override @override
@ -85,10 +93,15 @@ class TimelineRow extends StatelessWidget {
} }
} }
/// Backlog row with optional scheduling controls.
class BacklogRow extends StatefulWidget { class BacklogRow extends StatefulWidget {
/// Creates a backlog row.
const BacklogRow({required this.item, this.onSchedule, super.key}); const BacklogRow({required this.item, this.onSchedule, super.key});
/// Backlog item to render.
final BacklogItemReadModel item; final BacklogItemReadModel item;
/// Optional callback that schedules the item for a duration in minutes.
final Future<void> Function(int durationMinutes)? onSchedule; final Future<void> Function(int durationMinutes)? onSchedule;
@override @override
@ -154,9 +167,12 @@ class _BacklogRowState extends State<BacklogRow> {
} }
} }
/// Icon marker for backlog item staleness.
class StalenessMarker extends StatelessWidget { class StalenessMarker extends StatelessWidget {
/// Creates a staleness marker.
const StalenessMarker({required this.marker, super.key}); const StalenessMarker({required this.marker, super.key});
/// Staleness marker value to render.
final BacklogStalenessMarker marker; final BacklogStalenessMarker marker;
@override @override
@ -168,9 +184,12 @@ class StalenessMarker extends StatelessWidget {
} }
} }
/// Notice banner for pending Today-state messages.
class NoticeBanner extends StatelessWidget { class NoticeBanner extends StatelessWidget {
/// Creates a notice banner.
const NoticeBanner({required this.message, super.key}); const NoticeBanner({required this.message, super.key});
/// Notice message to display.
final String message; final String message;
@override @override
@ -187,6 +206,7 @@ class NoticeBanner extends StatelessWidget {
} }
} }
/// Formats a scheduler timeline item as display-ready time text.
String timeText(TimelineItem item) { String timeText(TimelineItem item) {
final start = item.start; final start = item.start;
final end = item.end; final end = item.end;

View file

@ -5,7 +5,9 @@ import 'package:flutter/material.dart';
import '../theme/focus_flow_tokens.dart'; import '../theme/focus_flow_tokens.dart';
/// Static sidebar for primary FocusFlow navigation.
class Sidebar extends StatelessWidget { class Sidebar extends StatelessWidget {
/// Creates the FocusFlow sidebar.
const Sidebar({super.key}); const Sidebar({super.key});
@override @override

View file

@ -8,14 +8,19 @@ import '../theme/focus_flow_tokens.dart';
import 'timeline/difficulty_bars.dart'; import 'timeline/difficulty_bars.dart';
import 'timeline/reward_icon.dart'; import 'timeline/reward_icon.dart';
/// Modal shown when a selectable timeline task card is opened.
class TaskSelectionModal extends StatelessWidget { class TaskSelectionModal extends StatelessWidget {
/// Creates a task selection modal for [card].
const TaskSelectionModal({ const TaskSelectionModal({
required this.card, required this.card,
required this.onClose, required this.onClose,
super.key, super.key,
}); });
/// Card model whose details and actions should be displayed.
final TimelineCardModel card; final TimelineCardModel card;
/// Callback invoked when the modal should close.
final VoidCallback onClose; final VoidCallback onClose;
@override @override

View file

@ -6,7 +6,9 @@ import 'package:scheduler_core/scheduler_core.dart';
import '../../theme/focus_flow_tokens.dart'; import '../../theme/focus_flow_tokens.dart';
/// Five-bar effort indicator for a timeline task.
class DifficultyBars extends StatelessWidget { class DifficultyBars extends StatelessWidget {
/// Creates difficulty bars for a scheduler difficulty token.
const DifficultyBars({ const DifficultyBars({
required this.difficulty, required this.difficulty,
this.color = FocusFlowTokens.restPurple, this.color = FocusFlowTokens.restPurple,
@ -14,10 +16,16 @@ class DifficultyBars extends StatelessWidget {
super.key, super.key,
}); });
/// Difficulty token that determines how many bars are filled.
final TimelineDifficultyIconToken difficulty; final TimelineDifficultyIconToken difficulty;
/// Color used for filled bars.
final Color color; final Color color;
/// Fixed paint size for the indicator.
final Size size; final Size size;
/// Converts a timeline difficulty token to a filled bar count.
static int fillCount(TimelineDifficultyIconToken difficulty) { static int fillCount(TimelineDifficultyIconToken difficulty) {
return switch (difficulty) { return switch (difficulty) {
TimelineDifficultyIconToken.notSet => 0, TimelineDifficultyIconToken.notSet => 0,
@ -29,6 +37,7 @@ class DifficultyBars extends StatelessWidget {
}; };
} }
/// Converts a domain difficulty level to a filled bar count.
static int fillCountForLevel(DifficultyLevel difficulty) { static int fillCountForLevel(DifficultyLevel difficulty) {
return switch (difficulty) { return switch (difficulty) {
DifficultyLevel.notSet => 0, DifficultyLevel.notSet => 0,

View file

@ -5,14 +5,19 @@ import 'package:flutter/material.dart';
import '../../theme/focus_flow_tokens.dart'; import '../../theme/focus_flow_tokens.dart';
/// Sparkle-style reward indicator for a timeline task.
class RewardIcon extends StatelessWidget { class RewardIcon extends StatelessWidget {
/// Creates a reward icon.
const RewardIcon({ const RewardIcon({
this.color = FocusFlowTokens.accentMagenta, this.color = FocusFlowTokens.accentMagenta,
this.size = 24, this.size = 24,
super.key, super.key,
}); });
/// Fill color used by the icon painter.
final Color color; final Color color;
/// Square size of the painted icon.
final double size; final double size;
@override @override

View file

@ -8,10 +8,15 @@ import '../../theme/focus_flow_tokens.dart';
import 'difficulty_bars.dart'; import 'difficulty_bars.dart';
import 'reward_icon.dart'; import 'reward_icon.dart';
/// Interactive card that renders a scheduled task on the compact timeline.
class TaskTimelineCard extends StatelessWidget { class TaskTimelineCard extends StatelessWidget {
/// Creates a task timeline card.
const TaskTimelineCard({required this.card, required this.onTap, super.key}); const TaskTimelineCard({required this.card, required this.onTap, super.key});
/// Card model to render.
final TimelineCardModel card; final TimelineCardModel card;
/// Callback invoked when the card is tapped.
final VoidCallback onTap; final VoidCallback onTap;
@override @override

View file

@ -6,9 +6,12 @@ import 'package:flutter/material.dart';
import '../../theme/focus_flow_tokens.dart'; import '../../theme/focus_flow_tokens.dart';
import 'timeline_geometry.dart'; import 'timeline_geometry.dart';
/// Time labels and rail shown beside the compact timeline.
class TimelineAxis extends StatelessWidget { class TimelineAxis extends StatelessWidget {
/// Creates a timeline axis for [geometry].
const TimelineAxis({required this.geometry, super.key}); const TimelineAxis({required this.geometry, super.key});
/// Geometry used to position labels and tick marks.
final TimelineGeometry geometry; final TimelineGeometry geometry;
@override @override
@ -60,9 +63,12 @@ class TimelineAxis extends StatelessWidget {
} }
} }
/// Background grid aligned to timeline tick marks.
class TimelineGrid extends StatelessWidget { class TimelineGrid extends StatelessWidget {
/// Creates a timeline grid for [geometry].
const TimelineGrid({required this.geometry, super.key}); const TimelineGrid({required this.geometry, super.key});
/// Geometry used to position grid lines.
final TimelineGeometry geometry; final TimelineGeometry geometry;
@override @override

View file

@ -3,7 +3,9 @@ library;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// Converts timeline clock values into vertical pixel positions.
class TimelineGeometry { class TimelineGeometry {
/// Creates geometry for a visible time range.
const TimelineGeometry({ const TimelineGeometry({
required this.visibleStart, required this.visibleStart,
required this.visibleEnd, required this.visibleEnd,
@ -11,26 +13,39 @@ class TimelineGeometry {
this.minCardHeight = 72, this.minCardHeight = 72,
}); });
/// First visible time on the timeline.
final TimeOfDay visibleStart; final TimeOfDay visibleStart;
/// Last visible time on the timeline.
final TimeOfDay visibleEnd; final TimeOfDay visibleEnd;
/// Vertical scale used to convert minutes into pixels.
final double pixelsPerMinute; final double pixelsPerMinute;
/// Minimum rendered height for a timeline card.
final double minCardHeight; final double minCardHeight;
/// First visible minute from midnight.
int get startMinutes => visibleStart.hour * 60 + visibleStart.minute; int get startMinutes => visibleStart.hour * 60 + visibleStart.minute;
/// Last visible minute from midnight.
int get endMinutes => visibleEnd.hour * 60 + visibleEnd.minute; int get endMinutes => visibleEnd.hour * 60 + visibleEnd.minute;
/// Total vertical height of the visible timeline.
double get totalHeight => (endMinutes - startMinutes) * pixelsPerMinute; double get totalHeight => (endMinutes - startMinutes) * pixelsPerMinute;
/// Returns the vertical offset for [minutes] since midnight.
double yForMinutesSinceMidnight(int minutes) { double yForMinutesSinceMidnight(int minutes) {
return (minutes - startMinutes) * pixelsPerMinute; return (minutes - startMinutes) * pixelsPerMinute;
} }
/// Returns the rendered height for a duration in minutes.
double heightForDuration(int minutes) { double heightForDuration(int minutes) {
final height = minutes * pixelsPerMinute; final height = minutes * pixelsPerMinute;
return height < minCardHeight ? minCardHeight : height; return height < minCardHeight ? minCardHeight : height;
} }
/// Generates timeline ticks at [stepMinutes] intervals.
List<TimelineTick> ticks({int stepMinutes = 15}) { List<TimelineTick> ticks({int stepMinutes = 15}) {
return [ return [
for ( for (
@ -60,7 +75,9 @@ class TimelineGeometry {
} }
} }
/// A labeled tick on the compact timeline axis.
class TimelineTick { class TimelineTick {
/// Creates a timeline tick.
const TimelineTick({ const TimelineTick({
required this.minutesSinceMidnight, required this.minutesSinceMidnight,
required this.y, required this.y,
@ -68,8 +85,15 @@ class TimelineTick {
required this.major, required this.major,
}); });
/// Minute from midnight represented by this tick.
final int minutesSinceMidnight; final int minutesSinceMidnight;
/// Vertical pixel offset for this tick.
final double y; final double y;
/// Display label for this tick.
final String label; final String label;
/// Whether this tick represents a major interval.
final bool major; final bool major;
} }

View file

@ -9,7 +9,9 @@ import 'task_timeline_card.dart';
import 'timeline_axis.dart'; import 'timeline_axis.dart';
import 'timeline_geometry.dart'; import 'timeline_geometry.dart';
/// Scrollable compact timeline view for Today cards.
class TimelineView extends StatelessWidget { class TimelineView extends StatelessWidget {
/// Creates a timeline view.
const TimelineView({ const TimelineView({
required this.cards, required this.cards,
required this.range, required this.range,
@ -17,8 +19,13 @@ class TimelineView extends StatelessWidget {
super.key, super.key,
}); });
/// Cards to position on the timeline.
final List<TimelineCardModel> cards; final List<TimelineCardModel> cards;
/// Visible range used to build the timeline geometry.
final TimelineRangeModel range; final TimelineRangeModel range;
/// Callback invoked when a card is selected.
final ValueChanged<TimelineCardModel> onCardSelected; final ValueChanged<TimelineCardModel> onCardSelected;
@override @override

View file

@ -9,14 +9,19 @@ import '../controllers/scheduler_read_controller.dart';
import 'read_state_view.dart'; import 'read_state_view.dart';
import 'schedule_components.dart'; import 'schedule_components.dart';
/// Read-driven pane that renders the Today view and optional commands.
class TodayPane extends StatelessWidget { class TodayPane extends StatelessWidget {
/// Creates a Today pane from a read [controller].
const TodayPane({ const TodayPane({
required this.controller, required this.controller,
this.commandController, this.commandController,
super.key, super.key,
}); });
/// Controller that loads Today state.
final UiReadController<TodayState> controller; final UiReadController<TodayState> controller;
/// Optional command controller for Today actions.
final SchedulerCommandController? commandController; final SchedulerCommandController? commandController;
@override @override
@ -32,10 +37,15 @@ class TodayPane extends StatelessWidget {
} }
} }
/// Populated Today view for a loaded Today state.
class TodayContent extends StatelessWidget { class TodayContent extends StatelessWidget {
/// Creates Today content for [state].
const TodayContent({required this.state, this.commandController, super.key}); const TodayContent({required this.state, this.commandController, super.key});
/// Loaded Today state to display.
final TodayState state; final TodayState state;
/// Optional command controller for completing tasks.
final SchedulerCommandController? commandController; final SchedulerCommandController? commandController;
@override @override

View file

@ -5,9 +5,12 @@ import 'package:flutter/material.dart';
import '../theme/focus_flow_tokens.dart'; import '../theme/focus_flow_tokens.dart';
/// Header bar for the compact Today view.
class TopBar extends StatelessWidget { class TopBar extends StatelessWidget {
/// Creates a top bar with a display-ready [dateLabel].
const TopBar({required this.dateLabel, super.key}); const TopBar({required this.dateLabel, super.key});
/// Date label shown beside the date navigation controls.
final String dateLabel; final String dateLabel;
@override @override

View file

@ -5,6 +5,7 @@ import 'dart:io';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
/// Runs app package-boundary tests.
void main() { void main() {
test('Flutter UI imports stay inside the UI Plan 1 package boundary', () { test('Flutter UI imports stay inside the UI Plan 1 package boundary', () {
final appRoot = Directory.current; final appRoot = Directory.current;

View file

@ -10,6 +10,7 @@ import 'package:focus_flow_flutter/app/focus_flow_app.dart';
import 'package:focus_flow_flutter/models/today_screen_models.dart'; import 'package:focus_flow_flutter/models/today_screen_models.dart';
import 'package:focus_flow_flutter/widgets/timeline/difficulty_bars.dart'; import 'package:focus_flow_flutter/widgets/timeline/difficulty_bars.dart';
/// Runs FocusFlow widget and visual adapter tests.
void main() { void main() {
testWidgets('app shell renders compact Today frame', (tester) async { testWidgets('app shell renders compact Today frame', (tester) async {
await _pumpPlanApp(tester); await _pumpPlanApp(tester);