focus-flow/apps/focus_flow_flutter/lib/widgets/sidebar.dart
pyr0ball 383130ebdb chore: merge upstream Backlog Board feature from Ashley's instance
Merges 20 commits from Ashley's smolblocks.com instance (main branch),
bringing in the full Backlog Board screen (BacklogBoardController, board
columns/cards, task detail drawer, summary panel, search/filter/sort/group,
Break Up and Someday actions) plus the unified FocusFlowSection navigation
model.

Reconciled with our own in-flight work:
- Replaced our placeholder BacklogPane/createBacklogController wiring with
  Ashley's real BacklogBoardScreen/BacklogBoardController (same problem,
  more complete implementation - avoids two competing Backlog UIs).
- Adopted the FocusFlowSection-based Sidebar API in place of our bespoke
  SidebarScreen enum; the Settings nav item now intercepts
  FocusFlowSection.settings in _selectSection to open our Settings dialog
  instead of falling through to a placeholder screen.
- Gave every sidebar nav item a stable key (not just the active one) so the
  Settings dialog tests can still target it reliably.
- Kept our additive owner-settings read controller, Settings dialog, and
  timeline Break-up wiring; renamed the Today controller field to
  todayController throughout to match upstream's now-multi-controller
  naming.
- Fixed a test-only regression: backlog_board_screen_test.dart's standalone
  SchedulerCommandController construction needed the new `management` param.
- Added .gitleaks.toml allowlisting the local, gitignored
  .claude/settings.local.json path (recorded bash command patterns, not
  repo secrets) that was blocking commits.

Verified via focusflow-flutter-ci:3.44.4 (Flutter 3.44.4/Dart 3.12.2):
flutter analyze clean, dart format clean, 106/106 Flutter tests passing,
348/348 scheduler_core + scheduler_persistence_sqlite tests passing.

Closes: #9
2026-07-10 22:46:09 -07:00

210 lines
7.7 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: ValueKey(
active ? 'nav-${section.name}-active' : 'nav-${section.name}',
),
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,
),
),
),
],
),
),
),
);
}
}