focus-flow/apps/focus_flow_flutter/lib/widgets/sidebar.dart
pyr0ball 6f0da3c8aa feat(ui): wire Settings dialog to persisted OwnerSettings
Adds a getOwnerSettings read query to scheduler_core (previously only
an internal helper existed) and an updateOwnerSettings command on the
Flutter command controller, then builds a shared Settings dialog
surfacing timezone, day window, compact mode, and backlog staleness
thresholds. Both existing no-op entry points (sidebar nav item, top
bar button) now open the same dialog and persist edits through the
existing OwnerSettings/BacklogStalenessSettings backend.

Per the design spec (circuitforge-plans/focus-flow/superpowers/specs/
2026-07-07-settings-screen-design.md), the compact-mode toggle
persists but is disclosed in-UI as not yet affecting Today rendering,
since that wiring is separate scope (#11).

Closes: #16
2026-07-07 14:38:16 -07:00

209 lines
7.5 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 '../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,
),
),
),
],
),
),
),
);
}
}