focus-flow/apps/focus_flow_flutter/lib/widgets/sidebar.dart

208 lines
7.6 KiB
Dart

// 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 '../models/navigation/focus_flow_section.dart';
import '../theme/focus_flow_tokens.dart';
/// Sidebar for primary FocusFlow navigation.
class Sidebar extends StatelessWidget {
/// Creates the FocusFlow sidebar.
const Sidebar({
required this.activeSection,
required this.onSectionSelected,
super.key,
});
/// Currently active app section.
final FocusFlowSection activeSection;
/// Callback invoked when a section is selected.
final ValueChanged<FocusFlowSection> onSectionSelected;
/// 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,
section: FocusFlowSection.today,
active: activeSection == FocusFlowSection.today,
onTap: onSectionSelected,
),
const SizedBox(height: 16),
_NavItem(
label: 'Backlog',
icon: Icons.layers,
section: FocusFlowSection.backlog,
active: activeSection == FocusFlowSection.backlog,
onTap: onSectionSelected,
),
const SizedBox(height: 16),
_NavItem(
label: 'Projects',
icon: Icons.work_outline,
section: FocusFlowSection.projects,
active: activeSection == FocusFlowSection.projects,
onTap: onSectionSelected,
),
const SizedBox(height: 16),
_NavItem(
label: 'Reports',
icon: Icons.bar_chart,
section: FocusFlowSection.reports,
active: activeSection == FocusFlowSection.reports,
onTap: onSectionSelected,
),
const SizedBox(height: 30),
const Divider(color: FocusFlowTokens.subtleBorder),
const SizedBox(height: 18),
_NavItem(
label: 'Settings',
icon: Icons.settings,
section: FocusFlowSection.settings,
active: activeSection == FocusFlowSection.settings,
onTap: onSectionSelected,
),
],
),
),
);
}
}
/// 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.section,
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 `section` value for this object.
/// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
final FocusFlowSection section;
/// 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 ValueChanged<FocusFlowSection> 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-${section.name}-active') : null,
borderRadius: BorderRadius.circular(FocusFlowTokens.navRadius),
onTap: () => onTap(section),
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,
),
),
),
],
),
),
),
);
}
}