diff --git a/apps/builder/app/builder/features/command-panel/command-panel.tsx b/apps/builder/app/builder/features/command-panel/command-panel.tsx index c72fd2b6df07..2d819e3bdf5e 100644 --- a/apps/builder/app/builder/features/command-panel/command-panel.tsx +++ b/apps/builder/app/builder/features/command-panel/command-panel.tsx @@ -39,6 +39,7 @@ import { } from "~/shared/nano-states"; import { getComponentTemplateData, + insertWebstudioElementAt, insertWebstudioFragmentAt, } from "~/shared/instance-utils"; import { humanizeString } from "~/shared/string-utils"; @@ -165,9 +166,6 @@ const $componentOptions = computed( ) { continue; } - if (isFeatureEnabled("element") === false && name === elementComponent) { - continue; - } const componentMeta = metas.get(name); const label = @@ -207,9 +205,13 @@ const ComponentOptionsGroup = ({ options }: { options: ComponentOption[] }) => { value={component} onSelect={() => { closeCommandPanel(); - const fragment = getComponentTemplateData(component); - if (fragment) { - insertWebstudioFragmentAt(fragment); + if (component === elementComponent) { + insertWebstudioElementAt(); + } else { + const fragment = getComponentTemplateData(component); + if (fragment) { + insertWebstudioFragmentAt(fragment); + } } }} > diff --git a/apps/builder/app/builder/features/components/components.tsx b/apps/builder/app/builder/features/components/components.tsx index 21051ca1a801..6c21db324a83 100644 --- a/apps/builder/app/builder/features/components/components.tsx +++ b/apps/builder/app/builder/features/components/components.tsx @@ -101,9 +101,6 @@ const $metas = computed( ) { continue; } - if (isFeatureEnabled("element") === false && name === elementComponent) { - continue; - } availableComponents.add(name); metas.push({ diff --git a/apps/builder/app/builder/features/settings-panel/props-section/props-section.tsx b/apps/builder/app/builder/features/settings-panel/props-section/props-section.tsx index dba1af50e889..bcf3873f0efc 100644 --- a/apps/builder/app/builder/features/settings-panel/props-section/props-section.tsx +++ b/apps/builder/app/builder/features/settings-panel/props-section/props-section.tsx @@ -104,7 +104,7 @@ const renderProperty = ( }, }); -const forbiddenProperties = new Set(["style", "class", "className"]); +const forbiddenProperties = new Set(["style"]); const $availableProps = computed( [ diff --git a/apps/builder/app/builder/shared/commands.ts b/apps/builder/app/builder/shared/commands.ts index 09c63289eaa7..eba64e0c1396 100644 --- a/apps/builder/app/builder/shared/commands.ts +++ b/apps/builder/app/builder/shared/commands.ts @@ -6,6 +6,7 @@ import { } from "@webstudio-is/sdk"; import type { Instance } from "@webstudio-is/sdk"; import { toast } from "@webstudio-is/design-system"; +import { isFeatureEnabled } from "@webstudio-is/feature-flags"; import { createCommandsEmitter, type Command } from "~/shared/commands-emitter"; import { $editingItemSelector, @@ -528,15 +529,19 @@ export const { emitCommand, subscribeCommands } = createCommandsEmitter({ handler: () => unwrap(), }, - { - name: "pasteHtmlWithTailwindClasses", - handler: async () => { - const html = await navigator.clipboard.readText(); - let fragment = generateFragmentFromHtml(html); - fragment = await generateFragmentFromTailwind(fragment); - return insertWebstudioFragmentAt(fragment); - }, - }, + ...(isFeatureEnabled("tailwind") + ? [ + { + name: "pasteHtmlWithTailwindClasses", + handler: async () => { + const html = await navigator.clipboard.readText(); + let fragment = generateFragmentFromHtml(html); + fragment = await generateFragmentFromTailwind(fragment); + return insertWebstudioFragmentAt(fragment); + }, + }, + ] + : []), // history diff --git a/apps/builder/app/shared/copy-paste/plugin-html.ts b/apps/builder/app/shared/copy-paste/plugin-html.ts index 7e1d45b59b03..5fb87d20cf42 100644 --- a/apps/builder/app/shared/copy-paste/plugin-html.ts +++ b/apps/builder/app/shared/copy-paste/plugin-html.ts @@ -1,4 +1,3 @@ -import { isFeatureEnabled } from "@webstudio-is/feature-flags"; import { generateFragmentFromHtml } from "../html"; import { insertWebstudioFragmentAt } from "../instance-utils"; import type { Plugin } from "./init-copy-paste"; @@ -6,9 +5,6 @@ import type { Plugin } from "./init-copy-paste"; export const html: Plugin = { mimeType: "text/plain", onPaste: (html: string) => { - if (!isFeatureEnabled("element")) { - return false; - } const fragment = generateFragmentFromHtml(html); return insertWebstudioFragmentAt(fragment); }, diff --git a/packages/css-engine/src/core/atomic.test.ts b/packages/css-engine/src/core/atomic.test.ts index e3070f4efe3b..5335af905599 100644 --- a/packages/css-engine/src/core/atomic.test.ts +++ b/packages/css-engine/src/core/atomic.test.ts @@ -279,3 +279,23 @@ test("generate merged properties as single rule", () => { }" `); }); + +test("convert :local-link to [aria-current=page] selector", () => { + const sheet = createRegularStyleSheet(); + const rule = sheet.addNestingRule(".instance"); + sheet.addMediaRule("x"); + rule.setDeclaration({ + breakpoint: "x", + selector: ":local-link", + property: "color", + value: { type: "keyword", value: "green" }, + }); + expect(generateAtomic(sheet, { getKey: () => "" }).cssText) + .toMatchInlineSnapshot(` + "@media all { + .c3mubaz[aria-current=page] { + color: green + } + }" + `); +}); diff --git a/packages/css-engine/src/core/rules.ts b/packages/css-engine/src/core/rules.ts index 2a6ae79fbbe2..bf42fba9dc49 100644 --- a/packages/css-engine/src/core/rules.ts +++ b/packages/css-engine/src/core/rules.ts @@ -258,7 +258,11 @@ export class NestingRule { if (declaration.breakpoint !== breakpoint) { continue; } - const { selector: nestedSelector } = declaration; + let nestedSelector = declaration.selector; + // polyfill :local-link with framework specific logic + if (nestedSelector === ":local-link") { + nestedSelector = "[aria-current=page]"; + } const selector = this.#selector + this.#descendantSuffix + nestedSelector; let style = styleBySelector.get(selector); if (style === undefined) { diff --git a/packages/css-engine/src/core/style-sheet-regular.test.ts b/packages/css-engine/src/core/style-sheet-regular.test.ts index b200a373ea19..5ef938639503 100644 --- a/packages/css-engine/src/core/style-sheet-regular.test.ts +++ b/packages/css-engine/src/core/style-sheet-regular.test.ts @@ -765,3 +765,19 @@ test("generate merged properties as single rule", () => { }" `); }); + +test("convert :local-link to [aria-current=page] selector", () => { + const sheet = createRegularStyleSheet(); + const rule = sheet.addNestingRule(".instance"); + rule.setDeclaration({ + breakpoint: "base", + selector: ":local-link", + property: "color", + value: { type: "keyword", value: "green" }, + }); + expect(rule.toString({ breakpoint: "base" })).toMatchInlineSnapshot(` + ".instance[aria-current=page] { + color: green + }" + `); +}); diff --git a/packages/feature-flags/src/flags.ts b/packages/feature-flags/src/flags.ts index 60319793f86c..f8a996de378d 100644 --- a/packages/feature-flags/src/flags.ts +++ b/packages/feature-flags/src/flags.ts @@ -5,4 +5,4 @@ export const aiRadixComponents = false; export const animation = false; export const videoAnimation = false; export const resourceProp = false; -export const element = false; +export const tailwind = false; diff --git a/packages/html-data/src/pseudo-classes.ts b/packages/html-data/src/pseudo-classes.ts index d68f3865bddd..9d33f6838dca 100644 --- a/packages/html-data/src/pseudo-classes.ts +++ b/packages/html-data/src/pseudo-classes.ts @@ -4,7 +4,7 @@ const location = [ // ':link', ":visited", // ':any-link', - // ':local-link', + ":local-link", // ':target', // ':target-within', ]; diff --git a/packages/sdk-components-react/src/box.ws.ts b/packages/sdk-components-react/src/box.ws.ts index e5c98e639151..4cd005322092 100644 --- a/packages/sdk-components-react/src/box.ws.ts +++ b/packages/sdk-components-react/src/box.ws.ts @@ -14,9 +14,6 @@ import { import { props } from "./__generated__/box.props"; export const meta: WsComponentMeta = { - category: "general", - description: - "A container for content. By default this is a Div, but the tag can be changed in settings.", presetStyle: { div, address, @@ -29,7 +26,6 @@ export const meta: WsComponentMeta = { nav, section, }, - order: 0, initialProps: ["tag", "id", "class"], props: { ...props, diff --git a/packages/sdk-components-react/src/head-slot.template.tsx b/packages/sdk-components-react/src/head-slot.template.tsx index f035205c5ff2..d66e58fbae2b 100644 --- a/packages/sdk-components-react/src/head-slot.template.tsx +++ b/packages/sdk-components-react/src/head-slot.template.tsx @@ -4,7 +4,7 @@ export const meta: TemplateMeta = { category: "general", description: "The Head Slot component lets you customize page-specific head elements (like canonical URLs), which merge with your site's global head settings, with Head Slot definitions taking priority over Page Settings. For site-wide head changes, use Project Settings instead.", - order: 6, + order: 5, template: ( <$.HeadSlot> <$.HeadTitle ws:label="Title">Title diff --git a/packages/sdk-components-react/src/html-embed.ws.ts b/packages/sdk-components-react/src/html-embed.ws.ts index ba84666ddefc..20f739ab9ecf 100644 --- a/packages/sdk-components-react/src/html-embed.ws.ts +++ b/packages/sdk-components-react/src/html-embed.ws.ts @@ -7,7 +7,7 @@ export const meta: WsComponentMeta = { label: "HTML Embed", description: "Used to add HTML code to the page, such as an SVG or script.", icon: EmbedIcon, - order: 2, + order: 3, contentModel: { category: "instance", children: [descendantComponent], diff --git a/packages/sdk-components-react/src/link.template.tsx b/packages/sdk-components-react/src/link.template.tsx deleted file mode 100644 index 5f724a8e21a4..000000000000 --- a/packages/sdk-components-react/src/link.template.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { type TemplateMeta, $ } from "@webstudio-is/template"; - -export const meta: TemplateMeta = { - category: "general", - description: - "Use a link to send your users to another page, section, or resource. Configure links in the Settings panel.", - order: 1, - template: <$.Link>, -}; diff --git a/packages/sdk-components-react/src/slot.ws.ts b/packages/sdk-components-react/src/slot.ws.ts index 717db8fa4eb0..2bf7903dca85 100644 --- a/packages/sdk-components-react/src/slot.ws.ts +++ b/packages/sdk-components-react/src/slot.ws.ts @@ -6,5 +6,5 @@ export const meta: WsComponentMeta = { description: "Slot is a container for content that you want to reference across the project. Changes made to a Slot's children will be reflected in all other instances of that Slot.", icon: SlotComponentIcon, - order: 5, + order: 4, }; diff --git a/packages/sdk-components-react/src/templates.ts b/packages/sdk-components-react/src/templates.ts index 61475c00b628..48c3633a5823 100644 --- a/packages/sdk-components-react/src/templates.ts +++ b/packages/sdk-components-react/src/templates.ts @@ -1,6 +1,5 @@ export { meta as ContentEmbed } from "./content-embed.template"; export { meta as MarkdownEmbed } from "./markdown-embed.template"; -export { meta as Link } from "./link.template"; export { meta as Form } from "./webhook-form.template"; export { meta as Vimeo } from "./vimeo.template"; export { meta as YouTube } from "./youtube.template"; diff --git a/packages/sdk-components-react/src/vimeo.template.tsx b/packages/sdk-components-react/src/vimeo.template.tsx index 0398a626ca42..7984b353c986 100644 --- a/packages/sdk-components-react/src/vimeo.template.tsx +++ b/packages/sdk-components-react/src/vimeo.template.tsx @@ -1,5 +1,5 @@ import { PlayIcon, SpinnerIcon } from "@webstudio-is/icons/svg"; -import { type TemplateMeta, $, css } from "@webstudio-is/template"; +import { type TemplateMeta, $, css, ws } from "@webstudio-is/template"; export const meta: TemplateMeta = { category: "media", @@ -64,7 +64,8 @@ export const meta: TemplateMeta = { `} aria-label="Play button" > - <$.Box + <$.HtmlEmbed ws:label="Play SVG" code={PlayIcon} /> - + ), diff --git a/packages/sdk-components-react/src/webhook-form.template.tsx b/packages/sdk-components-react/src/webhook-form.template.tsx index 2c597376a702..97587a53b9ac 100644 --- a/packages/sdk-components-react/src/webhook-form.template.tsx +++ b/packages/sdk-components-react/src/webhook-form.template.tsx @@ -1,6 +1,8 @@ import { $, + ws, ActionValue, + css, expression, PlaceholderValue, Variable, @@ -20,28 +22,59 @@ export const meta: TemplateMeta = { new ActionValue(["state"], expression`${formState} = state`) } > - <$.Box + - <$.Label>{new PlaceholderValue("Name")} - <$.Input name="name" /> - <$.Label>{new PlaceholderValue("Email")} - <$.Input name="email" /> - <$.Button>{new PlaceholderValue("Submit")} - - <$.Box + + {new PlaceholderValue("Name")} + + + + {new PlaceholderValue("Email")} + + + + {new PlaceholderValue("Submit")} + + + {new PlaceholderValue("Thank you for getting in touch!")} - - <$.Box + + {new PlaceholderValue("Sorry, something went wrong.")} - + ), }; diff --git a/packages/sdk-components-react/src/youtube.template.tsx b/packages/sdk-components-react/src/youtube.template.tsx index 2bd1ec55e10b..1dbb9c9222ca 100644 --- a/packages/sdk-components-react/src/youtube.template.tsx +++ b/packages/sdk-components-react/src/youtube.template.tsx @@ -1,5 +1,5 @@ import { PlayIcon, SpinnerIcon } from "@webstudio-is/icons/svg"; -import { type TemplateMeta, $, css } from "@webstudio-is/template"; +import { type TemplateMeta, $, css, ws } from "@webstudio-is/template"; export const meta: TemplateMeta = { label: "YouTube", @@ -68,7 +68,8 @@ export const meta: TemplateMeta = { `} aria-label="Play button" > - <$.Box + <$.HtmlEmbed ws:label="Play SVG" code={PlayIcon} /> - + ), diff --git a/packages/sdk/src/core-templates.tsx b/packages/sdk/src/core-templates.tsx index 67bec7b84a4a..a5e9d1bf6044 100644 --- a/packages/sdk/src/core-templates.tsx +++ b/packages/sdk/src/core-templates.tsx @@ -7,22 +7,37 @@ import { ws, type TemplateMeta, } from "@webstudio-is/template"; +import { CheckboxCheckedIcon, RadioCheckedIcon } from "@webstudio-is/icons/svg"; import { blockComponent, collectionComponent, descendantComponent, elementComponent, } from "./core-metas"; -import { CheckboxCheckedIcon, RadioCheckedIcon } from "@webstudio-is/icons/svg"; const elementMeta: TemplateMeta = { category: "general", - order: 0, + order: 1, description: "An HTML element is a core building block for web pages, structuring and displaying content like text, images, and links.", template: , }; +const linkMeta: TemplateMeta = { + category: "general", + description: + "Use a link to send your users to another page, section, or resource. Configure links in the Settings panel.", + order: 2, + template: ( + + ), +}; + const collectionItem = new Parameter("collectionItem"); const collectionMeta: TemplateMeta = { @@ -33,9 +48,9 @@ const collectionMeta: TemplateMeta = { data={["Collection Item 1", "Collection Item 2", "Collection Item 3"]} item={collectionItem} > - <$.Box> - <$.Text>{expression`${collectionItem}`} - + + {expression`${collectionItem}`} + ), }; @@ -52,20 +67,20 @@ const blockMeta: TemplateMeta = { template: ( - <$.Paragraph> - <$.Heading ws:label="Heading 1" ws:tag="h1"> - <$.Heading ws:label="Heading 2" ws:tag="h2"> - <$.Heading ws:label="Heading 3" ws:tag="h3"> - <$.Heading ws:label="Heading 4" ws:tag="h4"> - <$.Heading ws:label="Heading 5" ws:tag="h5"> - <$.Heading ws:label="Heading 6" ws:tag="h6"> - <$.List ws:label="List (Unordered)"> - <$.ListItem> - - <$.List ws:label="List (Ordered)" ordered={true}> - <$.ListItem> - - <$.Link> + + + + + + + + + + + + + + <$.Image ws:style={css` margin-right: auto; @@ -74,35 +89,35 @@ const blockMeta: TemplateMeta = { height: auto; `} /> - <$.Separator /> - <$.Blockquote> + + <$.HtmlEmbed /> - <$.CodeText /> + - <$.Paragraph> + The Content Block component designates regions on the page where pre-styled instances can be inserted in{" "} - <$.RichTextLink href="https://wstd.us/content-block"> + Content mode - + . - - <$.List> - <$.ListItem> + + + In Content mode, you can edit any direct child instances that were pre-added to the Content Block, as well as add new instances predefined in Templates. - - <$.ListItem> + + To predefine instances for insertion in Content mode, switch to Design mode and add them to the Templates container. - - <$.ListItem> + + To insert predefined instances in Content mode, click the + button while hovering over the Content Block on the canvas and choose an instance from the list. - - + + ), }; @@ -337,6 +352,7 @@ const forms: Record = { export const coreTemplates = { [elementComponent]: elementMeta, + link: linkMeta, [collectionComponent]: collectionMeta, [descendantComponent]: descendantMeta, [blockComponent]: blockMeta,