From c6a8da1b3a6b5f3a7fabcf495e0f18c7a3e75a8f Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Mon, 1 Jun 2026 17:20:09 +0200 Subject: [PATCH] show filter-box placeholders + make the search icon optional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Search tab's Find mode stacked four ClideFilterBoxes (search, replace, include, exclude) that all looked identical: every box drew the magnifying glass and the hint was only a semantics label, never visible text — so they read as four blank search boxes. Render the hint as placeholder text while empty, and make the leading icon optional (the replace + glob fields pass icon: null). General win — every filter box now shows its placeholder. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 4 ++ lib/builtin/search/src/search_panel_view.dart | 6 +-- lib/widgets/src/clide_filter_box.dart | 45 ++++++++++++++----- test/widgets/zero_coverage_widgets_test.dart | 15 +++++++ 4 files changed, 56 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e43ac79..35067d3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Fixed +- Filter/search inputs show their hint as visible placeholder text, and the + search-glass icon is now optional — so the Search tab's Find fields (search, + replace, include/exclude globs) are distinguishable instead of four identical + empty boxes. (T-201) - The sidebar icon rail no longer overflows when there are more tabs than fit: it centers the icons when they fit and scrolls horizontally otherwise. (T-200) - The editor pane now opens over the Claude pane when a file is opened — the diff --git a/lib/builtin/search/src/search_panel_view.dart b/lib/builtin/search/src/search_panel_view.dart index f7f89c12..5d4cbda2 100644 --- a/lib/builtin/search/src/search_panel_view.dart +++ b/lib/builtin/search/src/search_panel_view.dart @@ -137,7 +137,7 @@ class _SearchPanelViewState extends State { const SizedBox(height: 6), Row( children: [ - Expanded(child: ClideFilterBox(hint: 'Replace', debounce: Duration.zero, onChanged: c.setReplacement)), + Expanded(child: ClideFilterBox(hint: 'Replace', icon: null, debounce: Duration.zero, onChanged: c.setReplacement)), const SizedBox(width: 6), _ReplaceAllButton( enabled: c.replacement.isNotEmpty && c.matchCount > 0, @@ -147,9 +147,9 @@ class _SearchPanelViewState extends State { ], ), const SizedBox(height: 6), - ClideFilterBox(hint: 'files to include (e.g. *.dart)', debounce: Duration.zero, onChanged: (v) => c.include = v), + ClideFilterBox(hint: 'files to include (e.g. *.dart)', icon: null, debounce: Duration.zero, onChanged: (v) => c.include = v), const SizedBox(height: 4), - ClideFilterBox(hint: 'files to exclude', debounce: Duration.zero, onChanged: (v) => c.exclude = v), + ClideFilterBox(hint: 'files to exclude', icon: null, debounce: Duration.zero, onChanged: (v) => c.exclude = v), ], ), ), diff --git a/lib/widgets/src/clide_filter_box.dart b/lib/widgets/src/clide_filter_box.dart index af3d6e5e..01cfd356 100644 --- a/lib/widgets/src/clide_filter_box.dart +++ b/lib/widgets/src/clide_filter_box.dart @@ -13,6 +13,7 @@ class ClideFilterBox extends StatefulWidget { this.hint = 'Filter…', this.debounce = const Duration(milliseconds: 200), this.onSubmitted, + this.icon = PhosphorIcons.magnifyingGlass, }); final ValueChanged onChanged; @@ -20,6 +21,10 @@ class ClideFilterBox extends StatefulWidget { final Duration debounce; final ValueChanged? onSubmitted; + /// Leading glyph. Defaults to the search glass; pass null for inputs + /// that aren't searches (e.g. a replace or glob field). + final ClideIconPainter? icon; + @override State createState() => _ClideFilterBoxState(); } @@ -68,18 +73,36 @@ class _ClideFilterBoxState extends State { padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5), child: Row( children: [ - ClideIcon(PhosphorIcons.magnifyingGlass, size: 13, color: tokens.globalTextMuted), - const SizedBox(width: 6), + if (widget.icon != null) ...[ + ClideIcon(widget.icon!, size: 13, color: tokens.globalTextMuted), + const SizedBox(width: 6), + ], Expanded( - child: EditableText( - controller: _controller, - focusNode: _focus, - style: TextStyle(fontSize: clideFontCaption, color: tokens.globalForeground), - cursorColor: tokens.globalFocus, - backgroundCursorColor: tokens.globalTextMuted, - maxLines: 1, - onChanged: _onChanged, - onSubmitted: widget.onSubmitted != null ? (v) => widget.onSubmitted!(v) : null, + child: Stack( + alignment: Alignment.centerLeft, + children: [ + // Placeholder shown while empty — the hint was only a + // semantics label before, so empty boxes looked blank. + if (!hasText) + IgnorePointer( + child: Text( + widget.hint, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: clideFontCaption, color: tokens.globalTextMuted), + ), + ), + EditableText( + controller: _controller, + focusNode: _focus, + style: TextStyle(fontSize: clideFontCaption, color: tokens.globalForeground), + cursorColor: tokens.globalFocus, + backgroundCursorColor: tokens.globalTextMuted, + maxLines: 1, + onChanged: _onChanged, + onSubmitted: widget.onSubmitted != null ? (v) => widget.onSubmitted!(v) : null, + ), + ], ), ), if (hasText) diff --git a/test/widgets/zero_coverage_widgets_test.dart b/test/widgets/zero_coverage_widgets_test.dart index 404309fe..39a7b4f1 100644 --- a/test/widgets/zero_coverage_widgets_test.dart +++ b/test/widgets/zero_coverage_widgets_test.dart @@ -7,6 +7,7 @@ import 'package:clide/clide.dart'; import 'package:clide/extension/extension.dart'; import 'package:clide/widgets/src/clide_column_hat.dart'; import 'package:clide/widgets/src/clide_filter_box.dart'; +import 'package:clide/widgets/src/clide_icon.dart'; import 'package:clide/widgets/src/clide_icon_rail.dart'; import 'package:clide/widgets/src/clide_palette.dart'; import 'package:clide/widgets/src/clide_resize_border.dart'; @@ -291,6 +292,20 @@ void main() { await tester.pump(); expect(submitted, 'submit-me'); }); + + testWidgets('renders the hint as a placeholder (shown empty, hidden once typed); icon optional', (tester) async { + await tester.pumpWidget(harness( + f, + ClideFilterBox(onChanged: (_) {}, hint: 'Replace', icon: null), + )); + // Placeholder visible while empty; no leading search glyph (icon: null). + expect(find.text('Replace'), findsOneWidget); + expect(find.byType(ClideIcon), findsNothing); + // Typing hides the placeholder. + await tester.enterText(find.byType(EditableText), 'x'); + await tester.pump(); + expect(find.text('Replace'), findsNothing); + }); }); group('ColumnHat', () {