diff --git a/apps/designer/app/canvas/canvas.tsx b/apps/designer/app/canvas/canvas.tsx
index b6b37946dc..f9ab92f35c 100644
--- a/apps/designer/app/canvas/canvas.tsx
+++ b/apps/designer/app/canvas/canvas.tsx
@@ -3,10 +3,9 @@ import store from "immerhin";
import type { Build } from "@webstudio-is/project";
import {
createElementsTree,
- useGlobalStyles,
+ useAllUserProps,
type OnChangeChildren,
type Tree,
- useAllUserProps,
} from "@webstudio-is/react-sdk";
import { useSubscribe } from "~/shared/pubsub";
import { useShortcuts } from "./shared/use-shortcuts";
@@ -24,7 +23,7 @@ import {
useUnselectInstance,
useUpdateSelectedInstance,
} from "./shared/instance";
-import { useManageStyles } from "./shared/styles";
+import { useManageDesignModeStyles, GlobalStyles } from "./shared/styles";
import { useTrackSelectedElement } from "./shared/use-track-selected-element";
import { WrapperComponentDev } from "./features/wrapper-component";
import { useSync } from "./shared/sync";
@@ -91,7 +90,7 @@ type DesignModeProps = {
const DesignMode = ({ treeId, buildId }: DesignModeProps) => {
useManageBreakpoints();
- useManageStyles();
+ useManageDesignModeStyles();
useManageProps();
usePublishSelectedInstanceData(treeId);
useInsertInstance();
@@ -123,7 +122,6 @@ export const Canvas = ({ data }: CanvasProps): JSX.Element | null => {
const isDesignerReady = useSubscribeDesignerReady();
useInitializeBreakpoints(data.breakpoints);
const assets = useAssets(data.assets);
- useGlobalStyles({ assets });
useAllUserProps(data.props);
usePopulateRootInstance(data.tree);
// e.g. toggling preview is still needed in both modes
@@ -134,11 +132,17 @@ export const Canvas = ({ data }: CanvasProps): JSX.Element | null => {
if (elements === undefined) return null;
if (isPreviewMode || isDesignerReady === false) {
- return elements;
+ return (
+ <>
+
+ {elements}
+ >
+ );
}
return (
<>
+
{elements}
>
diff --git a/apps/designer/app/canvas/features/wrapper-component/wrapper-component.tsx b/apps/designer/app/canvas/features/wrapper-component/wrapper-component.tsx
index e346c8a7a6..a8458c8359 100644
--- a/apps/designer/app/canvas/features/wrapper-component/wrapper-component.tsx
+++ b/apps/designer/app/canvas/features/wrapper-component/wrapper-component.tsx
@@ -7,6 +7,7 @@ import {
components,
type Instance,
type OnChangeChildren,
+ idAttribute,
} from "@webstudio-is/react-sdk";
import { useTextEditingInstanceId } from "~/shared/nano-states";
import { useSelectedElement } from "~/canvas/shared/nano-states";
@@ -77,7 +78,7 @@ export const WrapperComponentDev = ({
// for this we need to also make search by id fast
id: instance.id,
"data-ws-component": instance.component,
- "data-ws-id": instance.id,
+ [idAttribute]: instance.id,
onClick: (event: MouseEvent) => {
if (instance.component === "Link") {
event.preventDefault();
diff --git a/apps/designer/app/canvas/shared/breakpoints.ts b/apps/designer/app/canvas/shared/breakpoints.ts
index 15a2bacef0..6d2a960d65 100644
--- a/apps/designer/app/canvas/shared/breakpoints.ts
+++ b/apps/designer/app/canvas/shared/breakpoints.ts
@@ -1,6 +1,6 @@
import { useEffect, useRef } from "react";
import store from "immerhin";
-import { type Breakpoint, setBreakpoints } from "@webstudio-is/react-sdk";
+import type { Breakpoint } from "@webstudio-is/react-sdk";
import { useSubscribe } from "~/shared/pubsub";
import { deleteCssRulesByBreakpoint } from "~/shared/css-utils";
import {
@@ -17,7 +17,6 @@ export const useInitializeBreakpoints = (breakpoints: Array) => {
const ref = useRef(false);
if (ref.current === false) {
ref.current = true;
- setBreakpoints(breakpoints);
setCurrentBreakpoints(breakpoints);
addMediaRules(breakpoints);
}
@@ -45,7 +44,7 @@ const useBreakpointChange = () => {
breakpoints.push(breakpoint);
}
- setBreakpoints(breakpoints);
+ addMediaRules(breakpoints);
});
});
};
diff --git a/apps/designer/app/canvas/shared/styles.ts b/apps/designer/app/canvas/shared/styles.ts
index 323145fca5..7d719af167 100644
--- a/apps/designer/app/canvas/shared/styles.ts
+++ b/apps/designer/app/canvas/shared/styles.ts
@@ -4,59 +4,98 @@ import { setInstanceStyleMutable } from "~/shared/tree-utils";
import { useSelectedInstance } from "./nano-states";
import { rootInstanceContainer } from "~/shared/nano-states";
import {
- toValue,
- toVarNamespace,
+ validStaticValueTypes,
+ idAttribute,
+ type Breakpoint,
+ type ValidStaticStyleValue,
type StyleValue,
type CssRule,
type StyleProperty,
type Style,
- Breakpoint,
- ValidStaticStyleValue,
- validStaticValueTypes,
} from "@webstudio-is/react-sdk";
-import { globalCss } from "@webstudio-is/design-system";
import { useEffect } from "react";
-import { createCssEngine, type StyleRule } from "@webstudio-is/css-engine";
+import {
+ createCssEngine,
+ toValue,
+ type CssEngine,
+ type StyleRule,
+ type PlaintextRule,
+} from "@webstudio-is/css-engine";
import { useIsomorphicLayoutEffect } from "react-use";
+import { Asset, FontAsset } from "@webstudio-is/asset-uploader";
+import { FontFormat, FONT_FORMATS, getFontFaces } from "@webstudio-is/fonts";
+
+const cssEngine = createCssEngine();
const voidElements =
"area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr";
// Helper styles on for canvas in design mode
-const styles = {
- "[data-ws-component]": {
- // When double clicking into an element to edit text, it should not select the word.
- userSelect: "none",
- },
- [`[data-ws-component]:not(${voidElements}):not(body):empty`]: {
- outline: "1px dashed #555",
- outlineOffset: -1,
- paddingTop: 50,
- paddingRight: 50,
- },
- "[data-ws-component][contenteditable], [data-ws-component]:focus": {
- outline: 0,
- },
- "[data-ws-component][contenteditable]": {
- boxShadow: "0 0 0px 4px rgb(36 150 255 / 20%)",
- },
+const helperStyles = [
+ // When double clicking into an element to edit text, it should not select the word.
+ `[${idAttribute}] {
+ user-select: none;
+ }`,
+ `[${idAttribute}]:not(${voidElements}):not(body):empty {
+ outline: 1px dashed #555;
+ outline-offset: -1;
+ padding-top: 50;
+ padding-right: 50;
+ }`,
+ `[${idAttribute}][contenteditable], [${idAttribute}]:focus {
+ outline: 0;
+ }`,
+ `[${idAttribute}][contenteditable] {
+ box-shadow: 0 0 0px 4px rgb(36 150 255 / 20%)
+ }`,
// Text Editor wraps each line into a p, so we need to make sure there is no jump between regular rendering and editing
- "[data-ws-component][contenteditable] p": {
- margin: 0,
- },
-};
+ `[${idAttribute}][contenteditable] p {
+ margin: 0
+ }`,
+];
-// @todo rewrite to use css engine and remove stitches from canvas entirely
-const wrapperComponentGlobalStyles = globalCss(styles);
-
-export const useManageStyles = () => {
- wrapperComponentGlobalStyles();
+export const useManageDesignModeStyles = () => {
useUpdateStyle();
usePreviewStyle();
useRemoveSsrStyles();
};
-const cssEngine = createCssEngine();
+export const addGlobalRules = (
+ engine: CssEngine,
+ { assets = [] }: { assets?: Array }
+) => {
+ // @todo we need to figure out all global resets while keeping
+ // the engine aware of all of them.
+ // Ideally, the user is somehow aware and in control of the reset
+ engine.addPlaintextRule("html {margin: 0; height: 100%}");
+
+ const fontAssets = assets.filter((asset) =>
+ FONT_FORMATS.has(asset.format as FontFormat)
+ ) as Array;
+ const fontFaces = getFontFaces(fontAssets);
+ for (const fontFace of fontFaces) {
+ engine.addFontFaceRule(fontFace);
+ }
+};
+
+const globalStylesCssEngine = createCssEngine();
+
+export const GlobalStyles = ({ assets }: { assets: Array }) => {
+ useIsomorphicLayoutEffect(() => {
+ globalStylesCssEngine.clear();
+
+ addGlobalRules(globalStylesCssEngine, { assets });
+
+ for (const style of helperStyles) {
+ globalStylesCssEngine.addPlaintextRule(style);
+ }
+
+ if (typeof document !== "undefined") {
+ globalStylesCssEngine.render();
+ }
+ });
+ return null;
+};
// Wrapps a normal StyleValue into a VarStyleValue that uses the previous style value as a fallback and allows
// to quickly pass the values over CSS variable witout rerendering the components tree.
@@ -94,11 +133,11 @@ export const addMediaRules = (breakpoints: Array) => {
}
};
-const wrappedRulesMap = new Map();
+const wrappedRulesMap = new Map();
const addRule = (id: string, cssRule: CssRule) => {
const key = id + cssRule.breakpoint;
- const selectorText = `[data-ws-id="${id}"]`;
+ const selectorText = `[${idAttribute}="${id}"]`;
const rule = cssEngine.addStyleRule(selectorText, {
...cssRule,
style: toVarStyleWithFallback(id, cssRule.style),
@@ -138,6 +177,10 @@ export const useCssRules = ({
}, [id, cssRules]);
};
+const toVarNamespace = (id: string, property: string) => {
+ return `${property}-${id}`;
+};
+
const setCssVar = (id: string, property: string, value?: StyleValue) => {
const customProperty = `--${toVarNamespace(id, property)}`;
if (value === undefined) {
diff --git a/apps/designer/app/designer/features/style-panel/controls/font-weight/font-weight-control.tsx b/apps/designer/app/designer/features/style-panel/controls/font-weight/font-weight-control.tsx
index a00f040f15..bfe647e781 100644
--- a/apps/designer/app/designer/features/style-panel/controls/font-weight/font-weight-control.tsx
+++ b/apps/designer/app/designer/features/style-panel/controls/font-weight/font-weight-control.tsx
@@ -1,6 +1,6 @@
import { Select } from "@webstudio-is/design-system";
import { FontWeight, fontWeights } from "@webstudio-is/fonts";
-import { toValue } from "@webstudio-is/react-sdk";
+import { toValue } from "@webstudio-is/css-engine";
import { useMemo } from "react";
import { useAssets } from "~/designer/shared/assets";
import { getFinalValue } from "../../shared/get-final-value";
diff --git a/apps/designer/app/designer/features/style-panel/controls/text/text-control.tsx b/apps/designer/app/designer/features/style-panel/controls/text/text-control.tsx
index ed1c01dbda..41aa81f7ed 100644
--- a/apps/designer/app/designer/features/style-panel/controls/text/text-control.tsx
+++ b/apps/designer/app/designer/features/style-panel/controls/text/text-control.tsx
@@ -1,7 +1,8 @@
import { CssValueInput } from "../../shared/css-value-input";
import { getFinalValue } from "../../shared/get-final-value";
import { ControlProps } from "../../style-sections";
-import { type StyleValue, toValue } from "@webstudio-is/react-sdk";
+import { type StyleValue } from "@webstudio-is/react-sdk";
+import { toValue } from "@webstudio-is/css-engine";
import { useState } from "react";
import { Box, Tooltip } from "@webstudio-is/design-system";
diff --git a/apps/designer/app/designer/features/style-panel/sections/spacing/spacing.tsx b/apps/designer/app/designer/features/style-panel/sections/spacing/spacing.tsx
index cb0328b750..c5fe5662ee 100644
--- a/apps/designer/app/designer/features/style-panel/sections/spacing/spacing.tsx
+++ b/apps/designer/app/designer/features/style-panel/sections/spacing/spacing.tsx
@@ -1,11 +1,11 @@
import { useEffect, useState } from "react";
import {
categories,
- toValue,
type StyleProperty,
type StyleValue,
} from "@webstudio-is/react-sdk";
import { Box } from "@webstudio-is/design-system";
+import { toValue } from "@webstudio-is/css-engine";
import { SetProperty } from "../../shared/use-style-data";
import { useIsFromCurrentBreakpoint } from "../../shared/use-is-from-current-breakpoint";
import { propertyNameColorForSelectedBreakpoint } from "../../shared/constants";
diff --git a/apps/designer/app/designer/features/style-panel/shared/css-value-input/unit-select.tsx b/apps/designer/app/designer/features/style-panel/shared/css-value-input/unit-select.tsx
index f92544cf75..6012a7dfc0 100644
--- a/apps/designer/app/designer/features/style-panel/shared/css-value-input/unit-select.tsx
+++ b/apps/designer/app/designer/features/style-panel/shared/css-value-input/unit-select.tsx
@@ -1,10 +1,6 @@
import { useState, useMemo } from "react";
-import {
- type Unit,
- type UnitValue,
- StyleValue,
- toValue,
-} from "@webstudio-is/react-sdk";
+import { type Unit, type UnitValue, StyleValue } from "@webstudio-is/react-sdk";
+import { toValue } from "@webstudio-is/css-engine";
import * as SelectPrimitive from "@radix-ui/react-select";
import {
styled,
diff --git a/apps/designer/app/designer/features/style-panel/shared/get-final-value.ts b/apps/designer/app/designer/features/style-panel/shared/get-final-value.ts
index ccdf9d8ce1..07fb72ba94 100644
--- a/apps/designer/app/designer/features/style-panel/shared/get-final-value.ts
+++ b/apps/designer/app/designer/features/style-panel/shared/get-final-value.ts
@@ -1,9 +1,5 @@
-import {
- Style,
- StyleValue,
- StyleProperty,
- toValue,
-} from "@webstudio-is/react-sdk";
+import type { Style, StyleValue, StyleProperty } from "@webstudio-is/react-sdk";
+import { toValue } from "@webstudio-is/css-engine";
import type { InheritedStyle } from "./get-inherited-style";
import { isValid } from "./parse-css-value";
diff --git a/apps/designer/app/designer/features/style-panel/style-sections.tsx b/apps/designer/app/designer/features/style-panel/style-sections.tsx
index 04112db691..ed77fa82d3 100644
--- a/apps/designer/app/designer/features/style-panel/style-sections.tsx
+++ b/apps/designer/app/designer/features/style-panel/style-sections.tsx
@@ -1,10 +1,9 @@
-import { Grid } from "@webstudio-is/design-system";
+import { Grid, type CSS } from "@webstudio-is/design-system";
import type { StyleConfig } from "./shared/configs";
import {
type Style,
type StyleProperty,
type Category,
- type CSS,
} from "@webstudio-is/react-sdk";
import type { SetProperty, CreateBatchUpdate } from "./shared/use-style-data";
import type { InheritedStyle } from "./shared/get-inherited-style";
diff --git a/apps/designer/app/routes/s/css.ts b/apps/designer/app/routes/s/css.ts
index 098924ea01..97d7a57feb 100644
--- a/apps/designer/app/routes/s/css.ts
+++ b/apps/designer/app/routes/s/css.ts
@@ -6,6 +6,8 @@ import env from "~/env.server";
import { sentryException } from "~/shared/sentry";
import { createCssEngine } from "@webstudio-is/css-engine";
import { getCssRules } from "~/shared/tree-utils";
+import { idAttribute } from "@webstudio-is/react-sdk";
+import { addGlobalRules } from "~/canvas/shared/styles";
export const loader: ActionFunction = async ({ request }) => {
try {
@@ -33,13 +35,15 @@ export const loader: ActionFunction = async ({ request }) => {
const engine = createCssEngine();
+ addGlobalRules(engine, canvasData);
+
for (const breakpoint of canvasData.breakpoints) {
engine.addMediaRule(breakpoint.id, breakpoint);
}
const cssRules = getCssRules(canvasData.tree?.root);
for (const [instanceId, cssRule] of cssRules) {
- engine.addStyleRule(`[data-ws-id="${instanceId}"]`, cssRule);
+ engine.addStyleRule(`[${idAttribute}="${instanceId}"]`, cssRule);
}
const { cssText } = engine;
diff --git a/packages/css-engine/src/core/css-engine.test.ts b/packages/css-engine/src/core/css-engine.test.ts
index 490fb26bd3..6ba8452d5f 100644
--- a/packages/css-engine/src/core/css-engine.test.ts
+++ b/packages/css-engine/src/core/css-engine.test.ts
@@ -164,4 +164,43 @@ describe("CssEngine", () => {
}"
`);
});
+
+ test("plaintext rule", () => {
+ engine.addPlaintextRule(".c { color: red }");
+ expect(engine.cssText).toMatchInlineSnapshot(`".c { color: red }"`);
+ });
+
+ test("plaintext - no duplicates", () => {
+ engine.addPlaintextRule(".c { color: red }");
+ engine.addPlaintextRule(".c { color: red }");
+ engine.addPlaintextRule(".c { color: green }");
+ expect(engine.cssText).toMatchInlineSnapshot(`
+ ".c { color: red }
+ .c { color: green }"
+ `);
+ });
+
+ test("font family rule", () => {
+ engine.addFontFaceRule({
+ fontFamily: "Roboto",
+ fontStyle: "normal",
+ fontWeight: 400,
+ fontDisplay: "swap",
+ src: "url(/src)",
+ });
+ expect(engine.cssText).toMatchInlineSnapshot(`
+ "@font-face {
+ font-family: Roboto; font-style: normal; font-weight: 400; font-display: swap; src: url(/src);
+ }"
+ `);
+ });
+
+ test("clear", () => {
+ engine.addStyleRule(".c", {
+ style: style0,
+ breakpoint: "0",
+ });
+ engine.clear();
+ expect(engine.cssText).toMatchInlineSnapshot(`""`);
+ });
});
diff --git a/packages/css-engine/src/core/css-engine.ts b/packages/css-engine/src/core/css-engine.ts
index b25a9703fb..f043972709 100644
--- a/packages/css-engine/src/core/css-engine.ts
+++ b/packages/css-engine/src/core/css-engine.ts
@@ -1,5 +1,12 @@
import { CssRule } from "@webstudio-is/react-sdk";
-import { MediaRule, StyleRule, type MediaRuleOptions } from "./rules";
+import {
+ FontFaceRule,
+ MediaRule,
+ PlaintextRule,
+ StyleRule,
+ type FontFaceOptions,
+ type MediaRuleOptions,
+} from "./rules";
import { StyleElement } from "./style-element";
import { StyleSheet } from "./style-sheet";
@@ -8,6 +15,8 @@ const defaultMediaRuleId = "__default-media-rule__";
export class CssEngine {
#element;
#mediaRules: Map = new Map();
+ #plainRules: Map = new Map();
+ #fontFaceRules: Array = [];
#sheet: StyleSheet;
#isDirty = false;
#cssText = "";
@@ -38,6 +47,20 @@ export class CssEngine {
}
return mediaRule.insertRule(styleRule);
}
+ addPlaintextRule(cssText: string) {
+ const rule = this.#plainRules.get(cssText);
+ if (rule !== undefined) return rule;
+ return this.#plainRules.set(cssText, new PlaintextRule(cssText));
+ }
+ addFontFaceRule(options: FontFaceOptions) {
+ return this.#fontFaceRules.push(new FontFaceRule(options));
+ }
+ clear() {
+ this.#mediaRules.clear();
+ this.#plainRules.clear();
+ this.#fontFaceRules = [];
+ this.#isDirty = true;
+ }
render() {
this.#element.mount();
// This isn't going to do anything if the `cssText` hasn't changed.
@@ -49,6 +72,11 @@ export class CssEngine {
}
this.#isDirty = false;
const css: Array = [];
+
+ css.push(...this.#fontFaceRules.map((rule) => rule.cssText));
+ for (const plaintextRule of this.#plainRules.values()) {
+ css.push(plaintextRule.cssText);
+ }
for (const mediaRule of this.#mediaRules.values()) {
const { cssText } = mediaRule;
if (cssText !== "") css.push(cssText);
diff --git a/packages/css-engine/src/core/index.ts b/packages/css-engine/src/core/index.ts
index 1b9ef7391f..a985ac4dc5 100644
--- a/packages/css-engine/src/core/index.ts
+++ b/packages/css-engine/src/core/index.ts
@@ -1,3 +1,10 @@
export { CssEngine } from "./css-engine";
-export type { AnyRule, StyleRule, MediaRule } from "./rules";
+export type {
+ AnyRule,
+ StyleRule,
+ MediaRule,
+ PlaintextRule,
+ FontFaceRule,
+} from "./rules";
export * from "./create-css-engine";
+export * from "./to-value";
diff --git a/packages/css-engine/src/core/rules.ts b/packages/css-engine/src/core/rules.ts
index 8438f96230..46db116cf1 100644
--- a/packages/css-engine/src/core/rules.ts
+++ b/packages/css-engine/src/core/rules.ts
@@ -1,10 +1,10 @@
import {
- toValue,
type Style,
type StyleProperty,
type StyleValue,
} from "@webstudio-is/react-sdk";
import hyphenate from "hyphenate-style-name";
+import { toValue } from "./to-value";
class StylePropertyMap {
#styleMap: Map = new Map();
@@ -60,13 +60,13 @@ export type MediaRuleOptions = {
export class MediaRule {
#options: MediaRuleOptions;
- rules: Array = [];
+ rules: Array = [];
#mediaType;
constructor(options: MediaRuleOptions = {}) {
this.#options = options;
this.#mediaType = options.mediaType ?? "all";
}
- insertRule(rule: StyleRule) {
+ insertRule(rule: StyleRule | PlaintextRule) {
this.rules.push(rule);
return rule;
}
@@ -87,4 +87,32 @@ export class MediaRule {
}
}
-export type AnyRule = StyleRule | MediaRule;
+export class PlaintextRule {
+ cssText;
+ styleMap = new Map();
+ constructor(cssText: string) {
+ this.cssText = cssText;
+ }
+}
+
+export type FontFaceOptions = {
+ fontFamily: string;
+ fontStyle: "normal" | "italic" | "oblique";
+ fontWeight: number;
+ fontDisplay: "swap" | "auto" | "block" | "fallback" | "optional";
+ src: string;
+};
+
+export class FontFaceRule {
+ #options: FontFaceOptions;
+ constructor(options: FontFaceOptions) {
+ this.#options = options;
+ }
+ get cssText() {
+ const { fontFamily, fontStyle, fontWeight, fontDisplay, src } =
+ this.#options;
+ return `@font-face {\n font-family: ${fontFamily}; font-style: ${fontStyle}; font-weight: ${fontWeight}; font-display: ${fontDisplay}; src: ${src};\n}`;
+ }
+}
+
+export type AnyRule = StyleRule | MediaRule | PlaintextRule | FontFaceRule;
diff --git a/packages/css-engine/src/core/to-value.test.ts b/packages/css-engine/src/core/to-value.test.ts
new file mode 100644
index 0000000000..7051daf341
--- /dev/null
+++ b/packages/css-engine/src/core/to-value.test.ts
@@ -0,0 +1,66 @@
+import { toValue } from "./to-value";
+
+describe("Convert WS CSS Values to native CSS strings", () => {
+ test("keyword", () => {
+ const value = toValue({ type: "keyword", value: "red" });
+ expect(value).toBe("red");
+ });
+
+ test("unit", () => {
+ const value = toValue({ type: "unit", value: 10, unit: "px" });
+ expect(value).toBe("10px");
+ });
+
+ test("invalid", () => {
+ const value = toValue({ type: "invalid", value: "bad" });
+ expect(value).toBe("bad");
+ });
+
+ test("unset", () => {
+ const value = toValue({ type: "unset", value: "" });
+ expect(value).toBe("");
+ });
+
+ test("var", () => {
+ const value = toValue({ type: "var", value: "namespace", fallbacks: [] });
+ expect(value).toBe("var(--namespace)");
+ });
+
+ test("var with fallbacks", () => {
+ const value = toValue({
+ type: "var",
+ value: "namespace",
+ fallbacks: [
+ {
+ type: "keyword",
+ value: "normal",
+ },
+ {
+ type: "unit",
+ value: 10,
+ unit: "px",
+ },
+ ],
+ });
+ expect(value).toBe("var(--namespace, normal, 10px)");
+ });
+
+ test("fontFamily", () => {
+ const value = toValue({
+ type: "fontFamily",
+ value: ["Courier New"],
+ });
+ expect(value).toBe("Courier New, monospace");
+ });
+
+ test("withFallback=false", () => {
+ const value = toValue(
+ {
+ type: "fontFamily",
+ value: ["Courier New"],
+ },
+ { withFallback: false }
+ );
+ expect(value).toBe("Courier New");
+ });
+});
diff --git a/packages/css-engine/src/core/to-value.ts b/packages/css-engine/src/core/to-value.ts
new file mode 100644
index 0000000000..a87bd37773
--- /dev/null
+++ b/packages/css-engine/src/core/to-value.ts
@@ -0,0 +1,41 @@
+import type { StyleValue } from "@webstudio-is/react-sdk";
+import { DEFAULT_FONT_FALLBACK, SYSTEM_FONTS } from "@webstudio-is/fonts";
+
+type ToCssOptions = {
+ withFallback: boolean;
+};
+
+const defaultOptions = {
+ withFallback: true,
+};
+
+export const toValue = (
+ value?: StyleValue,
+ options: ToCssOptions = defaultOptions
+): string => {
+ if (value === undefined) return "";
+ if (value.type === "unit") {
+ return value.value + (value.unit === "number" ? "" : value.unit);
+ }
+ if (value.type === "fontFamily") {
+ if (options.withFallback === false) {
+ return value.value[0];
+ }
+ const family = value.value[0];
+ const fallbacks = SYSTEM_FONTS.get(family);
+ if (Array.isArray(fallbacks)) {
+ return [...value.value, ...fallbacks].join(", ");
+ }
+ return [...value.value, DEFAULT_FONT_FALLBACK].join(", ");
+ }
+ if (value.type === "var") {
+ const fallbacks = [];
+ for (const fallback of value.fallbacks) {
+ fallbacks.push(toValue(fallback, options));
+ }
+ const fallbacksString =
+ fallbacks.length > 0 ? `, ${fallbacks.join(", ")}` : "";
+ return `var(--${value.value}${fallbacksString})`;
+ }
+ return value.value;
+};
diff --git a/packages/react-sdk/package.json b/packages/react-sdk/package.json
index 5c51b2848d..b176120cc8 100644
--- a/packages/react-sdk/package.json
+++ b/packages/react-sdk/package.json
@@ -59,7 +59,6 @@
"react-dom": "^17.0.2"
},
"dependencies": {
- "@stitches/core": "^1.2.7",
"@webstudio-is/asset-uploader": "^*",
"@webstudio-is/fonts": "*",
"@webstudio-is/icons": "*",
diff --git a/packages/react-sdk/src/index.ts b/packages/react-sdk/src/index.ts
index 7458f83c4d..ec43abd184 100644
--- a/packages/react-sdk/src/index.ts
+++ b/packages/react-sdk/src/index.ts
@@ -1,6 +1,5 @@
export * from "./css";
export * from "./tree";
-export * from "./stitches";
export * as components from "./components";
export * as componentsMeta from "./components/meta";
export * from "./user-props";
diff --git a/packages/react-sdk/src/remix/constants.ts b/packages/react-sdk/src/remix/constants.ts
deleted file mode 100644
index b5ddbd71f5..0000000000
--- a/packages/react-sdk/src/remix/constants.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const CRITICAL_CSS_MARKER = "__CRITICAL_CSS__";
diff --git a/packages/react-sdk/src/remix/handle-request.server.tsx b/packages/react-sdk/src/remix/handle-request.server.tsx
index 8efe2c9943..66f255cb8a 100644
--- a/packages/react-sdk/src/remix/handle-request.server.tsx
+++ b/packages/react-sdk/src/remix/handle-request.server.tsx
@@ -1,7 +1,6 @@
import { renderToString } from "react-dom/server";
import { RemixServer } from "@remix-run/react";
import type { EntryContext } from "@remix-run/node";
-import { insertCriticalCss } from "./insert-critical-css";
export const handleRequest = (
request: Request,
@@ -15,7 +14,7 @@ export const handleRequest = (
responseHeaders.set("Content-Type", "text/html");
- return new Response("" + insertCriticalCss(markup), {
+ return new Response(`${markup}`, {
status: responseStatusCode,
headers: responseHeaders,
});
diff --git a/packages/react-sdk/src/remix/insert-critical-css.ts b/packages/react-sdk/src/remix/insert-critical-css.ts
deleted file mode 100644
index 9f0bdd20ca..0000000000
--- a/packages/react-sdk/src/remix/insert-critical-css.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { flushCss } from "../stitches";
-import { CRITICAL_CSS_MARKER } from "./constants";
-
-export const insertCriticalCss = (markup: string): string => {
- return markup.replace(CRITICAL_CSS_MARKER, ``);
-};
diff --git a/packages/react-sdk/src/remix/root.tsx b/packages/react-sdk/src/remix/root.tsx
index c584e65ebf..3dc43f8d81 100644
--- a/packages/react-sdk/src/remix/root.tsx
+++ b/packages/react-sdk/src/remix/root.tsx
@@ -1,6 +1,5 @@
import { Links, Meta, Outlet as DefaultOutlet } from "@remix-run/react";
import { DynamicLinks } from "remix-utils";
-import { CRITICAL_CSS_MARKER } from "./constants";
/**
* We are using Outlet prop from index layout when user renders site from a subdomain.
@@ -20,7 +19,6 @@ export const Root = ({
- {typeof document === "undefined" ? CRITICAL_CSS_MARKER : null}
diff --git a/packages/react-sdk/src/stitches/index.ts b/packages/react-sdk/src/stitches/index.ts
deleted file mode 100644
index cba98204fc..0000000000
--- a/packages/react-sdk/src/stitches/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from "./to-css";
-export * from "./stitches";
diff --git a/packages/react-sdk/src/stitches/stitches.ts b/packages/react-sdk/src/stitches/stitches.ts
deleted file mode 100644
index e724c3db2e..0000000000
--- a/packages/react-sdk/src/stitches/stitches.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-import { createStitches, type CSS } from "@stitches/core";
-import type { FontFace } from "@webstudio-is/fonts";
-import type { Breakpoint } from "../css";
-
-let media = {};
-
-const createStitchesInstance = () => createStitches({ media });
-
-type StitchesInstance = ReturnType;
-
-let stitches: StitchesInstance | undefined;
-
-let fontsCss = "";
-
-export { type CSS };
-
-const getCachedConfig = () => {
- if (stitches === undefined) {
- stitches = createStitchesInstance();
- }
- return stitches;
-};
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export const css: StitchesInstance["css"] = (...args: any[]) => {
- return getCachedConfig().css(...args);
-};
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export const globalCss: StitchesInstance["globalCss"] = (...args: any[]) => {
- return getCachedConfig().globalCss(...args);
-};
-
-export const flushCss = () => {
- const config = getCachedConfig();
- const css = config.getCssText() + fontsCss;
- config.reset();
- fontsCss = "";
- return css;
-};
-
-export const setBreakpoints = (breakpoints: Array) => {
- const nextMedia: Record = {};
- for (const breakpoint of breakpoints) {
- nextMedia[breakpoint.id] = `(min-width: ${breakpoint.minWidth}px)`;
- }
- media = nextMedia;
- stitches = undefined;
-};
-
-let styleElement: HTMLStyleElement | undefined;
-
-/**
- * Stitches doesn't update fonts over globalCss
- */
-export const renderFontFaces = (fontFaces: Array) => {
- fontsCss = fontFaces
- .map(
- (fontFace) =>
- `
-@font-face {
- font-family: ${fontFace.fontFamily};
- font-style: ${fontFace.fontStyle};
- font-weight: ${fontFace.fontWeight};
- font-display: ${fontFace.fontDisplay};
- src: ${fontFace.src};
-}`
- )
- .join("");
-
- if (
- typeof document !== "undefined" &&
- typeof document.createElement === "function"
- ) {
- if (styleElement === undefined) {
- styleElement = document.createElement("style");
- document.head.appendChild(styleElement);
- }
- styleElement.textContent = fontsCss;
- }
-};
diff --git a/packages/react-sdk/src/stitches/to-css.test.ts b/packages/react-sdk/src/stitches/to-css.test.ts
deleted file mode 100644
index b18084d17f..0000000000
--- a/packages/react-sdk/src/stitches/to-css.test.ts
+++ /dev/null
@@ -1,231 +0,0 @@
-import type { Instance } from "../db";
-import { type CssRule, initialBreakpoints, type Breakpoint } from "../css";
-import { toCss } from "./to-css";
-
-const breakpoints: Array = initialBreakpoints.map(
- (breakpoint, index) => ({
- ...breakpoint,
- id: String(index),
- projectId: "projectId",
- })
-);
-
-const createInstance = (cssRules: Array): Instance => {
- return {
- id: "0",
- cssRules,
- component: "Box",
- children: [],
- };
-};
-
-describe("Convert WS CSS rules to stitches", () => {
- test("keyword", () => {
- const instance = createInstance([
- {
- style: {
- color: {
- type: "keyword",
- value: "red",
- },
- },
- breakpoint: "0",
- },
- ]);
- const stitchesCss = toCss(instance, breakpoints);
- expect(stitchesCss).toEqual({
- "@0": {
- color: "red",
- },
- });
- });
-
- test("unit", () => {
- const instance = createInstance([
- {
- style: {
- width: {
- type: "unit",
- value: 10,
- unit: "px",
- },
- },
- breakpoint: "0",
- },
- ]);
- const stitchesCss = toCss(instance, breakpoints);
- expect(stitchesCss).toEqual({
- "@0": {
- width: "10px",
- },
- });
- });
-
- test("invalid", () => {
- const instance = createInstance([
- {
- style: {
- width: {
- type: "invalid",
- value: "bad",
- },
- },
- breakpoint: "0",
- },
- ]);
- const stitchesCss = toCss(instance, breakpoints);
- expect(stitchesCss).toEqual({
- "@0": {
- width: "bad",
- },
- });
- });
-
- test("unset", () => {
- const instance = createInstance([
- {
- style: {
- width: {
- type: "unset",
- value: "",
- },
- },
- breakpoint: "0",
- },
- ]);
- const stitchesCss = toCss(instance, breakpoints);
- expect(stitchesCss).toEqual({
- "@0": {
- width: "",
- },
- });
- });
-
- test("var", () => {
- const instance = createInstance([
- {
- style: {
- width: {
- type: "var",
- value: "namespace",
- fallbacks: [],
- },
- },
- breakpoint: "0",
- },
- ]);
- const stitchesCss = toCss(instance, breakpoints);
- expect(stitchesCss).toEqual({
- "@0": {
- width: "var(--namespace)",
- },
- });
- });
-
- test("var with fallbacks", () => {
- const instance = createInstance([
- {
- style: {
- width: {
- type: "var",
- value: "namespace",
- fallbacks: [
- {
- type: "keyword",
- value: "normal",
- },
- {
- type: "unit",
- value: 10,
- unit: "px",
- },
- ],
- },
- },
- breakpoint: "0",
- },
- ]);
- const stitchesCss = toCss(instance, breakpoints);
- expect(stitchesCss).toEqual({
- "@0": {
- width: "var(--namespace, normal, 10px)",
- },
- });
- });
-
- test("fontFamily", () => {
- const instance = createInstance([
- {
- style: {
- fontFamily: {
- type: "fontFamily",
- value: ["Courier New"],
- },
- },
- breakpoint: "0",
- },
- ]);
- const stitchesCss = toCss(instance, breakpoints);
- expect(stitchesCss).toEqual({
- "@0": {
- fontFamily: "Courier New, monospace",
- },
- });
- });
-
- test("withFallback option", () => {
- const instance = createInstance([
- {
- style: {
- fontFamily: {
- type: "fontFamily",
- value: ["Courier New"],
- },
- },
- breakpoint: "0",
- },
- ]);
- const stitchesCss = toCss(instance, breakpoints, { withFallback: false });
- expect(stitchesCss).toEqual({
- "@0": {
- fontFamily: "Courier New",
- },
- });
- });
-
- test("sort order based on maxWidth in breakpoints", () => {
- const instance = createInstance([
- {
- style: {
- color: {
- type: "keyword",
- value: "green",
- },
- },
- breakpoint: "2",
- },
-
- {
- style: {
- color: {
- type: "keyword",
- value: "blue",
- },
- },
- breakpoint: "0",
- },
- {
- style: {
- color: {
- type: "keyword",
- value: "red",
- },
- },
- breakpoint: "1",
- },
- ]);
-
- const stitchesCss = toCss(instance, breakpoints);
- expect(Object.keys(stitchesCss)).toStrictEqual(["@0", "@1", "@2"]);
- });
-});
diff --git a/packages/react-sdk/src/stitches/to-css.ts b/packages/react-sdk/src/stitches/to-css.ts
deleted file mode 100644
index b5b49efabe..0000000000
--- a/packages/react-sdk/src/stitches/to-css.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import type { CSS } from "./stitches";
-import type { StyleProperty, StyleValue, Breakpoint } from "../css";
-import { DEFAULT_FONT_FALLBACK, SYSTEM_FONTS } from "@webstudio-is/fonts";
-import { Instance } from "..";
-
-type ToCssOptions = {
- withFallback: boolean;
-};
-
-const defaultOptions = {
- withFallback: true,
-};
-
-export const toValue = (
- value?: StyleValue,
- options: ToCssOptions = defaultOptions
-): string => {
- if (value === undefined) return "";
- if (value.type === "unit") {
- return value.value + (value.unit === "number" ? "" : value.unit);
- }
- if (value.type === "fontFamily") {
- if (options.withFallback === false) {
- return value.value[0];
- }
- const family = value.value[0];
- const fallbacks = SYSTEM_FONTS.get(family);
- if (Array.isArray(fallbacks)) {
- return [...value.value, ...fallbacks].join(", ");
- }
- return [...value.value, DEFAULT_FONT_FALLBACK].join(", ");
- }
- if (value.type === "var") {
- const fallbacks = [];
- for (const fallback of value.fallbacks) {
- fallbacks.push(toValue(fallback, options));
- }
- const fallbacksString =
- fallbacks.length > 0 ? `, ${fallbacks.join(", ")}` : "";
- return `var(--${value.value}${fallbacksString})`;
- }
- return value.value;
-};
-
-export const toVarNamespace = (id: string, property: string) => {
- return `${property}-${id}`;
-};
-
-/**
- * Convert instance cssRules to a stitches CSS object.
- */
-export const toCss = (
- instance: Instance,
- breakpoints: Array,
- options: ToCssOptions = defaultOptions
-): CSS => {
- const css: CSS = {};
- const { cssRules } = instance;
- const breakpointsMap: Record = {};
- for (const breakpoint of breakpoints) {
- breakpointsMap[breakpoint.id] = breakpoint.minWidth;
- }
-
- const sortedCssRules = [...cssRules].sort((ruleA, ruleB) => {
- // If a rule references a breakpoint that was not found in breakpoints,
- // we must have removed the breakpoint and now we fall back to 0.
- const maxWidthA = breakpointsMap[ruleA.breakpoint] ?? 0;
- const maxWidthB = breakpointsMap[ruleB.breakpoint] ?? 0;
- return maxWidthA - maxWidthB;
- });
-
- for (const cssRule of sortedCssRules) {
- const style: CSS = {};
- for (const property in cssRule.style) {
- const value = cssRule.style[property as StyleProperty];
- if (value === undefined) continue;
- style[property] = toValue(value, options);
- }
-
- if (cssRule.breakpoint in breakpointsMap) {
- css["@" + cssRule.breakpoint] = style;
- continue;
- }
-
- Object.assign(css, style);
- }
- return css;
-};
diff --git a/packages/react-sdk/src/tree/root.ts b/packages/react-sdk/src/tree/root.ts
index 784beadab7..aae3b24513 100644
--- a/packages/react-sdk/src/tree/root.ts
+++ b/packages/react-sdk/src/tree/root.ts
@@ -1,16 +1,10 @@
import { useAllUserProps } from "../user-props/";
import type { Tree, InstanceProps } from "../db";
import type { Breakpoint } from "../css";
-import { globalCss, setBreakpoints, renderFontFaces } from "../stitches";
import { createElementsTree } from "./create-elements-tree";
import { WrapperComponent } from "./wrapper-component";
-import {
- type FontFormat,
- FONT_FORMATS,
- getFontFaces,
-} from "@webstudio-is/fonts";
-import type { Asset, FontAsset } from "@webstudio-is/asset-uploader";
-import { type ComponentProps, useRef } from "react";
+import type { Asset } from "@webstudio-is/asset-uploader";
+import { type ComponentProps } from "react";
export type Data = {
tree: Tree | null;
@@ -19,27 +13,6 @@ export type Data = {
assets: Array;
};
-export const useGlobalStyles = ({ assets }: { assets: Array }) => {
- const ref = useRef>();
-
- // This may look weird, but globalCss API doesn't allow us creating global styles with data,
- // so we have to manually ensure calling it only once
- if (ref.current === assets) return;
-
- const fontAssets = assets.filter((asset) =>
- FONT_FORMATS.has(asset.format as FontFormat)
- ) as Array;
-
- renderFontFaces(getFontFaces(fontAssets));
- globalCss({
- html: {
- height: "100%",
- },
- })();
-
- ref.current = assets;
-};
-
type RootProps = {
data: Data;
Component?: (props: ComponentProps) => JSX.Element;
@@ -52,8 +25,6 @@ export const InstanceRoot = ({
if (data.tree === null) {
throw new Error("Tree is null");
}
- setBreakpoints(data.breakpoints);
- useGlobalStyles({ assets: data.assets });
useAllUserProps(data.props);
return createElementsTree({
instance: data.tree.root,
diff --git a/packages/react-sdk/src/tree/wrapper-component.tsx b/packages/react-sdk/src/tree/wrapper-component.tsx
index a362f5839a..fa775a717d 100644
--- a/packages/react-sdk/src/tree/wrapper-component.tsx
+++ b/packages/react-sdk/src/tree/wrapper-component.tsx
@@ -39,9 +39,10 @@ export const WrapperComponent = ({
}: WrapperComponentProps) => {
const { Component } = components[instance.component];
const userProps = useUserProps(instance.id);
+ const props = { ...userProps, ...rest, [idAttribute]: instance.id };
return (
-
- {renderWrapperComponentChildren(children)}
-
+ {renderWrapperComponentChildren(children)}
);
};
+
+export const idAttribute = "data-ws-id";
diff --git a/yarn.lock b/yarn.lock
index 97eb7b56f2..df74f1ea59 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3854,11 +3854,6 @@
dependencies:
"@sinonjs/commons" "^1.7.0"
-"@stitches/core@^1.2.7":
- version "1.2.8"
- resolved "https://registry.yarnpkg.com/@stitches/core/-/core-1.2.8.tgz#dce3b8fdc764fbc6dbea30c83b73bfb52cf96173"
- integrity sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg==
-
"@stitches/react@^1.2.8":
version "1.2.8"
resolved "https://registry.yarnpkg.com/@stitches/react/-/react-1.2.8.tgz#954f8008be8d9c65c4e58efa0937f32388ce3a38"