77} from "@webstudio-is/react-sdk" ;
88import { isFeatureEnabled } from "@webstudio-is/feature-flags" ;
99import {
10- Kbd ,
11- Text ,
10+ Command ,
1211 CommandDialog ,
1312 CommandInput ,
1413 CommandList ,
@@ -18,9 +17,11 @@ import {
1817 CommandIcon ,
1918 ScrollArea ,
2019 Flex ,
20+ Kbd ,
21+ Text ,
2122} from "@webstudio-is/design-system" ;
2223import { compareMedia } from "@webstudio-is/css-engine" ;
23- import type { Breakpoint } from "@webstudio-is/sdk" ;
24+ import type { Breakpoint , Page } from "@webstudio-is/sdk" ;
2425import {
2526 $breakpoints ,
2627 $pages ,
@@ -33,6 +34,9 @@ import { humanizeString } from "~/shared/string-utils";
3334import { setCanvasWidth } from "~/builder/features/breakpoints" ;
3435import { insert as insertComponent } from "~/builder/features/components/insert" ;
3536import { $selectedPage , selectPage } from "~/shared/awareness" ;
37+ import { useState } from "react" ;
38+ import { matchSorter } from "match-sorter" ;
39+ import { mapGroupBy } from "~/shared/shim" ;
3640
3741const $commandPanel = atom <
3842 | undefined
@@ -75,37 +79,57 @@ const getMetaScore = (meta: WsComponentMeta) => {
7579 return categoryScore * 1000 + componentScore ;
7680} ;
7781
78- const $visibleMetas = computed (
82+ type ComponentOption = {
83+ tokens : string [ ] ;
84+ type : "component" ;
85+ component : string ;
86+ label : string ;
87+ meta : WsComponentMeta ;
88+ } ;
89+
90+ const $componentOptions = computed (
7991 [ $registeredComponentMetas , $selectedPage ] ,
8092 ( metas , selectedPage ) => {
81- const entries = Array . from ( metas )
82- . sort (
83- ( [ _leftComponent , leftMeta ] , [ _rightComponent , rightMeta ] ) =>
84- getMetaScore ( leftMeta ) - getMetaScore ( rightMeta )
85- )
86- . filter ( ( [ component , meta ] ) => {
87- const category = meta . category ?? "hidden" ;
88- if ( category === "hidden" || category === "internal" ) {
89- return false ;
90- }
91- // show only xml category and collection component in xml documents
92- if ( selectedPage ?. meta . documentType === "xml" ) {
93- return category === "xml" || component === collectionComponent ;
93+ const componentOptions : ComponentOption [ ] = [ ] ;
94+ for ( const [ component , meta ] of metas ) {
95+ const category = meta . category ?? "hidden" ;
96+ if ( category === "hidden" || category === "internal" ) {
97+ continue ;
98+ }
99+ // show only xml category and collection component in xml documents
100+ if ( selectedPage ?. meta . documentType === "xml" ) {
101+ if ( category !== "xml" && component !== collectionComponent ) {
102+ continue ;
94103 }
104+ } else {
95105 // show everything except xml category in html documents
96- return category !== "xml" ;
106+ if ( category === "xml" ) {
107+ continue ;
108+ }
109+ }
110+ const label = getInstanceLabel ( { component } , meta ) ;
111+ componentOptions . push ( {
112+ tokens : [ "components" , label , category ] ,
113+ type : "component" ,
114+ component,
115+ label,
116+ meta,
97117 } ) ;
98- return new Map ( entries ) ;
118+ }
119+ componentOptions . sort (
120+ ( { meta : leftMeta } , { meta : rightMeta } ) =>
121+ getMetaScore ( leftMeta ) - getMetaScore ( rightMeta )
122+ ) ;
123+ return componentOptions ;
99124 }
100125) ;
101126
102- const ComponentsGroup = ( ) => {
103- const metas = useStore ( $visibleMetas ) ;
127+ const ComponentsGroup = ( { options } : { options : ComponentOption [ ] } ) => {
104128 return (
105129 < CommandGroup
106130 heading = { < CommandGroupHeading > Components</ CommandGroupHeading > }
107131 >
108- { Array . from ( metas ) . map ( ( [ component , meta ] ) => {
132+ { options . map ( ( { component, label , meta } ) => {
109133 return (
110134 < CommandItem
111135 key = { component }
@@ -119,9 +143,9 @@ const ComponentsGroup = () => {
119143 dangerouslySetInnerHTML = { { __html : meta . icon } }
120144 > </ CommandIcon >
121145 < Text variant = "labelsTitleCase" >
122- { getInstanceLabel ( { component } , meta ) } { " " }
146+ { label } { " " }
123147 < Text as = "span" color = "moreSubtle" >
124- ( { humanizeString ( meta . category ?? "" ) } )
148+ { humanizeString ( meta . category ?? "" ) }
125149 </ Text >
126150 </ Text >
127151 </ CommandItem >
@@ -131,6 +155,38 @@ const ComponentsGroup = () => {
131155 ) ;
132156} ;
133157
158+ type BreakpointOption = {
159+ tokens : string [ ] ;
160+ type : "breakpoint" ;
161+ breakpoint : Breakpoint ;
162+ shortcut : string ;
163+ } ;
164+
165+ const $breakpointOptions = computed (
166+ [ $breakpoints , $selectedBreakpoint ] ,
167+ ( breakpoints , selectedBreakpoint ) => {
168+ const sortedBreakpoints = Array . from ( breakpoints . values ( ) ) . sort (
169+ compareMedia
170+ ) ;
171+ const breakpointOptions : BreakpointOption [ ] = [ ] ;
172+ for ( let index = 0 ; index < sortedBreakpoints . length ; index += 1 ) {
173+ const breakpoint = sortedBreakpoints [ index ] ;
174+ if ( breakpoint . id === selectedBreakpoint ?. id ) {
175+ continue ;
176+ }
177+ const width =
178+ ( breakpoint . minWidth ?? breakpoint . maxWidth ) ?. toString ( ) ?? "" ;
179+ breakpointOptions . push ( {
180+ tokens : [ "breakpoints" , breakpoint . label , width ] ,
181+ type : "breakpoint" ,
182+ breakpoint,
183+ shortcut : ( index + 1 ) . toString ( ) ,
184+ } ) ;
185+ }
186+ return breakpointOptions ;
187+ }
188+ ) ;
189+
134190const getBreakpointLabel = ( breakpoint : Breakpoint ) => {
135191 let label = "All Sizes" ;
136192 if ( breakpoint . minWidth !== undefined ) {
@@ -142,90 +198,133 @@ const getBreakpointLabel = (breakpoint: Breakpoint) => {
142198 return `${ breakpoint . label } : ${ label } ` ;
143199} ;
144200
145- const BreakpointsGroup = ( ) => {
146- const breakpoints = useStore ( $breakpoints ) ;
147- const sortedBreakpoints = Array . from ( breakpoints . values ( ) ) . sort ( compareMedia ) ;
148- const selectedBreakpoint = useStore ( $selectedBreakpoint ) ;
201+ const BreakpointsGroup = ( { options } : { options : BreakpointOption [ ] } ) => {
149202 return (
150203 < CommandGroup
151204 heading = { < CommandGroupHeading > Breakpoints</ CommandGroupHeading > }
152205 >
153- { sortedBreakpoints . map (
154- ( breakpoint , index ) =>
155- breakpoint . id !== selectedBreakpoint ?. id && (
156- < CommandItem
157- key = { breakpoint . id }
158- keywords = { [ "Breakpoints" ] }
159- onSelect = { ( ) => {
160- closeCommandPanel ( { restoreFocus : true } ) ;
161- $selectedBreakpointId . set ( breakpoint . id ) ;
162- setCanvasWidth ( breakpoint . id ) ;
163- } }
164- >
165- < CommandIcon > </ CommandIcon >
166- < Text variant = "labelsTitleCase" >
167- { getBreakpointLabel ( breakpoint ) }
168- </ Text >
169- < Kbd value = { [ ( index + 1 ) . toString ( ) ] } />
170- </ CommandItem >
171- )
172- ) }
206+ { options . map ( ( { breakpoint, shortcut } ) => (
207+ < CommandItem
208+ key = { breakpoint . id }
209+ onSelect = { ( ) => {
210+ closeCommandPanel ( { restoreFocus : true } ) ;
211+ $selectedBreakpointId . set ( breakpoint . id ) ;
212+ setCanvasWidth ( breakpoint . id ) ;
213+ } }
214+ >
215+ < CommandIcon > </ CommandIcon >
216+ < Text variant = "labelsTitleCase" >
217+ { getBreakpointLabel ( breakpoint ) }
218+ </ Text >
219+ < Kbd value = { [ shortcut ] } />
220+ </ CommandItem >
221+ ) ) }
173222 </ CommandGroup >
174223 ) ;
175224} ;
176225
177- const PagesGroup = ( ) => {
178- const pagesData = useStore ( $pages ) ;
179- const selectedPage = useStore ( $selectedPage ) ;
180- if ( pagesData === undefined ) {
181- return ;
226+ type PageOption = {
227+ tokens : string [ ] ;
228+ type : "page" ;
229+ page : Page ;
230+ } ;
231+
232+ const $pageOptions = computed (
233+ [ $pages , $selectedPage ] ,
234+ ( pages , selectedPage ) => {
235+ const pageOptions : PageOption [ ] = [ ] ;
236+ if ( pages ) {
237+ for ( const page of [ pages . homePage , ...pages . pages ] ) {
238+ if ( page . id === selectedPage ?. id ) {
239+ continue ;
240+ }
241+ pageOptions . push ( {
242+ tokens : [ "pages" , page . name ] ,
243+ type : "page" ,
244+ page,
245+ } ) ;
246+ }
247+ }
248+ return pageOptions ;
182249 }
183- const pages = [ pagesData . homePage , ...pagesData . pages ] ;
250+ ) ;
251+
252+ const PagesGroup = ( { options } : { options : PageOption [ ] } ) => {
184253 return (
185254 < CommandGroup heading = { < CommandGroupHeading > Pages</ CommandGroupHeading > } >
186- { pages . map (
187- ( page ) =>
188- page . id !== selectedPage ?. id && (
189- < CommandItem
190- key = { page . id }
191- keywords = { [ "pages" ] }
192- onSelect = { ( ) => {
193- closeCommandPanel ( ) ;
194- selectPage ( page . id ) ;
195- } }
196- >
197- < CommandIcon > </ CommandIcon >
198- < Text variant = "labelsTitleCase" > { page . name } </ Text >
199- </ CommandItem >
200- )
201- ) }
255+ { options . map ( ( { page } ) => (
256+ < CommandItem
257+ key = { page . id }
258+ onSelect = { ( ) => {
259+ closeCommandPanel ( ) ;
260+ selectPage ( page . id ) ;
261+ } }
262+ >
263+ < CommandIcon > </ CommandIcon >
264+ < Text variant = "labelsTitleCase" > { page . name } </ Text >
265+ </ CommandItem >
266+ ) ) }
202267 </ CommandGroup >
203268 ) ;
204269} ;
205270
271+ const $options = computed (
272+ [ $componentOptions , $breakpointOptions , $pageOptions ] ,
273+ ( componentOptions , breakpointOptions , pageOptions ) => [
274+ ...componentOptions ,
275+ ...breakpointOptions ,
276+ ...pageOptions ,
277+ ]
278+ ) ;
279+
206280const CommandDialogContent = ( ) => {
281+ const [ search , setSearch ] = useState ( "" ) ;
282+ const options = useStore ( $options ) ;
283+ let matches = options ;
284+ for ( const word of search . trim ( ) . split ( / \s + / ) ) {
285+ matches = matchSorter ( matches , word , {
286+ keys : [ "tokens" ] ,
287+ } ) ;
288+ }
289+ const groups = mapGroupBy ( matches , ( match ) => match . type ) ;
207290 return (
208- < >
209- < CommandInput />
291+ < Command shouldFilter = { false } >
292+ < CommandInput value = { search } onValueChange = { setSearch } />
210293 < Flex direction = "column" css = { { maxHeight : 300 } } >
211294 < ScrollArea >
212295 < CommandList >
213- < ComponentsGroup />
214- < BreakpointsGroup />
215- < PagesGroup />
296+ { Array . from ( groups ) . map ( ( [ group , matches ] ) => {
297+ if ( group === "component" ) {
298+ return (
299+ < ComponentsGroup
300+ key = { group }
301+ options = { matches as ComponentOption [ ] }
302+ />
303+ ) ;
304+ }
305+ if ( group === "breakpoint" ) {
306+ return (
307+ < BreakpointsGroup
308+ key = { group }
309+ options = { matches as BreakpointOption [ ] }
310+ />
311+ ) ;
312+ }
313+ if ( group === "page" ) {
314+ return (
315+ < PagesGroup key = { group } options = { matches as PageOption [ ] } />
316+ ) ;
317+ }
318+ } ) }
216319 </ CommandList >
217320 </ ScrollArea >
218321 </ Flex >
219- </ >
322+ </ Command >
220323 ) ;
221324} ;
222325
223326export const CommandPanel = ( ) => {
224327 const isOpen = useStore ( $commandPanel ) !== undefined ;
225-
226- if ( isOpen === false ) {
227- return ;
228- }
229328 return (
230329 < CommandDialog
231330 open = { isOpen }
0 commit comments