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
# 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

View file

@ -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<TodayState> createTodayController() {
return ApplicationReadController<TodayState>(
read: () {
@ -118,6 +139,7 @@ class DemoSchedulerComposition {
);
}
/// Creates a generic read controller for backlog panes.
UiReadController<BacklogQueryResult> createBacklogController() {
return ApplicationReadController<BacklogQueryResult>(
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',

View file

@ -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

View file

@ -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<void> 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<void> 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<void> 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<void> completeFlexibleTask({
required String taskId,
DateTime? expectedUpdatedAt,

View file

@ -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<T> = Future<ApplicationResult<T>> Function();
/// Determines whether a successful read should be rendered as empty state.
typedef EmptyPredicate<T> = bool Function(T value);
/// Shared interface for read controllers consumed by UI widgets.
abstract class UiReadController<T> extends ChangeNotifier {
/// Current read state.
SchedulerReadState<T> get state;
/// Loads the backing value.
Future<void> load();
/// Retries the latest read.
Future<void> retry();
}
/// Base state for scheduler read operations.
sealed class SchedulerReadState<T> {
/// Creates a scheduler read state.
const SchedulerReadState();
}
/// Indicates that a scheduler read is in progress.
class SchedulerReadLoading<T> extends SchedulerReadState<T> {
/// Creates a loading read state.
const SchedulerReadLoading();
}
/// Indicates that a scheduler read returned renderable data.
class SchedulerReadData<T> extends SchedulerReadState<T> {
/// 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<T> extends SchedulerReadState<T> {
/// 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<T> extends SchedulerReadState<T> {
/// 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<T> extends SchedulerReadState<T> {
}
}
/// Default read controller backed by an [ApplicationReader].
class ApplicationReadController<T> extends UiReadController<T> {
/// 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<T> read;
/// Predicate used to classify a successful value as empty or populated.
final EmptyPredicate<T> isEmpty;
SchedulerReadState<T> _state = SchedulerReadLoading<T>();
/// Current read state.
@override
SchedulerReadState<T> get state => _state;
/// Loads the current value and updates [state].
@override
Future<void> load() async {
_setState(SchedulerReadLoading<T>());
@ -82,6 +112,7 @@ class ApplicationReadController<T> extends UiReadController<T> {
}
}
/// Retries by running [load] again.
@override
Future<void> retry() => load();

View file

@ -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<ApplicationResult<TodayState>> 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<void> 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;

View file

@ -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()));
}

View file

@ -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<TimelineCardModel> 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',

View file

@ -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,

View file

@ -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;
}

View file

@ -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,

View file

@ -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

View file

@ -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<BacklogQueryResult> 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<QuickCaptureForm> {
}
}
/// 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

View file

@ -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<T> extends StatelessWidget {
/// Creates a read-state view.
const ReadStateView({
required this.controller,
required this.title,
@ -15,10 +17,19 @@ class ReadStateView<T> extends StatelessWidget {
super.key,
});
/// Controller that owns the current read state.
final UiReadController<T> 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<T> 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<void> Function() onRetry;
@override

View file

@ -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

View file

@ -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<TodayTimelineItem> 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<void> 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<void> Function(int durationMinutes)? onSchedule;
@override
@ -154,9 +167,12 @@ class _BacklogRowState extends State<BacklogRow> {
}
}
/// 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;

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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<TimelineTick> 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;
}

View file

@ -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<TimelineCardModel> cards;
/// Visible range used to build the timeline geometry.
final TimelineRangeModel range;
/// Callback invoked when a card is selected.
final ValueChanged<TimelineCardModel> onCardSelected;
@override

View file

@ -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<TodayState> 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

View file

@ -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

View file

@ -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;

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/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);