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$.HeadTitle>
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>$.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} />
- $.Box>
+
$.VimeoPlayButton>
$.Vimeo>
),
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")}$.Label>
- <$.Input name="name" />
- <$.Label>{new PlaceholderValue("Email")}$.Label>
- <$.Input name="email" />
- <$.Button>{new PlaceholderValue("Submit")}$.Button>
- $.Box>
- <$.Box
+
+ {new PlaceholderValue("Name")}
+
+
+
+ {new PlaceholderValue("Email")}
+
+
+
+ {new PlaceholderValue("Submit")}
+
+
+
{new PlaceholderValue("Thank you for getting in touch!")}
- $.Box>
- <$.Box
+
+
{new PlaceholderValue("Sorry, something went wrong.")}
- $.Box>
+
$.Form>
),
};
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} />
- $.Box>
+
$.VimeoPlayButton>
$.YouTube>
),
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}`}$.Text>
- $.Box>
+
+ {expression`${collectionItem}`}
+
),
};
@@ -52,20 +67,20 @@ const blockMeta: TemplateMeta = {
template: (
- <$.Paragraph>$.Paragraph>
- <$.Heading ws:label="Heading 1" ws:tag="h1">$.Heading>
- <$.Heading ws:label="Heading 2" ws:tag="h2">$.Heading>
- <$.Heading ws:label="Heading 3" ws:tag="h3">$.Heading>
- <$.Heading ws:label="Heading 4" ws:tag="h4">$.Heading>
- <$.Heading ws:label="Heading 5" ws:tag="h5">$.Heading>
- <$.Heading ws:label="Heading 6" ws:tag="h6">$.Heading>
- <$.List ws:label="List (Unordered)">
- <$.ListItem>$.ListItem>
- $.List>
- <$.List ws:label="List (Ordered)" ordered={true}>
- <$.ListItem>$.ListItem>
- $.List>
- <$.Link>$.Link>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<$.Image
ws:style={css`
margin-right: auto;
@@ -74,35 +89,35 @@ const blockMeta: TemplateMeta = {
height: auto;
`}
/>
- <$.Separator />
- <$.Blockquote>$.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
- $.RichTextLink>
+
.
- $.Paragraph>
- <$.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>
- <$.ListItem>
+
+
To predefine instances for insertion in Content mode, switch to Design
mode and add them to the Templates container.
- $.ListItem>
- <$.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.
- $.ListItem>
- $.List>
+
+
),
};
@@ -337,6 +352,7 @@ const forms: Record = {
export const coreTemplates = {
[elementComponent]: elementMeta,
+ link: linkMeta,
[collectionComponent]: collectionMeta,
[descendantComponent]: descendantMeta,
[blockComponent]: blockMeta,