// SPDX-FileCopyrightText: 2026 FocusFlow contributors // SPDX-License-Identifier: AGPL-3.0-only /// Renders the Sidebar widget for the FocusFlow Flutter UI. library; import 'package:flutter/material.dart'; import '../theme/focus_flow_tokens.dart'; /// Primary screens the sidebar can switch the main content area to. /// /// Only [today] and [backlog] are wired to real navigation. Projects and /// Reports remain presentational-only nav items: they are outside the V1 MVP /// boundary (AGENTS.md section 8 scopes V1 to Today + Backlog only). enum SidebarScreen { /// The compact Today timeline screen. today, /// The backlog screen. backlog, } /// Sidebar for primary FocusFlow navigation. class Sidebar extends StatelessWidget { /// Creates the FocusFlow sidebar with the given [activeScreen] highlighted. const Sidebar({ required this.activeScreen, required this.onSelectToday, required this.onSelectBacklog, required this.onOpenSettings, super.key, }); /// Screen currently shown in the main content area. final SidebarScreen activeScreen; /// Invoked when the Today nav item is activated. final VoidCallback onSelectToday; /// Invoked when the Backlog nav item is activated. final VoidCallback onSelectBacklog; /// Invoked when the Settings nav item is activated. final VoidCallback onOpenSettings; /// Builds the widget subtree for this component. /// The method translates the current widget configuration and state into Flutter UI without mutating scheduler domain data. @override Widget build(BuildContext context) { return DecoratedBox( decoration: const BoxDecoration(color: FocusFlowTokens.sidebarBackground), child: Padding( padding: const EdgeInsets.fromLTRB(18, 22, 18, 28), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const _BrandRow(), const SizedBox(height: 34), _NavItem( label: 'Today', icon: Icons.calendar_today, active: activeScreen == SidebarScreen.today, onTap: onSelectToday, ), const SizedBox(height: 16), _NavItem( label: 'Backlog', icon: Icons.layers, active: activeScreen == SidebarScreen.backlog, onTap: onSelectBacklog, ), const SizedBox(height: 16), _NavItem(label: 'Projects', icon: Icons.work_outline, onTap: () {}), const SizedBox(height: 16), _NavItem(label: 'Reports', icon: Icons.bar_chart, onTap: () {}), const SizedBox(height: 30), const Divider(color: FocusFlowTokens.subtleBorder), const SizedBox(height: 18), _NavItem( label: 'Settings', icon: Icons.settings, onTap: onOpenSettings, ), ], ), ), ); } } /// Private implementation type for `_BrandRow` in this library. /// It keeps supporting state and behavior scoped to the owning file so the public API stays focused on the scheduler concepts that callers use directly. class _BrandRow extends StatelessWidget { /// Creates a `_BrandRow` instance with the values required by its invariants. /// Callers should pass already-normalized scheduler data because validation and defaulting belong at the model or service boundary, not in arbitrary UI code. const _BrandRow(); /// Builds the widget subtree for this component. /// The method translates the current widget configuration and state into Flutter UI without mutating scheduler domain data. @override Widget build(BuildContext context) { return Row( children: [ Container( width: 42, height: 42, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all(color: FocusFlowTokens.accentMagenta), color: FocusFlowTokens.glassPanelStrong, ), child: const Icon( Icons.auto_awesome, color: FocusFlowTokens.accentMagenta, ), ), const SizedBox(width: 14), Expanded( child: Text( 'FocusFlow', maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.titleMedium, ), ), ], ); } } /// Private implementation type for `_NavItem` in this library. /// It keeps supporting state and behavior scoped to the owning file so the public API stays focused on the scheduler concepts that callers use directly. class _NavItem extends StatelessWidget { /// Creates a `_NavItem` instance with the values required by its invariants. /// Callers should pass already-normalized scheduler data because validation and defaulting belong at the model or service boundary, not in arbitrary UI code. const _NavItem({ required this.label, required this.icon, required this.onTap, this.active = false, }); /// Stores the `label` value for this object. /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior. final String label; /// Stores the `icon` value for this object. /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior. final IconData icon; /// Stores the `onTap` value for this object. /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior. final VoidCallback onTap; /// Stores the `active` value for this object. /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior. final bool active; /// Builds the widget subtree for this component. /// The method translates the current widget configuration and state into Flutter UI without mutating scheduler domain data. @override Widget build(BuildContext context) { final color = active ? FocusFlowTokens.accentMagenta : FocusFlowTokens.textPrimary; return Material( color: Colors.transparent, child: InkWell( key: active ? ValueKey('nav-${label.toLowerCase()}-active') : ValueKey('nav-${label.toLowerCase()}'), borderRadius: BorderRadius.circular(FocusFlowTokens.navRadius), onTap: onTap, child: Container( height: 54, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(FocusFlowTokens.navRadius), border: Border.all( color: active ? FocusFlowTokens.navBorder : Colors.transparent, ), color: active ? FocusFlowTokens.glassPanelStrong : Colors.transparent, ), child: Row( children: [ Icon(icon, color: color, size: 24), const SizedBox(width: 16), Expanded( child: Text( label, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: color.withValues(alpha: active ? 1 : 0.88), fontSize: FocusFlowTokens.sidebarNavSize, fontWeight: FontWeight.w500, ), ), ), ], ), ), ), ); } }