feat(ui): theme-mode toggle (system / light / dark) with persistence

Three-way switch in the sidebar footer next to the settings
gear. Cycles system → light → dark → system. Choice persists
via shared_preferences across app launches; restored before the
first frame so there's no theme flicker on startup.

The icon changes per state (auto / sun / moon) and the tooltip
spells it out so the cycling behaviour is discoverable.

- StudioApp is now stateful; exposes StudioApp.of(context) to
  let descendants flip the theme without prop-drilling.
- ThemeModeValue enum lives in data/hub.dart so the persistence
  layer stays free of flutter/material imports.

Bumps fai_studio 0.6.0 -> 0.7.0.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-05-05 22:19:13 +02:00
parent b3fdd69139
commit 671194c105
3 changed files with 119 additions and 29 deletions

View file

@ -1,24 +1,18 @@
// Smoke test: app boots without crashing and shows the three
// MVP destinations in the navigation rail.
//
// Pages themselves now make live gRPC calls. We pump only one
// frame so they enter the loading state without actually
// awaiting the connection (which would fail without a running
// hub in CI).
// Smoke test: app boots without crashing and shows the four
// MVP destinations in the sidebar.
import 'package:flutter_test/flutter_test.dart';
import 'package:fai_studio/data/hub.dart';
import 'package:fai_studio/main.dart';
void main() {
testWidgets('Studio shell shows the four destinations',
(tester) async {
await tester.pumpWidget(const StudioApp());
await tester.pumpWidget(
const StudioApp(initialThemeMode: ThemeModeValue.system),
);
expect(find.text('F∆I Studio'), findsOneWidget);
// "Doctor" appears twice (sidebar label + app bar title of the
// first selected page) same for "Modules" / "Audit" /
// "Approvals" once their pages are first visited. We just check
// the labels are findable at all.
expect(find.text('Doctor'), findsWidgets);
expect(find.text('Modules'), findsWidgets);
expect(find.text('Audit'), findsWidgets);