@webstudio-is/css-engine 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/lib/cjs/core/create-css-engine.cjs +27 -0
  2. package/lib/cjs/core/css-engine.cjs +134 -0
  3. package/lib/cjs/core/index.cjs +27 -0
  4. package/lib/cjs/core/rules.cjs +168 -0
  5. package/lib/cjs/core/style-element.cjs +69 -0
  6. package/lib/cjs/core/style-sheet.cjs +57 -0
  7. package/lib/cjs/core/to-value.cjs +55 -0
  8. package/lib/cjs/index.cjs +18 -0
  9. package/lib/core/create-css-engine.js +5 -2
  10. package/lib/core/css-engine.js +107 -85
  11. package/lib/core/index.js +4 -1
  12. package/lib/core/rules.js +125 -95
  13. package/lib/core/style-element.js +43 -33
  14. package/lib/core/style-sheet.js +33 -22
  15. package/lib/core/to-value.js +28 -24
  16. package/package.json +7 -14
  17. package/src/core/create-css-engine.ts +5 -0
  18. package/src/core/css-engine.stories.tsx +48 -0
  19. package/src/core/css-engine.test.ts +206 -0
  20. package/src/core/css-engine.ts +98 -0
  21. package/src/core/index.ts +10 -0
  22. package/src/core/rules.ts +132 -0
  23. package/src/core/style-element.ts +24 -0
  24. package/src/core/style-sheet.ts +15 -0
  25. package/src/core/to-value.test.ts +66 -0
  26. package/src/core/to-value.ts +43 -0
  27. package/src/index.ts +1 -0
  28. package/lib/core/create-css-engine.d.ts +0 -3
  29. package/lib/core/create-css-engine.d.ts.map +0 -1
  30. package/lib/core/css-engine.d.ts +0 -15
  31. package/lib/core/css-engine.d.ts.map +0 -1
  32. package/lib/core/css-engine.stories.d.ts +0 -7
  33. package/lib/core/css-engine.stories.d.ts.map +0 -1
  34. package/lib/core/css-engine.stories.js +0 -31
  35. package/lib/core/css-engine.test.d.ts +0 -2
  36. package/lib/core/css-engine.test.d.ts.map +0 -1
  37. package/lib/core/css-engine.test.js +0 -182
  38. package/lib/core/index.d.ts +0 -5
  39. package/lib/core/index.d.ts.map +0 -1
  40. package/lib/core/rules.d.ts +0 -48
  41. package/lib/core/rules.d.ts.map +0 -1
  42. package/lib/core/style-element.d.ts +0 -8
  43. package/lib/core/style-element.d.ts.map +0 -1
  44. package/lib/core/style-sheet.d.ts +0 -7
  45. package/lib/core/style-sheet.d.ts.map +0 -1
  46. package/lib/core/to-value.d.ts +0 -7
  47. package/lib/core/to-value.d.ts.map +0 -1
  48. package/lib/core/to-value.test.d.ts +0 -2
  49. package/lib/core/to-value.test.d.ts.map +0 -1
  50. package/lib/core/to-value.test.js +0 -55
  51. package/lib/index.d.ts +0 -2
  52. package/lib/index.d.ts.map +0 -1
@@ -1,31 +1,35 @@
1
1
  import { DEFAULT_FONT_FALLBACK, SYSTEM_FONTS } from "@webstudio-is/fonts";
2
2
  const defaultOptions = {
3
- withFallback: true,
3
+ withFallback: true
4
4
  };
5
- export const toValue = (value, options = defaultOptions) => {
6
- if (value === undefined)
7
- return "";
8
- if (value.type === "unit") {
9
- return value.value + (value.unit === "number" ? "" : value.unit);
5
+ const toValue = (value, options = defaultOptions) => {
6
+ if (value === void 0) {
7
+ return "";
8
+ }
9
+ if (value.type === "unit") {
10
+ return value.value + (value.unit === "number" ? "" : value.unit);
11
+ }
12
+ if (value.type === "fontFamily") {
13
+ if (options.withFallback === false) {
14
+ return value.value[0];
10
15
  }
11
- if (value.type === "fontFamily") {
12
- if (options.withFallback === false) {
13
- return value.value[0];
14
- }
15
- const family = value.value[0];
16
- const fallbacks = SYSTEM_FONTS.get(family);
17
- if (Array.isArray(fallbacks)) {
18
- return [...value.value, ...fallbacks].join(", ");
19
- }
20
- return [...value.value, DEFAULT_FONT_FALLBACK].join(", ");
16
+ const family = value.value[0];
17
+ const fallbacks = SYSTEM_FONTS.get(family);
18
+ if (Array.isArray(fallbacks)) {
19
+ return [...value.value, ...fallbacks].join(", ");
21
20
  }
22
- if (value.type === "var") {
23
- const fallbacks = [];
24
- for (const fallback of value.fallbacks) {
25
- fallbacks.push(toValue(fallback, options));
26
- }
27
- const fallbacksString = fallbacks.length > 0 ? `, ${fallbacks.join(", ")}` : "";
28
- return `var(--${value.value}${fallbacksString})`;
21
+ return [...value.value, DEFAULT_FONT_FALLBACK].join(", ");
22
+ }
23
+ if (value.type === "var") {
24
+ const fallbacks = [];
25
+ for (const fallback of value.fallbacks) {
26
+ fallbacks.push(toValue(fallback, options));
29
27
  }
30
- return value.value;
28
+ const fallbacksString = fallbacks.length > 0 ? `, ${fallbacks.join(", ")}` : "";
29
+ return `var(--${value.value}${fallbacksString})`;
30
+ }
31
+ return value.value;
32
+ };
33
+ export {
34
+ toValue
31
35
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webstudio-is/css-engine",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "CSS Renderer for Webstudio",
5
5
  "author": "Webstudio <github@webstudio.is>",
6
6
  "homepage": "https://webstudio.is",
@@ -8,8 +8,8 @@
8
8
  "scripts": {
9
9
  "typecheck": "tsc --noEmit",
10
10
  "checks": "yarn typecheck && yarn lint && yarn test",
11
- "dev": "tsup --watch",
12
- "build": "rm -fr lib tsconfig.tsbuildinfo && tsc",
11
+ "dev": "build-package --watch",
12
+ "build": "build-package",
13
13
  "test": "NODE_OPTIONS=--experimental-vm-modules jest",
14
14
  "lint": "eslint ./src --ext .ts,.tsx --max-warnings 0",
15
15
  "storybook:run": "start-storybook -p 6006",
@@ -34,26 +34,19 @@
34
34
  "@storybook/testing-library": "^0.0.13",
35
35
  "@webstudio-is/jest-config": "*",
36
36
  "@webstudio-is/css-data": "*",
37
- "tsup": "^6.1.3",
37
+ "@webstudio-is/scripts": "*",
38
38
  "typescript": "4.7.4",
39
39
  "@types/react": "^17.0.24",
40
40
  "@types/react-dom": "^17.0.9"
41
41
  },
42
42
  "exports": "./lib/index.js",
43
- "types": "lib/index.d.ts",
43
+ "types": "src/index.ts",
44
44
  "files": [
45
45
  "lib/*",
46
- "README.md",
46
+ "src/*",
47
47
  "!*.test.*"
48
48
  ],
49
49
  "license": "MIT",
50
50
  "private": false,
51
- "sideEffects": false,
52
- "tsup": {
53
- "entry": [
54
- "src/index.ts"
55
- ],
56
- "format": "esm",
57
- "outDir": "lib"
58
- }
51
+ "sideEffects": false
59
52
  }
@@ -0,0 +1,5 @@
1
+ import { CssEngine } from "./css-engine";
2
+
3
+ export const createCssEngine = () => {
4
+ return new CssEngine();
5
+ };
@@ -0,0 +1,48 @@
1
+ import { CssEngine } from "./css-engine";
2
+
3
+ export default {
4
+ component: "CssEngine",
5
+ };
6
+
7
+ const style0 = {
8
+ color: { type: "keyword", value: "red" },
9
+ } as const;
10
+
11
+ const mediaRuleOptions0 = { minWidth: 0 } as const;
12
+ const mediaId = "0";
13
+
14
+ export const Basic = () => {
15
+ const engine = new CssEngine();
16
+ engine.addMediaRule(mediaId, mediaRuleOptions0);
17
+ const rule = engine.addStyleRule(".test", {
18
+ style: style0,
19
+ breakpoint: "0",
20
+ });
21
+ engine.render();
22
+ return (
23
+ <>
24
+ <div className="test">Should be red</div>
25
+ <button
26
+ onClick={() => {
27
+ rule.styleMap.set("color", { type: "keyword", value: "green" });
28
+ engine.render();
29
+ }}
30
+ >
31
+ Make it green
32
+ </button>
33
+ <button
34
+ onClick={() => {
35
+ engine.addStyleRule(".test", {
36
+ style: {
37
+ background: { type: "keyword", value: "yellow" },
38
+ },
39
+ breakpoint: "0",
40
+ });
41
+ engine.render();
42
+ }}
43
+ >
44
+ Add rule with yellow background
45
+ </button>
46
+ </>
47
+ );
48
+ };
@@ -0,0 +1,206 @@
1
+ import { CssEngine } from "./css-engine";
2
+
3
+ const style0 = {
4
+ display: { type: "keyword", value: "block" },
5
+ } as const;
6
+
7
+ const mediaRuleOptions0 = { minWidth: 0 } as const;
8
+ const mediaId = "0";
9
+
10
+ describe("CssEngine", () => {
11
+ let engine: CssEngine;
12
+
13
+ beforeEach(() => {
14
+ engine = new CssEngine();
15
+ });
16
+
17
+ test("use default media rule when there is no matching one registrered", () => {
18
+ engine.addStyleRule(".c", {
19
+ style: style0,
20
+ breakpoint: "x",
21
+ });
22
+ expect(engine.cssText).toMatchInlineSnapshot(`
23
+ "@media all {
24
+ .c { display: block }
25
+ }"
26
+ `);
27
+ engine.addStyleRule(".c1", {
28
+ style: { color: { type: "keyword", value: "red" } },
29
+ breakpoint: "x",
30
+ });
31
+ expect(engine.cssText).toMatchInlineSnapshot(`
32
+ "@media all {
33
+ .c { display: block }
34
+ .c1 { color: red }
35
+ }"
36
+ `);
37
+
38
+ engine.addMediaRule(mediaId, mediaRuleOptions0);
39
+ engine.addStyleRule(".c1", {
40
+ style: { color: { type: "keyword", value: "blue" } },
41
+ breakpoint: mediaId,
42
+ });
43
+ // Default media query should allways be the first to have the lowest source order specificity
44
+ expect(engine.cssText).toMatchInlineSnapshot(`
45
+ "@media all {
46
+ .c { display: block }
47
+ .c1 { color: red }
48
+ }
49
+ @media all and (min-width: 0px) {
50
+ .c1 { color: blue }
51
+ }"
52
+ `);
53
+ });
54
+
55
+ test("rule with multiple properties", () => {
56
+ engine.addMediaRule(mediaId, mediaRuleOptions0);
57
+ engine.addStyleRule(".c", {
58
+ style: {
59
+ ...style0,
60
+ color: { type: "keyword", value: "red" },
61
+ },
62
+ breakpoint: "0",
63
+ });
64
+ expect(engine.cssText).toMatchInlineSnapshot(`
65
+ "@media all and (min-width: 0px) {
66
+ .c { display: block; color: red }
67
+ }"
68
+ `);
69
+ });
70
+
71
+ test("hyphenate property", () => {
72
+ engine.addMediaRule(mediaId, mediaRuleOptions0);
73
+ engine.addStyleRule(".c", {
74
+ style: {
75
+ backgroundColor: { type: "keyword", value: "red" },
76
+ },
77
+ breakpoint: "0",
78
+ });
79
+ expect(engine.cssText).toMatchInlineSnapshot(`
80
+ "@media all and (min-width: 0px) {
81
+ .c { background-color: red }
82
+ }"
83
+ `);
84
+ });
85
+
86
+ test("add rule", () => {
87
+ engine.addMediaRule(mediaId, mediaRuleOptions0);
88
+ const rule1 = engine.addStyleRule(".c", {
89
+ style: {
90
+ ...style0,
91
+ color: { type: "keyword", value: "red" },
92
+ },
93
+ breakpoint: "0",
94
+ });
95
+ expect(engine.cssText).toMatchInlineSnapshot(`
96
+ "@media all and (min-width: 0px) {
97
+ .c { display: block; color: red }
98
+ }"
99
+ `);
100
+ expect(rule1.cssText).toMatchInlineSnapshot(
101
+ `".c { display: block; color: red }"`
102
+ );
103
+ engine.addStyleRule(".c2", {
104
+ style: {
105
+ ...style0,
106
+ color: { type: "keyword", value: "green" },
107
+ },
108
+ breakpoint: "0",
109
+ });
110
+ expect(engine.cssText).toMatchInlineSnapshot(`
111
+ "@media all and (min-width: 0px) {
112
+ .c { display: block; color: red }
113
+ .c2 { display: block; color: green }
114
+ }"
115
+ `);
116
+ });
117
+
118
+ test("update rule", () => {
119
+ engine.addMediaRule(mediaId, mediaRuleOptions0);
120
+ const rule = engine.addStyleRule(".c", {
121
+ style: {
122
+ ...style0,
123
+ color: { type: "keyword", value: "red" },
124
+ },
125
+ breakpoint: "0",
126
+ });
127
+ expect(engine.cssText).toMatchInlineSnapshot(`
128
+ "@media all and (min-width: 0px) {
129
+ .c { display: block; color: red }
130
+ }"
131
+ `);
132
+ expect(rule.cssText).toMatchInlineSnapshot(
133
+ `".c { display: block; color: red }"`
134
+ );
135
+
136
+ rule.styleMap.set("color", { type: "keyword", value: "green" });
137
+
138
+ expect(rule.cssText).toMatchInlineSnapshot(
139
+ `".c { display: block; color: green }"`
140
+ );
141
+
142
+ expect(engine.cssText).toMatchInlineSnapshot(`
143
+ "@media all and (min-width: 0px) {
144
+ .c { display: block; color: green }
145
+ }"
146
+ `);
147
+ });
148
+
149
+ test("don't override media queries", () => {
150
+ engine.addMediaRule(mediaId, mediaRuleOptions0);
151
+ engine.addStyleRule(".c", {
152
+ style: style0,
153
+ breakpoint: "0",
154
+ });
155
+ expect(engine.cssText).toMatchInlineSnapshot(`
156
+ "@media all and (min-width: 0px) {
157
+ .c { display: block }
158
+ }"
159
+ `);
160
+ engine.addMediaRule(mediaId, mediaRuleOptions0);
161
+ expect(engine.cssText).toMatchInlineSnapshot(`
162
+ "@media all and (min-width: 0px) {
163
+ .c { display: block }
164
+ }"
165
+ `);
166
+ });
167
+
168
+ test("plaintext rule", () => {
169
+ engine.addPlaintextRule(".c { color: red }");
170
+ expect(engine.cssText).toMatchInlineSnapshot(`".c { color: red }"`);
171
+ });
172
+
173
+ test("plaintext - no duplicates", () => {
174
+ engine.addPlaintextRule(".c { color: red }");
175
+ engine.addPlaintextRule(".c { color: red }");
176
+ engine.addPlaintextRule(".c { color: green }");
177
+ expect(engine.cssText).toMatchInlineSnapshot(`
178
+ ".c { color: red }
179
+ .c { color: green }"
180
+ `);
181
+ });
182
+
183
+ test("font family rule", () => {
184
+ engine.addFontFaceRule({
185
+ fontFamily: "Roboto",
186
+ fontStyle: "normal",
187
+ fontWeight: 400,
188
+ fontDisplay: "swap",
189
+ src: "url(/src)",
190
+ });
191
+ expect(engine.cssText).toMatchInlineSnapshot(`
192
+ "@font-face {
193
+ font-family: Roboto; font-style: normal; font-weight: 400; font-display: swap; src: url(/src);
194
+ }"
195
+ `);
196
+ });
197
+
198
+ test("clear", () => {
199
+ engine.addStyleRule(".c", {
200
+ style: style0,
201
+ breakpoint: "0",
202
+ });
203
+ engine.clear();
204
+ expect(engine.cssText).toMatchInlineSnapshot(`""`);
205
+ });
206
+ });
@@ -0,0 +1,98 @@
1
+ import type { CssRule } from "@webstudio-is/css-data";
2
+ import {
3
+ FontFaceRule,
4
+ MediaRule,
5
+ PlaintextRule,
6
+ StyleRule,
7
+ type FontFaceOptions,
8
+ type MediaRuleOptions,
9
+ } from "./rules";
10
+ import { StyleElement } from "./style-element";
11
+ import { StyleSheet } from "./style-sheet";
12
+
13
+ const defaultMediaRuleId = "__default-media-rule__";
14
+
15
+ export class CssEngine {
16
+ #element;
17
+ #mediaRules: Map<string, MediaRule> = new Map();
18
+ #plainRules: Map<string, PlaintextRule> = new Map();
19
+ #fontFaceRules: Array<FontFaceRule> = [];
20
+ #sheet: StyleSheet;
21
+ #isDirty = false;
22
+ #cssText = "";
23
+ constructor() {
24
+ this.#element = new StyleElement();
25
+ this.#sheet = new StyleSheet(this.#element);
26
+ }
27
+ addMediaRule(id: string, options?: MediaRuleOptions) {
28
+ let mediaRule = this.#mediaRules.get(id);
29
+ if (mediaRule === undefined) {
30
+ mediaRule = new MediaRule(options);
31
+ this.#mediaRules.set(id, mediaRule);
32
+ this.#isDirty = true;
33
+ }
34
+ return mediaRule;
35
+ }
36
+ addStyleRule(selectorText: string, rule: CssRule) {
37
+ const mediaRule = this.addMediaRule(rule.breakpoint || defaultMediaRuleId);
38
+ this.#isDirty = true;
39
+ const styleRule = new StyleRule(selectorText, rule.style);
40
+ styleRule.onChange = this.#onChangeRule;
41
+ if (mediaRule === undefined) {
42
+ // Should be impossible to reach.
43
+ throw new Error("No media rule found");
44
+ }
45
+ mediaRule.insertRule(styleRule);
46
+ return styleRule;
47
+ }
48
+ addPlaintextRule(cssText: string) {
49
+ const rule = this.#plainRules.get(cssText);
50
+ if (rule !== undefined) {
51
+ return rule;
52
+ }
53
+ this.#isDirty = true;
54
+ return this.#plainRules.set(cssText, new PlaintextRule(cssText));
55
+ }
56
+ addFontFaceRule(options: FontFaceOptions) {
57
+ this.#isDirty = true;
58
+ return this.#fontFaceRules.push(new FontFaceRule(options));
59
+ }
60
+ clear() {
61
+ this.#mediaRules.clear();
62
+ this.#plainRules.clear();
63
+ this.#fontFaceRules = [];
64
+ this.#isDirty = true;
65
+ }
66
+ render() {
67
+ this.#element.mount();
68
+ // This isn't going to do anything if the `cssText` hasn't changed.
69
+ this.#sheet.replaceSync(this.cssText);
70
+ }
71
+ unmount() {
72
+ this.#element.unmount();
73
+ }
74
+ get cssText() {
75
+ if (this.#isDirty === false) {
76
+ return this.#cssText;
77
+ }
78
+ this.#isDirty = false;
79
+ const css: Array<string> = [];
80
+
81
+ css.push(...this.#fontFaceRules.map((rule) => rule.cssText));
82
+ for (const plaintextRule of this.#plainRules.values()) {
83
+ css.push(plaintextRule.cssText);
84
+ }
85
+ for (const mediaRule of this.#mediaRules.values()) {
86
+ const { cssText } = mediaRule;
87
+ if (cssText !== "") {
88
+ css.push(cssText);
89
+ }
90
+ }
91
+ this.#cssText = css.join("\n");
92
+ return this.#cssText;
93
+ }
94
+
95
+ #onChangeRule = () => {
96
+ this.#isDirty = true;
97
+ };
98
+ }
@@ -0,0 +1,10 @@
1
+ export { CssEngine } from "./css-engine";
2
+ export type {
3
+ AnyRule,
4
+ StyleRule,
5
+ MediaRule,
6
+ PlaintextRule,
7
+ FontFaceRule,
8
+ } from "./rules";
9
+ export * from "./create-css-engine";
10
+ export * from "./to-value";
@@ -0,0 +1,132 @@
1
+ import type { Style, StyleProperty, StyleValue } from "@webstudio-is/css-data";
2
+ import hyphenate from "hyphenate-style-name";
3
+ import { toValue } from "./to-value";
4
+
5
+ class StylePropertyMap {
6
+ #styleMap: Map<StyleProperty, StyleValue | undefined> = new Map();
7
+ #isDirty = false;
8
+ #string = "";
9
+ onChange?: () => void;
10
+ set(property: StyleProperty, value?: StyleValue) {
11
+ this.#styleMap.set(property, value);
12
+ this.#isDirty = true;
13
+ this.onChange?.();
14
+ }
15
+ has(property: StyleProperty) {
16
+ return this.#styleMap.has(property);
17
+ }
18
+ clear() {
19
+ this.#styleMap.clear();
20
+ this.#isDirty = true;
21
+ this.onChange?.();
22
+ }
23
+ toString() {
24
+ if (this.#isDirty === false) {
25
+ return this.#string;
26
+ }
27
+ const block: Array<string> = [];
28
+ for (const [property, value] of this.#styleMap) {
29
+ if (value === undefined) {
30
+ continue;
31
+ }
32
+ block.push(`${hyphenate(property)}: ${toValue(value)}`);
33
+ }
34
+ this.#string = block.join("; ");
35
+ this.#isDirty = false;
36
+ return this.#string;
37
+ }
38
+ }
39
+
40
+ export class StyleRule {
41
+ styleMap;
42
+ selectorText;
43
+ onChange?: () => void;
44
+ constructor(selectorText: string, style: Style) {
45
+ this.styleMap = new StylePropertyMap();
46
+ this.selectorText = selectorText;
47
+ let property: StyleProperty;
48
+ for (property in style) {
49
+ this.styleMap.set(property, style[property]);
50
+ }
51
+ this.styleMap.onChange = this.#onChange;
52
+ }
53
+ #onChange = () => {
54
+ this.onChange?.();
55
+ };
56
+ get cssText() {
57
+ return `${this.selectorText} { ${this.styleMap} }`;
58
+ }
59
+ }
60
+
61
+ export type MediaRuleOptions = {
62
+ minWidth?: number;
63
+ maxWidth?: number;
64
+ mediaType?: "all" | "screen" | "print";
65
+ };
66
+
67
+ export class MediaRule {
68
+ #options: MediaRuleOptions;
69
+ rules: Array<StyleRule | PlaintextRule> = [];
70
+ #mediaType;
71
+ constructor(options: MediaRuleOptions = {}) {
72
+ this.#options = options;
73
+ this.#mediaType = options.mediaType ?? "all";
74
+ }
75
+ insertRule(rule: StyleRule | PlaintextRule) {
76
+ this.rules.push(rule);
77
+ return rule;
78
+ }
79
+ get cssText() {
80
+ if (this.rules.length === 0) {
81
+ return "";
82
+ }
83
+ const rules = [];
84
+ for (const rule of this.rules) {
85
+ rules.push(` ${rule.cssText}`);
86
+ }
87
+ let conditionText = "";
88
+ const { minWidth, maxWidth } = this.#options;
89
+ if (minWidth !== undefined) {
90
+ conditionText = `min-width: ${minWidth}px`;
91
+ }
92
+ if (maxWidth !== undefined) {
93
+ conditionText = `max-width: ${maxWidth}px`;
94
+ }
95
+ if (conditionText) {
96
+ conditionText = `and (${conditionText}) `;
97
+ }
98
+ return `@media ${this.#mediaType} ${conditionText}{\n${rules.join(
99
+ "\n"
100
+ )}\n}`;
101
+ }
102
+ }
103
+
104
+ export class PlaintextRule {
105
+ cssText;
106
+ styleMap = new StylePropertyMap();
107
+ constructor(cssText: string) {
108
+ this.cssText = cssText;
109
+ }
110
+ }
111
+
112
+ export type FontFaceOptions = {
113
+ fontFamily: string;
114
+ fontStyle: "normal" | "italic" | "oblique";
115
+ fontWeight: number;
116
+ fontDisplay: "swap" | "auto" | "block" | "fallback" | "optional";
117
+ src: string;
118
+ };
119
+
120
+ export class FontFaceRule {
121
+ #options: FontFaceOptions;
122
+ constructor(options: FontFaceOptions) {
123
+ this.#options = options;
124
+ }
125
+ get cssText() {
126
+ const { fontFamily, fontStyle, fontWeight, fontDisplay, src } =
127
+ this.#options;
128
+ return `@font-face {\n font-family: ${fontFamily}; font-style: ${fontStyle}; font-weight: ${fontWeight}; font-display: ${fontDisplay}; src: ${src};\n}`;
129
+ }
130
+ }
131
+
132
+ export type AnyRule = StyleRule | MediaRule | PlaintextRule | FontFaceRule;
@@ -0,0 +1,24 @@
1
+ export class StyleElement {
2
+ #element?: HTMLStyleElement;
3
+ get isMounted() {
4
+ return this.#element?.parentElement != null;
5
+ }
6
+ mount() {
7
+ if (this.isMounted === false) {
8
+ this.#element = document.createElement("style");
9
+ this.#element.setAttribute("data-webstudio", "");
10
+ document.head.appendChild(this.#element);
11
+ }
12
+ }
13
+ unmount() {
14
+ if (this.isMounted) {
15
+ this.#element?.parentElement?.removeChild(this.#element);
16
+ this.#element = undefined;
17
+ }
18
+ }
19
+ render(cssText: string) {
20
+ if (this.#element) {
21
+ this.#element.textContent = cssText;
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,15 @@
1
+ import { StyleElement } from "./style-element";
2
+
3
+ export class StyleSheet {
4
+ #cssText = "";
5
+ #element;
6
+ constructor(element: StyleElement) {
7
+ this.#element = element;
8
+ }
9
+ replaceSync(cssText: string) {
10
+ if (cssText !== this.#cssText) {
11
+ this.#cssText = cssText;
12
+ this.#element.render(cssText);
13
+ }
14
+ }
15
+ }