@@ -25,13 +25,16 @@ import {
2525 collectionComponent ,
2626 parseComponentName ,
2727 elementComponent ,
28+ tags ,
2829} from "@webstudio-is/sdk" ;
29- import type { Breakpoint , Page } from "@webstudio-is/sdk" ;
30+ import type { Breakpoint , Instance , Page } from "@webstudio-is/sdk" ;
3031import type { TemplateMeta } from "@webstudio-is/template" ;
3132import {
3233 $breakpoints ,
3334 $editingPageId ,
35+ $instances ,
3436 $pages ,
37+ $props ,
3538 $registeredComponentMetas ,
3639 $registeredTemplates ,
3740 $selectedBreakpoint ,
@@ -44,7 +47,11 @@ import {
4447} from "~/shared/instance-utils" ;
4548import { humanizeString } from "~/shared/string-utils" ;
4649import { setCanvasWidth } from "~/builder/features/breakpoints" ;
47- import { $selectedPage , selectPage } from "~/shared/awareness" ;
50+ import {
51+ $selectedInstancePath ,
52+ $selectedPage ,
53+ selectPage ,
54+ } from "~/shared/awareness" ;
4855import { mapGroupBy } from "~/shared/shim" ;
4956import { setActiveSidebarPanel } from "~/builder/shared/nano-states" ;
5057import { $commandMetas } from "~/shared/commands-emitter" ;
@@ -54,6 +61,7 @@ import {
5461 getInstanceLabel ,
5562 InstanceIcon ,
5663} from "~/builder/shared/instance-label" ;
64+ import { isTreeSatisfyingContentModel } from "~/shared/content-model" ;
5765
5866const $commandPanel = atom <
5967 | undefined
@@ -233,6 +241,101 @@ const ComponentOptionsGroup = ({ options }: { options: ComponentOption[] }) => {
233241 ) ;
234242} ;
235243
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+
236339type BreakpointOption = {
237340 tokens : string [ ] ;
238341 type : "breakpoint" ;
@@ -423,12 +526,25 @@ const ShortcutOptionsGroup = ({ options }: { options: ShortcutOption[] }) => {
423526} ;
424527
425528const $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+ ) => [
428543 ...componentOptions ,
429544 ...breakpointOptions ,
430545 ...pageOptions ,
431546 ...commandOptions ,
547+ ...tagOptions ,
432548 ]
433549) ;
434550
@@ -461,6 +577,14 @@ const CommandDialogContent = () => {
461577 />
462578 ) ;
463579 }
580+ if ( group === "tag" ) {
581+ return (
582+ < TagOptionsGroup
583+ key = { group }
584+ options = { matches as TagOption [ ] }
585+ />
586+ ) ;
587+ }
464588 if ( group === "breakpoint" ) {
465589 return (
466590 < BreakpointOptionsGroup
0 commit comments