feat(settings): category rail + navigation (T-447)

The settings modal's left rail now lists the registered categories (icon +
title, data-driven from the SettingsRegistry) with an accent left-stripe +
surfaceHi selection per ui-design surface.md. Selecting a category drives the
modal's selection state and swaps the right panel; the rail scrolls when the
set exceeds the height. The cross-category search box atop the rail lands in
T-450.

Tests: rail lists categories and selecting one swaps the panel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 12:09:56 +02:00
co-authored by Claude Opus 4.8
parent 4bbb0ee4b3
commit 6c6b0c731e
5 changed files with 122 additions and 15 deletions
@@ -30,6 +30,17 @@ const _category = SettingsCategory(
],
);
const _other = SettingsCategory(
id: 'other',
title: 'Other',
sections: [
SettingsSection(
label: 'Misc',
fields: [SettingsField(key: 'app.other.x', kind: SettingsFieldKind.toggle, label: 'OtherFlag', defaultValue: false)],
),
],
);
/// Tiny extension that registers [_category] via the contribution.
class _DemoSettingsExt extends ClideExtension {
@override
@@ -109,5 +120,21 @@ void main() {
expect(find.text('Flag'), findsOneWidget);
expect(find.text('No settings categories are registered yet.'), findsNothing);
});
testWidgets('rail lists categories and selecting one swaps the panel', (tester) async {
f.services.settingsRegistry.register(_category);
f.services.settingsRegistry.register(_other);
await tester.pumpWidget(harness(f, SettingsModal(onDismiss: () {})));
// Rail shows both titles; first (alphabetical) category's panel is shown.
expect(find.text('Demo'), findsOneWidget);
expect(find.text('Other'), findsOneWidget);
expect(find.text('Flag'), findsOneWidget);
expect(find.text('OtherFlag'), findsNothing);
// Selecting the second category swaps the panel.
await tester.tap(find.text('Other'));
await tester.pump();
expect(find.text('OtherFlag'), findsOneWidget);
expect(find.text('Flag'), findsNothing);
});
});
}