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
|
### 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:
|
- 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)
|
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
|
- 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),
|
const SizedBox(height: 6),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
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),
|
const SizedBox(width: 6),
|
||||||
_ReplaceAllButton(
|
_ReplaceAllButton(
|
||||||
enabled: c.replacement.isNotEmpty && c.matchCount > 0,
|
enabled: c.replacement.isNotEmpty && c.matchCount > 0,
|
||||||
@@ -147,9 +147,9 @@ class _SearchPanelViewState extends State<SearchPanelView> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
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),
|
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.hint = 'Filter…',
|
||||||
this.debounce = const Duration(milliseconds: 200),
|
this.debounce = const Duration(milliseconds: 200),
|
||||||
this.onSubmitted,
|
this.onSubmitted,
|
||||||
|
this.icon = PhosphorIcons.magnifyingGlass,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ValueChanged<String> onChanged;
|
final ValueChanged<String> onChanged;
|
||||||
@@ -20,6 +21,10 @@ class ClideFilterBox extends StatefulWidget {
|
|||||||
final Duration debounce;
|
final Duration debounce;
|
||||||
final ValueChanged<String>? onSubmitted;
|
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
|
@override
|
||||||
State<ClideFilterBox> createState() => _ClideFilterBoxState();
|
State<ClideFilterBox> createState() => _ClideFilterBoxState();
|
||||||
}
|
}
|
||||||
@@ -68,18 +73,36 @@ class _ClideFilterBoxState extends State<ClideFilterBox> {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
ClideIcon(PhosphorIcons.magnifyingGlass, size: 13, color: tokens.globalTextMuted),
|
if (widget.icon != null) ...[
|
||||||
const SizedBox(width: 6),
|
ClideIcon(widget.icon!, size: 13, color: tokens.globalTextMuted),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
],
|
||||||
Expanded(
|
Expanded(
|
||||||
child: EditableText(
|
child: Stack(
|
||||||
controller: _controller,
|
alignment: Alignment.centerLeft,
|
||||||
focusNode: _focus,
|
children: [
|
||||||
style: TextStyle(fontSize: clideFontCaption, color: tokens.globalForeground),
|
// Placeholder shown while empty — the hint was only a
|
||||||
cursorColor: tokens.globalFocus,
|
// semantics label before, so empty boxes looked blank.
|
||||||
backgroundCursorColor: tokens.globalTextMuted,
|
if (!hasText)
|
||||||
maxLines: 1,
|
IgnorePointer(
|
||||||
onChanged: _onChanged,
|
child: Text(
|
||||||
onSubmitted: widget.onSubmitted != null ? (v) => widget.onSubmitted!(v) : null,
|
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)
|
if (hasText)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:clide/clide.dart';
|
|||||||
import 'package:clide/extension/extension.dart';
|
import 'package:clide/extension/extension.dart';
|
||||||
import 'package:clide/widgets/src/clide_column_hat.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_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_icon_rail.dart';
|
||||||
import 'package:clide/widgets/src/clide_palette.dart';
|
import 'package:clide/widgets/src/clide_palette.dart';
|
||||||
import 'package:clide/widgets/src/clide_resize_border.dart';
|
import 'package:clide/widgets/src/clide_resize_border.dart';
|
||||||
@@ -291,6 +292,20 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(submitted, 'submit-me');
|
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', () {
|
group('ColumnHat', () {
|
||||||
|
|||||||
Reference in New Issue
Block a user