show filter-box placeholders + make the search icon optional

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) <noreply@anthropic.com>
This commit is contained in:
2026-06-01 17:20:09 +02:00
co-authored by Claude Opus 4.8
parent a02fc754a8
commit c6a8da1b3a
4 changed files with 56 additions and 14 deletions
+4
View File
@@ -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
@@ -137,7 +137,7 @@ class _SearchPanelViewState extends State<SearchPanelView> {
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<SearchPanelView> {
],
),
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),
],
),
),
+34 -11
View File
@@ -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<String> onChanged;
@@ -20,6 +21,10 @@ class ClideFilterBox extends StatefulWidget {
final Duration debounce;
final ValueChanged<String>? 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<ClideFilterBox> createState() => _ClideFilterBoxState();
}
@@ -68,18 +73,36 @@ class _ClideFilterBoxState extends State<ClideFilterBox> {
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)
@@ -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', () {