feat(settings): cross-category settings search (T-450)

A search box (ClideFilterBox) atop the rail filters fields across every
registered category. While searching, the panel swaps to a results view that
groups the matching fields under category subheaders — rendered with the same
carded rows and editable inline — and each rail row shows its match count with
zero-match categories dimmed. Completes the settings-UI infra spine (T-444):
shell, engine, rail, scope tags, search.

Tests: search filters across categories, hides non-matches, and surfaces the
per-category rail count.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 12:24:24 +02:00
co-authored by Claude Opus 4.8
parent f56ad88439
commit f643d6e747
12 changed files with 184 additions and 68 deletions
@@ -1,6 +1,7 @@
import 'package:clide/builtin/settings_ui/settings_ui.dart';
import 'package:clide/extension/extension.dart';
import 'package:clide/kernel/kernel.dart';
import 'package:clide/widgets/widgets.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -21,7 +22,10 @@ const _category = SettingsCategory(
kind: SettingsFieldKind.select,
label: 'Level',
defaultValue: 'info',
options: [SettingsOption(value: 'info', label: 'Info'), SettingsOption(value: 'debug', label: 'Debug')],
options: [
SettingsOption(value: 'info', label: 'Info'),
SettingsOption(value: 'debug', label: 'Debug'),
],
),
SettingsField(key: 'app.demo.name', kind: SettingsFieldKind.text, label: 'Name', defaultValue: ''),
SettingsField(key: 'app.demo.size', kind: SettingsFieldKind.number, label: 'Size', defaultValue: 4, min: 1, max: 8),
@@ -101,7 +105,6 @@ void main() {
await tester.pump();
expect(f.services.settings.get<String>('app.demo.level'), 'debug');
});
});
group('scope tag (T-449)', () {
@@ -150,5 +153,20 @@ void main() {
expect(find.text('OtherFlag'), findsOneWidget);
expect(find.text('Flag'), findsNothing);
});
testWidgets('searching filters fields across categories with rail counts', (tester) async {
f.services.settingsRegistry.register(_category);
f.services.settingsRegistry.register(_other);
await tester.pumpWidget(harness(f, SettingsModal(onDismiss: () {})));
final box = find.descendant(of: find.byType(ClideFilterBox), matching: find.byType(EditableText));
await tester.enterText(box, 'Other');
await tester.pump(const Duration(milliseconds: 250)); // past the filter debounce
await tester.pump();
// Only the matching field (in the Other category) is shown.
expect(find.text('OtherFlag'), findsOneWidget);
expect(find.text('Flag'), findsNothing);
// The rail shows the Other category's match count.
expect(find.text('1'), findsOneWidget);
});
});
}
+1 -2
View File
@@ -1,8 +1,7 @@
import 'package:clide/kernel/kernel.dart';
import 'package:flutter_test/flutter_test.dart';
SettingsCategory _cat(String id, {String? title, int priority = 0}) =>
SettingsCategory(id: id, title: title ?? id, priority: priority, sections: const []);
SettingsCategory _cat(String id, {String? title, int priority = 0}) => SettingsCategory(id: id, title: title ?? id, priority: priority, sections: const []);
void main() {
group('SettingsRegistry', () {