@webstudio-is/css-engine 0.50.0 → 0.52.0

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 (49) hide show
  1. package/lib/cjs/core/compare-media.js +32 -0
  2. package/lib/cjs/core/{css-engine.cjs → css-engine.js} +5 -1
  3. package/lib/cjs/core/equal-media.js +26 -0
  4. package/lib/cjs/core/{index.cjs → index.js} +3 -0
  5. package/lib/cjs/core/match-media.js +28 -0
  6. package/lib/cjs/core/{rules.cjs → rules.js} +3 -14
  7. package/lib/cjs/package.json +1 -0
  8. package/lib/core/compare-media.js +12 -0
  9. package/lib/core/css-engine.js +5 -1
  10. package/lib/core/equal-media.js +6 -0
  11. package/lib/core/index.js +3 -0
  12. package/lib/core/match-media.js +8 -0
  13. package/lib/core/rules.js +3 -14
  14. package/lib/types/core/compare-media.d.ts +6 -0
  15. package/lib/types/core/compare-media.test.d.ts +1 -0
  16. package/lib/types/core/create-css-engine.d.ts +2 -0
  17. package/lib/types/core/css-engine.d.ts +19 -0
  18. package/lib/types/core/css-engine.stories.d.ts +6 -0
  19. package/lib/types/core/css-engine.test.d.ts +1 -0
  20. package/lib/types/core/equal-media.d.ts +2 -0
  21. package/lib/types/core/equal-media.test.d.ts +1 -0
  22. package/lib/types/core/index.d.ts +7 -0
  23. package/lib/types/core/match-media.d.ts +2 -0
  24. package/lib/types/core/match-media.test.d.ts +1 -0
  25. package/lib/types/core/rules.d.ts +51 -0
  26. package/lib/types/core/style-element.d.ts +10 -0
  27. package/lib/types/core/style-sheet.d.ts +6 -0
  28. package/lib/types/core/to-property.d.ts +2 -0
  29. package/lib/types/core/to-property.test.d.ts +1 -0
  30. package/lib/types/core/to-value.d.ts +6 -0
  31. package/lib/types/core/to-value.test.d.ts +1 -0
  32. package/lib/types/index.d.ts +1 -0
  33. package/package.json +6 -5
  34. package/src/core/compare-media.test.ts +40 -0
  35. package/src/core/compare-media.ts +18 -0
  36. package/src/core/css-engine.test.ts +39 -0
  37. package/src/core/css-engine.ts +6 -1
  38. package/src/core/equal-media.test.ts +31 -0
  39. package/src/core/equal-media.ts +5 -0
  40. package/src/core/index.ts +3 -0
  41. package/src/core/match-media.test.ts +24 -0
  42. package/src/core/match-media.ts +7 -0
  43. package/src/core/rules.ts +3 -17
  44. /package/lib/cjs/core/{create-css-engine.cjs → create-css-engine.js} +0 -0
  45. /package/lib/cjs/core/{style-element.cjs → style-element.js} +0 -0
  46. /package/lib/cjs/core/{style-sheet.cjs → style-sheet.js} +0 -0
  47. /package/lib/cjs/core/{to-property.cjs → to-property.js} +0 -0
  48. /package/lib/cjs/core/{to-value.cjs → to-value.js} +0 -0
  49. /package/lib/cjs/{index.cjs → index.js} +0 -0
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var compare_media_exports = {};
20
+ __export(compare_media_exports, {
21
+ compareMedia: () => compareMedia
22
+ });
23
+ module.exports = __toCommonJS(compare_media_exports);
24
+ const compareMedia = (optionA, optionB) => {
25
+ if (optionA?.minWidth !== void 0 && optionB?.minWidth !== void 0) {
26
+ return optionA.minWidth - optionB.minWidth;
27
+ }
28
+ if (optionA?.maxWidth !== void 0 && optionB?.maxWidth !== void 0) {
29
+ return optionB.maxWidth - optionA.maxWidth;
30
+ }
31
+ return "minWidth" in optionA ? 1 : -1;
32
+ };
@@ -40,6 +40,7 @@ __export(css_engine_exports, {
40
40
  });
41
41
  module.exports = __toCommonJS(css_engine_exports);
42
42
  var import_rules = require("./rules");
43
+ var import_compare_media = require("./compare-media");
43
44
  var import_style_element = require("./style-element");
44
45
  var import_style_sheet = require("./style-sheet");
45
46
  var _element, _mediaRules, _plainRules, _fontFaceRules, _sheet, _isDirty, _cssText, _onChangeRule;
@@ -120,7 +121,10 @@ class CssEngine {
120
121
  for (const plaintextRule of __privateGet(this, _plainRules).values()) {
121
122
  css.push(plaintextRule.cssText);
122
123
  }
123
- for (const mediaRule of import_rules.MediaRule.sort(__privateGet(this, _mediaRules).values())) {
124
+ const sortedMediaRules = Array.from(__privateGet(this, _mediaRules).values()).sort(
125
+ (ruleA, ruleB) => (0, import_compare_media.compareMedia)(ruleA.options, ruleB.options)
126
+ );
127
+ for (const mediaRule of sortedMediaRules) {
124
128
  const { cssText } = mediaRule;
125
129
  if (cssText !== "") {
126
130
  css.push(cssText);
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var equal_media_exports = {};
20
+ __export(equal_media_exports, {
21
+ equalMedia: () => equalMedia
22
+ });
23
+ module.exports = __toCommonJS(equal_media_exports);
24
+ const equalMedia = (left, right) => {
25
+ return left.minWidth === right.minWidth && left.maxWidth === right.maxWidth;
26
+ };
@@ -25,3 +25,6 @@ module.exports = __toCommonJS(core_exports);
25
25
  var import_css_engine = require("./css-engine");
26
26
  __reExport(core_exports, require("./create-css-engine"), module.exports);
27
27
  __reExport(core_exports, require("./to-value"), module.exports);
28
+ __reExport(core_exports, require("./match-media"), module.exports);
29
+ __reExport(core_exports, require("./equal-media"), module.exports);
30
+ __reExport(core_exports, require("./compare-media"), module.exports);
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var match_media_exports = {};
20
+ __export(match_media_exports, {
21
+ matchMedia: () => matchMedia
22
+ });
23
+ module.exports = __toCommonJS(match_media_exports);
24
+ const matchMedia = (options, width) => {
25
+ const minWidth = options.minWidth ?? Number.MIN_SAFE_INTEGER;
26
+ const maxWidth = options.maxWidth ?? Number.MAX_SAFE_INTEGER;
27
+ return width >= minWidth && width <= maxWidth;
28
+ };
@@ -116,14 +116,6 @@ class MediaRule {
116
116
  this.options = options;
117
117
  __privateSet(this, _mediaType, options.mediaType ?? "all");
118
118
  }
119
- // Sort media rules by minWidth.
120
- // Needed to ensure that more specific media rules are inserted after less specific ones.
121
- // So that they get a higher specificity.
122
- static sort(mediaRules) {
123
- return Array.from(mediaRules).sort((ruleA, ruleB) => {
124
- return (ruleA.options.minWidth ?? -Number.MAX_SAFE_INTEGER) - (ruleB.options.minWidth ?? -Number.MAX_SAFE_INTEGER);
125
- });
126
- }
127
119
  insertRule(rule) {
128
120
  this.rules.push(rule);
129
121
  return rule;
@@ -139,15 +131,12 @@ class MediaRule {
139
131
  let conditionText = "";
140
132
  const { minWidth, maxWidth } = this.options;
141
133
  if (minWidth !== void 0) {
142
- conditionText = `min-width: ${minWidth}px`;
134
+ conditionText = ` and (min-width: ${minWidth}px)`;
143
135
  }
144
136
  if (maxWidth !== void 0) {
145
- conditionText = `max-width: ${maxWidth}px`;
146
- }
147
- if (conditionText) {
148
- conditionText = `and (${conditionText}) `;
137
+ conditionText += ` and (max-width: ${maxWidth}px)`;
149
138
  }
150
- return `@media ${__privateGet(this, _mediaType)} ${conditionText}{
139
+ return `@media ${__privateGet(this, _mediaType)}${conditionText} {
151
140
  ${rules.join(
152
141
  "\n"
153
142
  )}
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -0,0 +1,12 @@
1
+ const compareMedia = (optionA, optionB) => {
2
+ if (optionA?.minWidth !== void 0 && optionB?.minWidth !== void 0) {
3
+ return optionA.minWidth - optionB.minWidth;
4
+ }
5
+ if (optionA?.maxWidth !== void 0 && optionB?.maxWidth !== void 0) {
6
+ return optionB.maxWidth - optionA.maxWidth;
7
+ }
8
+ return "minWidth" in optionA ? 1 : -1;
9
+ };
10
+ export {
11
+ compareMedia
12
+ };
@@ -23,6 +23,7 @@ import {
23
23
  PlaintextRule,
24
24
  StyleRule
25
25
  } from "./rules";
26
+ import { compareMedia } from "./compare-media";
26
27
  import { StyleElement } from "./style-element";
27
28
  import { StyleSheet } from "./style-sheet";
28
29
  const defaultMediaRuleId = "__default-media-rule__";
@@ -102,7 +103,10 @@ class CssEngine {
102
103
  for (const plaintextRule of __privateGet(this, _plainRules).values()) {
103
104
  css.push(plaintextRule.cssText);
104
105
  }
105
- for (const mediaRule of MediaRule.sort(__privateGet(this, _mediaRules).values())) {
106
+ const sortedMediaRules = Array.from(__privateGet(this, _mediaRules).values()).sort(
107
+ (ruleA, ruleB) => compareMedia(ruleA.options, ruleB.options)
108
+ );
109
+ for (const mediaRule of sortedMediaRules) {
106
110
  const { cssText } = mediaRule;
107
111
  if (cssText !== "") {
108
112
  css.push(cssText);
@@ -0,0 +1,6 @@
1
+ const equalMedia = (left, right) => {
2
+ return left.minWidth === right.minWidth && left.maxWidth === right.maxWidth;
3
+ };
4
+ export {
5
+ equalMedia
6
+ };
package/lib/core/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import { CssEngine } from "./css-engine";
2
2
  export * from "./create-css-engine";
3
3
  export * from "./to-value";
4
+ export * from "./match-media";
5
+ export * from "./equal-media";
6
+ export * from "./compare-media";
4
7
  export {
5
8
  CssEngine
6
9
  };
@@ -0,0 +1,8 @@
1
+ const matchMedia = (options, width) => {
2
+ const minWidth = options.minWidth ?? Number.MIN_SAFE_INTEGER;
3
+ const maxWidth = options.maxWidth ?? Number.MAX_SAFE_INTEGER;
4
+ return width >= minWidth && width <= maxWidth;
5
+ };
6
+ export {
7
+ matchMedia
8
+ };
package/lib/core/rules.js CHANGED
@@ -90,14 +90,6 @@ class MediaRule {
90
90
  this.options = options;
91
91
  __privateSet(this, _mediaType, options.mediaType ?? "all");
92
92
  }
93
- // Sort media rules by minWidth.
94
- // Needed to ensure that more specific media rules are inserted after less specific ones.
95
- // So that they get a higher specificity.
96
- static sort(mediaRules) {
97
- return Array.from(mediaRules).sort((ruleA, ruleB) => {
98
- return (ruleA.options.minWidth ?? -Number.MAX_SAFE_INTEGER) - (ruleB.options.minWidth ?? -Number.MAX_SAFE_INTEGER);
99
- });
100
- }
101
93
  insertRule(rule) {
102
94
  this.rules.push(rule);
103
95
  return rule;
@@ -113,15 +105,12 @@ class MediaRule {
113
105
  let conditionText = "";
114
106
  const { minWidth, maxWidth } = this.options;
115
107
  if (minWidth !== void 0) {
116
- conditionText = `min-width: ${minWidth}px`;
108
+ conditionText = ` and (min-width: ${minWidth}px)`;
117
109
  }
118
110
  if (maxWidth !== void 0) {
119
- conditionText = `max-width: ${maxWidth}px`;
120
- }
121
- if (conditionText) {
122
- conditionText = `and (${conditionText}) `;
111
+ conditionText += ` and (max-width: ${maxWidth}px)`;
123
112
  }
124
- return `@media ${__privateGet(this, _mediaType)} ${conditionText}{
113
+ return `@media ${__privateGet(this, _mediaType)}${conditionText} {
125
114
  ${rules.join(
126
115
  "\n"
127
116
  )}
@@ -0,0 +1,6 @@
1
+ import type { MediaRuleOptions } from "./rules";
2
+ /**
3
+ * Sort by minWidth descending or maxWidth ascending
4
+ * We want media querries with bigger minWidth to override the smaller once, but the smaller maxWidth to override the bigger once.
5
+ */
6
+ export declare const compareMedia: (optionA: MediaRuleOptions, optionB: MediaRuleOptions) => number;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import { CssEngine, type CssEngineOptions } from "./css-engine";
2
+ export declare const createCssEngine: (options?: CssEngineOptions) => CssEngine;
@@ -0,0 +1,19 @@
1
+ import type { CssRule } from "@webstudio-is/css-data";
2
+ import { MediaRule, PlaintextRule, StyleRule, type FontFaceOptions, type MediaRuleOptions } from "./rules";
3
+ export type CssEngineOptions = {
4
+ name?: string;
5
+ };
6
+ export declare class CssEngine {
7
+ #private;
8
+ constructor({ name }: CssEngineOptions);
9
+ addMediaRule(id: string, options?: MediaRuleOptions): MediaRule;
10
+ addStyleRule(selectorText: string, rule: CssRule): StyleRule;
11
+ addPlaintextRule(cssText: string): PlaintextRule | Map<string, PlaintextRule>;
12
+ addFontFaceRule(options: FontFaceOptions): number;
13
+ clear(): void;
14
+ render(): void;
15
+ unmount(): void;
16
+ setAttribute(name: string, value: string): void;
17
+ getAttribute(name: string): string | null | undefined;
18
+ get cssText(): string;
19
+ }
@@ -0,0 +1,6 @@
1
+ /// <reference types="react" />
2
+ declare const _default: {
3
+ component: string;
4
+ };
5
+ export default _default;
6
+ export declare const Basic: () => JSX.Element;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { MediaRuleOptions } from "./rules";
2
+ export declare const equalMedia: (left: MediaRuleOptions, right: MediaRuleOptions) => boolean;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ export { CssEngine } from "./css-engine";
2
+ export type { AnyRule, StyleRule, MediaRule, PlaintextRule, FontFaceRule, } from "./rules";
3
+ export * from "./create-css-engine";
4
+ export * from "./to-value";
5
+ export * from "./match-media";
6
+ export * from "./equal-media";
7
+ export * from "./compare-media";
@@ -0,0 +1,2 @@
1
+ import type { MediaRuleOptions } from "./rules";
2
+ export declare const matchMedia: (options: MediaRuleOptions, width: number) => boolean;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,51 @@
1
+ import type { Style, StyleProperty, StyleValue } from "@webstudio-is/css-data";
2
+ declare class StylePropertyMap {
3
+ #private;
4
+ onChange?: () => void;
5
+ set(property: StyleProperty, value?: StyleValue): void;
6
+ has(property: StyleProperty): boolean;
7
+ keys(): IterableIterator<"color" | "left" | "right" | "top" | "bottom" | "contain" | "clip" | "content" | "filter" | "float" | "fontFamily" | "width" | "height" | `--${string}` | "accentColor" | "alignContent" | "alignItems" | "alignSelf" | "alignTracks" | "animationComposition" | "animationDelay" | "animationDirection" | "animationDuration" | "animationFillMode" | "animationIterationCount" | "animationName" | "animationPlayState" | "animationTimingFunction" | "animationTimeline" | "appearance" | "aspectRatio" | "backdropFilter" | "backfaceVisibility" | "backgroundAttachment" | "backgroundBlendMode" | "backgroundClip" | "backgroundColor" | "backgroundImage" | "backgroundOrigin" | "backgroundPosition" | "backgroundPositionX" | "backgroundPositionY" | "backgroundRepeat" | "backgroundSize" | "blockOverflow" | "blockSize" | "borderBlockColor" | "borderBlockStyle" | "borderBlockWidth" | "borderBlockEndColor" | "borderBlockEndStyle" | "borderBlockEndWidth" | "borderBlockStartColor" | "borderBlockStartStyle" | "borderBlockStartWidth" | "borderBottomColor" | "borderBottomLeftRadius" | "borderBottomRightRadius" | "borderBottomStyle" | "borderBottomWidth" | "borderCollapse" | "borderEndEndRadius" | "borderEndStartRadius" | "borderImageOutset" | "borderImageRepeat" | "borderImageSlice" | "borderImageSource" | "borderImageWidth" | "borderInlineColor" | "borderInlineStyle" | "borderInlineWidth" | "borderInlineEndColor" | "borderInlineEndStyle" | "borderInlineEndWidth" | "borderInlineStartColor" | "borderInlineStartStyle" | "borderInlineStartWidth" | "borderLeftColor" | "borderLeftStyle" | "borderLeftWidth" | "borderRightColor" | "borderRightStyle" | "borderRightWidth" | "borderSpacing" | "borderStartEndRadius" | "borderStartStartRadius" | "borderTopColor" | "borderTopLeftRadius" | "borderTopRightRadius" | "borderTopStyle" | "borderTopWidth" | "boxDecorationBreak" | "boxShadow" | "boxSizing" | "breakAfter" | "breakBefore" | "breakInside" | "captionSide" | "caretColor" | "caretShape" | "clear" | "clipPath" | "printColorAdjust" | "colorScheme" | "columnCount" | "columnFill" | "columnGap" | "columnRuleColor" | "columnRuleStyle" | "columnRuleWidth" | "columnSpan" | "columnWidth" | "containIntrinsicBlockSize" | "containIntrinsicHeight" | "containIntrinsicInlineSize" | "containIntrinsicWidth" | "contentVisibility" | "counterIncrement" | "counterReset" | "counterSet" | "cursor" | "direction" | "display" | "emptyCells" | "flexBasis" | "flexDirection" | "flexGrow" | "flexShrink" | "flexWrap" | "fontFeatureSettings" | "fontKerning" | "fontLanguageOverride" | "fontOpticalSizing" | "fontVariationSettings" | "fontSize" | "fontSizeAdjust" | "fontStretch" | "fontStyle" | "fontSynthesis" | "fontVariant" | "fontVariantAlternates" | "fontVariantCaps" | "fontVariantEastAsian" | "fontVariantLigatures" | "fontVariantNumeric" | "fontVariantPosition" | "fontWeight" | "forcedColorAdjust" | "gridAutoColumns" | "gridAutoFlow" | "gridAutoRows" | "gridColumnEnd" | "gridColumnStart" | "gridRowEnd" | "gridRowStart" | "gridTemplateAreas" | "gridTemplateColumns" | "gridTemplateRows" | "hangingPunctuation" | "hyphenateCharacter" | "hyphens" | "imageOrientation" | "imageRendering" | "imageResolution" | "initialLetter" | "initialLetterAlign" | "inlineSize" | "inputSecurity" | "insetBlockEnd" | "insetBlockStart" | "insetInlineEnd" | "insetInlineStart" | "isolation" | "justifyContent" | "justifyItems" | "justifySelf" | "justifyTracks" | "letterSpacing" | "lineBreak" | "lineClamp" | "lineHeight" | "lineHeightStep" | "listStyleImage" | "listStylePosition" | "listStyleType" | "marginBlockEnd" | "marginBlockStart" | "marginBottom" | "marginInlineEnd" | "marginInlineStart" | "marginLeft" | "marginRight" | "marginTop" | "marginTrim" | "maskBorderMode" | "maskBorderOutset" | "maskBorderRepeat" | "maskBorderSlice" | "maskBorderSource" | "maskBorderWidth" | "maskClip" | "maskComposite" | "maskImage" | "maskMode" | "maskOrigin" | "maskPosition" | "maskRepeat" | "maskSize" | "maskType" | "masonryAutoFlow" | "mathDepth" | "mathShift" | "mathStyle" | "maxBlockSize" | "maxHeight" | "maxInlineSize" | "maxLines" | "maxWidth" | "minBlockSize" | "minHeight" | "minInlineSize" | "minWidth" | "mixBlendMode" | "objectFit" | "objectPosition" | "offsetAnchor" | "offsetDistance" | "offsetPath" | "offsetPosition" | "offsetRotate" | "opacity" | "order" | "orphans" | "outlineColor" | "outlineOffset" | "outlineStyle" | "outlineWidth" | "overflow" | "overflowAnchor" | "overflowBlock" | "overflowClipMargin" | "overflowInline" | "overflowWrap" | "overflowX" | "overflowY" | "overscrollBehavior" | "overscrollBehaviorBlock" | "overscrollBehaviorInline" | "overscrollBehaviorX" | "overscrollBehaviorY" | "paddingBlockEnd" | "paddingBlockStart" | "paddingBottom" | "paddingInlineEnd" | "paddingInlineStart" | "paddingLeft" | "paddingRight" | "paddingTop" | "pageBreakAfter" | "pageBreakBefore" | "pageBreakInside" | "paintOrder" | "perspective" | "perspectiveOrigin" | "pointerEvents" | "position" | "quotes" | "resize" | "rotate" | "rowGap" | "rubyAlign" | "rubyMerge" | "rubyPosition" | "scale" | "scrollbarColor" | "scrollbarGutter" | "scrollbarWidth" | "scrollBehavior" | "scrollMarginBlockStart" | "scrollMarginBlockEnd" | "scrollMarginBottom" | "scrollMarginInlineStart" | "scrollMarginInlineEnd" | "scrollMarginLeft" | "scrollMarginRight" | "scrollMarginTop" | "scrollPaddingBlockStart" | "scrollPaddingBlockEnd" | "scrollPaddingBottom" | "scrollPaddingInlineStart" | "scrollPaddingInlineEnd" | "scrollPaddingLeft" | "scrollPaddingRight" | "scrollPaddingTop" | "scrollSnapAlign" | "scrollSnapStop" | "scrollSnapType" | "scrollTimelineAxis" | "scrollTimelineName" | "shapeImageThreshold" | "shapeMargin" | "shapeOutside" | "tabSize" | "tableLayout" | "textAlign" | "textAlignLast" | "textCombineUpright" | "textDecorationColor" | "textDecorationLine" | "textDecorationSkip" | "textDecorationSkipInk" | "textDecorationStyle" | "textDecorationThickness" | "textEmphasisColor" | "textEmphasisPosition" | "textEmphasisStyle" | "textIndent" | "textJustify" | "textOrientation" | "textOverflow" | "textRendering" | "textShadow" | "textSizeAdjust" | "textTransform" | "textUnderlineOffset" | "textUnderlinePosition" | "touchAction" | "transform" | "transformBox" | "transformOrigin" | "transformStyle" | "transitionDelay" | "transitionDuration" | "transitionProperty" | "transitionTimingFunction" | "translate" | "unicodeBidi" | "userSelect" | "verticalAlign" | "visibility" | "whiteSpace" | "widows" | "willChange" | "wordBreak" | "wordSpacing" | "wordWrap" | "writingMode" | "zIndex">;
8
+ delete(property: StyleProperty): void;
9
+ clear(): void;
10
+ toString(): string;
11
+ }
12
+ export declare class StyleRule {
13
+ #private;
14
+ styleMap: StylePropertyMap;
15
+ selectorText: string;
16
+ onChange?: () => void;
17
+ constructor(selectorText: string, style: Style);
18
+ get cssText(): string;
19
+ }
20
+ export type MediaRuleOptions = {
21
+ minWidth?: number;
22
+ maxWidth?: number;
23
+ mediaType?: "all" | "screen" | "print";
24
+ };
25
+ export declare class MediaRule {
26
+ #private;
27
+ options: MediaRuleOptions;
28
+ rules: Array<StyleRule | PlaintextRule>;
29
+ constructor(options?: MediaRuleOptions);
30
+ insertRule(rule: StyleRule | PlaintextRule): StyleRule | PlaintextRule;
31
+ get cssText(): string;
32
+ }
33
+ export declare class PlaintextRule {
34
+ cssText: string;
35
+ styleMap: StylePropertyMap;
36
+ constructor(cssText: string);
37
+ }
38
+ export type FontFaceOptions = {
39
+ fontFamily: string;
40
+ fontStyle?: "normal" | "italic" | "oblique";
41
+ fontWeight?: number | string;
42
+ fontDisplay: "swap" | "auto" | "block" | "fallback" | "optional";
43
+ src: string;
44
+ };
45
+ export declare class FontFaceRule {
46
+ options: FontFaceOptions;
47
+ constructor(options: FontFaceOptions);
48
+ get cssText(): string;
49
+ }
50
+ export type AnyRule = StyleRule | MediaRule | PlaintextRule | FontFaceRule;
51
+ export {};
@@ -0,0 +1,10 @@
1
+ export declare class StyleElement {
2
+ #private;
3
+ constructor(name?: string);
4
+ get isMounted(): boolean;
5
+ mount(): void;
6
+ unmount(): void;
7
+ render(cssText: string): void;
8
+ setAttribute(name: string, value: string): void;
9
+ getAttribute(name: string): string | null | undefined;
10
+ }
@@ -0,0 +1,6 @@
1
+ import type { StyleElement } from "./style-element";
2
+ export declare class StyleSheet {
3
+ #private;
4
+ constructor(element: StyleElement);
5
+ replaceSync(cssText: string): void;
6
+ }
@@ -0,0 +1,2 @@
1
+ import type { StyleProperty } from "@webstudio-is/css-data";
2
+ export declare const toProperty: (property: StyleProperty) => string;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { StyleValue } from "@webstudio-is/css-data";
2
+ type ToCssOptions = {
3
+ withFallback: boolean;
4
+ };
5
+ export declare const toValue: (value?: StyleValue, options?: ToCssOptions) => string;
6
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export * from "./core";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webstudio-is/css-engine",
3
- "version": "0.50.0",
3
+ "version": "0.52.0",
4
4
  "description": "CSS Renderer for Webstudio",
5
5
  "author": "Webstudio <github@webstudio.is>",
6
6
  "homepage": "https://webstudio.is",
@@ -9,7 +9,7 @@
9
9
  "hyphenate-style-name": "^1.0.4",
10
10
  "react": "^17.0.2",
11
11
  "react-dom": "^17.0.2",
12
- "@webstudio-is/fonts": "^0.50.0"
12
+ "@webstudio-is/fonts": "^0.52.0"
13
13
  },
14
14
  "devDependencies": {
15
15
  "@jest/globals": "^29.3.1",
@@ -18,8 +18,8 @@
18
18
  "@types/react": "^17.0.24",
19
19
  "@types/react-dom": "^17.0.9",
20
20
  "jest": "^29.3.1",
21
- "typescript": "4.9.5",
22
- "@webstudio-is/css-data": "^0.50.0",
21
+ "typescript": "5.0.3",
22
+ "@webstudio-is/css-data": "^0.52.0",
23
23
  "@webstudio-is/jest-config": "^1.0.2",
24
24
  "@webstudio-is/scripts": "^0.0.0",
25
25
  "@webstudio-is/storybook-config": "^0.0.0",
@@ -29,7 +29,7 @@
29
29
  "source": "./src/index.ts",
30
30
  "import": "./lib/index.js"
31
31
  },
32
- "types": "src/index.ts",
32
+ "types": "lib/types/index.d.ts",
33
33
  "files": [
34
34
  "lib/*",
35
35
  "src/*",
@@ -43,6 +43,7 @@
43
43
  "checks": "pnpm typecheck && pnpm lint && pnpm test",
44
44
  "dev": "build-package --watch",
45
45
  "build": "build-package",
46
+ "dts": "tsc --emitDeclarationOnly --declaration --declarationDir lib/types",
46
47
  "test": "NODE_OPTIONS=--experimental-vm-modules jest",
47
48
  "lint": "eslint ./src --ext .ts,.tsx --max-warnings 0",
48
49
  "storybook:run": "start-storybook -p 6006",
@@ -0,0 +1,40 @@
1
+ import { describe, test, expect } from "@jest/globals";
2
+ import { compareMedia } from "./compare-media";
3
+
4
+ describe("Compare media", () => {
5
+ test("mobile first", () => {
6
+ const initial = [
7
+ { minWidth: 0 },
8
+ { minWidth: 768 },
9
+ { minWidth: 1024 },
10
+ { minWidth: 1280 },
11
+ ];
12
+ expect(initial.sort(compareMedia)).toStrictEqual(initial);
13
+ });
14
+
15
+ test("random minWidth", () => {
16
+ const initial = [{ minWidth: 0 }, { minWidth: 3 }, { minWidth: 2 }];
17
+ const sorted = [{ minWidth: 0 }, { minWidth: 2 }, { minWidth: 3 }];
18
+ expect(initial.sort(compareMedia)).toStrictEqual(sorted);
19
+ });
20
+
21
+ test("webflow", () => {
22
+ const initial = [
23
+ { maxWidth: 991 },
24
+ { maxWidth: 767 },
25
+ { maxWidth: 479 },
26
+ { minWidth: 1280 },
27
+ { minWidth: 1440 },
28
+ { minWidth: 1920 },
29
+ ];
30
+ const sorted = [
31
+ { maxWidth: 991 },
32
+ { maxWidth: 767 },
33
+ { maxWidth: 479 },
34
+ { minWidth: 1280 },
35
+ { minWidth: 1440 },
36
+ { minWidth: 1920 },
37
+ ];
38
+ expect(initial.sort(compareMedia)).toStrictEqual(sorted);
39
+ });
40
+ });
@@ -0,0 +1,18 @@
1
+ import type { MediaRuleOptions } from "./rules";
2
+
3
+ /**
4
+ * Sort by minWidth descending or maxWidth ascending
5
+ * We want media querries with bigger minWidth to override the smaller once, but the smaller maxWidth to override the bigger once.
6
+ */
7
+ export const compareMedia = (
8
+ optionA: MediaRuleOptions,
9
+ optionB: MediaRuleOptions
10
+ ) => {
11
+ if (optionA?.minWidth !== undefined && optionB?.minWidth !== undefined) {
12
+ return optionA.minWidth - optionB.minWidth;
13
+ }
14
+ if (optionA?.maxWidth !== undefined && optionB?.maxWidth !== undefined) {
15
+ return optionB.maxWidth - optionA.maxWidth;
16
+ }
17
+ return "minWidth" in optionA ? 1 : -1;
18
+ };
@@ -29,6 +29,45 @@ describe("CssEngine", () => {
29
29
 
30
30
  beforeEach(reset);
31
31
 
32
+ test("minWidth media rule", () => {
33
+ engine.addMediaRule("0", { minWidth: 0 });
34
+ engine.addStyleRule(".c1", {
35
+ style: { color: { type: "keyword", value: "red" } },
36
+ breakpoint: "0",
37
+ });
38
+ expect(engine.cssText).toMatchInlineSnapshot(`
39
+ "@media all and (min-width: 0px) {
40
+ .c1 { color: red }
41
+ }"
42
+ `);
43
+ });
44
+
45
+ test("maxWidth media rule", () => {
46
+ engine.addMediaRule("0", { maxWidth: 1000 });
47
+ engine.addStyleRule(".c1", {
48
+ style: { color: { type: "keyword", value: "red" } },
49
+ breakpoint: "0",
50
+ });
51
+ expect(engine.cssText).toMatchInlineSnapshot(`
52
+ "@media all and (max-width: 1000px) {
53
+ .c1 { color: red }
54
+ }"
55
+ `);
56
+ });
57
+
58
+ test("maxWidth and maxWith media rule", () => {
59
+ engine.addMediaRule("0", { maxWidth: 1000, minWidth: 360 });
60
+ engine.addStyleRule(".c1", {
61
+ style: { color: { type: "keyword", value: "red" } },
62
+ breakpoint: "0",
63
+ });
64
+ expect(engine.cssText).toMatchInlineSnapshot(`
65
+ "@media all and (min-width: 360px) and (max-width: 1000px) {
66
+ .c1 { color: red }
67
+ }"
68
+ `);
69
+ });
70
+
32
71
  test("use default media rule when there is no matching one registered", () => {
33
72
  engine.addStyleRule(".c", {
34
73
  style: style0,
@@ -7,6 +7,7 @@ import {
7
7
  type FontFaceOptions,
8
8
  type MediaRuleOptions,
9
9
  } from "./rules";
10
+ import { compareMedia } from "./compare-media";
10
11
  import { StyleElement } from "./style-element";
11
12
  import { StyleSheet } from "./style-sheet";
12
13
 
@@ -90,7 +91,11 @@ export class CssEngine {
90
91
  for (const plaintextRule of this.#plainRules.values()) {
91
92
  css.push(plaintextRule.cssText);
92
93
  }
93
- for (const mediaRule of MediaRule.sort(this.#mediaRules.values())) {
94
+
95
+ const sortedMediaRules = Array.from(this.#mediaRules.values()).sort(
96
+ (ruleA, ruleB) => compareMedia(ruleA.options, ruleB.options)
97
+ );
98
+ for (const mediaRule of sortedMediaRules) {
94
99
  const { cssText } = mediaRule;
95
100
  if (cssText !== "") {
96
101
  css.push(cssText);
@@ -0,0 +1,31 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import { equalMedia } from "./equal-media";
3
+
4
+ describe("equalMedia", () => {
5
+ test("minWidth", () => {
6
+ expect(equalMedia({ minWidth: 100 }, { minWidth: 10 })).toBe(false);
7
+ expect(equalMedia({ minWidth: 100 }, { minWidth: 100 })).toBe(true);
8
+ expect(equalMedia({ minWidth: 100 }, { minWidth: 101 })).toBe(false);
9
+ });
10
+
11
+ test("maxWidth", () => {
12
+ expect(equalMedia({ maxWidth: 100 }, { maxWidth: 101 })).toBe(false);
13
+ expect(equalMedia({ maxWidth: 100 }, { maxWidth: 100 })).toBe(true);
14
+ expect(equalMedia({ maxWidth: 100 }, { maxWidth: 10 })).toBe(false);
15
+ });
16
+
17
+ test("minWidth and maxWidth", () => {
18
+ expect(equalMedia({ maxWidth: 100, minWidth: 10 }, { maxWidth: 100 })).toBe(
19
+ false
20
+ );
21
+ expect(equalMedia({ maxWidth: 100, minWidth: 10 }, { minWidth: 10 })).toBe(
22
+ false
23
+ );
24
+ expect(
25
+ equalMedia(
26
+ { maxWidth: 100, minWidth: 10 },
27
+ { maxWidth: 100, minWidth: 10 }
28
+ )
29
+ ).toBe(true);
30
+ });
31
+ });
@@ -0,0 +1,5 @@
1
+ import type { MediaRuleOptions } from "./rules";
2
+
3
+ export const equalMedia = (left: MediaRuleOptions, right: MediaRuleOptions) => {
4
+ return left.minWidth === right.minWidth && left.maxWidth === right.maxWidth;
5
+ };
package/src/core/index.ts CHANGED
@@ -8,3 +8,6 @@ export type {
8
8
  } from "./rules";
9
9
  export * from "./create-css-engine";
10
10
  export * from "./to-value";
11
+ export * from "./match-media";
12
+ export * from "./equal-media";
13
+ export * from "./compare-media";
@@ -0,0 +1,24 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import { matchMedia } from "./match-media";
3
+
4
+ describe("matchMedia", () => {
5
+ test("minWidth", () => {
6
+ expect(matchMedia({ minWidth: 100 }, 10)).toBe(false);
7
+ expect(matchMedia({ minWidth: 100 }, 100)).toBe(true);
8
+ expect(matchMedia({ minWidth: 100 }, 101)).toBe(true);
9
+ });
10
+
11
+ test("maxWidth", () => {
12
+ expect(matchMedia({ maxWidth: 100 }, 101)).toBe(false);
13
+ expect(matchMedia({ maxWidth: 100 }, 100)).toBe(true);
14
+ expect(matchMedia({ maxWidth: 100 }, 10)).toBe(true);
15
+ });
16
+
17
+ test("minWidth and maxWidth", () => {
18
+ expect(matchMedia({ maxWidth: 100, minWidth: 10 }, 9)).toBe(false);
19
+ expect(matchMedia({ maxWidth: 100, minWidth: 10 }, 101)).toBe(false);
20
+ expect(matchMedia({ maxWidth: 100, minWidth: 10 }, 100)).toBe(true);
21
+ expect(matchMedia({ maxWidth: 100, minWidth: 10 }, 10)).toBe(true);
22
+ expect(matchMedia({ maxWidth: 100, minWidth: 10 }, 11)).toBe(true);
23
+ });
24
+ });
@@ -0,0 +1,7 @@
1
+ import type { MediaRuleOptions } from "./rules";
2
+
3
+ export const matchMedia = (options: MediaRuleOptions, width: number) => {
4
+ const minWidth = options.minWidth ?? Number.MIN_SAFE_INTEGER;
5
+ const maxWidth = options.maxWidth ?? Number.MAX_SAFE_INTEGER;
6
+ return width >= minWidth && width <= maxWidth;
7
+ };
package/src/core/rules.ts CHANGED
@@ -73,17 +73,6 @@ export type MediaRuleOptions = {
73
73
  };
74
74
 
75
75
  export class MediaRule {
76
- // Sort media rules by minWidth.
77
- // Needed to ensure that more specific media rules are inserted after less specific ones.
78
- // So that they get a higher specificity.
79
- static sort(mediaRules: Iterable<MediaRule>) {
80
- return Array.from(mediaRules).sort((ruleA, ruleB) => {
81
- return (
82
- (ruleA.options.minWidth ?? -Number.MAX_SAFE_INTEGER) -
83
- (ruleB.options.minWidth ?? -Number.MAX_SAFE_INTEGER)
84
- );
85
- });
86
- }
87
76
  options: MediaRuleOptions;
88
77
  rules: Array<StyleRule | PlaintextRule> = [];
89
78
  #mediaType;
@@ -106,15 +95,12 @@ export class MediaRule {
106
95
  let conditionText = "";
107
96
  const { minWidth, maxWidth } = this.options;
108
97
  if (minWidth !== undefined) {
109
- conditionText = `min-width: ${minWidth}px`;
98
+ conditionText = ` and (min-width: ${minWidth}px)`;
110
99
  }
111
100
  if (maxWidth !== undefined) {
112
- conditionText = `max-width: ${maxWidth}px`;
113
- }
114
- if (conditionText) {
115
- conditionText = `and (${conditionText}) `;
101
+ conditionText += ` and (max-width: ${maxWidth}px)`;
116
102
  }
117
- return `@media ${this.#mediaType} ${conditionText}{\n${rules.join(
103
+ return `@media ${this.#mediaType}${conditionText} {\n${rules.join(
118
104
  "\n"
119
105
  )}\n}`;
120
106
  }
File without changes
File without changes