@@ -4,11 +4,13 @@ import {
44 DrawerHeader ,
55 DrawerHeaderTitle ,
66 OverlayDrawer ,
7+ SearchBox ,
8+ Text ,
79 ToolbarButton ,
810 Tooltip ,
911} from "@fluentui/react-components" ;
1012import { Dismiss24Regular , DocumentBulletList24Regular } from "@fluentui/react-icons" ;
11- import { useCallback , useState , type FunctionComponent } from "react" ;
13+ import { useCallback , useMemo , useState , type FunctionComponent } from "react" ;
1214import type { PlaygroundSample } from "../../types.js" ;
1315import { SampleCard } from "./sample-card.js" ;
1416import style from "./samples-drawer.module.css" ;
@@ -23,13 +25,49 @@ export interface SamplesDrawerOverlayProps extends SamplesDrawerProps {
2325 onOpenChange : ( open : boolean ) => void ;
2426}
2527
28+ interface SampleCategory {
29+ name : string ;
30+ entries : [ string , PlaygroundSample ] [ ] ;
31+ }
32+
33+ function groupAndFilterSamples (
34+ samples : Record < string , PlaygroundSample > ,
35+ searchQuery : string ,
36+ ) : SampleCategory [ ] {
37+ const query = searchQuery . toLowerCase ( ) . trim ( ) ;
38+ const categoryMap = new Map < string , [ string , PlaygroundSample ] [ ] > ( ) ;
39+
40+ for ( const [ name , sample ] of Object . entries ( samples ) ) {
41+ if ( query ) {
42+ const matchesName = name . toLowerCase ( ) . includes ( query ) ;
43+ const matchesDescription = sample . description ?. toLowerCase ( ) . includes ( query ) ;
44+ const matchesCategory = sample . category ?. toLowerCase ( ) . includes ( query ) ;
45+ if ( ! matchesName && ! matchesDescription && ! matchesCategory ) continue ;
46+ }
47+
48+ const category = sample . category ?? "Other" ;
49+ let entries = categoryMap . get ( category ) ;
50+ if ( ! entries ) {
51+ entries = [ ] ;
52+ categoryMap . set ( category , entries ) ;
53+ }
54+ entries . push ( [ name , sample ] ) ;
55+ }
56+
57+ return Array . from ( categoryMap . entries ( ) )
58+ . map ( ( [ name , entries ] ) => ( { name, entries } ) )
59+ . sort ( ( a , b ) => a . name . localeCompare ( b . name ) ) ;
60+ }
61+
2662/** The overlay drawer showing the sample gallery. Controlled via open/onOpenChange. */
2763export const SamplesDrawerOverlay : FunctionComponent < SamplesDrawerOverlayProps > = ( {
2864 samples,
2965 onSelectedSampleNameChange,
3066 open,
3167 onOpenChange,
3268} ) => {
69+ const [ searchQuery , setSearchQuery ] = useState ( "" ) ;
70+
3371 const handleSampleSelect = useCallback (
3472 ( sampleName : string ) => {
3573 onSelectedSampleNameChange ( sampleName ) ;
@@ -38,10 +76,23 @@ export const SamplesDrawerOverlay: FunctionComponent<SamplesDrawerOverlayProps>
3876 [ onSelectedSampleNameChange , onOpenChange ] ,
3977 ) ;
4078
79+ const categories = useMemo (
80+ ( ) => groupAndFilterSamples ( samples , searchQuery ) ,
81+ [ samples , searchQuery ] ,
82+ ) ;
83+ const hasCategories = useMemo ( ( ) => Object . values ( samples ) . some ( ( s ) => s . category ) , [ samples ] ) ;
84+ const totalFiltered = useMemo (
85+ ( ) => categories . reduce ( ( sum , c ) => sum + c . entries . length , 0 ) ,
86+ [ categories ] ,
87+ ) ;
88+
4189 return (
4290 < OverlayDrawer
4391 open = { open }
44- onOpenChange = { ( _ , data ) => onOpenChange ( data . open ) }
92+ onOpenChange = { ( _ , data ) => {
93+ onOpenChange ( data . open ) ;
94+ if ( ! data . open ) setSearchQuery ( "" ) ;
95+ } }
4596 position = "end"
4697 size = "large"
4798 >
@@ -60,11 +111,46 @@ export const SamplesDrawerOverlay: FunctionComponent<SamplesDrawerOverlayProps>
60111 </ DrawerHeaderTitle >
61112 </ DrawerHeader >
62113 < DrawerBody >
63- < div className = { style [ "samples-grid" ] } >
64- { Object . entries ( samples ) . map ( ( [ name , sample ] ) => (
65- < SampleCard key = { name } name = { name } sample = { sample } onSelect = { handleSampleSelect } />
66- ) ) }
114+ < div className = { style [ "samples-search" ] } >
115+ < SearchBox
116+ placeholder = "Search samples..."
117+ value = { searchQuery }
118+ onChange = { ( _ , data ) => setSearchQuery ( data . value ) }
119+ className = { style [ "search-input" ] }
120+ />
67121 </ div >
122+
123+ { totalFiltered === 0 ? (
124+ < div className = { style [ "samples-empty" ] } >
125+ < Text > No samples match your search.</ Text >
126+ </ div >
127+ ) : hasCategories ? (
128+ categories . map ( ( category ) => (
129+ < div key = { category . name } className = { style [ "samples-category" ] } >
130+ < Text as = "h3" weight = "semibold" className = { style [ "category-title" ] } >
131+ { category . name }
132+ </ Text >
133+ < div className = { style [ "samples-grid" ] } >
134+ { category . entries . map ( ( [ name , sample ] ) => (
135+ < SampleCard
136+ key = { name }
137+ name = { name }
138+ sample = { sample }
139+ onSelect = { handleSampleSelect }
140+ />
141+ ) ) }
142+ </ div >
143+ </ div >
144+ ) )
145+ ) : (
146+ < div className = { style [ "samples-grid" ] } >
147+ { categories . flatMap ( ( c ) =>
148+ c . entries . map ( ( [ name , sample ] ) => (
149+ < SampleCard key = { name } name = { name } sample = { sample } onSelect = { handleSampleSelect } />
150+ ) ) ,
151+ ) }
152+ </ div >
153+ ) }
68154 </ DrawerBody >
69155 </ OverlayDrawer >
70156 ) ;
0 commit comments