diff --git a/apps/builder/app/canvas/shared/styles.ts b/apps/builder/app/canvas/shared/styles.ts index ebada94686..f8399a6205 100644 --- a/apps/builder/app/canvas/shared/styles.ts +++ b/apps/builder/app/canvas/shared/styles.ts @@ -11,7 +11,6 @@ import { collapsedAttribute, idAttribute, addGlobalRules, - addPresetRules, createImageValueTransformer, descendantComponent, type Params, @@ -324,7 +323,22 @@ export const GlobalStyles = ({ params }: { params: Params }) => { useLayoutEffect(() => { presetSheet.clear(); - addPresetRules(presetSheet, metas); + presetSheet.addMediaRule("presets"); + for (const [component, meta] of metas) { + for (const [tag, styles] of Object.entries(meta.presetStyle ?? {})) { + const rule = presetSheet.addNestingRule( + `${tag}:where([data-ws-component="${component}"])` + ); + for (const declaration of styles) { + rule.setDeclaration({ + breakpoint: "presets", + selector: declaration.state ?? "", + property: declaration.property, + value: declaration.value, + }); + } + } + } presetSheet.render(); }, [metas]); diff --git a/fixtures/ssg/app/__generated__/[another-page]._index.tsx b/fixtures/ssg/app/__generated__/[another-page]._index.tsx index 4d1098fe8e..dc400d12f4 100644 --- a/fixtures/ssg/app/__generated__/[another-page]._index.tsx +++ b/fixtures/ssg/app/__generated__/[another-page]._index.tsx @@ -20,8 +20,16 @@ export const pageBackgroundImageAssets: ImageAsset[] = []; const Page = ({}: { system: any }) => { return ( - - + + {"Another page"} diff --git a/fixtures/ssg/app/__generated__/_index.tsx b/fixtures/ssg/app/__generated__/_index.tsx index 914abbcb0f..1f0a3395d2 100644 --- a/fixtures/ssg/app/__generated__/_index.tsx +++ b/fixtures/ssg/app/__generated__/_index.tsx @@ -29,15 +29,19 @@ const Page = ({}: { system: any }) => { - + {"Simple Project to test CLI"} {"Please don't change directly in the fixture"} @@ -45,6 +49,7 @@ const Page = ({}: { system: any }) => { data-ws-id="pjkZo5EiBqaeUXBcyHf_O" data-ws-component="Link" href={"/another-page"} + className="w-link" > {"Test another page link"} diff --git a/fixtures/ssg/app/__generated__/index.css b/fixtures/ssg/app/__generated__/index.css index 2370a7e646..bc9e936306 100644 --- a/fixtures/ssg/app/__generated__/index.css +++ b/fixtures/ssg/app/__generated__/index.css @@ -4,7 +4,7 @@ html { min-height: 100%; } @media all { - body:where([data-ws-component="Body"]) { + :where(body.w-body) { font-family: Arial, Roboto, sans-serif; font-size: 16px; line-height: 1.2; @@ -17,7 +17,7 @@ html { -moz-osx-font-smoothing: grayscale; margin: 0; } - h1:where([data-ws-component="Heading"]) { + :where(h1.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -25,7 +25,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h2:where([data-ws-component="Heading"]) { + :where(h2.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -33,7 +33,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h3:where([data-ws-component="Heading"]) { + :where(h3.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -41,7 +41,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h4:where([data-ws-component="Heading"]) { + :where(h4.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -49,7 +49,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h5:where([data-ws-component="Heading"]) { + :where(h5.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -57,7 +57,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h6:where([data-ws-component="Heading"]) { + :where(h6.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -65,7 +65,7 @@ html { border-left-width: 1px; outline-width: 1px; } - div:where([data-ws-component="Text"]) { + :where(div.w-text) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -74,7 +74,7 @@ html { outline-width: 1px; min-height: 1em; } - a:where([data-ws-component="Link"]) { + :where(a.w-link) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; diff --git a/fixtures/webstudio-cloudflare-template/app/__generated__/[another-page]._index.tsx b/fixtures/webstudio-cloudflare-template/app/__generated__/[another-page]._index.tsx index 7cf0971d61..d8c98243d2 100644 --- a/fixtures/webstudio-cloudflare-template/app/__generated__/[another-page]._index.tsx +++ b/fixtures/webstudio-cloudflare-template/app/__generated__/[another-page]._index.tsx @@ -18,8 +18,16 @@ export const pageBackgroundImageAssets: ImageAsset[] = []; const Page = ({}: { system: any }) => { return ( - - + + {"Another page"} diff --git a/fixtures/webstudio-cloudflare-template/app/__generated__/_index.tsx b/fixtures/webstudio-cloudflare-template/app/__generated__/_index.tsx index 3ceab9547b..b3f8b2e3ea 100644 --- a/fixtures/webstudio-cloudflare-template/app/__generated__/_index.tsx +++ b/fixtures/webstudio-cloudflare-template/app/__generated__/_index.tsx @@ -31,15 +31,19 @@ const Page = ({}: { system: any }) => { - + {"Simple Project to test CLI"} {"Please don't change directly in the fixture"} @@ -47,6 +51,7 @@ const Page = ({}: { system: any }) => { data-ws-id="pjkZo5EiBqaeUXBcyHf_O" data-ws-component="Link" href={"/another-page"} + className="w-link" > {"Test another page link"} diff --git a/fixtures/webstudio-cloudflare-template/app/__generated__/index.css b/fixtures/webstudio-cloudflare-template/app/__generated__/index.css index 2370a7e646..bc9e936306 100644 --- a/fixtures/webstudio-cloudflare-template/app/__generated__/index.css +++ b/fixtures/webstudio-cloudflare-template/app/__generated__/index.css @@ -4,7 +4,7 @@ html { min-height: 100%; } @media all { - body:where([data-ws-component="Body"]) { + :where(body.w-body) { font-family: Arial, Roboto, sans-serif; font-size: 16px; line-height: 1.2; @@ -17,7 +17,7 @@ html { -moz-osx-font-smoothing: grayscale; margin: 0; } - h1:where([data-ws-component="Heading"]) { + :where(h1.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -25,7 +25,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h2:where([data-ws-component="Heading"]) { + :where(h2.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -33,7 +33,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h3:where([data-ws-component="Heading"]) { + :where(h3.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -41,7 +41,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h4:where([data-ws-component="Heading"]) { + :where(h4.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -49,7 +49,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h5:where([data-ws-component="Heading"]) { + :where(h5.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -57,7 +57,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h6:where([data-ws-component="Heading"]) { + :where(h6.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -65,7 +65,7 @@ html { border-left-width: 1px; outline-width: 1px; } - div:where([data-ws-component="Text"]) { + :where(div.w-text) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -74,7 +74,7 @@ html { outline-width: 1px; min-height: 1em; } - a:where([data-ws-component="Link"]) { + :where(a.w-link) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; diff --git a/fixtures/webstudio-custom-template/app/__generated__/[script-test]._index.tsx b/fixtures/webstudio-custom-template/app/__generated__/[script-test]._index.tsx index f9ba511f01..b6af7a2126 100644 --- a/fixtures/webstudio-custom-template/app/__generated__/[script-test]._index.tsx +++ b/fixtures/webstudio-custom-template/app/__generated__/[script-test]._index.tsx @@ -37,15 +37,20 @@ const Page = ({}: { system: any }) => { - + {"Script Test"} {"Go Home"} @@ -55,6 +60,7 @@ const Page = ({}: { system: any }) => { code={ "
\n\n\nSCRIPTS ARE HERE 2
" } + className="w-html-embed" /> { "\n\nSCRIPTS ARE HERE
" } clientOnly={true} + className="w-html-embed" /> ); diff --git a/fixtures/webstudio-custom-template/app/__generated__/[sitemap.xml]._index.tsx b/fixtures/webstudio-custom-template/app/__generated__/[sitemap.xml]._index.tsx index c372f7c724..3d2aa3d7d3 100644 --- a/fixtures/webstudio-custom-template/app/__generated__/[sitemap.xml]._index.tsx +++ b/fixtures/webstudio-custom-template/app/__generated__/[sitemap.xml]._index.tsx @@ -30,7 +30,11 @@ const Body = (props: any) => props.children; const Page = ({ system: system }: { system: any }) => { let sitemapxml = useResource("sitemapxml_1"); return ( - + { return ( - - + + {"Привет Мир"} diff --git a/fixtures/webstudio-custom-template/app/__generated__/_index.tsx b/fixtures/webstudio-custom-template/app/__generated__/_index.tsx index d2b66726a4..c43aaff9d2 100644 --- a/fixtures/webstudio-custom-template/app/__generated__/_index.tsx +++ b/fixtures/webstudio-custom-template/app/__generated__/_index.tsx @@ -92,19 +92,19 @@ const Page = ({}: { system: any }) => { {"DO NOT TOUCH THIS PROJECT, IT'S USED FOR FIXTURES"} { } width={1024} height={1024} - className="ccgiojm cv4f3rn cer5re4 c9lz7ss" + className="w-image ccgiojm cv4f3rn cer5re4 c9lz7ss" /> - + {"Below Image"} { "\n\nSCRIPTS ARE HERE
" } clientOnly={true} + className="w-html-embed" /> { code={ "\n\nSCRIPTS ARE HERE
" } + className="w-html-embed" /> {"Goto Scr"} @@ -152,7 +159,7 @@ const Page = ({}: { system: any }) => { data-ws-component="Vimeo" url={"https://player.vimeo.com/video/831343124"} showPreview={true} - className="c17osjft c5kxibo c8mrdqb" + className="w-vimeo c17osjft c5kxibo c8mrdqb" > { alt={"Vimeo video preview image"} sizes={"100vw"} src={"/custom-folder/home_wsKvRSqvkajPPBeycZ-C8.svg"} - className="c13adha2 c19kjzjn c8mrdqb chv0hit c1vw7ut7 c64vnj4 cow7xo6 cddunt6 cqjhyu2" + className="w-vimeo-preview-image c13adha2 c19kjzjn c8mrdqb chv0hit c1vw7ut7 c64vnj4 cow7xo6 cddunt6 cqjhyu2" /> { '' } executeScriptOnCanvas={false} + className="w-html-embed" /> { code={ '' } + className="w-html-embed" /> diff --git a/fixtures/webstudio-custom-template/app/__generated__/index.css b/fixtures/webstudio-custom-template/app/__generated__/index.css index 57f6dc04b0..ba545c4834 100644 --- a/fixtures/webstudio-custom-template/app/__generated__/index.css +++ b/fixtures/webstudio-custom-template/app/__generated__/index.css @@ -12,7 +12,7 @@ html { min-height: 100%; } @media all { - body:where([data-ws-component="Body"]) { + :where(body.w-body) { font-family: Arial, Roboto, sans-serif; font-size: 16px; line-height: 1.2; @@ -25,7 +25,7 @@ html { -moz-osx-font-smoothing: grayscale; margin: 0; } - h1:where([data-ws-component="Heading"]) { + :where(h1.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -33,7 +33,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h2:where([data-ws-component="Heading"]) { + :where(h2.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -41,7 +41,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h3:where([data-ws-component="Heading"]) { + :where(h3.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -49,7 +49,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h4:where([data-ws-component="Heading"]) { + :where(h4.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -57,7 +57,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h5:where([data-ws-component="Heading"]) { + :where(h5.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -65,7 +65,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h6:where([data-ws-component="Heading"]) { + :where(h6.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -73,7 +73,7 @@ html { border-left-width: 1px; outline-width: 1px; } - a:where([data-ws-component="Link"]) { + :where(a.w-link) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -83,7 +83,7 @@ html { min-height: 1em; display: inline-block; } - div:where([data-ws-component="Vimeo"]) { + :where(div.w-vimeo) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -91,7 +91,7 @@ html { border-left-width: 1px; outline-width: 1px; } - img:where([data-ws-component="VimeoPreviewImage"]) { + :where(img.w-vimeo-preview-image) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -102,7 +102,7 @@ html { display: block; height: auto; } - button:where([data-ws-component="VimeoPlayButton"]) { + :where(button.w-vimeo-play-button) { font-family: inherit; font-size: 100%; line-height: 1.15; @@ -118,7 +118,7 @@ html { text-transform: none; margin: 0; } - div:where([data-ws-component="Box"]) { + :where(div.w-box) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -126,7 +126,7 @@ html { border-left-width: 1px; outline-width: 1px; } - address:where([data-ws-component="Box"]) { + :where(address.w-box) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -134,7 +134,7 @@ html { border-left-width: 1px; outline-width: 1px; } - article:where([data-ws-component="Box"]) { + :where(article.w-box) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -142,7 +142,7 @@ html { border-left-width: 1px; outline-width: 1px; } - aside:where([data-ws-component="Box"]) { + :where(aside.w-box) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -150,7 +150,7 @@ html { border-left-width: 1px; outline-width: 1px; } - figure:where([data-ws-component="Box"]) { + :where(figure.w-box) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -158,7 +158,7 @@ html { border-left-width: 1px; outline-width: 1px; } - footer:where([data-ws-component="Box"]) { + :where(footer.w-box) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -166,7 +166,7 @@ html { border-left-width: 1px; outline-width: 1px; } - header:where([data-ws-component="Box"]) { + :where(header.w-box) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -174,7 +174,7 @@ html { border-left-width: 1px; outline-width: 1px; } - main:where([data-ws-component="Box"]) { + :where(main.w-box) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -182,7 +182,7 @@ html { border-left-width: 1px; outline-width: 1px; } - nav:where([data-ws-component="Box"]) { + :where(nav.w-box) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -190,7 +190,7 @@ html { border-left-width: 1px; outline-width: 1px; } - section:where([data-ws-component="Box"]) { + :where(section.w-box) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -198,10 +198,10 @@ html { border-left-width: 1px; outline-width: 1px; } - div:where([data-ws-component="HtmlEmbed"]) { + :where(div.w-html-embed) { display: contents; } - div:where([data-ws-component="VimeoSpinner"]) { + :where(div.w-vimeo-spinner) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -209,7 +209,7 @@ html { border-left-width: 1px; outline-width: 1px; } - img:where([data-ws-component="Image"]) { + :where(img.w-image) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; diff --git a/fixtures/webstudio-remix-netlify-edge-functions/app/__generated__/[another-page]._index.tsx b/fixtures/webstudio-remix-netlify-edge-functions/app/__generated__/[another-page]._index.tsx index 7cf0971d61..d8c98243d2 100644 --- a/fixtures/webstudio-remix-netlify-edge-functions/app/__generated__/[another-page]._index.tsx +++ b/fixtures/webstudio-remix-netlify-edge-functions/app/__generated__/[another-page]._index.tsx @@ -18,8 +18,16 @@ export const pageBackgroundImageAssets: ImageAsset[] = []; const Page = ({}: { system: any }) => { return ( - - + + {"Another page"} diff --git a/fixtures/webstudio-remix-netlify-edge-functions/app/__generated__/_index.tsx b/fixtures/webstudio-remix-netlify-edge-functions/app/__generated__/_index.tsx index 3ceab9547b..b3f8b2e3ea 100644 --- a/fixtures/webstudio-remix-netlify-edge-functions/app/__generated__/_index.tsx +++ b/fixtures/webstudio-remix-netlify-edge-functions/app/__generated__/_index.tsx @@ -31,15 +31,19 @@ const Page = ({}: { system: any }) => { - + {"Simple Project to test CLI"} {"Please don't change directly in the fixture"} @@ -47,6 +51,7 @@ const Page = ({}: { system: any }) => { data-ws-id="pjkZo5EiBqaeUXBcyHf_O" data-ws-component="Link" href={"/another-page"} + className="w-link" > {"Test another page link"} diff --git a/fixtures/webstudio-remix-netlify-edge-functions/app/__generated__/index.css b/fixtures/webstudio-remix-netlify-edge-functions/app/__generated__/index.css index 2370a7e646..bc9e936306 100644 --- a/fixtures/webstudio-remix-netlify-edge-functions/app/__generated__/index.css +++ b/fixtures/webstudio-remix-netlify-edge-functions/app/__generated__/index.css @@ -4,7 +4,7 @@ html { min-height: 100%; } @media all { - body:where([data-ws-component="Body"]) { + :where(body.w-body) { font-family: Arial, Roboto, sans-serif; font-size: 16px; line-height: 1.2; @@ -17,7 +17,7 @@ html { -moz-osx-font-smoothing: grayscale; margin: 0; } - h1:where([data-ws-component="Heading"]) { + :where(h1.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -25,7 +25,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h2:where([data-ws-component="Heading"]) { + :where(h2.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -33,7 +33,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h3:where([data-ws-component="Heading"]) { + :where(h3.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -41,7 +41,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h4:where([data-ws-component="Heading"]) { + :where(h4.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -49,7 +49,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h5:where([data-ws-component="Heading"]) { + :where(h5.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -57,7 +57,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h6:where([data-ws-component="Heading"]) { + :where(h6.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -65,7 +65,7 @@ html { border-left-width: 1px; outline-width: 1px; } - div:where([data-ws-component="Text"]) { + :where(div.w-text) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -74,7 +74,7 @@ html { outline-width: 1px; min-height: 1em; } - a:where([data-ws-component="Link"]) { + :where(a.w-link) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; diff --git a/fixtures/webstudio-remix-netlify-functions/app/__generated__/[another-page]._index.tsx b/fixtures/webstudio-remix-netlify-functions/app/__generated__/[another-page]._index.tsx index 7cf0971d61..d8c98243d2 100644 --- a/fixtures/webstudio-remix-netlify-functions/app/__generated__/[another-page]._index.tsx +++ b/fixtures/webstudio-remix-netlify-functions/app/__generated__/[another-page]._index.tsx @@ -18,8 +18,16 @@ export const pageBackgroundImageAssets: ImageAsset[] = []; const Page = ({}: { system: any }) => { return ( - - + + {"Another page"} diff --git a/fixtures/webstudio-remix-netlify-functions/app/__generated__/_index.tsx b/fixtures/webstudio-remix-netlify-functions/app/__generated__/_index.tsx index 3ceab9547b..b3f8b2e3ea 100644 --- a/fixtures/webstudio-remix-netlify-functions/app/__generated__/_index.tsx +++ b/fixtures/webstudio-remix-netlify-functions/app/__generated__/_index.tsx @@ -31,15 +31,19 @@ const Page = ({}: { system: any }) => { - + {"Simple Project to test CLI"} {"Please don't change directly in the fixture"} @@ -47,6 +51,7 @@ const Page = ({}: { system: any }) => { data-ws-id="pjkZo5EiBqaeUXBcyHf_O" data-ws-component="Link" href={"/another-page"} + className="w-link" > {"Test another page link"} diff --git a/fixtures/webstudio-remix-netlify-functions/app/__generated__/index.css b/fixtures/webstudio-remix-netlify-functions/app/__generated__/index.css index 2370a7e646..bc9e936306 100644 --- a/fixtures/webstudio-remix-netlify-functions/app/__generated__/index.css +++ b/fixtures/webstudio-remix-netlify-functions/app/__generated__/index.css @@ -4,7 +4,7 @@ html { min-height: 100%; } @media all { - body:where([data-ws-component="Body"]) { + :where(body.w-body) { font-family: Arial, Roboto, sans-serif; font-size: 16px; line-height: 1.2; @@ -17,7 +17,7 @@ html { -moz-osx-font-smoothing: grayscale; margin: 0; } - h1:where([data-ws-component="Heading"]) { + :where(h1.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -25,7 +25,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h2:where([data-ws-component="Heading"]) { + :where(h2.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -33,7 +33,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h3:where([data-ws-component="Heading"]) { + :where(h3.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -41,7 +41,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h4:where([data-ws-component="Heading"]) { + :where(h4.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -49,7 +49,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h5:where([data-ws-component="Heading"]) { + :where(h5.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -57,7 +57,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h6:where([data-ws-component="Heading"]) { + :where(h6.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -65,7 +65,7 @@ html { border-left-width: 1px; outline-width: 1px; } - div:where([data-ws-component="Text"]) { + :where(div.w-text) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -74,7 +74,7 @@ html { outline-width: 1px; min-height: 1em; } - a:where([data-ws-component="Link"]) { + :where(a.w-link) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; diff --git a/fixtures/webstudio-remix-vercel/app/__generated__/[_route_with_symbols_]._index.tsx b/fixtures/webstudio-remix-vercel/app/__generated__/[_route_with_symbols_]._index.tsx index 713e384dcc..585a515cc2 100644 --- a/fixtures/webstudio-remix-vercel/app/__generated__/[_route_with_symbols_]._index.tsx +++ b/fixtures/webstudio-remix-vercel/app/__generated__/[_route_with_symbols_]._index.tsx @@ -28,12 +28,16 @@ export const pageBackgroundImageAssets: ImageAsset[] = []; const Page = ({}: { system: any }) => { return ( - + ); diff --git a/fixtures/webstudio-remix-vercel/app/__generated__/[form]._index.tsx b/fixtures/webstudio-remix-vercel/app/__generated__/[form]._index.tsx index 81714a7409..52087722e4 100644 --- a/fixtures/webstudio-remix-vercel/app/__generated__/[form]._index.tsx +++ b/fixtures/webstudio-remix-vercel/app/__generated__/[form]._index.tsx @@ -39,7 +39,11 @@ const Page = ({}: { system: any }) => { let [formState, set$formState] = useState("initial"); let [formState_1, set$formState_1] = useState("initial"); return ( - +
{ formState = state; set$formState(formState); }} + className="w-form" > {(formState === "initial" || formState === "error") && ( - + {"Default form"} - )} {formState === "success" && ( - + {"Thank you for getting in touch!"} )} {formState === "error" && ( - + {"Sorry, something went wrong."} )} @@ -103,47 +132,72 @@ const Page = ({}: { system: any }) => { }} method={"get"} action={"/custom"} + className="w-form" > {(formState_1 === "initial" || formState_1 === "error") && ( - + {"Form with custom action and method"} - )} {formState_1 === "success" && ( - + {"Thank you for getting in touch!"} )} {formState_1 === "error" && ( - + {"Sorry, something went wrong."} )} diff --git a/fixtures/webstudio-remix-vercel/app/__generated__/[heading-with-id]._index.tsx b/fixtures/webstudio-remix-vercel/app/__generated__/[heading-with-id]._index.tsx index 4519fdb85a..0f04d001a6 100644 --- a/fixtures/webstudio-remix-vercel/app/__generated__/[heading-with-id]._index.tsx +++ b/fixtures/webstudio-remix-vercel/app/__generated__/[heading-with-id]._index.tsx @@ -28,11 +28,16 @@ export const pageBackgroundImageAssets: ImageAsset[] = []; const Page = ({}: { system: any }) => { return ( - + {"Heading you can edit"} diff --git a/fixtures/webstudio-remix-vercel/app/__generated__/[nested].[nested-page]._index.tsx b/fixtures/webstudio-remix-vercel/app/__generated__/[nested].[nested-page]._index.tsx index d1f42fe479..d3233f3bf1 100644 --- a/fixtures/webstudio-remix-vercel/app/__generated__/[nested].[nested-page]._index.tsx +++ b/fixtures/webstudio-remix-vercel/app/__generated__/[nested].[nested-page]._index.tsx @@ -28,8 +28,16 @@ export const pageBackgroundImageAssets: ImageAsset[] = []; const Page = ({}: { system: any }) => { return ( - - + + {"Nested page"} diff --git a/fixtures/webstudio-remix-vercel/app/__generated__/[radix]._index.tsx b/fixtures/webstudio-remix-vercel/app/__generated__/[radix]._index.tsx index a275e329aa..b6dd6c57b7 100644 --- a/fixtures/webstudio-remix-vercel/app/__generated__/[radix]._index.tsx +++ b/fixtures/webstudio-remix-vercel/app/__generated__/[radix]._index.tsx @@ -40,7 +40,11 @@ export const pageBackgroundImageAssets: ImageAsset[] = []; const Page = ({}: { system: any }) => { let [accordionValue, set$accordionValue] = useState("0"); return ( - + { accordionValue = value; set$accordionValue(accordionValue); }} + className="w-accordion" > - + {"Is it accessible?"} { code={ '' } + className="w-html-embed" /> @@ -88,7 +98,7 @@ const Page = ({}: { system: any }) => { {"Yes. It adheres to the WAI-ARIA design pattern."} @@ -97,25 +107,29 @@ const Page = ({}: { system: any }) => { data-ws-id="C838wkvIcA1BQu30Xu2G8" data-ws-component="@webstudio-is/sdk-components-react-radix:AccordionItem" data-ws-index="1" - className="c2onvcp" + className="w-accordion-item c2onvcp" > - + {"Is it styled?"} { code={ '' } + className="w-html-embed" /> @@ -130,7 +145,7 @@ const Page = ({}: { system: any }) => { { "Yes. It comes with default styles that matches the other components' aesthetic." @@ -141,25 +156,29 @@ const Page = ({}: { system: any }) => { data-ws-id="65djoTmSBGemZ2L5izQ5M" data-ws-component="@webstudio-is/sdk-components-react-radix:AccordionItem" data-ws-index="2" - className="c2onvcp" + className="w-accordion-item c2onvcp" > - + {"Is it animated?"} { code={ '' } + className="w-html-embed" /> @@ -174,7 +194,7 @@ const Page = ({}: { system: any }) => { { "Yes. It's animated by default, but you can disable it if you prefer." diff --git a/fixtures/webstudio-remix-vercel/app/__generated__/[resources]._index.tsx b/fixtures/webstudio-remix-vercel/app/__generated__/[resources]._index.tsx index 7110813931..b448afd9a3 100644 --- a/fixtures/webstudio-remix-vercel/app/__generated__/[resources]._index.tsx +++ b/fixtures/webstudio-remix-vercel/app/__generated__/[resources]._index.tsx @@ -32,14 +32,23 @@ export const pageBackgroundImageAssets: ImageAsset[] = []; const Page = ({}: { system: any }) => { let list = useResource("list_1"); return ( - + {list?.data?.map((collectionItem: any, index: number) => ( - + diff --git a/fixtures/webstudio-remix-vercel/app/__generated__/_index.tsx b/fixtures/webstudio-remix-vercel/app/__generated__/_index.tsx index e76ad9d2b3..515a373dc6 100644 --- a/fixtures/webstudio-remix-vercel/app/__generated__/_index.tsx +++ b/fixtures/webstudio-remix-vercel/app/__generated__/_index.tsx @@ -64,34 +64,36 @@ const Page = ({}: { system: any }) => { {"DO NOT TOUCH THIS PROJECT, IT'S USED FOR FIXTURES"} {"Heading"} { "a little kitten painted in black and white gouache with a thick brush" @@ -101,6 +103,7 @@ const Page = ({}: { system: any }) => { data-ws-id="l9AI_pShC-BH4ibxK6kNT" data-ws-component="Link" href={"https://github.com/"} + className="w-link" > {"Click here to adore more kittens"} @@ -108,6 +111,7 @@ const Page = ({}: { system: any }) => { data-ws-id="D8wLZzLWQfxH9uaKsn-0L" data-ws-component="Text" tag={"span"} + className="w-text" > {" or "} @@ -115,15 +119,20 @@ const Page = ({}: { system: any }) => { data-ws-id="8AXawjUE3fJoOH_1qOAoq" data-ws-component="Link" href={"/assets/small-avif-kitty_FnabJsioMWpBtXZSGf4DR.webp"} + className="w-link" > {"go download this little kitten"} - + {"Symbols in path"} @@ -131,7 +140,7 @@ const Page = ({}: { system: any }) => { data-ws-id="81ejLVXyFEV1SxiJrWhyw" data-ws-component="Link" href={"/heading-with-id#my-heading"} - className="ch2exr5" + className="w-link ch2exr5" > {"Link to instance"} @@ -139,7 +148,7 @@ const Page = ({}: { system: any }) => { { src={ "/assets/_937084ed-a798-49fe-8664-df93a2af605e_uiBk3o6UWdqolyakMvQJ9.jpeg" } - className="c1czoo99" + className="w-image c1czoo99" /> diff --git a/fixtures/webstudio-remix-vercel/app/__generated__/index.css b/fixtures/webstudio-remix-vercel/app/__generated__/index.css index a4357f7529..06ca713c73 100644 --- a/fixtures/webstudio-remix-vercel/app/__generated__/index.css +++ b/fixtures/webstudio-remix-vercel/app/__generated__/index.css @@ -4,7 +4,7 @@ html { min-height: 100%; } @media all { - body:where([data-ws-component="Body"]) { + :where(body.w-body) { font-family: Arial, Roboto, sans-serif; font-size: 16px; line-height: 1.2; @@ -17,7 +17,7 @@ html { -moz-osx-font-smoothing: grayscale; margin: 0; } - h1:where([data-ws-component="Heading"]) { + :where(h1.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -25,7 +25,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h2:where([data-ws-component="Heading"]) { + :where(h2.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -33,7 +33,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h3:where([data-ws-component="Heading"]) { + :where(h3.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -41,7 +41,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h4:where([data-ws-component="Heading"]) { + :where(h4.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -49,7 +49,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h5:where([data-ws-component="Heading"]) { + :where(h5.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -57,7 +57,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h6:where([data-ws-component="Heading"]) { + :where(h6.w-heading) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -65,7 +65,7 @@ html { border-left-width: 1px; outline-width: 1px; } - div:where([data-ws-component="Box"]) { + :where(div.w-box) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -73,7 +73,7 @@ html { border-left-width: 1px; outline-width: 1px; } - address:where([data-ws-component="Box"]) { + :where(address.w-box) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -81,7 +81,7 @@ html { border-left-width: 1px; outline-width: 1px; } - article:where([data-ws-component="Box"]) { + :where(article.w-box) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -89,7 +89,7 @@ html { border-left-width: 1px; outline-width: 1px; } - aside:where([data-ws-component="Box"]) { + :where(aside.w-box) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -97,7 +97,7 @@ html { border-left-width: 1px; outline-width: 1px; } - figure:where([data-ws-component="Box"]) { + :where(figure.w-box) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -105,7 +105,7 @@ html { border-left-width: 1px; outline-width: 1px; } - footer:where([data-ws-component="Box"]) { + :where(footer.w-box) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -113,7 +113,7 @@ html { border-left-width: 1px; outline-width: 1px; } - header:where([data-ws-component="Box"]) { + :where(header.w-box) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -121,7 +121,7 @@ html { border-left-width: 1px; outline-width: 1px; } - main:where([data-ws-component="Box"]) { + :where(main.w-box) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -129,7 +129,7 @@ html { border-left-width: 1px; outline-width: 1px; } - nav:where([data-ws-component="Box"]) { + :where(nav.w-box) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -137,7 +137,7 @@ html { border-left-width: 1px; outline-width: 1px; } - section:where([data-ws-component="Box"]) { + :where(section.w-box) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -145,7 +145,7 @@ html { border-left-width: 1px; outline-width: 1px; } - p:where([data-ws-component="Paragraph"]) { + :where(p.w-paragraph) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -153,7 +153,7 @@ html { border-left-width: 1px; outline-width: 1px; } - img:where([data-ws-component="Image"]) { + :where(img.w-image) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -164,7 +164,7 @@ html { display: block; height: auto; } - a:where([data-ws-component="Link"]) { + :where(a.w-link) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -174,7 +174,7 @@ html { min-height: 1em; display: inline-block; } - div:where([data-ws-component="Text"]) { + :where(div.w-text) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -183,9 +183,7 @@ html { outline-width: 1px; min-height: 1em; } - div:where( - [data-ws-component="@webstudio-is/sdk-components-react-radix:Accordion"] - ) { + :where(div.w-accordion) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -193,9 +191,7 @@ html { border-left-width: 1px; outline-width: 1px; } - div:where( - [data-ws-component="@webstudio-is/sdk-components-react-radix:AccordionItem"] - ) { + :where(div.w-accordion-item) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -203,9 +199,7 @@ html { border-left-width: 1px; outline-width: 1px; } - h3:where( - [data-ws-component="@webstudio-is/sdk-components-react-radix:AccordionHeader"] - ) { + :where(h3.w-accordion-header) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -215,9 +209,7 @@ html { margin-top: 0px; margin-bottom: 0px; } - button:where( - [data-ws-component="@webstudio-is/sdk-components-react-radix:AccordionTrigger"] - ) { + :where(button.w-accordion-trigger) { font-family: inherit; font-size: 100%; line-height: 1.15; @@ -229,12 +221,10 @@ html { margin: 0; padding: 0px; } - div:where([data-ws-component="HtmlEmbed"]) { + :where(div.w-html-embed) { display: contents; } - div:where( - [data-ws-component="@webstudio-is/sdk-components-react-radix:AccordionContent"] - ) { + :where(div.w-accordion-content) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -242,7 +232,7 @@ html { border-left-width: 1px; outline-width: 1px; } - form:where([data-ws-component="Form"]) { + :where(form.w-form) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -251,7 +241,7 @@ html { outline-width: 1px; min-height: 20px; } - label:where([data-ws-component="Label"]) { + :where(label.w-label) { box-sizing: border-box; border-top-width: 1px; border-right-width: 1px; @@ -260,7 +250,7 @@ html { outline-width: 1px; display: block; } - input:where([data-ws-component="Input"]) { + :where(input.w-input) { font-family: inherit; font-size: 100%; line-height: 1.15; @@ -276,7 +266,7 @@ html { display: block; margin: 0; } - button:where([data-ws-component="Button"]) { + :where(button.w-button) { font-family: inherit; font-size: 100%; line-height: 1.15; diff --git a/packages/cli/src/prebuild.ts b/packages/cli/src/prebuild.ts index c790e7bc86..f32c404030 100644 --- a/packages/cli/src/prebuild.ts +++ b/packages/cli/src/prebuild.ts @@ -483,7 +483,7 @@ export const prebuild = async (options: { const assets = new Map(siteData.assets.map((asset) => [asset.id, asset])); - const { cssText, classesMap } = generateCss({ + const { cssText, classes } = generateCss({ instances: new Map(siteData.build.instances), props: new Map(siteData.build.props), assets, @@ -596,7 +596,7 @@ export const prebuild = async (options: { instances, props, dataSources, - classesMap, + classesMap: classes, indexesWithinAncestors: getIndexesWithinAncestors( projectMetas, instances, diff --git a/packages/css-engine/src/core/atomic.test.ts b/packages/css-engine/src/core/atomic.test.ts index feddbe2525..996a9608dd 100644 --- a/packages/css-engine/src/core/atomic.test.ts +++ b/packages/css-engine/src/core/atomic.test.ts @@ -67,10 +67,10 @@ test("added classes", () => { value: { type: "keyword", value: "block" }, }); const instances = new Map([[rule, "instanceId"]]); - const { classesMap } = generateAtomic(sheet, { + const { classes } = generateAtomic(sheet, { getKey: (rule) => instances.get(rule) ?? "", }); - expect(classesMap.get("instanceId")).toEqual(["ccqp4le"]); + expect(classes.get("instanceId")).toEqual(["ccqp4le"]); }); test("rule with multiple properties", () => { @@ -129,7 +129,7 @@ test("share atomic rules", () => { [rule1, "1"], [rule2, "2"], ]); - const { cssText, classesMap } = generateAtomic(sheet, { + const { cssText, classes } = generateAtomic(sheet, { getKey: (rule) => instances.get(rule) ?? "", }); expect(cssText).toMatchInlineSnapshot(` @@ -142,8 +142,8 @@ test("share atomic rules", () => { } }" `); - expect(classesMap.get("1")).toEqual(["ccqp4le", "cen0ymu"]); - expect(classesMap.get("2")).toEqual(["ccqp4le"]); + expect(classes.get("1")).toEqual(["ccqp4le", "cen0ymu"]); + expect(classes.get("2")).toEqual(["ccqp4le"]); }); test("distinct similar declarations from different breakpoints", () => { diff --git a/packages/css-engine/src/core/atomic.ts b/packages/css-engine/src/core/atomic.ts index 8a06ef24aa..6d6448ad99 100644 --- a/packages/css-engine/src/core/atomic.ts +++ b/packages/css-engine/src/core/atomic.ts @@ -6,21 +6,22 @@ import { toValue, type TransformValue } from "./to-value"; type Options = { getKey: (rule: NestingRule) => string; transformValue?: TransformValue; + classes?: Map; }; export const generateAtomic = (sheet: StyleSheet, options: Options) => { const { getKey, transformValue } = options; const atomicRules = new Map(); - const classesMap = new Map(); + const classes = options.classes ?? new Map(); for (const rule of sheet.nestingRules.values()) { const descendantSuffix = rule.getDescendantSuffix(); const groupKey = getKey(rule); // a few rules can be in the same group // when rule have descendant suffix - let classList = classesMap.get(groupKey); + let classList = classes.get(groupKey); if (classList === undefined) { classList = []; - classesMap.set(groupKey, classList); + classes.set(groupKey, classList); } // convert each declaration into separate rule for (const declaration of rule.getMergedDeclarations()) { @@ -51,5 +52,5 @@ export const generateAtomic = (sheet: StyleSheet, options: Options) => { nestingRules: Array.from(atomicRules.values()), transformValue, }); - return { cssText, classesMap }; + return { cssText, classes }; }; diff --git a/packages/react-sdk/src/css/css.test.tsx b/packages/react-sdk/src/css/css.test.tsx index b413f31027..21fa598579 100644 --- a/packages/react-sdk/src/css/css.test.tsx +++ b/packages/react-sdk/src/css/css.test.tsx @@ -2,24 +2,25 @@ import { expect, test } from "@jest/globals"; import type { Breakpoint } from "@webstudio-is/sdk"; import { generateCss, type CssConfig } from "./css"; import { descendantComponent } from "../core-components"; +import { $, renderJsx } from "@webstudio-is/sdk/testing"; const toMap = (list: T[]) => new Map(list.map((item) => [item.id, item] as const)); const generateAllCss = (config: Omit) => { - const { cssText } = generateCss({ + const { cssText, classes } = generateCss({ ...config, atomic: false, }); - const { cssText: atomicCssText, classesMap } = generateCss({ + const { cssText: atomicCssText, classes: atomicClasses } = generateCss({ ...config, atomic: true, }); - return { cssText, atomicCssText, classesMap }; + return { cssText, atomicCssText, classes, atomicClasses }; }; test("generate css for one instance with two tokens", () => { - const { cssText, atomicCssText, classesMap } = generateAllCss({ + const { cssText, atomicCssText, atomicClasses } = generateAllCss({ assets: new Map(), instances: new Map(), props: new Map(), @@ -67,7 +68,7 @@ test("generate css for one instance with two tokens", () => { } }" `); - expect(classesMap).toMatchInlineSnapshot(` + expect(atomicClasses).toMatchInlineSnapshot(` Map { "box" => [ "cawkhls", @@ -77,7 +78,7 @@ Map { }); test("generate descendant selector", () => { - const { cssText, atomicCssText, classesMap } = generateAllCss({ + const { cssText, atomicCssText, atomicClasses } = generateAllCss({ assets: new Map(), instances: toMap([ { @@ -165,7 +166,7 @@ test("generate descendant selector", () => { } }" `); - expect(classesMap).toMatchInlineSnapshot(` + expect(atomicClasses).toMatchInlineSnapshot(` Map { "root" => [ "c17hlgoh", @@ -177,8 +178,8 @@ Map { `); }); -test("generate component presets", () => { - const { cssText, atomicCssText, classesMap } = generateAllCss({ +test("generate component presets with multiple tags", () => { + const { cssText, atomicCssText, classes, atomicClasses } = generateAllCss({ assets: new Map(), instances: new Map(), props: new Map(), @@ -187,7 +188,7 @@ test("generate component presets", () => { styles: new Map(), componentMetas: new Map([ [ - "Box", + "ListItem", { type: "container", icon: "", @@ -213,28 +214,197 @@ test("generate component presets", () => { expect(cssText).toMatchInlineSnapshot(` "html {margin: 0; display: grid; min-height: 100%} @media all { - div:where([data-ws-component="Box"]) { + :where(div.w-list-item) { display: block } - a:where([data-ws-component="Box"]) { + :where(a.w-list-item) { -webkit-user-select: none; user-select: none } } " `); + expect(cssText).toEqual(atomicCssText); + expect(classes).toMatchInlineSnapshot(`Map {}`); + expect(classes).toEqual(atomicClasses); +}); + +test("deduplicate component presets for similarly named components", () => { + const { cssText, atomicCssText, classes, atomicClasses } = generateAllCss({ + assets: new Map(), + instances: new Map(), + props: new Map(), + breakpoints: new Map(), + styleSourceSelections: new Map([]), + styles: new Map(), + componentMetas: new Map([ + [ + "ListItem", + { + type: "container", + icon: "", + presetStyle: { + div: [ + { + property: "display", + value: { type: "keyword", value: "block" }, + }, + ], + }, + }, + ], + [ + "@webstudio/radix:ListItem", + { + type: "container", + icon: "", + presetStyle: { + div: [ + { + property: "display", + value: { type: "keyword", value: "flex" }, + }, + ], + }, + }, + ], + [ + "@webstudio/aria:ListItem", + { + type: "container", + icon: "", + presetStyle: { + div: [ + { + property: "display", + value: { type: "keyword", value: "grid" }, + }, + ], + }, + }, + ], + ]), + assetBaseUrl: "", + }); + expect(cssText).toMatchInlineSnapshot(` +"html {margin: 0; display: grid; min-height: 100%} +@media all { + :where(div.w-list-item) { + display: block + } + :where(div.w-list-item-1) { + display: flex + } + :where(div.w-list-item-2) { + display: grid + } +} +" +`); + expect(cssText).toEqual(atomicCssText); + expect(classes).toMatchInlineSnapshot(`Map {}`); + expect(classes).toEqual(atomicClasses); +}); + +test("expose preset classes to instances", () => { + const { atomicCssText, classes, atomicClasses } = generateAllCss({ + assets: new Map(), + ...renderJsx( + <$.Body ws:id="body"> + <$.Box ws:id="box"> + + ), + breakpoints: new Map(), + styleSourceSelections: new Map([ + ["body", { instanceId: "body", values: ["localBody"] }], + ["box", { instanceId: "box", values: ["localBox"] }], + ]), + styles: new Map([ + [ + "localBody:base:color", + { + styleSourceId: "localBody", + breakpointId: "base", + property: "color", + value: { type: "keyword", value: "blue" }, + }, + ], + [ + "localBox:base:color::hover", + { + styleSourceId: "localBox", + breakpointId: "base", + property: "color", + value: { type: "keyword", value: "red" }, + }, + ], + ]), + componentMetas: new Map([ + [ + "Body", + { + type: "container", + icon: "", + presetStyle: { + div: [ + { + property: "display", + value: { type: "keyword", value: "block" }, + }, + ], + }, + }, + ], + [ + "Box", + { + type: "container", + icon: "", + presetStyle: { + div: [ + { + property: "display", + value: { type: "keyword", value: "flex" }, + }, + ], + }, + }, + ], + ]), + assetBaseUrl: "", + }); expect(atomicCssText).toMatchInlineSnapshot(` "html {margin: 0; display: grid; min-height: 100%} @media all { - div:where([data-ws-component="Box"]) { + :where(div.w-body) { display: block } - a:where([data-ws-component="Box"]) { - -webkit-user-select: none; - user-select: none + :where(div.w-box) { + display: flex } } " `); - expect(classesMap).toMatchInlineSnapshot(`Map {}`); + expect(classes).toMatchInlineSnapshot(` +Map { + "body" => [ + "w-body", + ], + "box" => [ + "w-box", + ], +} +`); + expect(atomicClasses).toMatchInlineSnapshot(` +Map { + "body" => [ + "w-body", + "c17hlgoh", + ], + "box" => [ + "w-box", + "cawkhls", + ], +} +`); }); diff --git a/packages/react-sdk/src/css/css.ts b/packages/react-sdk/src/css/css.ts index e67c0b1454..93313273a7 100644 --- a/packages/react-sdk/src/css/css.ts +++ b/packages/react-sdk/src/css/css.ts @@ -4,19 +4,22 @@ import { type NestingRule, type TransformValue, } from "@webstudio-is/css-engine"; -import type { - Assets, - Breakpoints, - Instance, - Instances, - Props, - StyleSourceSelections, - Styles, +import { + createScope, + parseComponentName, + type Assets, + type Breakpoints, + type Instance, + type Instances, + type Props, + type StyleSourceSelections, + type Styles, } from "@webstudio-is/sdk"; import type { WsComponentMeta } from "../components/component-meta"; import { idAttribute } from "../props"; import { descendantComponent } from "../core-components"; -import { addGlobalRules, addPresetRules } from "./global-rules"; +import { addGlobalRules } from "./global-rules"; +import { kebabCase } from "change-case"; export type CssConfig = { assets: Assets; @@ -56,6 +59,8 @@ export const createImageValueTransformer = } }; +const normalizeClassName = (name: string) => kebabCase(name); + export const generateCss = ({ assets, instances, @@ -69,25 +74,35 @@ export const generateCss = ({ }: CssConfig) => { const globalSheet = createRegularStyleSheet({ name: "ssr" }); const sheet = createRegularStyleSheet({ name: "ssr" }); - const parentIdByInstanceId = new Map(); - for (const instance of instances.values()) { - for (const child of instance.children) { - if (child.type === "id") { - parentIdByInstanceId.set(child.value, instance.id); + + addGlobalRules(globalSheet, { assets, assetBaseUrl }); + globalSheet.addMediaRule("presets"); + const presetClasses = new Map(); + const scope = createScope([], normalizeClassName, "-"); + for (const [component, meta] of componentMetas) { + const [_namespace, componentName] = parseComponentName(component); + const className = `w-${scope.getName(component, componentName)}`; + const presetStyle = Object.entries(meta.presetStyle ?? {}); + if (presetStyle.length > 0) { + // add preset class only when at least one style is defined + presetClasses.set(component, className); + } + for (const [tag, styles] of presetStyle) { + // use :where() to reset specificity of preset selector + // and let user styles completely override it + // ideally switch to @layer when better supported + const rule = globalSheet.addNestingRule(`:where(${tag}.${className})`); + for (const declaration of styles) { + rule.setDeclaration({ + breakpoint: "presets", + selector: declaration.state ?? "", + property: declaration.property, + value: declaration.value, + }); } } } - const descendantSelectorByInstanceId = new Map(); - for (const prop of props.values()) { - if (prop.name === "selector" && prop.type === "string") { - descendantSelectorByInstanceId.set(prop.instanceId, prop.value); - } - } - - addGlobalRules(globalSheet, { assets, assetBaseUrl }); - addPresetRules(globalSheet, componentMetas); - for (const breakpoint of breakpoints.values()) { sheet.addMediaRule(breakpoint.id, breakpoint); } @@ -107,6 +122,27 @@ export const generateCss = ({ }); } + const classes = new Map(); + const parentIdByInstanceId = new Map(); + for (const instance of instances.values()) { + const presetClass = presetClasses.get(instance.component); + if (presetClass) { + classes.set(instance.id, [presetClass]); + } + for (const child of instance.children) { + if (child.type === "id") { + parentIdByInstanceId.set(child.value, instance.id); + } + } + } + + const descendantSelectorByInstanceId = new Map(); + for (const prop of props.values()) { + if (prop.name === "selector" && prop.type === "string") { + descendantSelectorByInstanceId.set(prop.instanceId, prop.value); + } + } + const instanceByRule = new Map(); for (const selection of styleSourceSelections.values()) { let { instanceId } = selection; @@ -131,14 +167,15 @@ export const generateCss = ({ } if (atomic) { - const { cssText, classesMap } = generateAtomic(sheet, { + const { cssText } = generateAtomic(sheet, { getKey: (rule) => instanceByRule.get(rule) ?? "", transformValue: imageValueTransformer, + classes, }); - return { cssText: `${globalSheet.cssText}\n${cssText}`, classesMap }; + return { cssText: `${globalSheet.cssText}\n${cssText}`, classes }; } return { cssText: `${globalSheet.cssText}\n${sheet.cssText}`, - classesMap: new Map>(), + classes, }; }; diff --git a/packages/react-sdk/src/css/global-rules.ts b/packages/react-sdk/src/css/global-rules.ts index 077a180d60..1652af2eb5 100644 --- a/packages/react-sdk/src/css/global-rules.ts +++ b/packages/react-sdk/src/css/global-rules.ts @@ -1,7 +1,6 @@ import type { StyleSheetRegular } from "@webstudio-is/css-engine"; import type { Assets, FontAsset } from "@webstudio-is/sdk"; import { getFontFaces } from "@webstudio-is/fonts"; -import type { WsComponentMeta } from "../components/component-meta"; export const addGlobalRules = ( sheet: StyleSheetRegular, @@ -25,25 +24,3 @@ export const addGlobalRules = ( sheet.addFontFaceRule(fontFace); } }; - -export const addPresetRules = ( - sheet: StyleSheetRegular, - metas: Map -) => { - sheet.addMediaRule("presets"); - for (const [component, meta] of metas) { - for (const [tag, styles] of Object.entries(meta.presetStyle ?? {})) { - const rule = sheet.addNestingRule( - `${tag}:where([data-ws-component="${component}"])` - ); - for (const declaration of styles) { - rule.setDeclaration({ - breakpoint: "presets", - selector: declaration.state ?? "", - property: declaration.property, - value: declaration.value, - }); - } - } - } -}; diff --git a/packages/sdk-cli/src/generate-stories.ts b/packages/sdk-cli/src/generate-stories.ts index 5724d38e77..361db8a182 100644 --- a/packages/sdk-cli/src/generate-stories.ts +++ b/packages/sdk-cli/src/generate-stories.ts @@ -146,7 +146,7 @@ export const generateStories = async () => { usedMetas.set(instance.component, meta); } } - const { cssText, classesMap } = generateCss({ + const { cssText, classes } = generateCss({ instances, props, assets: new Map(), @@ -175,7 +175,7 @@ export const generateStories = async () => { }); content += `\n`; content += generateWebstudioComponent({ - classesMap, + classesMap: classes, scope, name: `Component`, rootInstanceId, diff --git a/packages/sdk-components-react/src/__generated__/html-embed.props.ts b/packages/sdk-components-react/src/__generated__/html-embed.props.ts index 612a3b015a..96587c9b1a 100644 --- a/packages/sdk-components-react/src/__generated__/html-embed.props.ts +++ b/packages/sdk-components-react/src/__generated__/html-embed.props.ts @@ -1,6 +1,7 @@ import type { PropMeta } from "@webstudio-is/react-sdk"; export const props: Record = { + className: { required: false, control: "text", type: "string" }, clientOnly: { required: false, control: "boolean", type: "boolean" }, code: { required: true, control: "text", type: "string" }, executeScriptOnCanvas: { diff --git a/packages/sdk-components-react/src/html-embed.tsx b/packages/sdk-components-react/src/html-embed.tsx index 633fcfb6bb..c39b738060 100644 --- a/packages/sdk-components-react/src/html-embed.tsx +++ b/packages/sdk-components-react/src/html-embed.tsx @@ -129,6 +129,7 @@ type ChildProps = { innerRef: ForwardedRef; // code can be actually undefined when prop is not provided code?: string; + className?: string; }; const Placeholder = (props: ChildProps) => { @@ -214,6 +215,7 @@ type HtmlEmbedProps = { code: string; executeScriptOnCanvas?: boolean; clientOnly?: boolean; + className?: string; // avoid builder passing it to dom children?: never; }; diff --git a/packages/sdk-components-react/src/html-embed.ws.ts b/packages/sdk-components-react/src/html-embed.ws.ts index 61d1ff12fa..37df02e6e6 100644 --- a/packages/sdk-components-react/src/html-embed.ws.ts +++ b/packages/sdk-components-react/src/html-embed.ws.ts @@ -45,5 +45,5 @@ export const propsMeta: WsComponentPropsMeta = { type: "string", }, }, - initialProps: ["clientOnly", "executeScriptOnCanvas"], + initialProps: ["className", "clientOnly", "executeScriptOnCanvas"], }; diff --git a/packages/sdk/src/scope.ts b/packages/sdk/src/scope.ts index 090f7a5573..20e10ec5c7 100644 --- a/packages/sdk/src/scope.ts +++ b/packages/sdk/src/scope.ts @@ -8,7 +8,7 @@ export type Scope = { }; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#identifiers -const normalizeName = (name: string) => { +const normalizeJsName = (name: string) => { // only letters, digits, underscores and dollar signs allowed in names // delete everything else name = name.replaceAll(/[^\w$]/g, ""); @@ -30,7 +30,11 @@ const normalizeName = (name: string) => { * occupiedIdentifiers parameter prevents collision with hardcoded * identifiers. */ -export const createScope = (occupiedIdentifiers: string[] = []): Scope => { +export const createScope = ( + occupiedIdentifiers: string[] = [], + normalizeName = normalizeJsName, + separator = "_" +): Scope => { const freeIndexByPreferredName = new Map(); const scopedNameByIdMap = new Map(); for (const identifier of occupiedIdentifiers) { @@ -47,7 +51,7 @@ export const createScope = (occupiedIdentifiers: string[] = []): Scope => { freeIndexByPreferredName.set(preferredName, (index ?? 0) + 1); let scopedName = preferredName; if (index !== undefined) { - scopedName = `${preferredName}_${index}`; + scopedName = `${preferredName}${separator}${index}`; } scopedNameByIdMap.set(id, scopedName); return scopedName;