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:
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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', () {
|
||||
|
||||
Reference in New Issue
Block a user