From f985b3ee841d886f01a13c3142e239ab1aa8cfd4 Mon Sep 17 00:00:00 2001 From: Ashley Venn Date: Mon, 29 Jun 2026 21:27:34 -0700 Subject: [PATCH] docs: document focus flow flutter app --- apps/focus_flow_flutter/analysis_options.yaml | 1 + .../lib/app/demo_scheduler_composition.dart | 25 +++++++ .../lib/app/focus_flow_app.dart | 6 ++ .../scheduler_command_controller.dart | 31 ++++++++ .../scheduler_read_controller.dart | 31 ++++++++ .../controllers/today_screen_controller.dart | 21 ++++++ apps/focus_flow_flutter/lib/main.dart | 1 + .../lib/models/today_screen_models.dart | 64 +++++++++++++++++ .../lib/theme/focus_flow_theme.dart | 2 + .../lib/theme/focus_flow_tokens.dart | 71 +++++++++++++++++++ .../lib/theme/scheduler_visual_tokens.dart | 8 +++ .../lib/widgets/app_shell.dart | 5 ++ .../lib/widgets/backlog_pane.dart | 16 +++++ .../lib/widgets/read_state_view.dart | 27 +++++++ .../lib/widgets/required_banner.dart | 3 + .../lib/widgets/schedule_components.dart | 20 ++++++ .../lib/widgets/sidebar.dart | 2 + .../lib/widgets/task_selection_modal.dart | 5 ++ .../lib/widgets/timeline/difficulty_bars.dart | 9 +++ .../lib/widgets/timeline/reward_icon.dart | 5 ++ .../widgets/timeline/task_timeline_card.dart | 5 ++ .../lib/widgets/timeline/timeline_axis.dart | 6 ++ .../widgets/timeline/timeline_geometry.dart | 24 +++++++ .../lib/widgets/timeline/timeline_view.dart | 7 ++ .../lib/widgets/today_pane.dart | 10 +++ .../lib/widgets/top_bar.dart | 3 + .../test/forbidden_imports_test.dart | 1 + apps/focus_flow_flutter/test/widget_test.dart | 1 + 28 files changed, 410 insertions(+) diff --git a/apps/focus_flow_flutter/analysis_options.yaml b/apps/focus_flow_flutter/analysis_options.yaml index 0d29021..74a6706 100644 --- a/apps/focus_flow_flutter/analysis_options.yaml +++ b/apps/focus_flow_flutter/analysis_options.yaml @@ -21,6 +21,7 @@ linter: # `// ignore_for_file: name_of_lint` syntax on the line or in the file # producing the lint. rules: + public_member_api_docs: true # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule diff --git a/apps/focus_flow_flutter/lib/app/demo_scheduler_composition.dart b/apps/focus_flow_flutter/lib/app/demo_scheduler_composition.dart index 7672641..6bb91ed 100644 --- a/apps/focus_flow_flutter/lib/app/demo_scheduler_composition.dart +++ b/apps/focus_flow_flutter/lib/app/demo_scheduler_composition.dart @@ -7,6 +7,7 @@ import '../controllers/scheduler_command_controller.dart'; import '../controllers/scheduler_read_controller.dart'; import '../controllers/today_screen_controller.dart'; +/// Wires the Flutter demo UI to in-memory scheduler application use cases. class DemoSchedulerComposition { DemoSchedulerComposition._({ required this.store, @@ -17,19 +18,37 @@ class DemoSchedulerComposition { required this.readAt, }); + /// Fixed owner used by the seeded demo data. static const ownerId = 'owner-1'; + + /// Fixed owner time zone used by the seeded demo data. static const timeZoneId = 'UTC'; + + /// Fixed project id used by the seeded demo data. static const projectId = 'home'; + /// In-memory application store backing the demo. final InMemoryApplicationUnitOfWork store; + + /// Query object used to read the Today state. final GetTodayStateQuery todayQuery; + + /// Management use cases used by backlog-facing UI surfaces. final V1ApplicationManagementUseCases managementUseCases; + + /// Command use cases used by interactive UI controls. final V1ApplicationCommandUseCases commandUseCases; + + /// Local date represented by the seeded demo. final CivilDate date; + + /// Stable read clock used for deterministic demo operations. final DateTime readAt; + /// Human-readable label for [date]. String get dateLabel => formatDateLabel(date); + /// Creates a deterministic seeded composition for the demo app and tests. factory DemoSchedulerComposition.seeded({ Clock? clock, CivilDate? selectedDate, @@ -87,6 +106,7 @@ class DemoSchedulerComposition { ); } + /// Creates the controller used by the compact Today screen mockup. TodayScreenController createTodayScreenController() { return TodayScreenController( read: () { @@ -101,6 +121,7 @@ class DemoSchedulerComposition { ); } + /// Creates a generic read controller for Today-state panes. UiReadController createTodayController() { return ApplicationReadController( read: () { @@ -118,6 +139,7 @@ class DemoSchedulerComposition { ); } + /// Creates a generic read controller for backlog panes. UiReadController createBacklogController() { return ApplicationReadController( read: () { @@ -132,6 +154,7 @@ class DemoSchedulerComposition { ); } + /// Creates the command controller used by interactive scheduler controls. SchedulerCommandController createCommandController({ required ReadRefresh refreshReads, }) { @@ -143,6 +166,7 @@ class DemoSchedulerComposition { ); } + /// Creates an application operation context for the supplied [operationId]. ApplicationOperationContext context(String operationId) { return ApplicationOperationContext.start( operationId: operationId, @@ -155,6 +179,7 @@ class DemoSchedulerComposition { ); } + /// Formats a civil date as a long month/day/year label. static String formatDateLabel(CivilDate date) { const months = [ 'January', diff --git a/apps/focus_flow_flutter/lib/app/focus_flow_app.dart b/apps/focus_flow_flutter/lib/app/focus_flow_app.dart index 679f65c..af749f1 100644 --- a/apps/focus_flow_flutter/lib/app/focus_flow_app.dart +++ b/apps/focus_flow_flutter/lib/app/focus_flow_app.dart @@ -15,9 +15,12 @@ import '../widgets/timeline/timeline_view.dart'; import '../widgets/top_bar.dart'; import 'demo_scheduler_composition.dart'; +/// Root Material app for the FocusFlow desktop UI. class FocusFlowApp extends StatelessWidget { + /// Creates a FocusFlow app from an application composition. const FocusFlowApp({required this.composition, super.key}); + /// Factory and controller bundle used by the app tree. final DemoSchedulerComposition composition; @override @@ -31,9 +34,12 @@ class FocusFlowApp extends StatelessWidget { } } +/// Stateful home screen that owns the Today controller lifecycle. class FocusFlowHome extends StatefulWidget { + /// Creates the FocusFlow home screen from an application composition. const FocusFlowHome({required this.composition, super.key}); + /// Factory and controller bundle used by the home screen. final DemoSchedulerComposition composition; @override diff --git a/apps/focus_flow_flutter/lib/controllers/scheduler_command_controller.dart b/apps/focus_flow_flutter/lib/controllers/scheduler_command_controller.dart index 96054ed..638af12 100644 --- a/apps/focus_flow_flutter/lib/controllers/scheduler_command_controller.dart +++ b/apps/focus_flow_flutter/lib/controllers/scheduler_command_controller.dart @@ -4,37 +4,55 @@ library; import 'package:flutter/foundation.dart'; import 'package:scheduler_core/scheduler_core.dart'; +/// Builds an application operation context for a UI command operation. typedef OperationContextFactory = ApplicationOperationContext Function(String operationId); +/// Refreshes any reads that should reflect a completed command. typedef ReadRefresh = Future Function(); +/// Base state for scheduler command execution in the UI. sealed class SchedulerCommandState { + /// Creates a scheduler command state. const SchedulerCommandState(); } +/// Indicates that no scheduler command is currently running. class SchedulerCommandIdle extends SchedulerCommandState { + /// Creates an idle command state. const SchedulerCommandIdle(); } +/// Indicates that a scheduler command is currently running. class SchedulerCommandRunning extends SchedulerCommandState { + /// Creates a running command state with a user-facing [label]. const SchedulerCommandRunning(this.label); + /// User-facing description of the command in progress. final String label; } +/// Indicates that a scheduler command completed successfully. class SchedulerCommandSuccess extends SchedulerCommandState { + /// Creates a success command state with a user-facing [message]. const SchedulerCommandSuccess(this.message); + /// User-facing success message. final String message; } +/// Indicates that a scheduler command failed. class SchedulerCommandFailure extends SchedulerCommandState { + /// Creates a failure state from an application [failure] or unexpected [error]. const SchedulerCommandFailure({this.failure, this.error}); + /// Typed application failure returned by the scheduler core. final ApplicationFailure? failure; + + /// Unexpected error thrown while running the command. final Object? error; + /// Stable label for display and diagnostics. String get codeLabel { final typed = failure; 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 { + /// Creates a command controller from scheduler use cases and UI callbacks. SchedulerCommandController({ required this.commands, required this.contextFor, @@ -52,15 +72,24 @@ class SchedulerCommandController extends ChangeNotifier { required this.refreshReads, }); + /// Scheduler write use cases invoked by this controller. final V1ApplicationCommandUseCases commands; + + /// Factory for command-scoped application operation contexts. final OperationContextFactory contextFor; + + /// Local date commands should operate against. final CivilDate localDate; + + /// Refresh callback invoked after commands that mutate read state. final ReadRefresh refreshReads; var _sequence = 0; SchedulerCommandState _state = const SchedulerCommandIdle(); + /// Current command execution state. SchedulerCommandState get state => _state; + /// Captures [title] as a backlog task. Future quickCaptureToBacklog(String title) async { await _run( label: 'Capturing task', @@ -76,6 +105,7 @@ class SchedulerCommandController extends ChangeNotifier { ); } + /// Schedules a backlog task into the next available slot. Future scheduleBacklogItem({ required String taskId, required int durationMinutes, @@ -97,6 +127,7 @@ class SchedulerCommandController extends ChangeNotifier { ); } + /// Completes a planned flexible task from the Today view. Future completeFlexibleTask({ required String taskId, DateTime? expectedUpdatedAt, diff --git a/apps/focus_flow_flutter/lib/controllers/scheduler_read_controller.dart b/apps/focus_flow_flutter/lib/controllers/scheduler_read_controller.dart index 9b63eaf..f6e5d38 100644 --- a/apps/focus_flow_flutter/lib/controllers/scheduler_read_controller.dart +++ b/apps/focus_flow_flutter/lib/controllers/scheduler_read_controller.dart @@ -4,43 +4,66 @@ library; import 'package:flutter/foundation.dart'; import 'package:scheduler_core/scheduler_core.dart'; +/// Reads an application value for a UI surface. typedef ApplicationReader = Future> Function(); + +/// Determines whether a successful read should be rendered as empty state. typedef EmptyPredicate = bool Function(T value); +/// Shared interface for read controllers consumed by UI widgets. abstract class UiReadController extends ChangeNotifier { + /// Current read state. SchedulerReadState get state; + /// Loads the backing value. Future load(); + /// Retries the latest read. Future retry(); } +/// Base state for scheduler read operations. sealed class SchedulerReadState { + /// Creates a scheduler read state. const SchedulerReadState(); } +/// Indicates that a scheduler read is in progress. class SchedulerReadLoading extends SchedulerReadState { + /// Creates a loading read state. const SchedulerReadLoading(); } +/// Indicates that a scheduler read returned renderable data. class SchedulerReadData extends SchedulerReadState { + /// Creates a data state from [value]. const SchedulerReadData(this.value); + /// Value returned by the read operation. final T value; } +/// Indicates that a scheduler read succeeded but has no primary content. class SchedulerReadEmpty extends SchedulerReadState { + /// Creates an empty state that still carries the read [value]. const SchedulerReadEmpty(this.value); + /// Value returned by the read operation. final T value; } +/// Indicates that a scheduler read failed. class SchedulerReadFailure extends SchedulerReadState { + /// Creates a failure state from an application [failure] or unexpected [error]. const SchedulerReadFailure({this.failure, this.error}); + /// Typed application failure returned by the scheduler core. final ApplicationFailure? failure; + + /// Unexpected error thrown while running the read. final Object? error; + /// Stable label for display and diagnostics. String get codeLabel { final typed = failure; if (typed != null) { @@ -50,17 +73,24 @@ class SchedulerReadFailure extends SchedulerReadState { } } +/// Default read controller backed by an [ApplicationReader]. class ApplicationReadController extends UiReadController { + /// Creates a controller from a read callback and empty-state predicate. ApplicationReadController({required this.read, required this.isEmpty}); + /// Callback used to load the application value. final ApplicationReader read; + + /// Predicate used to classify a successful value as empty or populated. final EmptyPredicate isEmpty; SchedulerReadState _state = SchedulerReadLoading(); + /// Current read state. @override SchedulerReadState get state => _state; + /// Loads the current value and updates [state]. @override Future load() async { _setState(SchedulerReadLoading()); @@ -82,6 +112,7 @@ class ApplicationReadController extends UiReadController { } } + /// Retries by running [load] again. @override Future retry() => load(); diff --git a/apps/focus_flow_flutter/lib/controllers/today_screen_controller.dart b/apps/focus_flow_flutter/lib/controllers/today_screen_controller.dart index fb7e734..225d5cb 100644 --- a/apps/focus_flow_flutter/lib/controllers/today_screen_controller.dart +++ b/apps/focus_flow_flutter/lib/controllers/today_screen_controller.dart @@ -6,44 +6,63 @@ import 'package:scheduler_core/scheduler_core.dart'; import '../models/today_screen_models.dart'; +/// Reads Today state from the scheduler application layer. typedef TodayStateReader = Future> Function(); +/// Base state for the compact Today screen. sealed class TodayScreenState { + /// Creates a Today screen state. const TodayScreenState(); } +/// Indicates that the Today screen is loading. class TodayScreenLoading extends TodayScreenState { + /// Creates a loading Today screen state. const TodayScreenLoading(); } +/// Indicates that the Today screen has renderable timeline data. class TodayScreenReady extends TodayScreenState { + /// Creates a ready state with mapped screen [data]. const TodayScreenReady(this.data); + /// Presentation model for the Today screen. final TodayScreenData data; } +/// Indicates that the Today screen loaded successfully with no cards. class TodayScreenEmpty extends TodayScreenState { + /// Creates an empty Today screen state. const TodayScreenEmpty(); } +/// Indicates that the Today screen failed to load. class TodayScreenFailure extends TodayScreenState { + /// Creates a failure state with a displayable [message]. const TodayScreenFailure(this.message); + /// User-facing failure message or code. final String message; } +/// Loads Today screen data and tracks selected timeline cards. class TodayScreenController extends ChangeNotifier { + /// Creates a Today screen controller from a read callback. TodayScreenController({required this.read}); + /// Callback used to read Today state. final TodayStateReader read; TodayScreenState _state = const TodayScreenLoading(); TimelineCardModel? _selectedCard; + /// Current Today screen loading/data/failure state. TodayScreenState get state => _state; + /// Currently selected timeline card, if a modal should be visible. TimelineCardModel? get selectedCard => _selectedCard; + /// Loads Today state and maps it into presentation data. Future load() async { _state = const TodayScreenLoading(); notifyListeners(); @@ -67,6 +86,7 @@ class TodayScreenController extends ChangeNotifier { } } + /// Selects [card] when the card allows selection. void selectCard(TimelineCardModel card) { if (!card.isSelectable) { return; @@ -75,6 +95,7 @@ class TodayScreenController extends ChangeNotifier { notifyListeners(); } + /// Clears the selected timeline card. void clearSelection() { if (_selectedCard == null) { return; diff --git a/apps/focus_flow_flutter/lib/main.dart b/apps/focus_flow_flutter/lib/main.dart index e1262e0..1532609 100644 --- a/apps/focus_flow_flutter/lib/main.dart +++ b/apps/focus_flow_flutter/lib/main.dart @@ -9,6 +9,7 @@ import 'app/focus_flow_app.dart'; export 'app/demo_scheduler_composition.dart'; export 'app/focus_flow_app.dart'; +/// Starts the FocusFlow app with the seeded demo scheduler composition. void main() { runApp(FocusFlowApp(composition: DemoSchedulerComposition.seeded())); } diff --git a/apps/focus_flow_flutter/lib/models/today_screen_models.dart b/apps/focus_flow_flutter/lib/models/today_screen_models.dart index 839ba9f..905826a 100644 --- a/apps/focus_flow_flutter/lib/models/today_screen_models.dart +++ b/apps/focus_flow_flutter/lib/models/today_screen_models.dart @@ -3,32 +3,54 @@ library; import 'package:scheduler_core/scheduler_core.dart'; +/// Presentation model for the next-required-task banner. class RequiredBannerModel { + /// Creates a required banner model. const RequiredBannerModel({required this.title, required this.timeText}); + /// Title of the next required task. final String title; + + /// Display-ready time for the next required task. final String timeText; } +/// Visible timeline range expressed as minutes since midnight. class TimelineRangeModel { + /// Creates a timeline range model. const TimelineRangeModel({ required this.startMinutes, required this.endMinutes, }); + /// First visible minute from midnight. final int startMinutes; + + /// Last visible minute from midnight. final int endMinutes; } +/// Visual category used to style timeline cards. enum TaskVisualKind { + /// Flexible planned work. flexible, + + /// Critical required task. required, + + /// Inflexible appointment-style task. appointment, + + /// Intentional free time. freeSlot, + + /// Completed surprise task. completedSurprise, } +/// Presentation model for the compact Today screen. class TodayScreenData { + /// Creates Today screen data. const TodayScreenData({ required this.dateLabel, required this.timelineRange, @@ -36,6 +58,7 @@ class TodayScreenData { this.requiredBanner, }); + /// Creates an empty Today screen model for [dateLabel]. factory TodayScreenData.empty({required String dateLabel}) { return TodayScreenData( dateLabel: dateLabel, @@ -44,6 +67,7 @@ class TodayScreenData { ); } + /// Maps scheduler core [TodayState] into UI presentation data. factory TodayScreenData.fromTodayState(TodayState state) { final cards = [ 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( startMinutes: 16 * 60, endMinutes: 20 * 60 + 45, ); + /// Display-ready date label. final String dateLabel; + + /// Optional next-required-task banner model. final RequiredBannerModel? requiredBanner; + + /// Visible timeline range. final TimelineRangeModel timelineRange; + + /// Timeline cards to render. final List cards; } +/// Presentation model for a single compact timeline card. class TimelineCardModel { + /// Creates a timeline card model. const TimelineCardModel({ required this.id, required this.title, @@ -91,6 +125,7 @@ class TimelineCardModel { this.durationMinutes, }); + /// Maps a scheduler timeline item into card presentation data. factory TimelineCardModel.fromItem(TodayTimelineItem item) { final start = item.start; final end = item.end; @@ -121,21 +156,49 @@ class TimelineCardModel { ); } + /// Stable card identifier. final String id; + + /// Primary task title shown on the card. final String title; + + /// Short task type label. final String typeLabel; + + /// Secondary line shown under the title. final String subtitle; + + /// Display-ready time or duration text. final String timeText; + + /// Card start position in minutes since midnight. final int startMinutes; + + /// Card end position in minutes since midnight. final int endMinutes; + + /// Optional task duration in minutes. final int? durationMinutes; + + /// Visual category used for styling. final TaskVisualKind visualKind; + + /// Reward icon token from the scheduler core. final TimelineRewardIconToken rewardIconToken; + + /// Difficulty icon token from the scheduler core. final TimelineDifficultyIconToken difficultyIconToken; + + /// Whether the card represents completed work. final bool isCompleted; + + /// Whether tapping the card should open its selection modal. final bool isSelectable; + + /// Whether the card should show compact quick-action controls. final bool showsQuickActions; + /// Accessibility and modal label for the reward icon. String get rewardLabel { return switch (rewardIconToken) { TimelineRewardIconToken.notSet => 'Reward not set', @@ -147,6 +210,7 @@ class TimelineCardModel { }; } + /// Accessibility and modal label for the effort icon. String get effortLabel { return switch (difficultyIconToken) { TimelineDifficultyIconToken.notSet => 'Effort not set', diff --git a/apps/focus_flow_flutter/lib/theme/focus_flow_theme.dart b/apps/focus_flow_flutter/lib/theme/focus_flow_theme.dart index f4669e0..57ed6de 100644 --- a/apps/focus_flow_flutter/lib/theme/focus_flow_theme.dart +++ b/apps/focus_flow_flutter/lib/theme/focus_flow_theme.dart @@ -5,7 +5,9 @@ import 'package:flutter/material.dart'; import 'focus_flow_tokens.dart'; +/// Theme factory for the FocusFlow Flutter app. abstract final class FocusFlowTheme { + /// Builds the dark Material theme used by the desktop app. static ThemeData dark() { final scheme = ColorScheme.fromSeed( seedColor: FocusFlowTokens.accentMagenta, diff --git a/apps/focus_flow_flutter/lib/theme/focus_flow_tokens.dart b/apps/focus_flow_flutter/lib/theme/focus_flow_tokens.dart index 3fefe79..d6ab29a 100644 --- a/apps/focus_flow_flutter/lib/theme/focus_flow_tokens.dart +++ b/apps/focus_flow_flutter/lib/theme/focus_flow_tokens.dart @@ -3,48 +3,119 @@ library; import 'package:flutter/material.dart'; +/// Shared visual constants for the FocusFlow Flutter app. abstract final class FocusFlowTokens { + /// Root application background color. static const appBackground = Color(0xFF070811); + + /// Border color for the app frame. static const appFrameBorder = Color(0xFF3B3D4E); + + /// Sidebar background color. static const sidebarBackground = Color(0xFF090A14); + + /// Standard panel background color. static const panelBackground = Color(0xFF11121D); + + /// Elevated panel background color. static const elevatedPanel = Color(0xFF181724); + + /// Translucent panel color for soft surfaces. static const glassPanel = Color(0x662A1025); + + /// Stronger translucent panel color for selected surfaces. static const glassPanelStrong = Color(0x992A1025); + + /// Timeline grid line color. static const gridLine = Color(0x243B3D4E); + + /// Timeline rail color. static const rail = Color(0xFF7D7F91); + + /// Subtle border color for controls and panels. static const subtleBorder = Color(0xFF3A3A4A); + + /// Active navigation border color. static const navBorder = Color(0xFFB72D82); + /// Primary accent color. static const accentMagenta = Color(0xFFFF5BA8); + + /// Primary text color. static const textPrimary = Color(0xFFF7F2FA); + + /// Muted text color. static const textMuted = Color(0xFFBBB5C8); + + /// Faint text color. static const textFaint = Color(0xFF8E889D); + + /// Text color for completed task labels. static const completedText = Color(0xFF8F8A99); + /// Accent color for flexible tasks. static const flexibleGreen = Color(0xFF4BE064); + + /// Accent color for required tasks. static const requiredPink = Color(0xFFFF4E7D); + + /// Accent color for appointment tasks. static const appointmentBlue = Color(0xFF38A8FF); + + /// Accent color for rest and free-slot surfaces. static const restPurple = Color(0xFFB45CFF); + + /// Accent color for completed tasks. static const completedGray = Color(0xFF777482); + + /// Warning accent color. static const warningYellow = Color(0xFFFFCC66); + /// Fixed sidebar width. static const sidebarWidth = 250.0; + + /// Horizontal gutter for main app content. static const mainGutter = 32.0; + + /// Width of the timeline time rail. static const timelineRailWidth = 120.0; + + /// Horizontal padding inside timeline cards. static const cardHorizontalPadding = 18.0; + /// Border radius for the outer app frame. static const appFrameRadius = 8.0; + + /// Border radius for navigation items. static const navRadius = 8.0; + + /// Border radius for required-task banners. static const bannerRadius = 8.0; + + /// Border radius for timeline cards. static const cardRadius = 8.0; + + /// Border radius for task selection modals. static const modalRadius = 8.0; + + /// Border radius for compact controls. static const smallButtonRadius = 8.0; + /// Font size for page titles. static const pageTitleSize = 40.0; + + /// Font size for timeline card titles. static const cardTitleSize = 24.0; + + /// Font size for timeline card metadata. static const cardMetaSize = 16.0; + + /// Font size for sidebar navigation labels. static const sidebarNavSize = 18.0; + + /// Font size for modal titles. static const modalTitleSize = 26.0; + + /// Font size for modal action buttons. static const modalButtonSize = 18.0; } diff --git a/apps/focus_flow_flutter/lib/theme/scheduler_visual_tokens.dart b/apps/focus_flow_flutter/lib/theme/scheduler_visual_tokens.dart index 43404a8..00a8b97 100644 --- a/apps/focus_flow_flutter/lib/theme/scheduler_visual_tokens.dart +++ b/apps/focus_flow_flutter/lib/theme/scheduler_visual_tokens.dart @@ -4,7 +4,9 @@ library; import 'package:flutter/material.dart'; import 'package:scheduler_core/scheduler_core.dart'; +/// Maps scheduler domain tokens to Flutter colors and icons. abstract final class SchedulerVisualTokens { + /// Returns the color associated with a project color [token]. static Color projectColor(String token) { return switch (token) { '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( TimelineBackgroundToken token, ColorScheme scheme, @@ -27,6 +30,7 @@ abstract final class SchedulerVisualTokens { }; } + /// Returns the icon associated with a task [type]. static IconData taskIcon(TaskType type) { return switch (type) { 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) { return switch (token) { 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) { return switch (token) { 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) { return switch (marker) { BacklogStalenessMarker.green => Icons.circle, @@ -68,6 +75,7 @@ abstract final class SchedulerVisualTokens { }; } + /// Returns the color associated with a backlog staleness marker. static Color stalenessColor( BacklogStalenessMarker marker, ColorScheme scheme, diff --git a/apps/focus_flow_flutter/lib/widgets/app_shell.dart b/apps/focus_flow_flutter/lib/widgets/app_shell.dart index df171c0..6c94d90 100644 --- a/apps/focus_flow_flutter/lib/widgets/app_shell.dart +++ b/apps/focus_flow_flutter/lib/widgets/app_shell.dart @@ -5,10 +5,15 @@ import 'package:flutter/material.dart'; import '../theme/focus_flow_tokens.dart'; +/// Top-level shell that frames the sidebar and main content area. class AppShell extends StatelessWidget { + /// Creates an app shell with a [sidebar] and main [child]. const AppShell({required this.sidebar, required this.child, super.key}); + /// Sidebar widget displayed at the left edge of the app. final Widget sidebar; + + /// Primary content widget displayed beside the sidebar. final Widget child; @override diff --git a/apps/focus_flow_flutter/lib/widgets/backlog_pane.dart b/apps/focus_flow_flutter/lib/widgets/backlog_pane.dart index 11d8216..9d56f95 100644 --- a/apps/focus_flow_flutter/lib/widgets/backlog_pane.dart +++ b/apps/focus_flow_flutter/lib/widgets/backlog_pane.dart @@ -9,14 +9,19 @@ import '../controllers/scheduler_read_controller.dart'; import 'read_state_view.dart'; import 'schedule_components.dart'; +/// Read-driven pane that renders backlog content and command controls. class BacklogPane extends StatelessWidget { + /// Creates a backlog pane from a read [controller]. const BacklogPane({ required this.controller, this.commandController, super.key, }); + /// Controller that loads backlog data. final UiReadController controller; + + /// Optional command controller for mutating backlog items. final SchedulerCommandController? commandController; @override @@ -32,14 +37,19 @@ class BacklogPane extends StatelessWidget { } } +/// Populated backlog view for a loaded backlog query result. class BacklogContent extends StatelessWidget { + /// Creates backlog content for [result]. const BacklogContent({ required this.result, this.commandController, super.key, }); + /// Loaded backlog result to display. final BacklogQueryResult result; + + /// Optional command controller for capture and scheduling actions. final SchedulerCommandController? commandController; @override @@ -73,9 +83,12 @@ class BacklogContent extends StatelessWidget { } } +/// Small form for quickly capturing a task into the backlog. class QuickCaptureForm extends StatefulWidget { + /// Creates a quick capture form. const QuickCaptureForm({required this.commandController, super.key}); + /// Command controller used to submit captured task titles. final SchedulerCommandController commandController; @override @@ -121,9 +134,12 @@ class _QuickCaptureFormState extends State { } } +/// Displays the latest command state for backlog interactions. class CommandStatusBanner extends StatelessWidget { + /// Creates a command status banner. const CommandStatusBanner({required this.controller, super.key}); + /// Command controller whose state should be rendered. final SchedulerCommandController controller; @override diff --git a/apps/focus_flow_flutter/lib/widgets/read_state_view.dart b/apps/focus_flow_flutter/lib/widgets/read_state_view.dart index 5721af4..8ffd8ec 100644 --- a/apps/focus_flow_flutter/lib/widgets/read_state_view.dart +++ b/apps/focus_flow_flutter/lib/widgets/read_state_view.dart @@ -5,7 +5,9 @@ import 'package:flutter/material.dart'; import '../controllers/scheduler_read_controller.dart'; +/// Renders loading, empty, failure, and data states from a read controller. class ReadStateView extends StatelessWidget { + /// Creates a read-state view. const ReadStateView({ required this.controller, required this.title, @@ -15,10 +17,19 @@ class ReadStateView extends StatelessWidget { super.key, }); + /// Controller that owns the current read state. final UiReadController controller; + + /// Name of the surface being loaded, used in failure copy. final String title; + + /// Title shown when the read succeeds with empty content. final String emptyTitle; + + /// Message shown when the read succeeds with empty content. final String emptyMessage; + + /// Builds the populated content for a read value. final Widget Function(BuildContext context, T value) dataBuilder; @override @@ -52,7 +63,9 @@ class ReadStateView extends StatelessWidget { } } +/// Standard empty-state panel with optional surrounding content. class EmptyStatePanel extends StatelessWidget { + /// Creates an empty-state panel. const EmptyStatePanel({ required this.title, required this.message, @@ -60,8 +73,13 @@ class EmptyStatePanel extends StatelessWidget { super.key, }); + /// Primary empty-state title. final String title; + + /// Supporting empty-state message. final String message; + + /// Optional content shown alongside the empty-state panel. final Widget? child; @override @@ -97,7 +115,9 @@ class EmptyStatePanel extends StatelessWidget { } } +/// Standard failure panel with a retry action. class FailureStatePanel extends StatelessWidget { + /// Creates a failure-state panel. const FailureStatePanel({ required this.title, required this.code, @@ -106,9 +126,16 @@ class FailureStatePanel extends StatelessWidget { super.key, }); + /// Primary failure title. final String title; + + /// Displayable failure code. final String code; + + /// Optional technical detail for unexpected errors. final String? technicalDetail; + + /// Callback invoked when the user retries the read. final Future Function() onRetry; @override diff --git a/apps/focus_flow_flutter/lib/widgets/required_banner.dart b/apps/focus_flow_flutter/lib/widgets/required_banner.dart index de60125..3a2c83f 100644 --- a/apps/focus_flow_flutter/lib/widgets/required_banner.dart +++ b/apps/focus_flow_flutter/lib/widgets/required_banner.dart @@ -6,9 +6,12 @@ import 'package:flutter/material.dart'; import '../models/today_screen_models.dart'; import '../theme/focus_flow_tokens.dart'; +/// Banner that highlights the next required task, if one exists. class RequiredBanner extends StatelessWidget { + /// Creates a required-task banner. const RequiredBanner({this.model, super.key}); + /// Optional required task model to render. final RequiredBannerModel? model; @override diff --git a/apps/focus_flow_flutter/lib/widgets/schedule_components.dart b/apps/focus_flow_flutter/lib/widgets/schedule_components.dart index ed46286..1aae2db 100644 --- a/apps/focus_flow_flutter/lib/widgets/schedule_components.dart +++ b/apps/focus_flow_flutter/lib/widgets/schedule_components.dart @@ -6,9 +6,12 @@ import 'package:scheduler_core/scheduler_core.dart'; import '../theme/scheduler_visual_tokens.dart'; +/// Compact summary panel for timeline items. class CompactPanel extends StatelessWidget { + /// Creates a compact panel for [items]. const CompactPanel({required this.items, super.key}); + /// Timeline items to display in compact form. final List items; @override @@ -28,10 +31,15 @@ class CompactPanel extends StatelessWidget { } } +/// Card row for a single Today timeline item. class TimelineRow extends StatelessWidget { + /// Creates a timeline row. const TimelineRow({required this.item, this.onDone, super.key}); + /// Timeline item to render. final TodayTimelineItem item; + + /// Optional completion callback for eligible tasks. final Future Function()? onDone; @override @@ -85,10 +93,15 @@ class TimelineRow extends StatelessWidget { } } +/// Backlog row with optional scheduling controls. class BacklogRow extends StatefulWidget { + /// Creates a backlog row. const BacklogRow({required this.item, this.onSchedule, super.key}); + /// Backlog item to render. final BacklogItemReadModel item; + + /// Optional callback that schedules the item for a duration in minutes. final Future Function(int durationMinutes)? onSchedule; @override @@ -154,9 +167,12 @@ class _BacklogRowState extends State { } } +/// Icon marker for backlog item staleness. class StalenessMarker extends StatelessWidget { + /// Creates a staleness marker. const StalenessMarker({required this.marker, super.key}); + /// Staleness marker value to render. final BacklogStalenessMarker marker; @override @@ -168,9 +184,12 @@ class StalenessMarker extends StatelessWidget { } } +/// Notice banner for pending Today-state messages. class NoticeBanner extends StatelessWidget { + /// Creates a notice banner. const NoticeBanner({required this.message, super.key}); + /// Notice message to display. final String message; @override @@ -187,6 +206,7 @@ class NoticeBanner extends StatelessWidget { } } +/// Formats a scheduler timeline item as display-ready time text. String timeText(TimelineItem item) { final start = item.start; final end = item.end; diff --git a/apps/focus_flow_flutter/lib/widgets/sidebar.dart b/apps/focus_flow_flutter/lib/widgets/sidebar.dart index 329b7df..fce3bbd 100644 --- a/apps/focus_flow_flutter/lib/widgets/sidebar.dart +++ b/apps/focus_flow_flutter/lib/widgets/sidebar.dart @@ -5,7 +5,9 @@ import 'package:flutter/material.dart'; import '../theme/focus_flow_tokens.dart'; +/// Static sidebar for primary FocusFlow navigation. class Sidebar extends StatelessWidget { + /// Creates the FocusFlow sidebar. const Sidebar({super.key}); @override diff --git a/apps/focus_flow_flutter/lib/widgets/task_selection_modal.dart b/apps/focus_flow_flutter/lib/widgets/task_selection_modal.dart index a9b1d90..b7fd4de 100644 --- a/apps/focus_flow_flutter/lib/widgets/task_selection_modal.dart +++ b/apps/focus_flow_flutter/lib/widgets/task_selection_modal.dart @@ -8,14 +8,19 @@ import '../theme/focus_flow_tokens.dart'; import 'timeline/difficulty_bars.dart'; import 'timeline/reward_icon.dart'; +/// Modal shown when a selectable timeline task card is opened. class TaskSelectionModal extends StatelessWidget { + /// Creates a task selection modal for [card]. const TaskSelectionModal({ required this.card, required this.onClose, super.key, }); + /// Card model whose details and actions should be displayed. final TimelineCardModel card; + + /// Callback invoked when the modal should close. final VoidCallback onClose; @override diff --git a/apps/focus_flow_flutter/lib/widgets/timeline/difficulty_bars.dart b/apps/focus_flow_flutter/lib/widgets/timeline/difficulty_bars.dart index d6c8a5a..aa9926b 100644 --- a/apps/focus_flow_flutter/lib/widgets/timeline/difficulty_bars.dart +++ b/apps/focus_flow_flutter/lib/widgets/timeline/difficulty_bars.dart @@ -6,7 +6,9 @@ import 'package:scheduler_core/scheduler_core.dart'; import '../../theme/focus_flow_tokens.dart'; +/// Five-bar effort indicator for a timeline task. class DifficultyBars extends StatelessWidget { + /// Creates difficulty bars for a scheduler difficulty token. const DifficultyBars({ required this.difficulty, this.color = FocusFlowTokens.restPurple, @@ -14,10 +16,16 @@ class DifficultyBars extends StatelessWidget { super.key, }); + /// Difficulty token that determines how many bars are filled. final TimelineDifficultyIconToken difficulty; + + /// Color used for filled bars. final Color color; + + /// Fixed paint size for the indicator. final Size size; + /// Converts a timeline difficulty token to a filled bar count. static int fillCount(TimelineDifficultyIconToken difficulty) { return switch (difficulty) { 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) { return switch (difficulty) { DifficultyLevel.notSet => 0, diff --git a/apps/focus_flow_flutter/lib/widgets/timeline/reward_icon.dart b/apps/focus_flow_flutter/lib/widgets/timeline/reward_icon.dart index 82a557e..2586f53 100644 --- a/apps/focus_flow_flutter/lib/widgets/timeline/reward_icon.dart +++ b/apps/focus_flow_flutter/lib/widgets/timeline/reward_icon.dart @@ -5,14 +5,19 @@ import 'package:flutter/material.dart'; import '../../theme/focus_flow_tokens.dart'; +/// Sparkle-style reward indicator for a timeline task. class RewardIcon extends StatelessWidget { + /// Creates a reward icon. const RewardIcon({ this.color = FocusFlowTokens.accentMagenta, this.size = 24, super.key, }); + /// Fill color used by the icon painter. final Color color; + + /// Square size of the painted icon. final double size; @override diff --git a/apps/focus_flow_flutter/lib/widgets/timeline/task_timeline_card.dart b/apps/focus_flow_flutter/lib/widgets/timeline/task_timeline_card.dart index 3d17b93..d7046a1 100644 --- a/apps/focus_flow_flutter/lib/widgets/timeline/task_timeline_card.dart +++ b/apps/focus_flow_flutter/lib/widgets/timeline/task_timeline_card.dart @@ -8,10 +8,15 @@ import '../../theme/focus_flow_tokens.dart'; import 'difficulty_bars.dart'; import 'reward_icon.dart'; +/// Interactive card that renders a scheduled task on the compact timeline. class TaskTimelineCard extends StatelessWidget { + /// Creates a task timeline card. const TaskTimelineCard({required this.card, required this.onTap, super.key}); + /// Card model to render. final TimelineCardModel card; + + /// Callback invoked when the card is tapped. final VoidCallback onTap; @override diff --git a/apps/focus_flow_flutter/lib/widgets/timeline/timeline_axis.dart b/apps/focus_flow_flutter/lib/widgets/timeline/timeline_axis.dart index 72cbf99..3858515 100644 --- a/apps/focus_flow_flutter/lib/widgets/timeline/timeline_axis.dart +++ b/apps/focus_flow_flutter/lib/widgets/timeline/timeline_axis.dart @@ -6,9 +6,12 @@ import 'package:flutter/material.dart'; import '../../theme/focus_flow_tokens.dart'; import 'timeline_geometry.dart'; +/// Time labels and rail shown beside the compact timeline. class TimelineAxis extends StatelessWidget { + /// Creates a timeline axis for [geometry]. const TimelineAxis({required this.geometry, super.key}); + /// Geometry used to position labels and tick marks. final TimelineGeometry geometry; @override @@ -60,9 +63,12 @@ class TimelineAxis extends StatelessWidget { } } +/// Background grid aligned to timeline tick marks. class TimelineGrid extends StatelessWidget { + /// Creates a timeline grid for [geometry]. const TimelineGrid({required this.geometry, super.key}); + /// Geometry used to position grid lines. final TimelineGeometry geometry; @override diff --git a/apps/focus_flow_flutter/lib/widgets/timeline/timeline_geometry.dart b/apps/focus_flow_flutter/lib/widgets/timeline/timeline_geometry.dart index 511ebbd..e36478f 100644 --- a/apps/focus_flow_flutter/lib/widgets/timeline/timeline_geometry.dart +++ b/apps/focus_flow_flutter/lib/widgets/timeline/timeline_geometry.dart @@ -3,7 +3,9 @@ library; import 'package:flutter/material.dart'; +/// Converts timeline clock values into vertical pixel positions. class TimelineGeometry { + /// Creates geometry for a visible time range. const TimelineGeometry({ required this.visibleStart, required this.visibleEnd, @@ -11,26 +13,39 @@ class TimelineGeometry { this.minCardHeight = 72, }); + /// First visible time on the timeline. final TimeOfDay visibleStart; + + /// Last visible time on the timeline. final TimeOfDay visibleEnd; + + /// Vertical scale used to convert minutes into pixels. final double pixelsPerMinute; + + /// Minimum rendered height for a timeline card. final double minCardHeight; + /// First visible minute from midnight. int get startMinutes => visibleStart.hour * 60 + visibleStart.minute; + /// Last visible minute from midnight. int get endMinutes => visibleEnd.hour * 60 + visibleEnd.minute; + /// Total vertical height of the visible timeline. double get totalHeight => (endMinutes - startMinutes) * pixelsPerMinute; + /// Returns the vertical offset for [minutes] since midnight. double yForMinutesSinceMidnight(int minutes) { return (minutes - startMinutes) * pixelsPerMinute; } + /// Returns the rendered height for a duration in minutes. double heightForDuration(int minutes) { final height = minutes * pixelsPerMinute; return height < minCardHeight ? minCardHeight : height; } + /// Generates timeline ticks at [stepMinutes] intervals. List ticks({int stepMinutes = 15}) { return [ for ( @@ -60,7 +75,9 @@ class TimelineGeometry { } } +/// A labeled tick on the compact timeline axis. class TimelineTick { + /// Creates a timeline tick. const TimelineTick({ required this.minutesSinceMidnight, required this.y, @@ -68,8 +85,15 @@ class TimelineTick { required this.major, }); + /// Minute from midnight represented by this tick. final int minutesSinceMidnight; + + /// Vertical pixel offset for this tick. final double y; + + /// Display label for this tick. final String label; + + /// Whether this tick represents a major interval. final bool major; } diff --git a/apps/focus_flow_flutter/lib/widgets/timeline/timeline_view.dart b/apps/focus_flow_flutter/lib/widgets/timeline/timeline_view.dart index ebe41a9..a32e786 100644 --- a/apps/focus_flow_flutter/lib/widgets/timeline/timeline_view.dart +++ b/apps/focus_flow_flutter/lib/widgets/timeline/timeline_view.dart @@ -9,7 +9,9 @@ import 'task_timeline_card.dart'; import 'timeline_axis.dart'; import 'timeline_geometry.dart'; +/// Scrollable compact timeline view for Today cards. class TimelineView extends StatelessWidget { + /// Creates a timeline view. const TimelineView({ required this.cards, required this.range, @@ -17,8 +19,13 @@ class TimelineView extends StatelessWidget { super.key, }); + /// Cards to position on the timeline. final List cards; + + /// Visible range used to build the timeline geometry. final TimelineRangeModel range; + + /// Callback invoked when a card is selected. final ValueChanged onCardSelected; @override diff --git a/apps/focus_flow_flutter/lib/widgets/today_pane.dart b/apps/focus_flow_flutter/lib/widgets/today_pane.dart index 0c0ff33..de0ec10 100644 --- a/apps/focus_flow_flutter/lib/widgets/today_pane.dart +++ b/apps/focus_flow_flutter/lib/widgets/today_pane.dart @@ -9,14 +9,19 @@ import '../controllers/scheduler_read_controller.dart'; import 'read_state_view.dart'; import 'schedule_components.dart'; +/// Read-driven pane that renders the Today view and optional commands. class TodayPane extends StatelessWidget { + /// Creates a Today pane from a read [controller]. const TodayPane({ required this.controller, this.commandController, super.key, }); + /// Controller that loads Today state. final UiReadController controller; + + /// Optional command controller for Today actions. final SchedulerCommandController? commandController; @override @@ -32,10 +37,15 @@ class TodayPane extends StatelessWidget { } } +/// Populated Today view for a loaded Today state. class TodayContent extends StatelessWidget { + /// Creates Today content for [state]. const TodayContent({required this.state, this.commandController, super.key}); + /// Loaded Today state to display. final TodayState state; + + /// Optional command controller for completing tasks. final SchedulerCommandController? commandController; @override diff --git a/apps/focus_flow_flutter/lib/widgets/top_bar.dart b/apps/focus_flow_flutter/lib/widgets/top_bar.dart index 6c52cb9..f779190 100644 --- a/apps/focus_flow_flutter/lib/widgets/top_bar.dart +++ b/apps/focus_flow_flutter/lib/widgets/top_bar.dart @@ -5,9 +5,12 @@ import 'package:flutter/material.dart'; import '../theme/focus_flow_tokens.dart'; +/// Header bar for the compact Today view. class TopBar extends StatelessWidget { + /// Creates a top bar with a display-ready [dateLabel]. const TopBar({required this.dateLabel, super.key}); + /// Date label shown beside the date navigation controls. final String dateLabel; @override diff --git a/apps/focus_flow_flutter/test/forbidden_imports_test.dart b/apps/focus_flow_flutter/test/forbidden_imports_test.dart index 93e2390..2aaeaa6 100644 --- a/apps/focus_flow_flutter/test/forbidden_imports_test.dart +++ b/apps/focus_flow_flutter/test/forbidden_imports_test.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; +/// Runs app package-boundary tests. void main() { test('Flutter UI imports stay inside the UI Plan 1 package boundary', () { final appRoot = Directory.current; diff --git a/apps/focus_flow_flutter/test/widget_test.dart b/apps/focus_flow_flutter/test/widget_test.dart index 2824db8..3b111f0 100644 --- a/apps/focus_flow_flutter/test/widget_test.dart +++ b/apps/focus_flow_flutter/test/widget_test.dart @@ -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/widgets/timeline/difficulty_bars.dart'; +/// Runs FocusFlow widget and visual adapter tests. void main() { testWidgets('app shell renders compact Today frame', (tester) async { await _pumpPlanApp(tester);