docs: document focus flow flutter app
This commit is contained in:
parent
fc1d100b1f
commit
f985b3ee84
28 changed files with 410 additions and 0 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue