11<template >
2- <div ref =" searchBlock" >
2+ <div ref =" searchBlock" class =" focus-within:outline-none" @keydown =" handleKeydown" >
3+ <div class =" mb-4" >
4+ <OptionToggle
5+ v-model =" searchMode"
6+ :options =" [
7+ { label: 'Search', value: 'search', icon: 'search' },
8+ { label: 'Find & Replace', value: 'replace', icon: 'edit-3' },
9+ ]" />
10+ </div >
11+
312 <div class =" mb-4 flex gap-2" >
413 <BuilderInput
14+ ref =" searchInput"
515 class =" flex-1"
616 type =" text"
7- :placeholder =" searchMode === 'replace' ? 'Find...' : 'Search'"
17+ :placeholder =" searchMode === 'replace' ? 'Find...' : 'Search blocks... '"
818 v-model =" query"
9- @input =" setQuery" />
10- <BuilderButton
11- @click =" toggleSearchMode"
12- :variant =" searchMode === 'replace' ? 'solid' : 'outline'"
13- icon =" repeat"
14- :title ="
15- searchMode === 'replace' ? 'Switch to Search mode' : 'Switch to Find & Replace mode'
16- " ></BuilderButton >
19+ @input =" setQuery"
20+ @keydown.enter =" handlePrimaryAction" />
21+
22+ <Popover class =" relative inline-block text-left" >
23+ <template #target =" { isOpen , togglePopover } " >
24+ <BuilderButton
25+ @click =" togglePopover"
26+ variant =" outline"
27+ icon =" filter"
28+ label =" Filters"
29+ :class =" [
30+ 'flex items-center gap-2 text-sm',
31+ selectedFiltersCount > 0 ? 'border-ink-gray-6 bg-ink-gray-1' : '',
32+ ]" >
33+ <span
34+ v-if =" selectedFiltersCount > 0"
35+ class =" bg-ink-gray-7 ml-1 rounded-full px-2 py-0.5 text-xs text-white" >
36+ {{ selectedFiltersCount }}
37+ </span >
38+ <FeatherIcon :name =" isOpen ? 'chevron-up' : 'chevron-down'" class =" size-4" />
39+ </BuilderButton >
40+ </template >
41+ <template #body >
42+ <div class =" w-48 rounded-lg bg-white py-2 shadow-lg ring-1 ring-black ring-opacity-5" >
43+ <div class =" px-3 py-2 text-xs font-medium text-ink-gray-5" >Filter search results by:</div >
44+ <div class =" space-y-1 px-2" >
45+ <label
46+ v-for =" filter in filters"
47+ :key =" filter.name"
48+ class =" flex cursor-pointer items-center rounded px-2 py-1.5 text-sm hover:bg-surface-gray-1" >
49+ <Input
50+ type =" checkbox"
51+ :checked =" filter.selected"
52+ @change =" toggleFilter(filter)"
53+ class =" focus:ring-ink-gray-5 mr-3 size-4 rounded border-gray-300 text-ink-gray-7" />
54+ <span >{{ filter.name }}</span >
55+ </label >
56+ </div >
57+ <div class =" border-surface-gray-3 border-t px-2 pt-2" >
58+ <button
59+ @click =" clearAllFilters"
60+ class =" w-full rounded px-2 py-1.5 text-left text-sm text-ink-gray-5 hover:bg-surface-gray-1" >
61+ Clear all filters
62+ </button >
63+ </div >
64+ </div >
65+ </template >
66+ </Popover >
1767 </div >
1868
19- <BuilderInput
20- v-if =" searchMode === 'replace'"
21- class =" mb-4"
22- type =" text"
23- placeholder =" Replace with..."
24- v-model =" replaceQuery" />
25-
26- <div class =" mb-4 flex flex-wrap gap-2 text-sm" >
27- <Badge
28- v-for =" filter in filters"
29- :key =" filter.name"
30- :label =" filter.name"
31- theme =" gray"
32- :variant =" filter.selected ? 'solid' : 'outline'"
33- size =" sm"
34- class =" cursor-pointer"
35- @click =" toggleFilter(filter)" />
69+ <div v-if =" searchMode === 'replace'" class =" mb-4" >
70+ <BuilderInput
71+ class =" w-full"
72+ type =" text"
73+ placeholder =" Replace with..."
74+ v-model =" replaceQuery"
75+ @keydown.enter =" handlePrimaryAction" />
3676 </div >
3777
38- <div v-if =" searchMode === 'replace' && results.length > 0" class =" mb-4 space-y-3" >
39- <BuilderButton @click =" replaceAll" variant =" solid" class =" w-full" :disabled =" !replaceQuery" >
78+ <div
79+ v-if =" query && (searchMode === 'search' || (searchMode === 'replace' && results.length > 0))"
80+ class =" mb-4" >
81+ <BuilderButton
82+ v-if =" searchMode === 'replace'"
83+ @click =" handlePrimaryAction"
84+ variant =" solid"
85+ class =" w-full"
86+ :disabled =" !replaceQuery" >
4087 Replace All ({{ results.length }} matches)
4188 </BuilderButton >
42- <div class =" text-xs text-ink-gray-5" >
43- {{ replacedCount > 0 ? `${replacedCount} replacements made` : "" }}
89+ <div v-if = " searchMode === 'replace' && replacedCount > 0 " class =" mt-2 text-xs text-ink-gray-5" >
90+ {{ replacedCount }} replacements made
4491 </div >
4592 </div >
4693
4794 <div v-if =" !query" class =" mt-6 text-center" >
48- <!-- Empty State -->
4995 <div class =" flex flex-col items-center justify-center py-8" >
50- <FeatherIcon name = " search " class =" mb-4 size-8 text-ink- gray-4 " / >
51- < p class =" mb-4 text-p-xs text- ink-gray-5 " >
52- Find blocks by content, styles, attributes, or element type.
53- </ p >
96+ <div class =" mb-4 flex size-16 items-center justify-center rounded-full bg-surface- gray-2 " >
97+ < FeatherIcon name = " search " class =" size-8 text-ink-gray-4 " / >
98+ </ div >
99+ <h3 class = " mb-2 text-sm font-medium text-ink-gray-6 " >Search your blocks</ h3 >
54100 </div >
55101 </div >
56102
93139import type Block from " @/block" ;
94140import useCanvasStore from " @/stores/canvasStore" ;
95141import { watchDebounced } from " @vueuse/core" ;
96- import { Badge , FeatherIcon } from " frappe-ui" ;
97- import { nextTick , onMounted , Ref , ref } from " vue" ;
142+ import { FeatherIcon , Popover } from " frappe-ui" ;
143+ import Input from " frappe-ui/src/components/Input.vue" ;
144+ import { computed , nextTick , onMounted , Ref , ref } from " vue" ;
98145import { toast } from " vue-sonner" ;
99146import BuilderButton from " ./BuilderButton.vue" ;
147+ import OptionToggle from " ./OptionToggle.vue" ;
100148
101149const canvasStore = useCanvasStore ();
102150
103151const searchBlock = ref (null ) as Ref <HTMLInputElement | null >;
152+ const searchInput = ref (null ) as Ref <HTMLInputElement | null >;
104153const query = ref (" " );
105154const replaceQuery = ref (" " );
106155const searchMode = ref <" search" | " replace" >(" search" );
@@ -210,20 +259,51 @@ const filters = ref(
210259 })),
211260);
212261
262+ const selectedFiltersCount = computed (() => {
263+ return filters .value .filter ((f ) => f .selected ).length ;
264+ });
265+
213266const setQuery = (value : string ) => {
214267 query .value = value ;
215268 replacedCount .value = 0 ;
216269};
217270
218- const toggleSearchMode = () => {
219- searchMode .value = searchMode .value === " search" ? " replace" : " search" ;
271+ const handlePrimaryAction = () => {
272+ if (searchMode .value === " search" ) {
273+ performSearch ();
274+ } else if (searchMode .value === " replace" && results .value .length > 0 ) {
275+ replaceAll ();
276+ }
277+ };
278+
279+ const handleKeydown = (event : KeyboardEvent ) => {
280+ // Cmd+F or Ctrl+F for quick search
281+ if ((event .metaKey || event .ctrlKey ) && event .key === " f" ) {
282+ event .preventDefault ();
283+ const input = searchInput .value ?.querySelector ?.(" input" ) || searchInput .value ;
284+ if (input && " focus" in input && typeof input .focus === " function" ) {
285+ input .focus ();
286+ }
287+ }
288+ // Escape to clear search
289+ if (event .key === " Escape" ) {
290+ query .value = " " ;
291+ replaceQuery .value = " " ;
292+ }
220293};
221294
222295const toggleFilter = (filter : any ) => {
223296 filter .selected = ! filter .selected ;
224297 performSearch ();
225298};
226299
300+ const clearAllFilters = () => {
301+ filters .value .forEach ((filter ) => {
302+ filter .selected = false ;
303+ });
304+ performSearch ();
305+ };
306+
227307const getMatchDetails = (block : Block ) => {
228308 if (! query .value ) return " " ;
229309
@@ -351,8 +431,10 @@ const searchWithFilters = (searchTerm: string): Block[] => {
351431
352432onMounted (async () => {
353433 await nextTick ();
354- const input = searchBlock .value ?.querySelector (" input" );
355- input ?.focus ();
434+ const input = searchInput .value ?.querySelector ?.(" input" ) || searchInput .value ;
435+ if (input && " focus" in input && typeof input .focus === " function" ) {
436+ input .focus ();
437+ }
356438});
357439
358440watchDebounced (query , performSearch , {
@@ -362,4 +444,13 @@ watchDebounced(query, performSearch, {
362444watchDebounced (() => filters .value .map ((f ) => f .selected ).join (" ," ), performSearch , {
363445 debounce: 300 ,
364446});
447+
448+ // Reset replaced count when switching modes
449+ watchDebounced (
450+ searchMode ,
451+ () => {
452+ replacedCount .value = 0 ;
453+ },
454+ { debounce: 100 },
455+ );
365456 </script >
0 commit comments