@@ -25,13 +25,16 @@ import {
25
25
collectionComponent ,
26
26
parseComponentName ,
27
27
elementComponent ,
28
+ tags ,
28
29
} from "@webstudio-is/sdk" ;
29
- import type { Breakpoint , Page } from "@webstudio-is/sdk" ;
30
+ import type { Breakpoint , Instance , Page } from "@webstudio-is/sdk" ;
30
31
import type { TemplateMeta } from "@webstudio-is/template" ;
31
32
import {
32
33
$breakpoints ,
33
34
$editingPageId ,
35
+ $instances ,
34
36
$pages ,
37
+ $props ,
35
38
$registeredComponentMetas ,
36
39
$registeredTemplates ,
37
40
$selectedBreakpoint ,
@@ -44,7 +47,11 @@ import {
44
47
} from "~/shared/instance-utils" ;
45
48
import { humanizeString } from "~/shared/string-utils" ;
46
49
import { setCanvasWidth } from "~/builder/features/breakpoints" ;
47
- import { $selectedPage , selectPage } from "~/shared/awareness" ;
50
+ import {
51
+ $selectedInstancePath ,
52
+ $selectedPage ,
53
+ selectPage ,
54
+ } from "~/shared/awareness" ;
48
55
import { mapGroupBy } from "~/shared/shim" ;
49
56
import { setActiveSidebarPanel } from "~/builder/shared/nano-states" ;
50
57
import { $commandMetas } from "~/shared/commands-emitter" ;
@@ -54,6 +61,7 @@ import {
54
61
getInstanceLabel ,
55
62
InstanceIcon ,
56
63
} from "~/builder/shared/instance-label" ;
64
+ import { isTreeSatisfyingContentModel } from "~/shared/content-model" ;
57
65
58
66
const $commandPanel = atom <
59
67
| undefined
@@ -233,6 +241,101 @@ const ComponentOptionsGroup = ({ options }: { options: ComponentOption[] }) => {
233
241
) ;
234
242
} ;
235
243
244
+ type TagOption = {
245
+ tokens : string [ ] ;
246
+ type : "tag" ;
247
+ tag : string ;
248
+ } ;
249
+
250
+ const $tagOptions = computed (
251
+ [ $selectedInstancePath , $instances , $props , $registeredComponentMetas ] ,
252
+ ( instancePath , instances , props , metas ) => {
253
+ const tagOptions : TagOption [ ] = [ ] ;
254
+ if ( instancePath === undefined ) {
255
+ return tagOptions ;
256
+ }
257
+ const [ { instance, instanceSelector } ] = instancePath ;
258
+ const childInstance : Instance = {
259
+ type : "instance" ,
260
+ id : "new_instance" ,
261
+ component : elementComponent ,
262
+ children : [ ] ,
263
+ } ;
264
+ const newInstances = new Map ( instances ) ;
265
+ newInstances . set ( childInstance . id , childInstance ) ;
266
+ newInstances . set ( instance . id , {
267
+ ...instance ,
268
+ children : [ ...instance . children , { type : "id" , value : childInstance . id } ] ,
269
+ } ) ;
270
+ for ( const tag of tags ) {
271
+ childInstance . tag = tag ;
272
+ const isSatisfying = isTreeSatisfyingContentModel ( {
273
+ instances : newInstances ,
274
+ props,
275
+ metas,
276
+ instanceSelector,
277
+ } ) ;
278
+ if ( isSatisfying ) {
279
+ tagOptions . push ( {
280
+ tokens : [ "tags" , tag , `<${ tag } >` ] ,
281
+ type : "tag" ,
282
+ tag,
283
+ } ) ;
284
+ }
285
+ }
286
+ return tagOptions ;
287
+ }
288
+ ) ;
289
+
290
+ const TagOptionsGroup = ( { options } : { options : TagOption [ ] } ) => {
291
+ return (
292
+ < CommandGroup
293
+ name = "tag"
294
+ heading = { < CommandGroupHeading > Tags</ CommandGroupHeading > }
295
+ actions = { [ "add" ] }
296
+ >
297
+ { options . map ( ( { tag } ) => {
298
+ return (
299
+ < CommandItem
300
+ key = { tag }
301
+ // preserve selected state when rerender
302
+ value = { tag }
303
+ onSelect = { ( ) => {
304
+ closeCommandPanel ( ) ;
305
+ const newInstance : Instance = {
306
+ type : "instance" ,
307
+ id : "new_instance" ,
308
+ component : elementComponent ,
309
+ tag,
310
+ children : [ ] ,
311
+ } ;
312
+ insertWebstudioFragmentAt ( {
313
+ children : [ { type : "id" , value : newInstance . id } ] ,
314
+ instances : [ newInstance ] ,
315
+ props : [ ] ,
316
+ dataSources : [ ] ,
317
+ styleSourceSelections : [ ] ,
318
+ styleSources : [ ] ,
319
+ styles : [ ] ,
320
+ breakpoints : [ ] ,
321
+ assets : [ ] ,
322
+ resources : [ ] ,
323
+ } ) ;
324
+ } }
325
+ >
326
+ < Flex gap = { 2 } >
327
+ < CommandIcon >
328
+ < InstanceIcon instance = { { component : elementComponent , tag } } />
329
+ </ CommandIcon >
330
+ < Text variant = "labelsSentenceCase" > { `<${ tag } >` } </ Text >
331
+ </ Flex >
332
+ </ CommandItem >
333
+ ) ;
334
+ } ) }
335
+ </ CommandGroup >
336
+ ) ;
337
+ } ;
338
+
236
339
type BreakpointOption = {
237
340
tokens : string [ ] ;
238
341
type : "breakpoint" ;
@@ -423,12 +526,25 @@ const ShortcutOptionsGroup = ({ options }: { options: ShortcutOption[] }) => {
423
526
} ;
424
527
425
528
const $options = computed (
426
- [ $componentOptions , $breakpointOptions , $pageOptions , $shortcutOptions ] ,
427
- ( componentOptions , breakpointOptions , pageOptions , commandOptions ) => [
529
+ [
530
+ $componentOptions ,
531
+ $breakpointOptions ,
532
+ $pageOptions ,
533
+ $shortcutOptions ,
534
+ $tagOptions ,
535
+ ] ,
536
+ (
537
+ componentOptions ,
538
+ breakpointOptions ,
539
+ pageOptions ,
540
+ commandOptions ,
541
+ tagOptions
542
+ ) => [
428
543
...componentOptions ,
429
544
...breakpointOptions ,
430
545
...pageOptions ,
431
546
...commandOptions ,
547
+ ...tagOptions ,
432
548
]
433
549
) ;
434
550
@@ -461,6 +577,14 @@ const CommandDialogContent = () => {
461
577
/>
462
578
) ;
463
579
}
580
+ if ( group === "tag" ) {
581
+ return (
582
+ < TagOptionsGroup
583
+ key = { group }
584
+ options = { matches as TagOption [ ] }
585
+ />
586
+ ) ;
587
+ }
464
588
if ( group === "breakpoint" ) {
465
589
return (
466
590
< BreakpointOptionsGroup
0 commit comments