Merge pull request #456 from webstudio-is/css-engine

Removing stitches from sdk and canvas entirely
This commit is contained in:
Oleg Isonen authored and GitHub committed 2022-11-17 12:51:16 +01:00
commit a12008edcc
30 files changed
+329 -524

No files matched your search

+10 -6
View File
@@ -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 (
<>
<GlobalStyles assets={assets} />
{elements}
</>
);
}
return (
<>
<GlobalStyles assets={assets} />
<DesignMode treeId={data.tree.id} buildId={data.buildId} />
{elements}
</>
@@ -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();
@@ -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<Breakpoint>) => {
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);
});
});
};
+79 -36
View File
@@ -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<Asset> }
) => {
// @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<FontAsset>;
const fontFaces = getFontFaces(fontAssets);
for (const fontFace of fontFaces) {
engine.addFontFaceRule(fontFace);
}
};
const globalStylesCssEngine = createCssEngine();
export const GlobalStyles = ({ assets }: { assets: Array<Asset> }) => {
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<Breakpoint>) => {
}
};
const wrappedRulesMap = new Map<string, StyleRule>();
const wrappedRulesMap = new Map<string, StyleRule | PlaintextRule>();
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) {
@@ -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";
@@ -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";
@@ -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";
@@ -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,
@@ -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";
@@ -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";
+5 -1
View File
@@ -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;
@@ -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(`""`);
});
});
+29 -1
View File
@@ -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<string, MediaRule> = new Map();
#plainRules: Map<string, PlaintextRule> = new Map();
#fontFaceRules: Array<FontFaceRule> = [];
#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<string> = [];
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);
+8 -1
View File
@@ -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";
+32 -4
View File
@@ -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<StyleProperty, StyleValue | undefined> = new Map();
@@ -60,13 +60,13 @@ export type MediaRuleOptions = {
export class MediaRule {
#options: MediaRuleOptions;
rules: Array<StyleRule> = [];
rules: Array<StyleRule | PlaintextRule> = [];
#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;
@@ -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");
});
});
+41
View File
@@ -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;
};
-1
View File
@@ -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": "*",
-1
View File
@@ -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";
@@ -1 +0,0 @@
export const CRITICAL_CSS_MARKER = "__CRITICAL_CSS__";
@@ -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("<!DOCTYPE html>" + insertCriticalCss(markup), {
return new Response(`<!DOCTYPE html>${markup}`, {
status: responseStatusCode,
headers: responseHeaders,
});
@@ -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, `<style>${flushCss()}</style>`);
};
-2
View File
@@ -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 = ({
<Meta />
<DynamicLinks />
<Links />
{typeof document === "undefined" ? CRITICAL_CSS_MARKER : null}
</head>
<Outlet />
-2
View File
@@ -1,2 +0,0 @@
export * from "./to-css";
export * from "./stitches";
@@ -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<typeof createStitchesInstance>;
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<Breakpoint>) => {
const nextMedia: Record<string, string> = {};
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<FontFace>) => {
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;
}
};
@@ -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<Breakpoint> = initialBreakpoints.map(
(breakpoint, index) => ({
...breakpoint,
id: String(index),
projectId: "projectId",
})
);
const createInstance = (cssRules: Array<CssRule>): 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"]);
});
});
-88
View File
@@ -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<Breakpoint>,
options: ToCssOptions = defaultOptions
): CSS => {
const css: CSS = {};
const { cssRules } = instance;
const breakpointsMap: Record<Breakpoint["id"], number> = {};
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;
};
+2 -31
View File
@@ -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<Asset>;
};
export const useGlobalStyles = ({ assets }: { assets: Array<Asset> }) => {
const ref = useRef<Array<Asset>>();
// 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<FontAsset>;
renderFontFaces(getFontFaces(fontAssets));
globalCss({
html: {
height: "100%",
},
})();
ref.current = assets;
};
type RootProps = {
data: Data;
Component?: (props: ComponentProps<typeof WrapperComponent>) => 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,
@@ -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 (
<Component {...userProps} {...rest} data-ws-id={instance.id}>
{renderWrapperComponentChildren(children)}
</Component>
<Component {...props}>{renderWrapperComponentChildren(children)}</Component>
);
};
export const idAttribute = "data-ws-id";
-5
View File
@@ -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"