reclaim/app/lib/pages/shell_page.dart
flemming-it 136d3b48c6 perf(ui): stop the nav-rail brand pulse to fix web scroll jank
The DeltaMark ran an endless 9s breathing animation (60fps repaint)
inside the persistent nav rail, so it repainted on every scroll
frame and stuttered scrolling on Flutter web. Make the rail mark
static (animated: false) and isolate the animated variant (landing
page) behind a RepaintBoundary.

Signed-off-by: flemming-it <sf@flemming.it>
2026-06-19 02:25:38 +02:00

104 lines
3.4 KiB
Dart

import 'package:flutter/material.dart';
import '../data/repository.dart';
import '../theme/reclaim_tokens.dart';
import '../widgets/brand_wordmark.dart';
import '../widgets/delta_mark.dart';
import 'about_page.dart';
import 'heatmap_page.dart';
import 'hub_status_page.dart';
import 'legal_page.dart';
import 'norms_list_page.dart';
/// App shell with a NavigationRail. Each rail destination is one
/// page; pages do their own scrolling.
class ShellPage extends StatefulWidget {
const ShellPage({super.key, required this.repository});
final EvaluationRepository repository;
@override
State<ShellPage> createState() => _ShellPageState();
}
class _ShellPageState extends State<ShellPage> {
int _index = 0;
@override
Widget build(BuildContext context) {
final t = Theme.of(context).textTheme;
final pages = <Widget>[
HeatmapPage(repository: widget.repository),
NormsListPage(repository: widget.repository),
HubStatusPage(repository: widget.repository),
const AboutPage(),
const LegalPage(),
];
return Scaffold(
body: Row(
children: [
NavigationRail(
backgroundColor: Theme.of(context).colorScheme.surface,
extended: MediaQuery.of(context).size.width > 1100,
selectedIndex: _index,
onDestinationSelected: (i) => setState(() => _index = i),
leading: Padding(
padding: const EdgeInsets.symmetric(
vertical: ReclaimSpace.lg,
horizontal: ReclaimSpace.md,
),
child: Column(
children: [
// Static in the persistent nav rail: a 60fps pulse
// here repaints every scroll frame and stutters web
// scrolling. The breathing mark stays on the
// landing page (which isn't scrolled).
const DeltaMark(size: 36, animated: false),
const SizedBox(height: ReclaimSpace.sm),
const BrandWordmark(size: 18),
Text(
'F∆I',
style: t.labelSmall?.copyWith(
letterSpacing: 2,
color: ReclaimColors.mute,
),
),
],
),
),
destinations: const [
NavigationRailDestination(
icon: Icon(Icons.grid_view_outlined),
selectedIcon: Icon(Icons.grid_view),
label: Text('Heatmap'),
),
NavigationRailDestination(
icon: Icon(Icons.list_alt_outlined),
selectedIcon: Icon(Icons.list_alt),
label: Text('Normen'),
),
NavigationRailDestination(
icon: Icon(Icons.hub_outlined),
selectedIcon: Icon(Icons.hub),
label: Text('Hub'),
),
NavigationRailDestination(
icon: Icon(Icons.info_outline),
selectedIcon: Icon(Icons.info),
label: Text('Methodik'),
),
NavigationRailDestination(
icon: Icon(Icons.gavel_outlined),
selectedIcon: Icon(Icons.gavel),
label: Text('Recht'),
),
],
),
const VerticalDivider(width: 1),
Expanded(child: pages[_index]),
],
),
);
}
}