diff --git a/apps/builder/app/shared/copy-paste.test.tsx b/apps/builder/app/shared/copy-paste.test.tsx index 2e4f16af18db..4c816070851f 100644 --- a/apps/builder/app/shared/copy-paste.test.tsx +++ b/apps/builder/app/shared/copy-paste.test.tsx @@ -28,6 +28,7 @@ import { } from "./instance-utils"; import { $project } from "./nano-states"; import { findAvailableVariables } from "./data-variables"; +import { camelCaseProperty } from "@webstudio-is/css-data"; $project.set({ id: "current_project" } as Project); @@ -83,8 +84,14 @@ const insertStyles = ({ styleSourceId: string; style: TemplateStyleDecl[]; }) => { - for (const styleDecl of style) { - const newStyleDecl = { breakpointId, styleSourceId, ...styleDecl }; + for (const { state, property, value } of style) { + const newStyleDecl = { + breakpointId, + styleSourceId, + state, + property: camelCaseProperty(property), + value, + }; data.styles.set(getStyleDeclKey(newStyleDecl), newStyleDecl); } }; diff --git a/fixtures/react-router-docker/app/__generated__/index.css b/fixtures/react-router-docker/app/__generated__/index.css index 2170b71f3d8f..d226704c7a4f 100644 --- a/fixtures/react-router-docker/app/__generated__/index.css +++ b/fixtures/react-router-docker/app/__generated__/index.css @@ -26,46 +26,6 @@ border-left-width: 1px; outline-width: 1px; } - h2.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - h3.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - h4.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - h5.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - h6.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } div.w-text { box-sizing: border-box; border-top-width: 1px; diff --git a/fixtures/react-router-netlify/app/__generated__/index.css b/fixtures/react-router-netlify/app/__generated__/index.css index 2170b71f3d8f..d226704c7a4f 100644 --- a/fixtures/react-router-netlify/app/__generated__/index.css +++ b/fixtures/react-router-netlify/app/__generated__/index.css @@ -26,46 +26,6 @@ border-left-width: 1px; outline-width: 1px; } - h2.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - h3.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - h4.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - h5.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - h6.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } div.w-text { box-sizing: border-box; border-top-width: 1px; diff --git a/fixtures/react-router-vercel/app/__generated__/index.css b/fixtures/react-router-vercel/app/__generated__/index.css index 2170b71f3d8f..d226704c7a4f 100644 --- a/fixtures/react-router-vercel/app/__generated__/index.css +++ b/fixtures/react-router-vercel/app/__generated__/index.css @@ -26,46 +26,6 @@ border-left-width: 1px; outline-width: 1px; } - h2.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - h3.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - h4.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - h5.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - h6.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } div.w-text { box-sizing: border-box; border-top-width: 1px; diff --git a/fixtures/ssg-netlify-by-project-id/app/__generated__/index.css b/fixtures/ssg-netlify-by-project-id/app/__generated__/index.css index f94277d8c8e5..82f21c895eeb 100644 --- a/fixtures/ssg-netlify-by-project-id/app/__generated__/index.css +++ b/fixtures/ssg-netlify-by-project-id/app/__generated__/index.css @@ -26,44 +26,4 @@ border-left-width: 1px; outline-width: 1px; } - h2.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - h3.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - h4.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - h5.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - h6.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } } diff --git a/fixtures/ssg/app/__generated__/index.css b/fixtures/ssg/app/__generated__/index.css index 2170b71f3d8f..d226704c7a4f 100644 --- a/fixtures/ssg/app/__generated__/index.css +++ b/fixtures/ssg/app/__generated__/index.css @@ -26,46 +26,6 @@ border-left-width: 1px; outline-width: 1px; } - h2.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - h3.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - h4.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - h5.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - h6.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } div.w-text { box-sizing: border-box; border-top-width: 1px; diff --git a/fixtures/webstudio-cloudflare-template/app/__generated__/index.css b/fixtures/webstudio-cloudflare-template/app/__generated__/index.css index 2170b71f3d8f..d226704c7a4f 100644 --- a/fixtures/webstudio-cloudflare-template/app/__generated__/index.css +++ b/fixtures/webstudio-cloudflare-template/app/__generated__/index.css @@ -26,46 +26,6 @@ border-left-width: 1px; outline-width: 1px; } - h2.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - h3.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - h4.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - h5.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - h6.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } div.w-text { box-sizing: border-box; border-top-width: 1px; diff --git a/fixtures/webstudio-features/app/__generated__/index.css b/fixtures/webstudio-features/app/__generated__/index.css index 91816fd03eb4..a4e795d25e5e 100644 --- a/fixtures/webstudio-features/app/__generated__/index.css +++ b/fixtures/webstudio-features/app/__generated__/index.css @@ -26,14 +26,6 @@ border-left-width: 1px; outline-width: 1px; } - h2.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } h3.w-heading { box-sizing: border-box; border-top-width: 1px; @@ -42,30 +34,6 @@ border-left-width: 1px; outline-width: 1px; } - h4.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - h5.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - h6.w-heading { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } div.w-box { box-sizing: border-box; border-top-width: 1px; @@ -74,78 +42,6 @@ border-left-width: 1px; outline-width: 1px; } - address.w-box { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - article.w-box { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - aside.w-box { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - figure.w-box { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - footer.w-box { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - header.w-box { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - main.w-box { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - nav.w-box { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } - section.w-box { - box-sizing: border-box; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - outline-width: 1px; - } p.w-paragraph { box-sizing: border-box; border-top-width: 1px; diff --git a/packages/sdk/src/css.test.tsx b/packages/sdk/src/css.test.tsx index 56e3049f0019..1660b9242c63 100644 --- a/packages/sdk/src/css.test.tsx +++ b/packages/sdk/src/css.test.tsx @@ -1,5 +1,5 @@ import { expect, test } from "vitest"; -import { $, ws, css, renderData } from "@webstudio-is/template"; +import { $, ws, css, renderData, createProxy } from "@webstudio-is/template"; import { generateCss, type CssConfig } from "./css"; import type { Breakpoint } from "./schema/breakpoints"; import { rootComponent } from "./core-metas"; @@ -150,9 +150,12 @@ Map { test("generate component presets with multiple tags", () => { const { cssText, atomicCssText, classes, atomicClasses } = generateAllCss({ + ...renderData( + <$.ListItem tag="div"> + <$.ListItem tag="a"> + + ), assets: new Map(), - instances: new Map(), - props: new Map(), breakpoints: new Map(), styleSourceSelections: new Map([]), styles: new Map(), @@ -163,18 +166,12 @@ test("generate component presets with multiple tags", () => { type: "container", icon: "", presetStyle: { - div: [ - { - property: "display", - value: { type: "keyword", value: "block" }, - }, - ], - a: [ - { - property: "user-select", - value: { type: "keyword", value: "none" }, - }, - ], + div: css` + display: block; + `, + a: css` + user-select: none; + `, }, }, ], @@ -194,15 +191,27 @@ test("generate component presets with multiple tags", () => { " `); expect(cssText).toEqual(atomicCssText); - expect(classes).toMatchInlineSnapshot(`Map {}`); + expect(classes).toEqual( + new Map([ + ["0", ["w-list-item"]], + ["1", ["w-list-item"]], + ]) + ); expect(classes).toEqual(atomicClasses); }); test("deduplicate component presets for similarly named components", () => { + const radix = createProxy("@webstudio/radix:"); + const aria = createProxy("@webstudio/aria:"); const { cssText, atomicCssText, classes, atomicClasses } = generateAllCss({ + ...renderData( + <$.ListItem> + + + + + ), assets: new Map(), - instances: new Map(), - props: new Map(), breakpoints: new Map(), styleSourceSelections: new Map([]), styles: new Map(), @@ -213,12 +222,9 @@ test("deduplicate component presets for similarly named components", () => { type: "container", icon: "", presetStyle: { - div: [ - { - property: "display", - value: { type: "keyword", value: "block" }, - }, - ], + div: css` + display: block; + `, }, }, ], @@ -228,12 +234,9 @@ test("deduplicate component presets for similarly named components", () => { type: "container", icon: "", presetStyle: { - div: [ - { - property: "display", - value: { type: "keyword", value: "flex" }, - }, - ], + div: css` + display: flex; + `, }, }, ], @@ -243,12 +246,9 @@ test("deduplicate component presets for similarly named components", () => { type: "container", icon: "", presetStyle: { - div: [ - { - property: "display", - value: { type: "keyword", value: "grid" }, - }, - ], + div: css` + display: grid; + `, }, }, ], @@ -270,7 +270,13 @@ test("deduplicate component presets for similarly named components", () => { " `); expect(cssText).toEqual(atomicCssText); - expect(classes).toMatchInlineSnapshot(`Map {}`); + expect(classes).toEqual( + new Map([ + ["0", ["w-list-item"]], + ["1", ["w-list-item-1"]], + ["2", ["w-list-item-2"]], + ]) + ); expect(classes).toEqual(atomicClasses); }); @@ -298,12 +304,9 @@ test("expose preset classes to instances", () => { type: "container", icon: "", presetStyle: { - div: [ - { - property: "display", - value: { type: "keyword", value: "block" }, - }, - ], + div: css` + display: block; + `, }, }, ], @@ -313,12 +316,9 @@ test("expose preset classes to instances", () => { type: "container", icon: "", presetStyle: { - div: [ - { - property: "display", - value: { type: "keyword", value: "flex" }, - }, - ], + div: css` + display: flex; + `, }, }, ], @@ -395,12 +395,9 @@ test("generate classes with instance and meta label", () => { icon: "", label: "body meta label", presetStyle: { - div: [ - { - property: "display", - value: { type: "keyword", value: "block" }, - }, - ], + div: css` + display: block; + `, }, }, ], @@ -411,12 +408,9 @@ test("generate classes with instance and meta label", () => { icon: "", label: "box meta label", presetStyle: { - div: [ - { - property: "display", - value: { type: "keyword", value: "flex" }, - }, - ], + div: css` + display: flex; + `, }, }, ], @@ -494,12 +488,9 @@ test("generate :root preset and user styles", () => { icon: "", label: "Global Root", presetStyle: { - html: [ - { - property: "display", - value: { type: "keyword", value: "grid" }, - }, - ], + html: css` + display: grid; + `, }, }, ], @@ -535,3 +526,69 @@ test("generate :root preset and user styles", () => { `); expect(atomicClasses).toEqual(new Map()); }); + +test("generate presets only for used tags", () => { + const { cssText, classes } = generateCss({ + ...renderData( + <$.Body ws:id="body"> + {/* first tag in preset */} + <$.Box> + {/* legacy tag property */} + <$.Box tag="span"> + {/* modern ws:tag property */} + <$.Box ws:tag="article"> + + ), + atomic: false, + breakpoints: toMap([{ id: "base", label: "" }]), + styleSourceSelections: new Map(), + styles: new Map(), + componentMetas: new Map([ + [ + "Box", + { + type: "container", + icon: "", + presetStyle: { + div: css` + display: block; + `, + span: css` + display: block; + `, + article: css` + display: block; + `, + section: css` + display: block; + `, + main: css` + display: block; + `, + }, + }, + ], + ]), + assetBaseUrl: "", + }); + expect(cssText).toMatchInlineSnapshot(` +"@layer presets { + div.w-box { + display: block + } + span.w-box { + display: block + } + article.w-box { + display: block + } +} +"`); + expect(classes).toEqual( + new Map([ + ["0", ["w-box"]], + ["1", ["w-box"]], + ["2", ["w-box"]], + ]) + ); +}); diff --git a/packages/sdk/src/css.ts b/packages/sdk/src/css.ts index 44e164c526db..d15ddd3ad6e5 100644 --- a/packages/sdk/src/css.ts +++ b/packages/sdk/src/css.ts @@ -98,7 +98,32 @@ export const generateCss = ({ presetSheet.addMediaRule("presets"); const presetClasses = new Map(); const scope = createScope([], normalizeClassName, "-"); + + const tagsByComponent = new Map>(); + tagsByComponent.set(rootComponent, new Set(["html"])); + const tagByInstanceId = new Map(); + for (const prop of props.values()) { + if (prop.type === "string" && prop.name === "tag") { + tagByInstanceId.set(prop.instanceId, prop.value); + } + } + for (const instance of instances.values()) { + const propTag = tagByInstanceId.get(instance.id); + const meta = componentMetas.get(instance.component); + const metaTag = Object.keys(meta?.presetStyle ?? {}).at(0); + let componentTags = tagsByComponent.get(instance.component); + if (componentTags === undefined) { + componentTags = new Set(); + tagsByComponent.set(instance.component, componentTags); + } + const tag = instance.tag ?? propTag ?? metaTag; + if (tag) { + componentTags.add(tag); + } + } + for (const [component, meta] of componentMetas) { + const componentTags = tagsByComponent.get(component); const [_namespace, componentName] = parseComponentName(component); const className = `w-${scope.getName(component, meta.label ?? componentName)}`; const presetStyle = Object.entries(meta.presetStyle ?? {}); @@ -106,12 +131,11 @@ export const generateCss = ({ // add preset class only when at least one style is defined presetClasses.set(component, className); } - // @todo reset specificity with css cascade layers instead of :where 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 - // render root preset styles without changes + // ignore unused tags + if (!componentTags?.has(tag)) { + continue; + } const selector = component === rootComponent ? ":root" : `${tag}.${className}`; const rule = presetSheet.addNestingRule(selector); diff --git a/packages/template/src/css.ts b/packages/template/src/css.ts index 5496a6583dbe..93858f475f61 100644 --- a/packages/template/src/css.ts +++ b/packages/template/src/css.ts @@ -1,9 +1,9 @@ -import { camelCaseProperty, parseCss } from "@webstudio-is/css-data"; -import type { StyleProperty, StyleValue } from "@webstudio-is/css-engine"; +import { parseCss } from "@webstudio-is/css-data"; +import type { CssProperty, StyleValue } from "@webstudio-is/css-engine"; export type TemplateStyleDecl = { state?: string; - property: StyleProperty; + property: CssProperty; value: StyleValue; }; @@ -14,7 +14,7 @@ export const css = ( const cssString = `.styles{ ${String.raw({ raw: strings }, ...values)} }`; const styles: TemplateStyleDecl[] = []; for (const { state, property, value } of parseCss(cssString)) { - styles.push({ state, property: camelCaseProperty(property), value }); + styles.push({ state, property: property, value }); } return styles; }; diff --git a/packages/template/src/jsx.ts b/packages/template/src/jsx.ts index 718657ef5d10..b0d7f5f49eea 100644 --- a/packages/template/src/jsx.ts +++ b/packages/template/src/jsx.ts @@ -14,6 +14,7 @@ import type { } from "@webstudio-is/sdk"; import { showAttribute } from "@webstudio-is/react-sdk"; import type { TemplateStyleDecl } from "./css"; +import { camelCaseProperty } from "@webstudio-is/css-data"; export class Variable { name: string; @@ -276,11 +277,13 @@ export const renderTemplate = ( values: [styleSourceId], }); const localStyles = value as TemplateStyleDecl[]; - for (const styleDecl of localStyles) { + for (const { state, property, value } of localStyles) { styles.push({ breakpointId: getBreakpointId(), styleSourceId, - ...styleDecl, + state, + property: camelCaseProperty(property), + value, }); } continue;