Repository: exadel-inc/esl
Pull request #232 (2021-06-07 01:32:40)

feat: media query v2
Really early preview of ESLMediaQuery reimplementation

- [x] ESLMediaQuery.for  
- [x] Separate shortcut processing
- [x] DeviceDetection shortcuts
- [x] Cache(memoization) optimization  
- [x] Shortcusts unification
- [x] FIx for logical operations
- [x] API rework
- [x] Tests & Docs

github-actions[bot]: ****CLA Assistant Lite bot**** All contributors have signed the CLA  ✍️ 

Patch:
diff --git a/.eleventy.js b/.eleventy.js
index 31300253d..4325f1ace 100644
--- a/.eleventy.js
+++ b/.eleventy.js
@@ -1,17 +1,18 @@
-const { isDev } = require('./pages/views/_data/env')
+const { isDev } = require('./pages/views/_data/env');
+const htmlmin = require('html-minifier');
 
-module.exports = config => {
+module.exports = (config) => {
   config.addPassthroughCopy({
     'pages/static/assets': 'assets',
     'pages/static/tools': '.',
   });
 
-  config.addFilter("sortByName", (values) => {
+  config.addFilter('sortByName', (values) => {
     if (!values || !Array.isArray(values)) {
-      console.error(`Unexpected values in "sortByName" filter: ${values}`);
+      console.error(`Unexpected values in 'sortByName' filter: ${values}`);
       return values;
     }
-    return [...values].sort((a, b) => a.data.name.localeCompare(b.data.name))
+    return [...values].sort((a, b) => a.data.name.localeCompare(b.data.name));
   });
 
   config.setBrowserSyncConfig({
@@ -23,16 +24,31 @@ module.exports = config => {
     open: isDev,
   });
 
+  config.addTransform('htmlmin', function (content, outputPath) {
+    if (outputPath && outputPath.endsWith('.html') && !isDev) {
+      let minified = htmlmin.minify(content, {
+        useShortDoctype: true,
+        removeComments: true,
+        collapseWhitespace: true,
+        keepClosingSlash: true,
+        minifyJS: true,
+        minifyCSS: true,
+      });
+      return minified;
+    }
+    return content;
+  });
+
   return {
     dir: {
       input: 'pages/views',
       output: 'pages/dist',
-      layouts: "_layouts",
+      layouts: '_layouts',
     },
     dataTemplateEngine: 'njk',
     htmlTemplateEngine: 'njk',
     passthroughFileCopy: true,
     templateFormats: ['md', 'njk'],
-    pathPrefix: "/esl/",
+    pathPrefix: '/esl/',
   };
 };
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c26398975..81c9f7db5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,8 @@
+# [2.8.1](https://github.com/exadel-inc/esl/compare/v2.8.0...v2.8.1) (2021-07-20)
+
 # [3.0.0-beta.13](https://github.com/exadel-inc/esl/compare/v3.0.0-beta.12...v3.0.0-beta.13) (2021-07-06)
 
+# [2.8.0](https://github.com/exadel-inc/esl/compare/v2.7.3...v2.8.0) (2021-07-06)
 
 ### Bug Fixes
 
diff --git a/package-lock.json b/package-lock.json
index cd4d8e730..af758df7a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -584,88 +584,105 @@
       "dev": true
     },
     "@commitlint/cli": {
-      "version": "12.1.4",
-      "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-12.1.4.tgz",
-      "integrity": "sha512-ZR1WjXLvqEffYyBPT0XdnSxtt3Ty1TMoujEtseW5o3vPnkA1UNashAMjQVg/oELqfaiAMnDw8SERPMN0e/0kLg==",
+      "version": "13.1.0",
+      "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-13.1.0.tgz",
+      "integrity": "sha512-xN/uNYWtGTva5OMSd+xA6e6/c2jk8av7MUbdd6w2cw89u6z3fAWoyiH87X0ewdSMNYmW/6B3L/2dIVGHRDID5w==",
       "dev": true,
       "requires": {
-        "@commitlint/format": "^12.1.4",
-        "@commitlint/lint": "^12.1.4",
-        "@commitlint/load": "^12.1.4",
-        "@commitlint/read": "^12.1.4",
-        "@commitlint/types": "^12.1.4",
+        "@commitlint/format": "^13.1.0",
+        "@commitlint/lint": "^13.1.0",
+        "@commitlint/load": "^13.1.0",
+        "@commitlint/read": "^13.1.0",
+        "@commitlint/types": "^13.1.0",
         "lodash": "^4.17.19",
         "resolve-from": "5.0.0",
         "resolve-global": "1.0.0",
-        "yargs": "^16.2.0"
+        "yargs": "^17.0.0"
+      },
+      "dependencies": {
+        "yargs": {
+          "version": "17.0.1",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.0.1.tgz",
+          "integrity": "sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ==",
+          "dev": true,
+          "requires": {
+            "cliui": "^7.0.2",
+            "escalade": "^3.1.1",
+            "get-caller-file": "^2.0.5",
+            "require-directory": "^2.1.1",
+            "string-width": "^4.2.0",
+            "y18n": "^5.0.5",
+            "yargs-parser": "^20.2.2"
+          }
+        }
       }
     },
     "@commitlint/config-conventional": {
-      "version": "12.1.4",
-      "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-12.1.4.tgz",
-      "integrity": "sha512-ZIdzmdy4o4WyqywMEpprRCrehjCSQrHkaRTVZV411GyLigFQHlEBSJITAihLAWe88Qy/8SyoIe5uKvAsV5vRqQ==",
+      "version": "13.1.0",
+      "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-13.1.0.tgz",
+      "integrity": "sha512-zukJXqdr6jtMiVRy3tTHmwgKcUMGfqKDEskRigc5W3k2aYF4gBAtCEjMAJGZgSQE4DMcHeok0pEV2ANmTpb0cw==",
       "dev": true,
       "requires": {
         "conventional-changelog-conventionalcommits": "^4.3.1"
       }
     },
     "@commitlint/ensure": {
-      "version": "12.1.4",
-      "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-12.1.4.tgz",
-      "integrity": "sha512-MxHIBuAG9M4xl33qUfIeMSasbv3ktK0W+iygldBxZOL4QSYC2Gn66pZAQMnV9o3V+sVFHoAK2XUKqBAYrgbEqw==",
+      "version": "13.1.0",
+      "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-13.1.0.tgz",
+      "integrity": "sha512-NRGyjOdZQnlYwm9it//BZJ2Vm+4x7G9rEnHpLCvNKYY0c6RA8Qf7hamLAB8dWO12RLuFt06JaOpHZoTt/gHutA==",
       "dev": true,
       "requires": {
-        "@commitlint/types": "^12.1.4",
+        "@commitlint/types": "^13.1.0",
         "lodash": "^4.17.19"
       }
     },
     "@commitlint/execute-rule": {
-      "version": "12.1.4",
-      "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-12.1.4.tgz",
-      "integrity": "sha512-h2S1j8SXyNeABb27q2Ok2vD1WfxJiXvOttKuRA9Or7LN6OQoC/KtT3844CIhhWNteNMu/wE0gkTqGxDVAnJiHg==",
+      "version": "13.0.0",
+      "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-13.0.0.tgz",
+      "integrity": "sha512-lBz2bJhNAgkkU/rFMAw3XBNujbxhxlaFHY3lfKB/MxpAa+pIfmWB3ig9i1VKe0wCvujk02O0WiMleNaRn2KJqw==",
       "dev": true
     },
     "@commitlint/format": {
-      "version": "12.1.4",
-      "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-12.1.4.tgz",
-      "integrity": "sha512-h28ucMaoRjVvvgS6Bdf85fa/+ZZ/iu1aeWGCpURnQV7/rrVjkhNSjZwGlCOUd5kDV1EnZ5XdI7L18SUpRjs26g==",
+      "version": "13.1.0",
+      "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-13.1.0.tgz",
+      "integrity": "sha512-n46rYvzf+6Sm99TJjTLjJBkjm6JVcklt31lDO5Q+pCIV0NnJ4qIUcwa6wIL9a9Vqb1XzlMgtp27E0zyYArkvSg==",
       "dev": true,
       "requires": {
-        "@commitlint/types": "^12.1.4",
+        "@commitlint/types": "^13.1.0",
         "chalk": "^4.0.0"
       }
     },
     "@commitlint/is-ignored": {
-      "version": "12.1.4",
-      "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-12.1.4.tgz",
-      "integrity": "sha512-uTu2jQU2SKvtIRVLOzMQo3KxDtO+iJ1p0olmncwrqy4AfPLgwoyCP2CiULq5M7xpR3+dE3hBlZXbZTQbD7ycIw==",
+      "version": "13.1.0",
+      "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-13.1.0.tgz",
+      "integrity": "sha512-P6zenLE5Tn3FTNjRzmL9+/KooTXEI0khA2TmUbuei9KiycemeO4q7Xk7w7aXwFPNAbN0O9oI7z3z7cFpzKJWmQ==",
       "dev": true,
       "requires": {
-        "@commitlint/types": "^12.1.4",
+        "@commitlint/types": "^13.1.0",
         "semver": "7.3.5"
       }
     },
     "@commitlint/lint": {
-      "version": "12.1.4",
-      "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-12.1.4.tgz",
-      "integrity": "sha512-1kZ8YDp4to47oIPFELUFGLiLumtPNKJigPFDuHt2+f3Q3IKdQ0uk53n3CPl4uoyso/Og/EZvb1mXjFR/Yce4cA==",
+      "version": "13.1.0",
+      "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-13.1.0.tgz",
+      "integrity": "sha512-qH9AYSQDDTaSWSdtOvB3G1RdPpcYSgddAdFYqpFewlKQ1GJj/L+sM7vwqCG7/ip6AiM04Sry1sgmFzaEoFREUA==",
       "dev": true,
       "requires": {
-        "@commitlint/is-ignored": "^12.1.4",
-        "@commitlint/parse": "^12.1.4",
-        "@commitlint/rules": "^12.1.4",
-        "@commitlint/types": "^12.1.4"
+        "@commitlint/is-ignored": "^13.1.0",
+        "@commitlint/parse": "^13.1.0",
+        "@commitlint/rules": "^13.1.0",
+        "@commitlint/types": "^13.1.0"
       }
     },
     "@commitlint/load": {
-      "version": "12.1.4",
-      "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-12.1.4.tgz",
-      "integrity": "sha512-Keszi0IOjRzKfxT+qES/n+KZyLrxy79RQz8wWgssCboYjKEp+wC+fLCgbiMCYjI5k31CIzIOq/16J7Ycr0C0EA==",
+      "version": "13.1.0",
+      "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-13.1.0.tgz",
+      "integrity": "sha512-zlZbjJCWnWmBOSwTXis8H7I6pYk6JbDwOCuARA6B9Y/qt2PD+NCo0E/7EuaaFoxjHl+o56QR5QttuMBrf+BJzg==",
       "dev": true,
       "requires": {
-        "@commitlint/execute-rule": "^12.1.4",
-        "@commitlint/resolve-extends": "^12.1.4",
-        "@commitlint/types": "^12.1.4",
+        "@commitlint/execute-rule": "^13.0.0",
+        "@commitlint/resolve-extends": "^13.0.0",
+        "@commitlint/types": "^13.1.0",
         "chalk": "^4.0.0",
         "cosmiconfig": "^7.0.0",
         "lodash": "^4.17.19",
@@ -673,38 +690,51 @@
       }
     },
     "@commitlint/message": {
-      "version": "12.1.4",
-      "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-12.1.4.tgz",
-      "integrity": "sha512-6QhalEKsKQ/Y16/cTk5NH4iByz26fqws2ub+AinHPtM7Io0jy4e3rym9iE+TkEqiqWZlUigZnTwbPvRJeSUBaA==",
+      "version": "13.0.0",
+      "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-13.0.0.tgz",
+      "integrity": "sha512-W/pxhesVEk8747BEWJ+VGQ9ILHmCV27/pEwJ0hGny1wqVquUR8SxvScRCbUjHCB1YtWX4dEnOPXOS9CLH/CX7A==",
       "dev": true
     },
     "@commitlint/parse": {
-      "version": "12.1.4",
-      "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-12.1.4.tgz",
-      "integrity": "sha512-yqKSAsK2V4X/HaLb/yYdrzs6oD/G48Ilt0EJ2Mp6RJeWYxG14w/Out6JrneWnr/cpzemyN5hExOg6+TB19H/Lw==",
+      "version": "13.1.0",
+      "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-13.1.0.tgz",
+      "integrity": "sha512-xFybZcqBiKVjt6vTStvQkySWEUYPI0AcO4QQELyy29o8EzYZqWkhUfrb7K61fWiHsplWL1iL6F3qCLoxSgTcrg==",
       "dev": true,
       "requires": {
-        "@commitlint/types": "^12.1.4",
+        "@commitlint/types": "^13.1.0",
         "conventional-changelog-angular": "^5.0.11",
         "conventional-commits-parser": "^3.0.0"
       }
     },
     "@commitlint/read": {
-      "version": "12.1.4",
-      "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-12.1.4.tgz",
-      "integrity": "sha512-TnPQSJgD8Aod5Xeo9W4SaYKRZmIahukjcCWJ2s5zb3ZYSmj6C85YD9cR5vlRyrZjj78ItLUV/X4FMWWVIS38Jg==",
+      "version": "13.1.0",
+      "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-13.1.0.tgz",
+      "integrity": "sha512-NrVe23GMKyL6i1yDJD8IpqCBzhzoS3wtLfDj8QBzc01Ov1cYBmDojzvBklypGb+MLJM1NbzmRM4PR5pNX0U/NQ==",
       "dev": true,
       "requires": {
-        "@commitlint/top-level": "^12.1.4",
-        "@commitlint/types": "^12.1.4",
-        "fs-extra": "^9.0.0",
+        "@commitlint/top-level": "^13.0.0",
+        "@commitlint/types": "^13.1.0",
+        "fs-extra": "^10.0.0",
         "git-raw-commits": "^2.0.0"
+      },
+      "dependencies": {
+        "fs-extra": {
+          "version": "10.0.0",
+          "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz",
+          "integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==",
+          "dev": true,
+          "requires": {
+            "graceful-fs": "^4.2.0",
+            "jsonfile": "^6.0.1",
+            "universalify": "^2.0.0"
+          }
+        }
       }
     },
     "@commitlint/resolve-extends": {
-      "version": "12.1.4",
-      "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-12.1.4.tgz",
-      "integrity": "sha512-R9CoUtsXLd6KSCfsZly04grsH6JVnWFmVtWgWs1KdDpdV+G3TSs37tColMFqglpkx3dsWu8dsPD56+D9YnJfqg==",
+      "version": "13.0.0",
+      "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-13.0.0.tgz",
+      "integrity": "sha512-1SyaE+UOsYTkQlTPUOoj4NwxQhGFtYildVS/d0TJuK8a9uAJLw7bhCLH2PEeH5cC2D1do4Eqhx/3bLDrSLH3hg==",
       "dev": true,
       "requires": {
         "import-fresh": "^3.0.0",
@@ -714,27 +744,59 @@
       }
     },
     "@commitlint/rules": {
-      "version": "12.1.4",
-      "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-12.1.4.tgz",
-      "integrity": "sha512-W8m6ZSjg7RuIsIfzQiFHa48X5mcPXeKT9yjBxVmjHvYfS2FDBf1VxCQ7vO0JTVIdV4ohjZ0eKg/wxxUuZHJAZg==",
+      "version": "13.1.0",
+      "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-13.1.0.tgz",
+      "integrity": "sha512-b6F+vBqEXsHVghrhomG0Y6YJimHZqkzZ0n5QEpk03dpBXH2OnsezpTw5e+GvbyYCc7PutGbYVQkytuv+7xCxYA==",
       "dev": true,
       "requires": {
-        "@commitlint/ensure": "^12.1.4",
-        "@commitlint/message": "^12.1.4",
-        "@commitlint/to-lines": "^12.1.4",
-        "@commitlint/types": "^12.1.4"
+        "@commitlint/ensure": "^13.1.0",
+        "@commitlint/message": "^13.0.0",
+        "@commitlint/to-lines": "^13.0.0",
+        "@commitlint/types": "^13.1.0",
+        "execa": "^5.0.0"
+      },
+      "dependencies": {
+        "execa": {
+          "version": "5.1.1",
+          "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+          "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+          "dev": true,
+          "requires": {
+            "cross-spawn": "^7.0.3",
+            "get-stream": "^6.0.0",
+            "human-signals": "^2.1.0",
+            "is-stream": "^2.0.0",
+            "merge-stream": "^2.0.0",
+            "npm-run-path": "^4.0.1",
+            "onetime": "^5.1.2",
+            "signal-exit": "^3.0.3",
+            "strip-final-newline": "^2.0.0"
+          }
+        },
+        "get-stream": {
+          "version": "6.0.1",
+          "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+          "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+          "dev": true
+        },
+        "human-signals": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+          "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+          "dev": true
+        }
       }
     },
     "@commitlint/to-lines": {
-      "version": "12.1.4",
-      "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-12.1.4.tgz",
-      "integrity": "sha512-TParumvbi8bdx3EdLXz2MaX+e15ZgoCqNUgqHsRLwyqLUTRbqCVkzrfadG1UcMQk8/d5aMbb327ZKG3Q4BRorw==",
+      "version": "13.0.0",
+      "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-13.0.0.tgz",
+      "integrity": "sha512-mzxWwCio1M4/kG9/69TTYqrraQ66LmtJCYTzAZdZ2eJX3I5w52pSjyP/DJzAUVmmJCYf2Kw3s+RtNVShtnZ+Rw==",
       "dev": true
     },
     "@commitlint/top-level": {
-      "version": "12.1.4",
-      "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-12.1.4.tgz",
-      "integrity": "sha512-d4lTJrOT/dXlpY+NIt4CUl77ciEzYeNVc0VFgUQ6VA+b1rqYD2/VWFjBlWVOrklxtSDeKyuEhs36RGrppEFAvg==",
+      "version": "13.0.0",
+      "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-13.0.0.tgz",
+      "integrity": "sha512-baBy3MZBF28sR93yFezd4a5TdHsbXaakeladfHK9dOcGdXo9oQe3GS5hP3BmlN680D6AiQSN7QPgEJgrNUWUCg==",
       "dev": true,
       "requires": {
         "find-up": "^5.0.0"
@@ -780,9 +842,9 @@
       }
     },
     "@commitlint/types": {
-      "version": "12.1.4",
-      "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-12.1.4.tgz",
-      "integrity": "sha512-KRIjdnWNUx6ywz+SJvjmNCbQKcKP6KArhjZhY2l+CWKxak0d77SOjggkMwFTiSgLODOwmuLTbarR2ZfWPiPMlw==",
+      "version": "13.1.0",
+      "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-13.1.0.tgz",
+      "integrity": "sha512-zcVjuT+OfKt8h91vhBxt05RMcTGEx6DM7Q9QZeuMbXFk6xgbsSEDMMapbJPA1bCZ81fa/1OQBijSYPrKvtt06g==",
       "dev": true,
       "requires": {
         "chalk": "^4.0.0"
@@ -888,9 +950,9 @@
       "dev": true
     },
     "@eslint/eslintrc": {
-      "version": "0.4.2",
-      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.2.tgz",
-      "integrity": "sha512-8nmGq/4ycLpIwzvhI4tNDmQztZ8sp+hI7cyG8i1nQDhkAbRzHpXPidRAHlNvCZQpJTKw5ItIpMw9RSToGF00mg==",
+      "version": "0.4.3",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz",
+      "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==",
       "dev": true,
       "requires": {
         "ajv": "^6.12.4",
@@ -905,9 +967,9 @@
       },
       "dependencies": {
         "debug": {
-          "version": "4.3.1",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
-          "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
+          "version": "4.3.2",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
+          "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
           "dev": true,
           "requires": {
             "ms": "2.1.2"
@@ -1989,9 +2051,9 @@
       }
     },
     "@types/eslint": {
-      "version": "7.2.13",
-      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.13.tgz",
-      "integrity": "sha512-LKmQCWAlnVHvvXq4oasNUMTJJb2GwSyTY8+1C7OH5ILR8mPLaljv1jxL1bXW3xB3jFbQxTKxJAvI8PyjB09aBg==",
+      "version": "7.28.0",
+      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.28.0.tgz",
+      "integrity": "sha512-07XlgzX0YJUn4iG1ocY4IX9DzKSmMGUs6ESKlxWhZRaa0fatIWaHWUVapcuGa8r5HFnTqzj+4OCjd5f7EZ/i/A==",
       "dev": true,
       "requires": {
         "@types/estree": "*",
@@ -1999,9 +2061,9 @@
       }
     },
     "@types/eslint-scope": {
-      "version": "3.7.0",
-      "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.0.tgz",
-      "integrity": "sha512-O/ql2+rrCUe2W2rs7wMR+GqPRcgB6UiqN5RhrR5xruFlY7l9YLMn0ZkDzjoHLeiFkR8MCQZVudUuuvQ2BLC9Qw==",
+      "version": "3.7.1",
+      "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.1.tgz",
+      "integrity": "sha512-SCFeogqiptms4Fg29WpOTk5nHIzfpKCemSN63ksBQYKTcXoJEmJagV+DhVmbapZzY4/5YaOV1nZwrsU79fFm1g==",
       "dev": true,
       "requires": {
         "@types/eslint": "*",
@@ -2058,9 +2120,9 @@
       }
     },
     "@types/json-schema": {
-      "version": "7.0.7",
-      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
-      "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==",
+      "version": "7.0.8",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.8.tgz",
+      "integrity": "sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg==",
       "dev": true
     },
     "@types/json5": {
@@ -2144,9 +2206,9 @@
       "dev": true
     },
     "@types/video.js": {
-      "version": "7.3.24",
-      "resolved": "https://registry.npmjs.org/@types/video.js/-/video.js-7.3.24.tgz",
-      "integrity": "sha512-j9uJ7NgJEvuMQSz5EE2YfyCRgdfspm87TLSnpjC9cZ0z/drbTjNWSYyr3dZrV+WGWkrnXmvK5QWt+prubEOzCw=="
+      "version": "7.3.26",
+      "resolved": "https://registry.npmjs.org/@types/video.js/-/video.js-7.3.26.tgz",
+      "integrity": "sha512-RspjR7uqX6+zRXJLQ5NrsPPppxJ/jta2X7U/7E5LZDSztVMy9+AjB1SOWcC3td1JzoEo67f0ns1tZo36zBCUqQ=="
     },
     "@types/yargs": {
       "version": "15.0.13",
@@ -2169,13 +2231,13 @@
       "integrity": "sha512-bvBETO6kkqq32Yuk5CI3k0V/UCOTzhsqImJsboABsnCUA8BzawvnrgaRe51wqXQPqZkTwLFkRvhS7iSvsgp4pg=="
     },
     "@typescript-eslint/eslint-plugin": {
-      "version": "4.28.3",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.3.tgz",
-      "integrity": "sha512-jW8sEFu1ZeaV8xzwsfi6Vgtty2jf7/lJmQmDkDruBjYAbx5DA8JtbcMnP0rNPUG+oH5GoQBTSp+9613BzuIpYg==",
+      "version": "4.28.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.5.tgz",
+      "integrity": "sha512-m31cPEnbuCqXtEZQJOXAHsHvtoDi9OVaeL5wZnO2KZTnkvELk+u6J6jHg+NzvWQxk+87Zjbc4lJS4NHmgImz6Q==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/experimental-utils": "4.28.3",
-        "@typescript-eslint/scope-manager": "4.28.3",
+        "@typescript-eslint/experimental-utils": "4.28.5",
+        "@typescript-eslint/scope-manager": "4.28.5",
         "debug": "^4.3.1",
         "functional-red-black-tree": "^1.0.1",
         "regexpp": "^3.1.0",
@@ -2201,28 +2263,28 @@
       }
     },
     "@typescript-eslint/experimental-utils": {
-      "version": "4.28.3",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.3.tgz",
-      "integrity": "sha512-zZYl9TnrxwEPi3FbyeX0ZnE8Hp7j3OCR+ELoUfbwGHGxWnHg9+OqSmkw2MoCVpZksPCZYpQzC559Ee9pJNHTQw==",
+      "version": "4.28.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.5.tgz",
+      "integrity": "sha512-bGPLCOJAa+j49hsynTaAtQIWg6uZd8VLiPcyDe4QPULsvQwLHGLSGKKcBN8/lBxIX14F74UEMK2zNDI8r0okwA==",
       "dev": true,
       "requires": {
         "@types/json-schema": "^7.0.7",
-        "@typescript-eslint/scope-manager": "4.28.3",
-        "@typescript-eslint/types": "4.28.3",
-        "@typescript-eslint/typescript-estree": "4.28.3",
+        "@typescript-eslint/scope-manager": "4.28.5",
+        "@typescript-eslint/types": "4.28.5",
+        "@typescript-eslint/typescript-estree": "4.28.5",
         "eslint-scope": "^5.1.1",
         "eslint-utils": "^3.0.0"
       }
     },
     "@typescript-eslint/parser": {
-      "version": "4.28.3",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.3.tgz",
-      "integrity": "sha512-ZyWEn34bJexn/JNYvLQab0Mo5e+qqQNhknxmc8azgNd4XqspVYR5oHq9O11fLwdZMRcj4by15ghSlIEq+H5ltQ==",
+      "version": "4.28.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.5.tgz",
+      "integrity": "sha512-NPCOGhTnkXGMqTznqgVbA5LqVsnw+i3+XA1UKLnAb+MG1Y1rP4ZSK9GX0kJBmAZTMIktf+dTwXToT6kFwyimbw==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/scope-manager": "4.28.3",
-        "@typescript-eslint/types": "4.28.3",
-        "@typescript-eslint/typescript-estree": "4.28.3",
+        "@typescript-eslint/scope-manager": "4.28.5",
+        "@typescript-eslint/types": "4.28.5",
+        "@typescript-eslint/typescript-estree": "4.28.5",
         "debug": "^4.3.1"
       },
       "dependencies": {
@@ -2244,29 +2306,29 @@
       }
     },
     "@typescript-eslint/scope-manager": {
-      "version": "4.28.3",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.3.tgz",
-      "integrity": "sha512-/8lMisZ5NGIzGtJB+QizQ5eX4Xd8uxedFfMBXOKuJGP0oaBBVEMbJVddQKDXyyB0bPlmt8i6bHV89KbwOelJiQ==",
+      "version": "4.28.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.5.tgz",
+      "integrity": "sha512-PHLq6n9nTMrLYcVcIZ7v0VY1X7dK309NM8ya9oL/yG8syFINIMHxyr2GzGoBYUdv3NUfCOqtuqps0ZmcgnZTfQ==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/types": "4.28.3",
-        "@typescript-eslint/visitor-keys": "4.28.3"
+        "@typescript-eslint/types": "4.28.5",
+        "@typescript-eslint/visitor-keys": "4.28.5"
       }
     },
     "@typescript-eslint/types": {
-      "version": "4.28.3",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.3.tgz",
-      "integrity": "sha512-kQFaEsQBQVtA9VGVyciyTbIg7S3WoKHNuOp/UF5RG40900KtGqfoiETWD/v0lzRXc+euVE9NXmfer9dLkUJrkA==",
+      "version": "4.28.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.5.tgz",
+      "integrity": "sha512-MruOu4ZaDOLOhw4f/6iudyks/obuvvZUAHBDSW80Trnc5+ovmViLT2ZMDXhUV66ozcl6z0LJfKs1Usldgi/WCA==",
       "dev": true
     },
     "@typescript-eslint/typescript-estree": {
-      "version": "4.28.3",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.3.tgz",
-      "integrity": "sha512-YAb1JED41kJsqCQt1NcnX5ZdTA93vKFCMP4lQYG6CFxd0VzDJcKttRlMrlG+1qiWAw8+zowmHU1H0OzjWJzR2w==",
+      "version": "4.28.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.5.tgz",
+      "integrity": "sha512-FzJUKsBX8poCCdve7iV7ShirP8V+ys2t1fvamVeD1rWpiAnIm550a+BX/fmTHrjEpQJ7ZAn+Z7ZZwJjytk9rZw==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/types": "4.28.3",
-        "@typescript-eslint/visitor-keys": "4.28.3",
+        "@typescript-eslint/types": "4.28.5",
+        "@typescript-eslint/visitor-keys": "4.28.5",
         "debug": "^4.3.1",
         "globby": "^11.0.3",
         "is-glob": "^4.0.1",
@@ -2292,12 +2354,12 @@
       }
     },
     "@typescript-eslint/visitor-keys": {
-      "version": "4.28.3",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.3.tgz",
-      "integrity": "sha512-ri1OzcLnk1HH4gORmr1dllxDzzrN6goUIz/P4MHFV0YZJDCADPR3RvYNp0PW2SetKTThar6wlbFTL00hV2Q+fg==",
+      "version": "4.28.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.5.tgz",
+      "integrity": "sha512-dva/7Rr+EkxNWdJWau26xU/0slnFlkh88v3TsyTgRS/IIYFi5iIfpCFM4ikw0vQTFUR9FYSSyqgK4w64gsgxhg==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/types": "4.28.3",
+        "@typescript-eslint/types": "4.28.5",
         "eslint-visitor-keys": "^2.0.0"
       }
     },
@@ -2547,9 +2609,9 @@
       }
     },
     "acorn-jsx": {
-      "version": "5.3.1",
-      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz",
-      "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+      "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
       "dev": true
     },
     "acorn-walk": {
@@ -2793,95 +2855,6 @@
       "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
       "dev": true
     },
-    "autoprefixer": {
-      "version": "9.8.6",
-      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz",
-      "integrity": "sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg==",
-      "dev": true,
-      "requires": {
-        "browserslist": "^4.12.0",
-        "caniuse-lite": "^1.0.30001109",
-        "colorette": "^1.2.1",
-        "normalize-range": "^0.1.2",
-        "num2fraction": "^1.2.2",
-        "postcss": "^7.0.32",
-        "postcss-value-parser": "^4.1.0"
-      },
-      "dependencies": {
-        "ansi-styles": {
-          "version": "3.2.1",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
-          "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-          "dev": true,
-          "requires": {
-            "color-convert": "^1.9.0"
-          }
-        },
-        "chalk": {
-          "version": "2.4.2",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
-          "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
-          "dev": true,
-          "requires": {
-            "ansi-styles": "^3.2.1",
-            "escape-string-regexp": "^1.0.5",
-            "supports-color": "^5.3.0"
-          },
-          "dependencies": {
-            "supports-color": {
-              "version": "5.5.0",
-              "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
-              "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
-              "dev": true,
-              "requires": {
-                "has-flag": "^3.0.0"
-              }
-            }
-          }
-        },
-        "color-convert": {
-          "version": "1.9.3",
-          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
-          "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-          "dev": true,
-          "requires": {
-            "color-name": "1.1.3"
-          }
-        },
-        "color-name": {
-          "version": "1.1.3",
-          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-          "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
-          "dev": true
-        },
-        "has-flag": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
-          "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
-          "dev": true
-        },
-        "postcss": {
-          "version": "7.0.36",
-          "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz",
-          "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==",
-          "dev": true,
-          "requires": {
-            "chalk": "^2.4.2",
-            "source-map": "^0.6.1",
-            "supports-color": "^6.1.0"
-          }
-        },
-        "supports-color": {
-          "version": "6.1.0",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
-          "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
-          "dev": true,
-          "requires": {
-            "has-flag": "^3.0.0"
-          }
-        }
-      }
-    },
     "aws-sign2": {
       "version": "0.7.0",
       "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
@@ -3102,13 +3075,13 @@
       "dev": true
     },
     "browser-sync": {
-      "version": "2.27.4",
-      "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.27.4.tgz",
-      "integrity": "sha512-zgjrI6oUXxLa671SxVmWfIH+XiG6yZiGuvsQ1huuGEBlKkWuBVKgYjh+j9kagKm891FARgmK4Ct4PAhckLKaYg==",
+      "version": "2.27.5",
+      "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.27.5.tgz",
+      "integrity": "sha512-0GMEPDqccbTxwYOUGCk5AZloDj9I/1eDZCLXUKXu7iBJPznGGOnMHs88mrhaFL0fTA0R23EmsXX9nLZP+k5YzA==",
       "dev": true,
       "requires": {
-        "browser-sync-client": "^2.27.4",
-        "browser-sync-ui": "^2.27.4",
+        "browser-sync-client": "^2.27.5",
+        "browser-sync-ui": "^2.27.5",
         "bs-recipes": "1.3.4",
         "bs-snippet-injector": "^2.0.1",
         "chokidar": "^3.5.1",
@@ -3234,9 +3207,9 @@
       }
     },
     "browser-sync-client": {
-      "version": "2.27.4",
-      "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-2.27.4.tgz",
-      "integrity": "sha512-l0krAGZnpLaD+tUYdM25WeS4FP73ZoPeaxlVzOvmtL9uKSlvpmywsnDwa3PJzc3ubmDPAcD74ifJjl6MmVksXw==",
+      "version": "2.27.5",
+      "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-2.27.5.tgz",
+      "integrity": "sha512-l2jtf60/exv0fQiZkhi3z8RgexYYLGS7DVDnyepkrp+oFAPlKW69daL6NrVSgrwu6lzSTCCTAiPXnUSrQ57e/Q==",
       "dev": true,
       "requires": {
         "etag": "1.8.1",
@@ -3246,9 +3219,9 @@
       }
     },
     "browser-sync-ui": {
-      "version": "2.27.4",
-      "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-2.27.4.tgz",
-      "integrity": "sha512-E58Mb6ycz57Nm393oqVJj4jxuLJH3MhZnY8AV+zd9LsNVGZjrKRNNIw5JPYYguyb37ZjLjq2x4u+38mRv3Sb7g==",
+      "version": "2.27.5",
+      "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-2.27.5.tgz",
+      "integrity": "sha512-KxBJhQ6XNbQ8w8UlkPa9/J5R0nBHgHuJUtDpEXQx1jBapDy32WGzD0NENDozP4zGNvJUgZk3N80hqB7YCieC3g==",
       "dev": true,
       "requires": {
         "async-each-series": "0.1.1",
@@ -3330,6 +3303,16 @@
       "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
       "dev": true
     },
+    "camel-case": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz",
+      "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=",
+      "dev": true,
+      "requires": {
+        "no-case": "^2.2.0",
+        "upper-case": "^1.1.1"
+      }
+    },
     "camelcase": {
       "version": "5.3.1",
       "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
@@ -3425,28 +3408,40 @@
       "dev": true
     },
     "chokidar": {
-      "version": "3.5.1",
-      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
-      "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==",
+      "version": "3.5.2",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
+      "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==",
       "dev": true,
       "requires": {
-        "anymatch": "~3.1.1",
+        "anymatch": "~3.1.2",
         "braces": "~3.0.2",
-        "fsevents": "~2.3.1",
-        "glob-parent": "~5.1.0",
+        "fsevents": "~2.3.2",
+        "glob-parent": "~5.1.2",
         "is-binary-path": "~2.1.0",
         "is-glob": "~4.0.1",
         "normalize-path": "~3.0.0",
-        "readdirp": "~3.5.0"
+        "readdirp": "~3.6.0"
+      },
+      "dependencies": {
+        "anymatch": {
+          "version": "3.1.2",
+          "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
+          "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
+          "dev": true,
+          "requires": {
+            "normalize-path": "^3.0.0",
+            "picomatch": "^2.0.4"
+          }
+        }
       }
     },
     "chokidar-cli": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/chokidar-cli/-/chokidar-cli-2.1.0.tgz",
-      "integrity": "sha512-6n21AVpW6ywuEPoxJcLXMA2p4T+SLjWsXKny/9yTWFz0kKxESI3eUylpeV97LylING/27T/RVTY0f2/0QaWq9Q==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/chokidar-cli/-/chokidar-cli-3.0.0.tgz",
+      "integrity": "sha512-xVW+Qeh7z15uZRxHOkP93Ux8A0xbPzwK4GaqD8dQOYc34TlkqUhVSS59fK36DOp5WdJlrRzlYSy02Ht99FjZqQ==",
       "dev": true,
       "requires": {
-        "chokidar": "^3.2.3",
+        "chokidar": "^3.5.2",
         "lodash.debounce": "^4.0.8",
         "lodash.throttle": "^4.1.1",
         "yargs": "^13.3.0"
@@ -3571,9 +3566,9 @@
           }
         },
         "y18n": {
-          "version": "4.0.1",
-          "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
-          "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
+          "version": "4.0.3",
+          "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+          "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
           "dev": true
         },
         "yargs": {
@@ -3630,6 +3625,34 @@
       "integrity": "sha512-jVamGdJPDeuQilKhvVn1h3knuMOZzr8QDnpk+M9aMlCaMkTDd6fBWPhiDqFvFZ07pL0liqabAiuy8SY4jGHeaw==",
       "dev": true
     },
+    "clean-css": {
+      "version": "5.1.3",
+      "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.1.3.tgz",
+      "integrity": "sha512-qGXzUCDpLwAlPx0kYeU4QXjzQIcIYZbJjD4FNm7NnSjoP0hYMVZhHOpUYJ6AwfkMX2cceLRq54MeCgHy/va1cA==",
+      "dev": true,
+      "requires": {
+        "source-map": "~0.6.0"
+      }
+    },
+    "clean-css-cli": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/clean-css-cli/-/clean-css-cli-5.3.0.tgz",
+      "integrity": "sha512-Jri2KVAcan/Y1JuEDo1rUlm3vUoGvMKSOpIL2YEPgalksvoGur/yL2KbtMF2dPs2VlMVj1afzp76Vu0KjxKfvA==",
+      "dev": true,
+      "requires": {
+        "clean-css": "^5.1.2",
+        "commander": "7.x",
+        "glob": "^7.1.6"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+          "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+          "dev": true
+        }
+      }
+    },
     "clean-stack": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
@@ -4915,13 +4938,13 @@
       }
     },
     "eslint": {
-      "version": "7.30.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.30.0.tgz",
-      "integrity": "sha512-VLqz80i3as3NdloY44BQSJpFw534L9Oh+6zJOUaViV4JPd+DaHwutqP7tcpkW3YiXbK6s05RZl7yl7cQn+lijg==",
+      "version": "7.31.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.31.0.tgz",
+      "integrity": "sha512-vafgJpSh2ia8tnTkNUkwxGmnumgckLh5aAbLa1xRmIn9+owi8qBNGKL+B881kNKNTy7FFqTEkpNkUvmw0n6PkA==",
       "dev": true,
       "requires": {
         "@babel/code-frame": "7.12.11",
-        "@eslint/eslintrc": "^0.4.2",
+        "@eslint/eslintrc": "^0.4.3",
         "@humanwhocodes/config-array": "^0.5.0",
         "ajv": "^6.10.0",
         "chalk": "^4.0.0",
@@ -4972,9 +4995,9 @@
           }
         },
         "debug": {
-          "version": "4.3.1",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
-          "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
+          "version": "4.3.2",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
+          "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
           "dev": true,
           "requires": {
             "ms": "2.1.2"
@@ -6094,9 +6117,9 @@
       }
     },
     "globals": {
-      "version": "13.9.0",
-      "resolved": "https://registry.npmjs.org/globals/-/globals-13.9.0.tgz",
-      "integrity": "sha512-74/FduwI/JaIrr1H8e71UbDE+5x7pIPs1C2rrwC52SszOo043CsWOZEMW7o2Y58xwm9b+0RBKDxY5n2sUpEFxA==",
+      "version": "13.10.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-13.10.0.tgz",
+      "integrity": "sha512-piHC3blgLGFjvOuMmWZX60f+na1lXFDhQXBf1UYp2fXPXqvEUbOhNwi6BsQ0bQishwedgnjkwv1d9zKf+MWw3g==",
       "dev": true,
       "requires": {
         "type-fest": "^0.20.2"
@@ -6263,6 +6286,12 @@
       "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
       "dev": true
     },
+    "he": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+      "dev": true
+    },
     "hook-std": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/hook-std/-/hook-std-2.0.0.tgz",
@@ -6293,6 +6322,32 @@
       "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
       "dev": true
     },
+    "html-minifier": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz",
+      "integrity": "sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==",
+      "dev": true,
+      "requires": {
+        "camel-case": "^3.0.0",
+        "clean-css": "^4.2.1",
+        "commander": "^2.19.0",
+        "he": "^1.2.0",
+        "param-case": "^2.1.1",
+        "relateurl": "^0.2.7",
+        "uglify-js": "^3.5.1"
+      },
+      "dependencies": {
+        "clean-css": {
+          "version": "4.2.3",
+          "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz",
+          "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==",
+          "dev": true,
+          "requires": {
+            "source-map": "~0.6.0"
+          }
+        }
+      }
+    },
     "html-tags": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz",
@@ -8402,12 +8457,12 @@
       }
     },
     "jest-util": {
-      "version": "27.0.2",
-      "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.0.2.tgz",
-      "integrity": "sha512-1d9uH3a00OFGGWSibpNYr+jojZ6AckOMCXV2Z4K3YXDnzpkAaXQyIpY14FOJPiUmil7CD+A6Qs+lnnh6ctRbIA==",
+      "version": "27.0.6",
+      "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.0.6.tgz",
+      "integrity": "sha512-1JjlaIh+C65H/F7D11GNkGDDZtDfMEM8EBXsvd+l/cxtgQ6QhxuloOaiayt89DxUvDarbVhqI98HhgrM1yliFQ==",
       "dev": true,
       "requires": {
-        "@jest/types": "^27.0.2",
+        "@jest/types": "^27.0.6",
         "@types/node": "*",
         "chalk": "^4.0.0",
         "graceful-fs": "^4.2.4",
@@ -8416,9 +8471,9 @@
       },
       "dependencies": {
         "@jest/types": {
-          "version": "27.0.2",
-          "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.2.tgz",
-          "integrity": "sha512-XpjCtJ/99HB4PmyJ2vgmN7vT+JLP7RW1FBT9RgnMFS4Dt7cvIyBee8O3/j98aUZ34ZpenPZFqmaaObWSeL65dg==",
+          "version": "27.0.6",
+          "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz",
+          "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==",
           "dev": true,
           "requires": {
             "@types/istanbul-lib-coverage": "^2.0.0",
@@ -8429,9 +8484,9 @@
           }
         },
         "@types/yargs": {
-          "version": "16.0.3",
-          "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.3.tgz",
-          "integrity": "sha512-YlFfTGS+zqCgXuXNV26rOIeETOkXnGQXP/pjjL9P0gO/EP9jTmc7pUBhx+jVEIxpq41RX33GQ7N3DzOSfZoglQ==",
+          "version": "16.0.4",
+          "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz",
+          "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==",
           "dev": true,
           "requires": {
             "@types/yargs-parser": "*"
@@ -9198,6 +9253,12 @@
       "integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==",
       "dev": true
     },
+    "lower-case": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz",
+      "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=",
+      "dev": true
+    },
     "lru-cache": {
       "version": "6.0.0",
       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -9664,6 +9725,15 @@
       "integrity": "sha1-5tq3/r9a2Bbqgc9cYpxaDr3nLBo=",
       "dev": true
     },
+    "no-case": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz",
+      "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==",
+      "dev": true,
+      "requires": {
+        "lower-case": "^1.1.1"
+      }
+    },
     "node-emoji": {
       "version": "1.10.0",
       "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz",
@@ -11464,6 +11534,15 @@
       "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
       "dev": true
     },
+    "param-case": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz",
+      "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=",
+      "dev": true,
+      "requires": {
+        "no-case": "^2.2.0"
+      }
+    },
     "parent-module": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -11841,9 +11920,9 @@
       }
     },
     "postcss": {
-      "version": "8.3.5",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.5.tgz",
-      "integrity": "sha512-NxTuJocUhYGsMiMFHDUkmjSKT3EdH4/WbGF6GCi1NDGk+vbcUTun4fpbOqaPtD8IIsztA2ilZm2DhYCuyN58gA==",
+      "version": "8.3.6",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.6.tgz",
+      "integrity": "sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A==",
       "dev": true,
       "requires": {
         "colorette": "^1.2.2",
@@ -12610,9 +12689,9 @@
       }
     },
     "readdirp": {
-      "version": "3.5.0",
-      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
-      "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
       "dev": true,
       "requires": {
         "picomatch": "^2.2.1"
@@ -12754,6 +12833,12 @@
         "rc": "^1.2.8"
       }
     },
+    "relateurl": {
+      "version": "0.2.7",
+      "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
+      "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=",
+      "dev": true
+    },
     "remark": {
       "version": "13.0.0",
       "resolved": "https://registry.npmjs.org/remark/-/remark-13.0.0.tgz",
@@ -13005,12 +13090,12 @@
       }
     },
     "schema-utils": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz",
-      "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==",
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
+      "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==",
       "dev": true,
       "requires": {
-        "@types/json-schema": "^7.0.6",
+        "@types/json-schema": "^7.0.8",
         "ajv": "^6.12.5",
         "ajv-keywords": "^3.5.2"
       }
@@ -13538,12 +13623,6 @@
         "isarray": "2.0.1"
       }
     },
-    "source-list-map": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
-      "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==",
-      "dev": true
-    },
     "source-map": {
       "version": "0.6.1",
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -13898,6 +13977,21 @@
         "write-file-atomic": "^3.0.3"
       },
       "dependencies": {
+        "autoprefixer": {
+          "version": "9.8.6",
+          "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz",
+          "integrity": "sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg==",
+          "dev": true,
+          "requires": {
+            "browserslist": "^4.12.0",
+            "caniuse-lite": "^1.0.30001109",
+            "colorette": "^1.2.1",
+            "normalize-range": "^0.1.2",
+            "num2fraction": "^1.2.2",
+            "postcss": "^7.0.32",
+            "postcss-value-parser": "^4.1.0"
+          }
+        },
         "balanced-match": {
           "version": "2.0.0",
           "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz",
@@ -14545,9 +14639,9 @@
       "dev": true
     },
     "ts-jest": {
-      "version": "27.0.3",
-      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.0.3.tgz",
-      "integrity": "sha512-U5rdMjnYam9Ucw+h0QvtNDbc5+88nxt7tbIvqaZUhFrfG4+SkWhMXjejCLVGcpILTPuV+H3W/GZDZrnZFpPeXw==",
+      "version": "27.0.4",
+      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.0.4.tgz",
+      "integrity": "sha512-c4E1ECy9Xz2WGfTMyHbSaArlIva7Wi2p43QOMmCqjSSjHP06KXv+aT+eSY+yZMuqsMi3k7pyGsGj2q5oSl5WfQ==",
       "dev": true,
       "requires": {
         "bs-logger": "0.x",
@@ -14574,9 +14668,9 @@
       }
     },
     "ts-loader": {
-      "version": "9.2.3",
-      "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.2.3.tgz",
-      "integrity": "sha512-sEyWiU3JMHBL55CIeC4iqJQadI0U70A5af0kvgbNLHVNz2ACztQg0j/9x10bjjIht8WfFYLKfn4L6tkZ+pu+8Q==",
+      "version": "9.2.4",
+      "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.2.4.tgz",
+      "integrity": "sha512-Ats2BCqPFBkgsoZUmmYMjuQu+iBNExt4o3QDsJqRMuPdStWlnOthdqX/GHHJnxSSgw7Gu6Hll/MD5b4usgKFOg==",
       "dev": true,
       "requires": {
         "chalk": "^4.1.0",
@@ -14676,11 +14770,10 @@
       "dev": true
     },
     "uglify-js": {
-      "version": "3.13.9",
-      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.13.9.tgz",
-      "integrity": "sha512-wZbyTQ1w6Y7fHdt8sJnHfSIuWeDgk6B5rCb4E/AM6QNNPbOMIZph21PW5dRB3h7Df0GszN+t7RuUH6sWK5bF0g==",
-      "dev": true,
-      "optional": true
+      "version": "3.14.1",
+      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.14.1.tgz",
+      "integrity": "sha512-JhS3hmcVaXlp/xSo3PKY5R0JqKs5M3IV+exdLHW99qKvKivPO4Z8qbej6mte17SOPqAOVMjt/XGgWacnFSzM3g==",
+      "dev": true
     },
     "unbox-primitive": {
       "version": "1.0.1",
@@ -14785,6 +14878,12 @@
       "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==",
       "dev": true
     },
+    "upper-case": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz",
+      "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=",
+      "dev": true
+    },
     "uri-js": {
       "version": "4.4.1",
       "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
@@ -14950,9 +15049,9 @@
       "dev": true
     },
     "webpack": {
-      "version": "5.44.0",
-      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.44.0.tgz",
-      "integrity": "sha512-I1S1w4QLoKmH19pX6YhYN0NiSXaWY8Ou00oA+aMcr9IUGeF5azns+IKBkfoAAG9Bu5zOIzZt/mN35OffBya8AQ==",
+      "version": "5.47.1",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.47.1.tgz",
+      "integrity": "sha512-cW+Mzy9SCDapFV4OrkHuP6EFV2mAsiQd+gOa3PKtHNoKg6qPqQXZzBlHH+CnQG1osplBCqwsJZ8CfGO6XWah0g==",
       "dev": true,
       "requires": {
         "@types/eslint-scope": "^3.7.0",
@@ -14973,11 +15072,11 @@
         "loader-runner": "^4.2.0",
         "mime-types": "^2.1.27",
         "neo-async": "^2.6.2",
-        "schema-utils": "^3.0.0",
+        "schema-utils": "^3.1.0",
         "tapable": "^2.1.1",
         "terser-webpack-plugin": "^5.1.3",
         "watchpack": "^2.2.0",
-        "webpack-sources": "^2.3.0"
+        "webpack-sources": "^3.1.1"
       },
       "dependencies": {
         "acorn": {
@@ -15057,14 +15156,10 @@
       }
     },
     "webpack-sources": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.0.tgz",
-      "integrity": "sha512-WyOdtwSvOML1kbgtXbTDnEW0jkJ7hZr/bDByIwszhWd/4XX1A3XMkrbFMsuH4+/MfLlZCUzlAdg4r7jaGKEIgQ==",
-      "dev": true,
-      "requires": {
-        "source-list-map": "^2.0.1",
-        "source-map": "^0.6.1"
-      }
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.1.1.tgz",
+      "integrity": "sha512-ztUmIWq0LWaw+1YyR3bXtUPjt8vQedtI9WxGn/q1V1ASHsombnaso7MN9S25lzKS/OuC9Q8lEg3GsZexjDbdlQ==",
+      "dev": true
     },
     "whatwg-encoding": {
       "version": "1.0.5",
diff --git a/package.json b/package.json
index f94a0541b..a24e754dc 100644
--- a/package.json
+++ b/package.json
@@ -48,12 +48,13 @@
     "build:less:cpy": "copyfiles \"src/**/*.less\" \".\" --up 1 --all",
     "build:less:css": "foreach -x \"lessc #{path} #{dir}/#{name}.css\" -i \"**/*.mixin.less\" -g \"modules/{*,*/*,{draft,esl-utils,esl-forms}/*/*}.less\"",
     "build:docs": "copyfiles \"src/**/*.md\" \".\" --up 1 --all",
+    "minify:css": "cleancss -o pages/dist/bundles/localdev.css pages/dist/bundles/localdev.css",
     "build-pages": "concurrently \"npm run build-pages:ts\" \"npm run build-pages:less\" && npm run build-pages:eleventy",
-    "build-pages:ts": "cd pages && webpack",
-    "build-pages:less": "lessc pages/src/localdev.less pages/dist/bundles/localdev.css --source-map=pages/dist/bundles/localdev.css.map",
+    "build-pages:ts": "cd pages && webpack --mode=production",
+    "build-pages:less": "lessc pages/src/localdev.less pages/dist/bundles/localdev.css --source-map=pages/dist/bundles/localdev.css.map && npm run minify:css",
     "build-pages:eleventy": "npx @11ty/eleventy -- --env=production",
     "watch:ts": "cd pages && webpack --watch",
-    "watch:less": "chokidar --initial \"**/*.less\" -c \"npm run build-pages:less\"",
+    "watch:less": "chokidar --initial \"**/*.less\" -c \"lessc pages/src/localdev.less pages/dist/bundles/localdev.css --source-map=pages/dist/bundles/localdev.css.map\"",
     "watch:eleventy": "npx @11ty/eleventy --serve --port=3005 -- --env=development",
     "test": "concurrently \"npm run lint\" \"npm run test:unit\"",
     "test:unit": "jest --silent --noStackTrace",
@@ -67,13 +68,13 @@
   },
   "dependencies": {
     "@types/resize-observer-browser": "0.1.6",
-    "@types/video.js": "7.3.24",
+    "@types/video.js": "7.3.26",
     "@types/youtube": "0.0.44"
   },
   "devDependencies": {
     "@11ty/eleventy": "^0.12.1",
-    "@commitlint/cli": "^12.1.4",
-    "@commitlint/config-conventional": "^12.1.4",
+    "@commitlint/cli": "^13.1.0",
+    "@commitlint/config-conventional": "^13.1.0",
     "@juggle/resize-observer": "^3.3.1",
     "@semantic-release/changelog": "^5.0.1",
     "@semantic-release/commit-analyzer": "^8.0.1",
@@ -83,34 +84,36 @@
     "@semantic-release/release-notes-generator": "^9.0.3",
     "@types/jest": "^26.0.24",
     "@types/smoothscroll-polyfill": "^0.3.1",
-    "@typescript-eslint/eslint-plugin": "^4.28.3",
-    "@typescript-eslint/parser": "^4.28.3",
+    "@typescript-eslint/eslint-plugin": "^4.28.5",
+    "@typescript-eslint/parser": "^4.28.5",
     "@webcomponents/custom-elements": "^1.4.3",
     "@webcomponents/webcomponents-platform": "^1.0.1",
-    "browser-sync": "^2.27.4",
-    "chokidar-cli": "^2.1.0",
+    "browser-sync": "^2.27.5",
+    "chokidar-cli": "^3.0.0",
+    "clean-css-cli": "^5.3.0",
     "concurrently": "^6.2.0",
     "copyfiles": "^2.4.1",
-    "eslint": "^7.30.0",
+    "eslint": "^7.31.0",
     "eslint-plugin-editorconfig": "^3.0.2",
     "eslint-plugin-import": "^2.23.4",
     "eslint-plugin-sonarjs": "^0.9.1",
     "foreach-cli": "^1.8.1",
+    "html-minifier": "^4.0.0",
     "husky": "^7.0.1",
     "intersection-observer": "^0.12.0",
     "jest": "^27.0.6",
     "less": "^4.1.1",
     "lintspaces-cli": "^0.7.1",
-    "postcss": "^8.3.5",
+    "postcss": "^8.3.6",
     "promise-polyfill": "^8.2.0",
     "rimraf": "^3.0.2",
     "semantic-release": "^17.4.4",
     "smoothscroll-polyfill": "^0.4.4",
     "stylelint": "^13.13.1",
-    "ts-jest": "^27.0.3",
-    "ts-loader": "^9.2.3",
+    "ts-jest": "^27.0.4",
+    "ts-loader": "^9.2.4",
     "typescript": "4.3.5",
-    "webpack": "^5.44.0",
+    "webpack": "^5.47.1",
     "webpack-cli": "^4.7.2"
   },
   "browserslist": [
@@ -146,4 +149,4 @@
       "email": "yadamska@exadel.com"
     }
   ]
-}
+}
\ No newline at end of file
diff --git a/pages/views/_layouts/basic.njk b/pages/views/_layouts/basic.njk
index 007351b84..da231ed28 100644
--- a/pages/views/_layouts/basic.njk
+++ b/pages/views/_layouts/basic.njk
@@ -16,7 +16,7 @@
   </script>
 
   <link rel="icon" type="image/png" href="{{ '/assets/logo-64x64.png' | url }}" />
-  <link type="text/css" rel="stylesheet" href="{{ '/bundles/localdev.css' | url }} " />
+  <link type="text/css" rel="stylesheet" href="{{ '/bundles/localdev.css' | url }}" />
 
   {% block extra_head %}{% endblock %}
 {% endblock %}
@@ -40,5 +40,7 @@
     {% include 'footer.njk' %}
   </footer>
 
+  {% block extra_body %}{% endblock %}
+
   <script src="{{ '/bundles/localdev.js' | url }}"></script>
 {% endblock %}
diff --git a/pages/views/_layouts/popup.njk b/pages/views/_layouts/popup.njk
new file mode 100644
index 000000000..6ac82b51d
--- /dev/null
+++ b/pages/views/_layouts/popup.njk
@@ -0,0 +1,28 @@
+{% extends "./basic.njk" %}
+
+{% block extra_body %}
+<esl-popup id="popup1" position="top">
+  Exadel Smart Library Popup content
+  <span class="esl-popup-arrow"></span>
+</esl-popup>
+<esl-popup id="popup2" position="bottom">
+  Exadel Smart Library Popup content
+  <span class="esl-popup-arrow"></span>
+</esl-popup>
+<esl-popup id="popup3" position="left">
+  <p>Exadel</p>
+  <p>Smart</p>
+  <p>Library</p>
+  <p>Popup</p>
+  <p>content</p>
+  <span class="esl-popup-arrow"></span>
+</esl-popup>
+<esl-popup id="popup4" position="right">
+  <p>Exadel</p>
+  <p>Smart</p>
+  <p>Library</p>
+  <p>Popup</p>
+  <p>content</p>
+  <span class="esl-popup-arrow"></span>
+</esl-popup>
+{% endblock %}
diff --git a/pages/views/drafts/esl-popup.njk b/pages/views/drafts/esl-popup.njk
new file mode 100644
index 000000000..d6faa10b6
--- /dev/null
+++ b/pages/views/drafts/esl-popup.njk
@@ -0,0 +1,47 @@
+---
+layout: popup
+title: ESL Popup component
+name: Esl Popup component
+containerCls: container
+tags: drafts
+---
+
+<section class="row justify-content-between">
+  <esl-trigger target="#popup1" mode="toggle" class="btn btn-primary">Top</esl-trigger>
+  <esl-trigger target="#popup1" mode="toggle" class="btn btn-primary">Top</esl-trigger>
+  <esl-trigger target="#popup1" mode="toggle" class="btn btn-primary">Top</esl-trigger>
+</section>
+
+<section class="row justify-content-between">
+  <esl-trigger target="#popup3" mode="toggle" class="btn btn-primary">Left</esl-trigger>
+  <esl-trigger target="#popup4" mode="toggle" class="btn btn-primary">Right</esl-trigger>
+</section>
+
+<section class="row justify-content-between">
+  <esl-trigger target="#popup2" mode="toggle" class="btn btn-primary">Bottom</esl-trigger>
+  <esl-trigger target="#popup2" mode="toggle" class="btn btn-primary">Bottom</esl-trigger>
+  <esl-trigger target="#popup2" mode="toggle" class="btn btn-primary">Bottom</esl-trigger>
+</section>
+
+<section class="row">
+  <div id="a" style="margin: 599px 0; height: 800px; overflow-y: auto; width: 100%; background-color: azure;">
+    <p>Lorem</p><p>ipsum</p><p>dot</p><p>color</p>
+    <esl-trigger target="#popup1" mode="toggle" class="btn btn-primary">Trigger B</esl-trigger>
+    <div id="b" style="margin: 599px 0; height: 650px; overflow-y: auto; width: 100%; background-color: beige;">
+      <p>Lorem</p><p>ipsum</p><p>dot</p><p>color</p>
+      <esl-trigger target="#popup1" mode="toggle" class="btn btn-secondary">Trigger B</esl-trigger>
+      <div id="c" style="margin: 599px 0; height: 500px; overflow-y: auto; width: 100%; background-color: bisque;">
+        <p>Lorem</p><p>ipsum</p><p>dot</p><p>color</p>
+        <esl-trigger target="#popup1" mode="toggle" class="btn btn-primary">Trigger C</esl-trigger>
+        <div id="d" style="margin: 599px 0; height: 350px; overflow-y: auto; width: 100%; background-color: tan;">
+          <p>Lorem</p><p>ipsum</p><p>dot</p><p>color</p>
+          <esl-trigger target="#popup1" mode="toggle" class="btn btn-primary">Trigger D (top)</esl-trigger>
+          <esl-trigger target="#popup2" mode="toggle" class="btn btn-primary">Trigger D (bottom)</esl-trigger>
+          <esl-trigger target="#popup3" mode="toggle" class="btn btn-secondary">Trigger D (left)</esl-trigger>
+          <esl-trigger target="#popup4" mode="toggle" class="btn btn-secondary">Trigger D (right)</esl-trigger>
+          <esl-popup id="popup9" position="top">Exadel Smart Library Popup content<span class="esl-popup-arrow"></span></esl-popup>
+        </div>
+      </div>
+    </div>
+  </div>
+</section>
diff --git a/pages/webpack.config.js b/pages/webpack.config.js
index 2020bede0..874f4ef73 100644
--- a/pages/webpack.config.js
+++ b/pages/webpack.config.js
@@ -30,3 +30,4 @@ module.exports = {
     filename: '[name].js'
   }
 };
+
diff --git a/src/modules/draft/esl-carousel/core/view/esl-carousel-view.ts b/src/modules/draft/esl-carousel/core/view/esl-carousel-view.ts
index fcc046eba..81b8df70b 100644
--- a/src/modules/draft/esl-carousel/core/view/esl-carousel-view.ts
+++ b/src/modules/draft/esl-carousel/core/view/esl-carousel-view.ts
@@ -21,7 +21,7 @@ export abstract class ESLCarouselView {
 export type ESLCarouselViewConstructor = new(carousel: ESLCarousel) => ESLCarouselView;
 
 let eslRegistryInstance: ESLCarouselViewRegistry | null = null;
-export class ESLCarouselViewRegistry extends Observable {
+export class ESLCarouselViewRegistry extends Observable<(name: string, view: ESLCarouselViewConstructor) => void> {
   private registry = new Map<string, ESLCarouselViewConstructor>();
 
   public static get instance() {
diff --git a/src/modules/esl-footnotes/core/esl-footnotes.less b/src/modules/esl-footnotes/core/esl-footnotes.less
index 906a65ab3..04ae93294 100644
--- a/src/modules/esl-footnotes/core/esl-footnotes.less
+++ b/src/modules/esl-footnotes/core/esl-footnotes.less
@@ -25,7 +25,7 @@ esl-footnotes {
     cursor: pointer;
 
     &::before {
-      content: '[\2BC5]';
+      content: '[ ^ ]';
     }
   }
 }
diff --git a/src/modules/esl-footnotes/core/esl-footnotes.ts b/src/modules/esl-footnotes/core/esl-footnotes.ts
index 6e4adcba6..609a71280 100644
--- a/src/modules/esl-footnotes/core/esl-footnotes.ts
+++ b/src/modules/esl-footnotes/core/esl-footnotes.ts
@@ -24,7 +24,7 @@ export class ESLFootnotes extends ESLBaseElement {
     super.connectedCallback();
 
     this.bindEvents();
-    EventUtils.dispatch(this, `${ESLNote.eventNs}:request`);
+    this._sendRequestToNote();
   }
 
   protected disconnectedCallback() {
@@ -102,4 +102,8 @@ export class ESLFootnotes extends ESLBaseElement {
       note?.activate();
     }
   }
+
+  protected _sendRequestToNote() {
+    EventUtils.dispatch(this, `${ESLNote.eventNs}:request`);
+  }
 }
diff --git a/src/modules/esl-image/core/esl-image.ts b/src/modules/esl-image/core/esl-image.ts
index 54601b4ea..f0b47ef77 100644
--- a/src/modules/esl-image/core/esl-image.ts
+++ b/src/modules/esl-image/core/esl-image.ts
@@ -99,7 +99,7 @@ export class ESLImage extends ESLBaseElement {
         this.updateA11y();
         break;
       case 'data-src':
-        this.srcRules = ESLMediaRuleList.parse<string>(newVal, ESLMediaRuleList.STRING_PARSER);
+        this.srcRules = ESLMediaRuleList.parse(newVal);
         this.refresh();
         break;
       case 'data-src-base':
@@ -116,7 +116,7 @@ export class ESLImage extends ESLBaseElement {
 
   public get srcRules() {
     if (!this._srcRules) {
-      this.srcRules = ESLMediaRuleList.parse<string>(this.src, ESLMediaRuleList.STRING_PARSER);
+      this.srcRules = ESLMediaRuleList.parse(this.src);
     }
     return this._srcRules;
   }
diff --git a/src/modules/esl-media-query/README.md b/src/modules/esl-media-query/README.md
index b6fee97f6..340712c66 100644
--- a/src/modules/esl-media-query/README.md
+++ b/src/modules/esl-media-query/README.md
@@ -1,11 +1,13 @@
 # [ESL](../../../README.md) Media Query Utils
 
-Version: *1.1.0*
+Version: *2.0.0*
 
 Authors: *Alexey Stsefanovich (ala'n)*, *Yuliya Adamskaya*, *Julia Murashko*
 
 ESL Media Query is an extended browser MediaQueryList object and related utils.
 In addition to native Media Features, ESL Media Query allows special *shortcuts* to simplify syntax and query usage.
+ESLMediaQuery is more tolerant to logical operations and allows using of multiple 'not' operators or 
+having extra conditions inside the query (`all and all` still a valid condition).
 
 ### Supported ESL Media Query features
 
@@ -14,29 +16,88 @@ In addition to native Media Features, ESL Media Query allows special *shortcuts*
 - Shortcuts for DPR (Device Pixel Ratio) detection  
   Syntax `@x<dpr_value>` (e.g. `@x1`, `@x2`, `@x1.5`).
 - Can block queries with DPR value greater than 1 for PageSpeedBots.
-- Screen breakpoints shortcuts  
-  (e.g. `@XS`, `@md`, `@+LG`, `@-MD`)  
-  Supports breakpoints declared in [ESLMediaBreakpoints](#eslmediabreakpoints) registry.   
+- Screen breakpoints shortcuts (e.g. `@XS`, `@md`, `@+LG`, `@-MD`)  
+  Supports breakpoints declared in [ESLScreenBreakpoints](#eslscreenbreakpoints) registry.   
   Accepts modifiers like `+` for upper breakpoints and `-` for lower breakpoints.
-- `@MOBILE` and `@DESKTOP` shortcuts for device detection.
+- Device and browser detection shortcuts that are registered through [ESLEnvShortcuts](#eslenvshortcuts) registry.
+  The list of predefined environment shortcuts is the following:
+  - `@BOT` - detects search/pagespeed bots
+  - `@MOBILE` - detects mobile devices 
+  - `@DESKTOP` - detects desktops
+  - `@ANDROID` - detects Android devices
+  - `@IOS` - detects iOS devices  
+  - `@TOUCH` - cross-browser touch support detection
+  - `@IE` - detects InternetExplorer 11
+  - `@EDGE` - detects legacy Edge browser (<18) based on EdgeHTML engine
+  - `@GECKO` - detects Gecko engine based browsers (e.g. Firefox)
+  - `@BLINK` - detects Blink engine based browsers (e.g. Google Chrome, Opera)
+  - `@SAFARI` - detects safari browsers
+  - `@SAFARI-IOS` - detects mobile safari browsers  
+- `or` operation alias in addition to `,` (`@xs or @mobile` is the same as `@xs, @mobile`)
+- Multiple `not` operators, that is not sensitive to position
+  (e.g. `not @ie`, `not @xs and not @mobile`)
+---
+
+### ESLScreenBreakpoints
+
+ESLScreenBreakpoints is a custom screen breakpoints registry. It allows defining a named screen width range.
+
+Use `ESLScreenBreakpoints.add` static method to change default or define your own Screen Breakpoint.
+
+You can also use `ESLScreenBreakpoints.remove` to exclude breakpoint shortcut from registry.
+
+You can get all available screen breakpoints through `ESLScreenBreakpoints.names` property or access full breakpoint 
+definition through `ESLScreenBreakpoints.get`.
 
 ---
 
-### ESLMediaBreakpoints
+### ESLScreenDPR
 
-ESLMediaBreakpoints is a custom screen breakpoints registry. It allows defining a named screen width range.
+Additional preprocessor to provide DPR shortcuts.
 
-Use `ESLMediaBreakpoints.addCustomBreakpoint` static method to change default or define your own Screen Breakpoint.
+Use `ESLScreenDPR.ignoreBotsDpr` marker to enable DPR ignoring for PageSpeed Bots.
 
 ---
 
-### ESLMediaQuery 
+### ESLEnvShortcuts
 
-ESLMediaQuery is an extended MediaQueryList wrapper that supports native api and accepts extended ESL Media Query features.
+ESLEnvShortcuts is a simple registry for a static shortcuts to describe environment related conditions
 
-*Note: Use `ESLMediaQuery.ignoreBotsDpr` marker to enable DPR ignoring for PageSpeed Bots.*
+An additional shortcuts can be added to the registry through `ESLEnvShortcuts.add` method
+ESLEnvShortcuts allows adding boolean result that will be converted to `all` / `not all` query conditions or setup result as native MediaQuery string. 
 
---- 
+Environment shortcuts can be removed with `ESLEnvShortcuts.remove` method.
+
+---
+
+### ESLMediaQuery 
+
+ESLMediaQuery is a central API to create an extended Media Query conditions.
+
+ - Use `ESLMediaQuery.for('query')` to create ESLMediaQuery condition.  
+   Note: method `ESLMediaQuery.for` is memoized so multiple calls with the same query will return the same query 
+   instance as the result, which also means you might not need store or optimize ESLMediaQuery.for calls from your side.
+   
+ - Use `ESLMediaQuery.from('query')` to create new ESLMediaQuery condition instance. 
+   Not memoized in opposite to `ESLMediaQuery.for`;
+
+ - Use `ESLMediaQuery.use` to register custom query preprocessor. 
+   Note: `ESLScreenBreakpoints`, `ESLScreenDPR`, `ESLEnvShortcuts` are three preprocessors that are registered by default.
+   
+Note: ESLMediaQuery has no real instances and represents `IMediaQueryConditionInterface` interface.
+`IMediaQueryConditionInterface` is implemented by 4 types of inner conditions:
+  - `MediaQueryConstCondition` with only two possible instances `ALL` and `NOT_ALL`
+  - `MediaQueryCondition` - simple wrapper around native MediaQueryList object
+  - `MediaQueryConjunction` - `and` condition container
+  - `MediaQueryDisjunction` - `or' condition container
+
+The `ESLMediaQuery`(`IMediaQueryConditionInterface`) instances provide the following set of properties and methods:
+  - `matches` - boolean getter that returns if the current environment configuration is acceptable for current query condition
+  - `addListener(cb)` - to subscribe on condition state changes
+  - `removeListener(cb)` - to unsubscribe from condition state changes
+  - `optimize` - inner method to rebuild condition tree, and provide static and logic expression optimization.
+
+---
 
 ### ESLMediaRule
 
diff --git a/src/modules/esl-media-query/core.ts b/src/modules/esl-media-query/core.ts
index 833054cc2..190884db2 100644
--- a/src/modules/esl-media-query/core.ts
+++ b/src/modules/esl-media-query/core.ts
@@ -1,4 +1,8 @@
-export * from './core/esl-media-breakpoints';
+export * from './core/common/env-shortcuts';
+export * from './core/common/screen-dpr';
+export * from './core/common/screen-breakpoint';
+
 export * from './core/esl-media-query';
+
 export * from './core/esl-media-rule';
 export * from './core/esl-media-rule-list';
diff --git a/src/modules/esl-media-query/core/common/env-shortcuts.ts b/src/modules/esl-media-query/core/common/env-shortcuts.ts
new file mode 100644
index 000000000..8afc7409c
--- /dev/null
+++ b/src/modules/esl-media-query/core/common/env-shortcuts.ts
@@ -0,0 +1,54 @@
+import {DeviceDetector} from '../../../esl-utils/environment/device-detector';
+import {ExportNs} from '../../../esl-utils/environment/export-ns';
+
+const shortcuts = new Map<string, string | boolean>();
+
+/**
+ * Static shortcuts preprocessor. Used to store device related shortcuts.
+ * @author Alexey Stsefanovich (ala'n)
+ *
+ * @implements IMediaQueryPreprocessor statically
+ */
+@ExportNs('EnvShortcuts')
+export class ESLEnvShortcuts {
+  // For debug purposes
+  private static readonly _shortcuts = shortcuts;
+
+  /**
+   * Add mapping
+   * @param shortcut - term to find in query
+   * @param value - media query string or boolean result (that represents `all` or `not all` conditions)
+   */
+  public static add(shortcut: string, value: string | boolean) {
+    if (!['boolean', 'string'].includes(typeof value)) value = false;
+    return shortcuts.set(shortcut.toLowerCase(), value);
+  }
+
+  /** Remove mapping for passed shortcut term */
+  public static remove(shortcut: string) {
+    return shortcuts.delete(shortcut.toLowerCase());
+  }
+
+  /** Replaces shortcut to registered result */
+  public static process(match: string) {
+    if (shortcuts.has(match)) return shortcuts.get(match);
+  }
+}
+
+// Touch check
+ESLEnvShortcuts.add('touch', DeviceDetector.isTouchDevice);
+
+// Basic device type shortcuts
+ESLEnvShortcuts.add('bot', DeviceDetector.isBot);
+ESLEnvShortcuts.add('mobile', DeviceDetector.isMobile);
+ESLEnvShortcuts.add('desktop', !DeviceDetector.isMobile);
+ESLEnvShortcuts.add('android', DeviceDetector.isAndroid);
+ESLEnvShortcuts.add('ios', DeviceDetector.isMobileIOS);
+
+// Basic browser shortcuts
+ESLEnvShortcuts.add('ie', DeviceDetector.isIE);
+ESLEnvShortcuts.add('edge', DeviceDetector.isEdgeHTML);
+ESLEnvShortcuts.add('gecko', DeviceDetector.isGecko);
+ESLEnvShortcuts.add('blink', DeviceDetector.isBlink);
+ESLEnvShortcuts.add('safari', DeviceDetector.isSafari);
+ESLEnvShortcuts.add('safari-ios', DeviceDetector.isMobileSafari);
diff --git a/src/modules/esl-media-query/core/common/screen-breakpoint.ts b/src/modules/esl-media-query/core/common/screen-breakpoint.ts
new file mode 100644
index 000000000..fc6a30307
--- /dev/null
+++ b/src/modules/esl-media-query/core/common/screen-breakpoint.ts
@@ -0,0 +1,90 @@
+import {ExportNs} from '../../../esl-utils/environment/export-ns';
+
+const registry = new Map<string, ESLScreenBreakpoint>();
+
+/**
+ * ESL Screen Breakpoints registry
+ * @author Yuliya Adamskaya, Alexey Stsefanovich (ala'n)
+ *
+ * Screen Breakpoint registry is used to provide custom breakpoints for {@link ESLMediaQuery}
+ */
+@ExportNs('ScreenBreakpoint')
+export abstract class ESLScreenBreakpoints {
+  protected static readonly BP_REGEXP = /^([+-]?)([a-z]+)/i;
+  protected static readonly BP_NAME_REGEXP = /^[a-z]+/i;
+
+  /**
+   * Add or replace breakpoint shortcut that can be used inside ESLMediaQuery
+   * @param name - name of shortcut
+   * @param minWidth - min width for breakpoint
+   * @param maxWidth - max width for breakpoint
+   */
+  public static add(name: string, minWidth: number, maxWidth: number): ESLScreenBreakpoint | undefined {
+    name = name.toLowerCase();
+    if (ESLScreenBreakpoints.BP_NAME_REGEXP.test(name)) {
+      const current = registry.get(name);
+      registry.set(name, new ESLScreenBreakpoint(name, minWidth, maxWidth));
+      return current;
+    }
+    throw new Error('The shortcut should consist only of Latin characters and be at least one character long.');
+  }
+
+  /** Removes screen breakpoint */
+  public static remove(name: string): boolean {
+    return registry.delete(name.toLowerCase());
+  }
+
+  /** @returns known breakpoint shortcut instance */
+  public static get(name: string) {
+    return registry.get((name || '').toLowerCase());
+  }
+
+  /** All available breakpoints names */
+  public static get names() {
+    const keys: string[] = [];
+    registry.forEach((value, key) => keys.push(key));
+    return keys;
+  }
+
+  /** @returns breakpoints shortcut replacement */
+  public static process(term: string) {
+    const [, sign, bp] = term.match(ESLScreenBreakpoints.BP_REGEXP) || [];
+    const shortcut = ESLScreenBreakpoints.get(bp);
+    if (!shortcut) return;
+    if (sign === '+') return shortcut.mediaQueryGE;
+    if (sign === '-') return shortcut.mediaQueryLE;
+    return shortcut.mediaQuery;
+  }
+}
+
+/** ESL Screen Breakpoint description */
+export class ESLScreenBreakpoint {
+  constructor(
+    public readonly name: string,
+    public readonly min: number,
+    public readonly max: number
+  ) {}
+
+  public get mediaQuery(): string {
+    return `(min-width: ${this.min}px) and (max-width: ${this.max}px)`;
+  }
+
+  public get mediaQueryGE(): string {
+    return `(min-width: ${this.min}px)`;
+  }
+
+  public get mediaQueryLE(): string {
+    return `(max-width: ${this.max}px)`;
+  }
+
+  public toString() {
+    return `[${this.name}]: ${this.min} to ${this.max}`;
+  }
+}
+
+// Defaults
+ESLScreenBreakpoints.add('xs', 1, 767);
+ESLScreenBreakpoints.add('sm', 768, 991);
+ESLScreenBreakpoints.add('md', 992, 1199);
+ESLScreenBreakpoints.add('lg', 1200, 1599);
+ESLScreenBreakpoints.add('xl', 1600, 999999);
diff --git a/src/modules/esl-media-query/core/common/screen-dpr.ts b/src/modules/esl-media-query/core/common/screen-dpr.ts
new file mode 100644
index 000000000..c3e9d82a7
--- /dev/null
+++ b/src/modules/esl-media-query/core/common/screen-dpr.ts
@@ -0,0 +1,29 @@
+import {ExportNs} from '../../../esl-utils/environment/export-ns';
+import {DeviceDetector} from '../../../esl-utils/environment/device-detector';
+
+/**
+ * DPR preprocessor. Used to replace DPR shortcuts.
+ * @author Alexey Stsefanovich (ala'n)
+ *
+ * @implements IMediaQueryPreprocessor statically
+ */
+@ExportNs('ScreenDPR')
+export class ESLScreenDPR {
+  protected static readonly VALUE_REGEXP = /(\d(\.\d)?)x/;
+
+  /** Option to exclude dpr > 2 for bots */
+  public static ignoreBotsDpr = false;
+
+  public static toDPI(dpr: number) {
+    return (96 * dpr).toFixed(1);
+  }
+
+  public static process(match: string) {
+    if (!ESLScreenDPR.VALUE_REGEXP.test(match)) return;
+    const dpr = parseFloat(match);
+    if (dpr < 0 || isNaN(dpr)) return;
+    if (ESLScreenDPR.ignoreBotsDpr && DeviceDetector.isBot && dpr > 1) return 'not all';
+    if (DeviceDetector.isSafari) return `(-webkit-min-device-pixel-ratio: ${dpr})`;
+    return `(min-resolution: ${ESLScreenDPR.toDPI(dpr)}dpi)`;
+  }
+}
diff --git a/src/modules/esl-media-query/core/conditions/media-query-base.ts b/src/modules/esl-media-query/core/conditions/media-query-base.ts
new file mode 100644
index 000000000..38c1eb730
--- /dev/null
+++ b/src/modules/esl-media-query/core/conditions/media-query-base.ts
@@ -0,0 +1,40 @@
+export interface IMediaQueryCondition {
+  /** @returns true if current environment satisfies query */
+  matches: boolean;
+  /** Attach listener to wrapped media query list */
+  addListener(cb: VoidFunction): void;
+  /** Detach listener from wrapped media query list */
+  removeListener(cb: VoidFunction): void;
+  /** Optimize condition with nested hierarchy */
+  optimize(): IMediaQueryCondition;
+}
+
+/**
+ * Const media condition implementation
+ * @author Alexey Stsefanovich (ala'n)
+ *
+ * Ignores listeners always return the same result.
+ * Have only two instances: {@link ALL} and {@link NOT_ALL}
+ */
+class MediaQueryConstCondition implements IMediaQueryCondition {
+  constructor(private readonly _matches: boolean) {}
+
+  public get matches() {
+    return this._matches;
+  }
+
+  public addListener(cb: VoidFunction) {}
+  public removeListener(cb: VoidFunction) {}
+
+  public optimize() { return this; }
+
+  public toString() { return this._matches ? 'all' : 'not all'; }
+
+  /** Compare const media condition with the passed query instance or string */
+  public eq(val: IMediaQueryCondition | string): boolean {
+    return val.toString().trim() === this.toString();
+  }
+}
+
+export const ALL = new MediaQueryConstCondition(true);
+export const NOT_ALL = new MediaQueryConstCondition(false);
diff --git a/src/modules/esl-media-query/core/conditions/media-query-condition.ts b/src/modules/esl-media-query/core/conditions/media-query-condition.ts
new file mode 100644
index 000000000..374b3962d
--- /dev/null
+++ b/src/modules/esl-media-query/core/conditions/media-query-condition.ts
@@ -0,0 +1,51 @@
+import {ALL, NOT_ALL} from './media-query-base';
+import type {IMediaQueryCondition} from './media-query-base';
+
+/**
+ * Simple media condition implementation
+ * @author Alexey Stsefanovich (ala'n)
+ *
+ * Wraps matchMedia instance
+ */
+export class MediaQueryCondition implements IMediaQueryCondition {
+  protected readonly _inverted: boolean;
+  protected readonly _mq: MediaQueryList;
+
+  constructor(query: string, inverted = false) {
+    this._inverted = inverted;
+    this._mq = matchMedia(query.trim());
+  }
+
+  public get matches() {
+    return this._inverted ? !this._mq.matches : this._mq.matches;
+  }
+
+  public addListener(listener: VoidFunction) {
+    if (typeof this._mq.addEventListener === 'function') {
+      this._mq.addEventListener('change', listener);
+    } else {
+      this._mq.addListener(listener);
+    }
+  }
+  public removeListener(listener: VoidFunction) {
+    if (typeof this._mq.removeEventListener === 'function') {
+      this._mq.removeEventListener('change', listener);
+    } else {
+      this._mq.removeListener(listener);
+    }
+  }
+
+  /** Optimize query. Can simplify query to {@link MediaQueryConstCondition} */
+  public optimize(): IMediaQueryCondition {
+    if (ALL.eq(this)) return this._inverted ? NOT_ALL : ALL;
+    if (NOT_ALL.eq(this)) return this._inverted ? ALL : NOT_ALL;
+    return this;
+  }
+
+  public toString() {
+    const query = this._mq.media;
+    const inverted = this._inverted;
+    const complex = inverted && /\)[\s\w]+\(/.test(query);
+    return (inverted ? 'not ' : '') + (complex ? `(${query})` : query);
+  }
+}
diff --git a/src/modules/esl-media-query/core/conditions/media-query-containers.ts b/src/modules/esl-media-query/core/conditions/media-query-containers.ts
new file mode 100644
index 000000000..3e7f6517f
--- /dev/null
+++ b/src/modules/esl-media-query/core/conditions/media-query-containers.ts
@@ -0,0 +1,83 @@
+import {Observable} from '../../../esl-utils/abstract/observable';
+import {ALL, NOT_ALL} from './media-query-base';
+
+import type {IMediaQueryCondition} from './media-query-base';
+
+/**
+ * Abstract multiple media conditions container
+ * @author Alexey Stsefanovich (ala'n)
+ *
+ * Observe all child items. Dispatch changes when the whole condition result is changed
+ */
+class MediaQueryContainer extends Observable<(matches: boolean) => void> implements IMediaQueryCondition {
+  private _matches: boolean;
+
+  constructor(protected readonly items: IMediaQueryCondition[] = []) {
+    super();
+    this._matches = this.matches;
+    this._onChildChange = this._onChildChange.bind(this);
+  }
+
+  public addListener(listener: (matches: boolean) => void) {
+    super.addListener(listener);
+    if (this._listeners.size > 1) return;
+    this.items.forEach((item) => item.addListener(this._onChildChange));
+  }
+  public removeListener(listener: (matches: boolean) => void) {
+    super.removeListener(listener);
+    if (this._listeners.size) return;
+    this.items.forEach((item) => item.removeListener(this._onChildChange));
+  }
+
+  public get matches() { return false; }
+
+  /** Exclude const conditions. Unwrap empty or trivial (with one item) containers */
+  public optimize(): IMediaQueryCondition { return this; }
+
+  /** Handle query change and dispatch it on top level in case result value is changed */
+  protected _onChildChange() {
+    const {matches} = this;
+    if (this._matches ===  matches) return;
+    this.fire(this._matches = matches);
+  }
+}
+
+/** Conjunction (AND) group of media conditions */
+export class MediaQueryConjunction extends MediaQueryContainer {
+  public get matches() {
+    return this.items.every((item) => item.matches);
+  }
+
+  public optimize(): IMediaQueryCondition {
+    const optimizedItems = this.items.map((item) => item.optimize());
+    if (optimizedItems.some((item) => NOT_ALL.eq(item))) return NOT_ALL;
+    const items = optimizedItems.filter((item) => !ALL.eq(item));
+    if (items.length === 0) return ALL;
+    if (items.length === 1) return items[0];
+    return new MediaQueryConjunction(items);
+  }
+
+  public toString() {
+    return this.items.join(' and ');
+  }
+}
+
+/** Disjunction (OR) group of media conditions */
+export class MediaQueryDisjunction extends MediaQueryContainer {
+  public get matches() {
+    return this.items.some((item) => item.matches);
+  }
+
+  public optimize(): IMediaQueryCondition {
+    const optimizedItems = this.items.map((item) => item.optimize());
+    if (optimizedItems.some((item) => ALL.eq(item))) return ALL;
+    const items = optimizedItems.filter((item) => !NOT_ALL.eq(item));
+    if (items.length === 0) return NOT_ALL;
+    if (items.length === 1) return items[0];
+    return new MediaQueryDisjunction(items);
+  }
+
+  public toString(pretty = false) {
+    return this.items.join(pretty ? ' or ' : ', ');
+  }
+}
diff --git a/src/modules/esl-media-query/core/esl-media-breakpoints.ts b/src/modules/esl-media-query/core/esl-media-breakpoints.ts
deleted file mode 100644
index 92690b734..000000000
--- a/src/modules/esl-media-query/core/esl-media-breakpoints.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-import {ExportNs} from '../../esl-utils/environment/export-ns';
-
-/**
- * ESL Query Breakpoint Registry
- * @author Yuliya Adamskaya
- *
- * Breakpoint Registry is used to provide custom breakpoints for ESL Query
- */
-class ScreenBreakpoint {
-  public min: number;
-  public max: number;
-
-  constructor(min: number, max: number) {
-    this.min = min;
-    this.max = max;
-  }
-
-  get mediaQuery(): string {
-    return `(min-width: ${this.min}px) and (max-width: ${this.max}px)`;
-  }
-
-  get mediaQueryGE(): string {
-    return `(min-width: ${this.min}px)`;
-  }
-
-  get mediaQueryLE(): string {
-    return `(max-width: ${this.max}px)`;
-  }
-}
-
-interface BreakpointsMapping {
-  [key: string]: ScreenBreakpoint
-}
-
-// Default breakpoints provided in registry
-const registry: BreakpointsMapping = {
-  xs: new ScreenBreakpoint(1, 767),
-  sm: new ScreenBreakpoint(768, 991),
-  md: new ScreenBreakpoint(992, 1199),
-  lg: new ScreenBreakpoint(1200, 1599),
-  xl: new ScreenBreakpoint(1600, 999999)
-};
-
-const BP_NAME_REGEXP = /^[a-z]+/i;
-
-@ExportNs('MediaBreakpoints')
-export abstract class ESLMediaBreakpoints {
-  /**
-   * Add or replace breakpoint shortcut that could be used inside of ESLMediaQuery
-   * @param name - name of shortcut
-   * @param minWidth - min width for breakpoint
-   * @param maxWidth - max width for breakpoint
-   */
-  public static addCustomBreakpoint(name: string, minWidth: number, maxWidth: number): ScreenBreakpoint {
-    name = name.toLowerCase();
-    if (BP_NAME_REGEXP.test(name)) {
-      const current = registry[name];
-      registry[name] = new ScreenBreakpoint(minWidth, maxWidth);
-      return current;
-    }
-    throw new Error('Shortcut should consist only from Latin characters. Length should be at least one character.');
-  }
-
-  /**
-   * @return known breakpoint shortcut instance
-   */
-  public static getBreakpoint(name: string): ScreenBreakpoint {
-    return registry[(name || '').toLowerCase()];
-  }
-
-  /**
-   * @returns all available breakpoints shortcuts
-   */
-  public static getAllBreakpointsNames() {
-    return Object.keys(registry);
-  }
-
-  /**
-   * Replaces known breakpoints shortcuts to the real media queries
-   * @param query - original query string
-   */
-  public static apply(query: string) {
-    const breakpoints = Object.keys(registry);
-    const breakpointRegex = new RegExp(`@([+-]?)(${breakpoints.join('|')})\\b`, 'ig');
-
-    return query.replace(breakpointRegex, (match, sign, bp) => {
-      const shortcut = ESLMediaBreakpoints.getBreakpoint(bp);
-      switch (sign) {
-        case '+':
-          return shortcut.mediaQueryGE;
-        case '-':
-          return shortcut.mediaQueryLE;
-        default:
-          return shortcut.mediaQuery;
-      }
-    });
-  }
-}
diff --git a/src/modules/esl-media-query/core/esl-media-query.ts b/src/modules/esl-media-query/core/esl-media-query.ts
index d41f07efd..b625964db 100644
--- a/src/modules/esl-media-query/core/esl-media-query.ts
+++ b/src/modules/esl-media-query/core/esl-media-query.ts
@@ -1,105 +1,105 @@
 import {memoize} from '../../esl-utils/decorators/memoize';
-import {DeviceDetector} from '../../esl-utils/environment/device-detector';
 import {ExportNs} from '../../esl-utils/environment/export-ns';
 
-import {ESLMediaBreakpoints} from './esl-media-breakpoints';
+import {ESLScreenDPR} from './common/screen-dpr';
+import {ESLScreenBreakpoints} from './common/screen-breakpoint';
+import {ESLEnvShortcuts} from './common/env-shortcuts';
+
+import {ALL, NOT_ALL} from './conditions/media-query-base';
+import {MediaQueryCondition} from './conditions/media-query-condition';
+import {MediaQueryConjunction, MediaQueryDisjunction} from './conditions/media-query-containers';
+
+import type {IMediaQueryCondition} from './conditions/media-query-base';
+
+export interface IMediaQueryPreprocessor {
+  process: (match: string) => string | boolean | undefined;
+}
 
 /**
  * ESL Media Query
- * Provides special media condition syntax - ESLQuery
+ * Provides special media condition syntax - ESLMediaQuery
  * @author Alexey Stsefanovich (ala'n), Yuliya Adamskaya, Natallia Harshunova
  *
- * Helper class that extends MediaQueryList class
+ * Utility to support extended MediaQuery features
  * Supports
- * - CSS query matching check
+ * - CSS MediaQuery matching check
  * - DPR display queries (@x1 | @x2 | @x3)
- * - Screen default sizes shortcuts @[-|+](XS|SM|MD|LG|XL)
+ * - Registered screen default sizes (breakpoints) shortcuts @[-|+](XS|SM|MD|LG|XL)
+ * - Device and browser shortcuts (@MOBILE|@DESKTOP|@ie)
+ * - Custom static shortcuts and custom query preprocessors
+ * - `not` logic operation (can have multiple not operators before any term of the query)
+ * - `or` or `,` logical operator (have a lowest priority)
  * - Query matching change listeners
- * - Mobile / full browser detection (@MOBILE|@DESKTOP)
- * - Exclude upper DPRs for bots
+ *
+ * Building query process:
+ *
+ * [Building query logical tree] -> [preprocess nodes queries] -> [building native MediaQueryList nodes] -> [query tree optimization]
  */
 @ExportNs('MediaQuery')
-export class ESLMediaQuery {
-  static get BreakpointRegistry() {
-    return ESLMediaBreakpoints;
+export abstract class ESLMediaQuery implements IMediaQueryCondition {
+  /** Always true condition */
+  public static readonly ALL: IMediaQueryCondition = ALL;
+  /** Always false condition */
+  public static readonly NOT_ALL: IMediaQueryCondition = NOT_ALL;
+
+  protected static readonly SHORTCUT_PATTERN = /@([a-z0-9.+-]+)/i;
+  protected static readonly _preprocessors: IMediaQueryPreprocessor[] = [];
+
+  /** Add {@link IMediaQueryPreprocessor} instance for query preprocessing step */
+  public static use(preprocessor: IMediaQueryPreprocessor): typeof ESLMediaQuery {
+    this._preprocessors.unshift(preprocessor);
+    return this;
   }
 
+  /** Cached method to create {@link ESLMediaQuery} condition instance from query string */
   @memoize()
-  static for(query: string) {
-    return new ESLMediaQuery(query);
+  public static for(query: string): IMediaQueryCondition {
+    return ESLMediaQuery.from(query);
   }
 
-  @memoize()
-  static matchMediaCached(query: string) {
-    return matchMedia(query);
-  }
-
-  static buildDprQuery(dpr: number) {
-    if (ESLMediaQuery.ignoreBotsDpr && DeviceDetector.isBot && dpr !== 1) return ESLMediaQuery.NOT_ALL;
-    if (DeviceDetector.isSafari) return `(-webkit-min-device-pixel-ratio: ${dpr})`;
-    return `(min-resolution: ${(96 * dpr).toFixed(1)}dpi)`;
-  }
-
-  static readonly ALL = 'all';
-  static readonly NOT_ALL = 'not all';
-
-  /**
-   * Option to disable DPR images handling for bots
-   */
-  static ignoreBotsDpr = false;
-
-  private readonly _query: MediaQueryList;
-
-  constructor(query: string) {
-    // Applying known breakpoints shortcut
-    query = ESLMediaBreakpoints.apply(query);
-
-    // Applying dpr shortcut
-    query = query.replace(
-      /@(\d(\.\d)?)x/g,
-      (match, ratio) => ESLMediaQuery.buildDprQuery(+ratio)
-    );
-
-    // Applying dpr shortcut for device detection
-    query = query.replace(/(and )?(@MOBILE|@DESKTOP)( and)?/ig, (match, pre, type, post) => {
-      if (DeviceDetector.isMobile !== (type.toUpperCase() === '@MOBILE')) {
-        return ESLMediaQuery.NOT_ALL; // whole query became invalid
-      }
-      return pre && post ? 'and' : '';
+  /** Creates {@link ESLMediaQuery} condition instance from query string */
+  public static from(query: string): IMediaQueryCondition {
+    const conjunctions = query.split(/\sor\s|,/).map((term) => {
+      const conditions = term.split(/\sand\s/).map(ESLMediaQuery.parseSimpleQuery);
+      return new MediaQueryConjunction(conditions);
     });
-
-    this._query = ESLMediaQuery.matchMediaCached(query.trim() || ESLMediaQuery.ALL);
-  }
-
-  /** inner MediaQueryList instance */
-  public get query(): MediaQueryList {
-    return this._query;
-  }
-
-  /** true if current environment satisfies query */
-  public get matches(): boolean {
-    return this.query.matches;
+    return new MediaQueryDisjunction(conjunctions).optimize();
   }
 
-  /** Attach listener to wrapped media query list */
-  public addListener(listener: () => void) {
-    if (typeof this.query.addEventListener === 'function') {
-      this.query.addEventListener('change', listener);
-    } else {
-      this.query.addListener(listener);
+  /** Preprocess simple query term by applying replacers and shortcuts rules */
+  protected static preprocess(term: string) {
+    if (!this.SHORTCUT_PATTERN.test(term)) return term;
+    const shortcut = term.trim().substr(1).toLowerCase();
+    for (const replacer of this._preprocessors) {
+      const result = replacer.process(shortcut);
+      if (typeof result === 'string') return result;
+      if (typeof result === 'boolean') return result ? 'all' : 'not all';
     }
+    return term;
   }
 
-  /** Detach listener from wrapped media query list */
-  public removeListener(listener: () => void) {
-    if (typeof this.query.removeEventListener === 'function') {
-      this.query.removeEventListener('change', listener);
-    } else {
-      this.query.removeListener(listener);
-    }
+  /** Creates simple {@link ESLMediaQuery} condition */
+  protected static parseSimpleQuery(term: string): IMediaQueryCondition {
+    const query = term.replace(/^\s*not\s+/, '');
+    const queryInverted = query !== term;
+    const processedQuery = ESLMediaQuery.preprocess(query);
+    const sanitizedQuery = processedQuery.replace(/^\s*not\s+/, '');
+    const resultInverted = processedQuery !== sanitizedQuery;
+    const invert = queryInverted !== resultInverted;
+
+    if (ALL.eq(sanitizedQuery)) return invert ? NOT_ALL : ALL;
+    if (NOT_ALL.eq(sanitizedQuery)) return invert ? ALL : NOT_ALL;
+    return new MediaQueryCondition(sanitizedQuery, invert);
   }
 
-  public toString(): string {
-    return '[ESL MQ] (' + this.query.media + ')';
-  }
+  // Implements IESLMQCondition to allow use ESLMediaQuery as IESLMQCondition type alias
+  public abstract matches: boolean;
+  public abstract optimize(): IMediaQueryCondition;
+  public abstract addListener(cb: () => void): void;
+  public abstract removeListener(cb: () => void): void;
 }
+
+// Register otb preprocessors
+ESLMediaQuery.use(ESLScreenDPR);
+ESLMediaQuery.use(ESLScreenBreakpoints);
+ESLMediaQuery.use(ESLEnvShortcuts);
diff --git a/src/modules/esl-media-query/core/esl-media-rule-list.ts b/src/modules/esl-media-query/core/esl-media-rule-list.ts
index 941812b16..2596e188d 100644
--- a/src/modules/esl-media-query/core/esl-media-rule-list.ts
+++ b/src/modules/esl-media-query/core/esl-media-rule-list.ts
@@ -1,83 +1,131 @@
+import {ExportNs} from '../../esl-utils/environment/export-ns';
 import {Observable} from '../../esl-utils/abstract/observable';
-import {isPrimitive} from '../../esl-utils/misc/object';
 import {evaluate} from '../../esl-utils/misc/format';
+import {isPrimitive} from '../../esl-utils/misc/object';
 import {ESLMediaRule} from './esl-media-rule';
 
-type PayloadParser<T> = (val: string) => T | undefined;
+export type RulePayloadParser<T> = (val: string) => T | undefined;
+export type RuleChangedCallback<T> = (rule: ESLMediaRule<T | undefined>, list: ESLMediaRuleList<T>) => void;
 
 /**
- * ESL Rule List - ESLMediaRule observable collection
+ * ESLMediaRuleList - {@link ESLMediaRule} observable collection
  * @author Yuliya Adamskaya
+ *
+ * Represents observable object that wraps environment to value mapping
  */
-export class ESLMediaRuleList<T> extends Observable {
-  private _active: ESLMediaRule<T | undefined>;
-  private readonly _default: ESLMediaRule<T>;
-  private readonly _rules: ESLMediaRule<T>[];
-
-  public static STRING_PARSER = (val: string) => val;
+@ExportNs('MediaRuleList')
+export class ESLMediaRuleList<T = any> extends Observable<RuleChangedCallback<T>> {
+  /**
+   * String value parser (used as a default)
+   * @returns value string as it is
+   */
+  public static STRING_PARSER: RulePayloadParser<string> = (val: string): string => val;
+  /**
+   * Object value parser. Uses {@link evaluate} to parse value
+   * @returns value - parsed JS Object
+   */
   public static OBJECT_PARSER = <U>(val: string): U | undefined => evaluate(val);
 
-  private static parseRules<U>(str: string, parser: PayloadParser<U>): ESLMediaRule<U>[] {
-    const parts = str.split('|');
-    const rules: ESLMediaRule<U>[] = [];
-    parts.forEach((_lex: string) => {
-      const lex = _lex.trim();
-      if (!lex) {
-        return;
-      }
-      if (lex.indexOf('=>') === -1) {
-        const value = parser(lex);
-        // Default rule should have lower priority
-        typeof value !== 'undefined' && rules.unshift(ESLMediaRule.all(value));
-      } else {
-        const rule = ESLMediaRule.parse(lex, parser);
-        rule && rules.push(rule);
-      }
+  /**
+   * Creates `ESLMediaRuleList` from string query representation
+   * Uses exact strings as rule list values
+   * @param query - query string
+   */
+  public static parse(query: string): ESLMediaRuleList<string>;
+  /**
+   * Creates `ESLMediaRuleList` from string query representation
+   * @param query - query string
+   * @param parser - value parser function. See built in {@link ESLMediaRuleList.OBJECT_PARSER} or {@link ESLMediaRuleList.STRING_PARSER} (default)
+   */
+  public static parse<U>(query: string, parser: RulePayloadParser<U>): ESLMediaRuleList<U>;
+  public static parse(query: string, parser: RulePayloadParser<any> = ESLMediaRuleList.STRING_PARSER): ESLMediaRuleList<any> {
+    const rules: ESLMediaRule<any>[] = [];
+    query.split('|').forEach((part: string) => {
+      const lex = part.trim();
+      const rule = ESLMediaRule.parse(lex, parser);
+      if (!rule) return;
+      rule.default ? rules.unshift(rule) : rules.push(rule);
     });
-    return rules;
+    return new ESLMediaRuleList(rules);
   }
 
-  public static parse<U>(query: string, parser: PayloadParser<U>) {
-    return new ESLMediaRuleList<U>(query, parser);
+  /**
+   * Creates `ESLMediaRuleList` from two strings with a value  and conditions tuple
+   *
+   * @example
+   * ESLMediaRuleList.fromTuple('1|2|3|4|5', '@XS|@SM|@MD|@LG|@XL')
+   *
+   * @param values - values tuple string (uses '|' as separator)
+   * @param mask - media conditions tuple string (uses '|' as separator)
+   */
+  public static parseTuple(values: string, mask: string) {
+    const valueList = values.split('|');
+    const conditions = mask.split('|');
+    if (valueList.length !== conditions.length) throw new Error('Value doesn\'t correspond to mask');
+    const rules = conditions.map((query, i) => new ESLMediaRule(valueList[i], query));
+    return new ESLMediaRuleList(rules);
   }
 
-  private constructor(query: string, parser: PayloadParser<T>) {
+  private readonly _rules: ESLMediaRule<T>[];
+  private readonly _default: ESLMediaRule<T | undefined>;
+  private _active: ESLMediaRule<T | undefined>;
+
+  private constructor(rules: ESLMediaRule<T>[]) {
     super();
-    if (typeof query !== 'string') {
-      throw new Error('ESLRuleList require first parameter (query) typeof string');
-    }
-    this._rules = ESLMediaRuleList.parseRules(query, parser);
+    this._rules = rules;
+    this._default = rules.filter((rule) => rule.default)[0];
+    this._onMatchChanged = this._onMatchChanged.bind(this);
+  }
+
+  /** Subscribes to the instance active rule change */
+  public addListener(listener: RuleChangedCallback<T>) {
+    super.addListener(listener);
+    if (this._listeners.size > 1) return;
     this._rules.forEach((rule) => rule.addListener(this._onMatchChanged));
-    this._default = this._rules.filter((rule) => rule.default)[0];
   }
 
-  get rules() {
-    return this._rules;
+  /** Unsubscribes from the instance active rule change */
+  public removeListener(listener: RuleChangedCallback<T>) {
+    super.removeListener(listener);
+    if (this._listeners.size) return;
+    this._rules.forEach((rule) => rule.removeListener(this._onMatchChanged));
   }
 
-  get _activeRule(): ESLMediaRule<T | undefined> {
-    const satisfied = this.rules.filter((rule) => rule.matches);
-    return satisfied.length > 0 ? satisfied[satisfied.length - 1] : ESLMediaRule.empty();
+  /** List of inner {@link ESLMediaRule}s */
+  public get rules() {
+    return this._rules;
   }
 
-  get active() {
-    if (!this._active) {
-      this._active = this._activeRule;
+  /** Cached active {@link ESLMediaRule} */
+  public get active() {
+    if (!this._active || !this._listeners.size) {
+      this._active = this.activeRule;
     }
     return this._active;
   }
 
-  get activeValue(): T | undefined {
+  /** Returns last active rule in the list */
+  public get activeRule(): ESLMediaRule<T | undefined> {
+    const satisfied = this.rules.filter((rule) => rule.matches);
+    return satisfied.length > 0 ? satisfied[satisfied.length - 1] : ESLMediaRule.empty();
+  }
+
+  /** Active rule payload value */
+  public get activeValue(): T | undefined {
     const value = this.active.payload;
-    if (isPrimitive(value) || !this._default || isPrimitive(this._default.payload)) return value;
+    if (isPrimitive(value) || !this.default || isPrimitive(this.default.payload)) return value;
     return Object.assign({}, this._default.payload || {}, value);
   }
 
-  private _onMatchChanged = () => {
-    const rule = this._activeRule;
-    if (this._active !== rule) {
-      this._active = rule;
-      this.fire(rule);
-    }
-  };
+  /** {@link ESLMediaRule} that is used as a default */
+  public get default() {
+    return this._default;
+  }
+
+  /** Handle inner rules state change */
+  private _onMatchChanged() {
+    const rule = this.activeRule;
+    if (this._active === rule) return;
+    this.fire(this._active = rule, this);
+  }
 }
diff --git a/src/modules/esl-media-query/core/esl-media-rule.ts b/src/modules/esl-media-query/core/esl-media-rule.ts
index d4b6e51ab..f61c9c2d3 100644
--- a/src/modules/esl-media-query/core/esl-media-rule.ts
+++ b/src/modules/esl-media-query/core/esl-media-rule.ts
@@ -3,48 +3,72 @@ import {ESLMediaQuery} from './esl-media-query';
 type PayloadParser<T> = (val: string) => T | undefined;
 
 /**
- * ESL Rule
+ * ESL Media Rule
  * @author Yuliya Adamskaya
  *
- * Helper class that extend provide Observable Rule Handler that resolve payload based on current device configuration.
- * Supports
- * - CSS query matching check
- * - DPR display queries (@x1 | @x2 | @x3)
- * - Screen default sizes shortcuts @[-|+](XS|SM|MD|LG|XL)
- * - Query matching change listeners
- * - Mobile / full browser detection (@MOBILE|@DESKTOP)
+ * Helper class to wrap {@link ESLMediaQuery} with the payload value
+ * @see ESLMediaQuery
+ * @see ESLMediaRuleList
  */
-export class ESLMediaRule<T> extends ESLMediaQuery {
+export class ESLMediaRule<T> {
+  private readonly _query: ESLMediaQuery;
   private readonly _payload: T;
   private readonly _default: boolean;
 
-  constructor(payload: T, query: string) {
-    super(query);
+  constructor(payload: T, query: string = '') {
+    this._query = ESLMediaQuery.for(query);
     this._default = !query;
     this._payload = payload;
   }
 
   public toString() {
-    return `${super.toString()} => ${this._payload}`;
+    return `${this._query} => ${this._payload}`;
   }
 
-  get payload(): T {
+  /** Subscribes on inner {@link ESLMediaQuery} changes */
+  public addListener(listener: () => void) {
+    this._query.addListener(listener);
+  }
+  /** Unsubscribes from inner {@link ESLMediaQuery} changes */
+  public removeListener(listener: () => void) {
+    this._query.removeListener(listener);
+  }
+
+  /** Check if the inner {@link ESLMediaQuery} is matching current device configuration */
+  public get matches(): boolean {
+    return this._query.matches;
+  }
+  /** @returns wrapped payload value */
+  public get payload(): T {
     return this._payload;
   }
-  get default(): boolean {
+  /**
+   * Check if the rule was created with an empty query
+   * @see ESLMediaRuleList
+   */
+  public get default(): boolean {
     return this._default;
   }
 
+  /** Parse the rule string to the {@link ESLMediaRule} instance */
   public static parse<U>(lex: string, parser: PayloadParser<U>) {
-    const [query, payload] = lex.split('=>');
+    const parts = lex.split('=>');
+    const query = parts.length === 2 ? parts[0] : '';
+    const payload = parts.length === 2 ? parts[1] : parts[0];
     const payloadValue = parser(payload.trim());
     if (typeof payloadValue === 'undefined') return undefined;
     return new ESLMediaRule<U>(payloadValue, query.trim());
   }
 
+  /** Shortcut to create always active {@link ESLMediaRule} with passed value */
   public static all<U>(payload: U) {
     return new ESLMediaRule<U>(payload, 'all');
   }
+  /** Shortcut to create condition-less {@link ESLMediaRule} */
+  public static default<U>(payload: U) {
+    return new ESLMediaRule<U>(payload);
+  }
+  /** Shortcut to create always inactive {@link ESLMediaRule} */
   public static empty(): ESLMediaRule<undefined> {
     return new ESLMediaRule(undefined, 'all');
   }
diff --git a/src/modules/esl-media/core/esl-media-registry.ts b/src/modules/esl-media/core/esl-media-registry.ts
index 3ef2bdb57..0f863e271 100644
--- a/src/modules/esl-media/core/esl-media-registry.ts
+++ b/src/modules/esl-media/core/esl-media-registry.ts
@@ -8,7 +8,7 @@ let evRegistryInstance: ESLMediaProviderRegistry | null = null;
  * ESLMediaProviderRegistry class to store media API providers
  * @author Yuliya Adamskaya, Natallia Harshunova
  */
-export class ESLMediaProviderRegistry extends Observable {
+export class ESLMediaProviderRegistry extends Observable<(name: string, provider: ProviderType) => void> {
   private providersMap: Map<string, ProviderType> = new Map();
 
   public static get instance() {
diff --git a/src/modules/esl-media/core/esl-media.ts b/src/modules/esl-media/core/esl-media.ts
index b5ec14659..cc86da575 100644
--- a/src/modules/esl-media/core/esl-media.ts
+++ b/src/modules/esl-media/core/esl-media.ts
@@ -369,7 +369,7 @@ export class ESLMedia extends ESLBaseElement {
   public get conditionQuery() {
     if (!this._conditionQuery && this._conditionQuery !== null) {
       const query = this.getAttribute('load-condition');
-      this._conditionQuery = query ? new ESLMediaQuery(query) : null;
+      this._conditionQuery = query ? ESLMediaQuery.for(query) : null;
     }
     return this._conditionQuery;
   }
diff --git a/src/modules/esl-note/core/esl-note.ts b/src/modules/esl-note/core/esl-note.ts
index 0a902d373..698dc06aa 100644
--- a/src/modules/esl-note/core/esl-note.ts
+++ b/src/modules/esl-note/core/esl-note.ts
@@ -53,7 +53,7 @@ export class ESLNote extends ESLBaseElement {
     this._innerHTML = this.innerHTML;
     super.connectedCallback();
     this.bindEvents();
-    EventUtils.dispatch(this, `${ESLNote.eventNs}:response`);
+    this._sendResponseToFootnote();
   }
 
   @ready
@@ -96,7 +96,7 @@ export class ESLNote extends ESLBaseElement {
     this._$footnotes = null;
     this.innerHTML = this.html;
     this.tabIndex = -1;
-    EventUtils.dispatch(this, `${ESLNote.eventNs}:ready`);
+    this._sendResponseToFootnote();
   }
 
   /** Merge params to pass to the toggleable */
@@ -160,7 +160,10 @@ export class ESLNote extends ESLBaseElement {
   @bind
   protected _onFootnotesReady(e: CustomEvent) {
     if (this.linked) return;
-    EventUtils.dispatch(this, `${ESLNote.eventNs}:response`);
+    this._sendResponseToFootnote();
   }
 
+  protected _sendResponseToFootnote() {
+    EventUtils.dispatch(this, `${ESLNote.eventNs}:response`);
+  }
 }
diff --git a/src/modules/esl-panel/core/esl-panel-group.ts b/src/modules/esl-panel/core/esl-panel-group.ts
index c63f14684..3d5781531 100644
--- a/src/modules/esl-panel/core/esl-panel-group.ts
+++ b/src/modules/esl-panel/core/esl-panel-group.ts
@@ -42,7 +42,7 @@ export class ESLPanelGroup extends ESLBaseElement {
   protected attributeChangedCallback(attrName: string, oldVal: string, newVal: string) {
     if (!this.connected || oldVal === newVal) return;
     if (attrName === 'mode') {
-      this.modeRules = ESLMediaRuleList.parse<string>(newVal, ESLMediaRuleList.STRING_PARSER);
+      this.modeRules = ESLMediaRuleList.parse(newVal);
       this.updateMode();
     }
   }
@@ -191,7 +191,7 @@ export class ESLPanelGroup extends ESLBaseElement {
   /** ESLMediaRuleList instance of the mode mapping */
   public get modeRules() {
     if (!this._modeRules) {
-      this.modeRules = ESLMediaRuleList.parse<string>(this.mode, ESLMediaRuleList.STRING_PARSER);
+      this.modeRules = ESLMediaRuleList.parse(this.mode);
     }
     return this._modeRules;
   }
diff --git a/src/modules/esl-popup/core/esl-popup-position.ts b/src/modules/esl-popup/core/esl-popup-position.ts
new file mode 100644
index 000000000..faf72b04e
--- /dev/null
+++ b/src/modules/esl-popup/core/esl-popup-position.ts
@@ -0,0 +1,191 @@
+import {Rect} from '../../esl-utils/dom/rect';
+
+export type PositionType = 'top' | 'bottom' | 'left' | 'right';
+
+export interface ArrowPositionValue {
+  x: number;
+  y: number;
+  position: string;
+}
+
+export interface PopupPositionValue {
+  x: number;
+  y: number;
+  arrow: ArrowPositionValue;
+}
+
+export interface IntersectionRatioRect {
+  top?: number;
+  left?: number;
+  right?: number;
+  bottom?: number;
+}
+
+export interface PopupPositionConfig {
+  position: PositionType;
+  behavior: string;
+  intersectionRatio: IntersectionRatioRect,
+  element: DOMRect;
+  inner: Rect;
+  outer: Rect;
+  trigger: Rect;
+}
+
+/**
+ * Calculate Rect for given popup position config.
+ * @param cfg - popup position config
+ * */
+function calcPopupBasicRect(cfg: PopupPositionConfig): Rect {
+  let x = cfg.inner.cx - cfg.element.width / 2;
+  let y = cfg.inner.y - cfg.element.height;
+  switch (cfg.position) {
+    case 'left':
+      x = cfg.inner.x - cfg.element.width;
+      y = cfg.inner.cy - cfg.element.height / 2;
+      break;
+    case 'right':
+      x = cfg.inner.right;
+      y = cfg.inner.cy - cfg.element.height / 2;
+      break;
+    case 'bottom':
+      x = cfg.inner.cx - cfg.element.width / 2;
+      y = cfg.inner.bottom;
+      break;
+  }
+
+  return new Rect(x, y, cfg.element.width, cfg.element.height);
+}
+
+/**
+ * Get opposite position.
+ * @param position - name of position
+ * */
+function getOppositePosition(position: PositionType): PositionType {
+  return ({
+    top: 'bottom',
+    left: 'right',
+    right: 'left',
+    bottom: 'top'
+  }[position] || position) as PositionType;
+}
+
+/**
+ * Update popup and arrow positions to fit by major axis.
+ * @param cfg - popup position config
+ * @param rect - popup position rect
+ * @param arrow - arrow position value
+ * */
+function fitByMajorAxis(cfg: PopupPositionConfig, rect: Rect, arrow: ArrowPositionValue) {
+  if (cfg.behavior !== 'fit') return;
+
+  let isMirrored = false;
+  switch (cfg.position) {
+    case 'bottom':
+      if (cfg.intersectionRatio.bottom || cfg.outer.bottom < rect.bottom) {
+        rect.y = cfg.inner.top - cfg.element.height;
+        isMirrored = true;
+      }
+      break;
+    case 'left':
+      if (cfg.intersectionRatio.left || rect.x < cfg.outer.x) {
+        rect.x = cfg.inner.right;
+        isMirrored = true;
+      }
+      break;
+    case 'right':
+      if (cfg.intersectionRatio.right || cfg.outer.right < rect.right) {
+        rect.x = cfg.inner.x - cfg.element.width;
+        isMirrored = true;
+      }
+      break;
+    default:
+      if (cfg.intersectionRatio.top || rect.y < cfg.outer.y) {
+        rect.y = cfg.inner.bottom;
+        isMirrored = true;
+      }
+      break;
+  }
+  if (isMirrored) {
+    arrow.position = getOppositePosition(cfg.position);
+  }
+}
+
+/**
+ * Update popup and arrow positions to fit by minor horizontal axis.
+ * @param cfg - popup position config
+ * @param rect - popup position rect
+ * @param arrow - arrow position value
+ * */
+function fitByMinorAxisHorizontal(cfg: PopupPositionConfig, rect: Rect, arrow: ArrowPositionValue) {
+  if (cfg.trigger.x < cfg.outer.x || cfg.trigger.right > cfg.outer.right) return; // cancel fit mode if the element is out of window offset bounds
+
+  let arrowAdjust = 0;
+  if (rect.x < cfg.outer.x) {
+    arrowAdjust = rect.x - cfg.outer.x;
+    rect.x = cfg.outer.x;
+  }
+  if (rect.right > cfg.outer.right) {
+    arrowAdjust = rect.right - cfg.outer.right;
+    rect.x -= arrowAdjust;
+  }
+  arrow.x += arrowAdjust;
+}
+
+/**
+ * Update popup and arrow positions to fit by minor vertical axis.
+ * @param cfg - popup position config
+ * @param rect - popup position rect
+ * @param arrow - arrow position value
+ * */
+function fitByMinorAxisVertical(cfg: PopupPositionConfig, rect: Rect, arrow: ArrowPositionValue) {
+  if (cfg.trigger.y < cfg.outer.y || cfg.trigger.bottom > cfg.outer.bottom) return; // cancel fit mode if the element is out of window offset bounds
+
+  let arrowAdjust = 0;
+  if (rect.y < cfg.outer.y) {
+    arrowAdjust = rect.y - cfg.outer.y;
+    rect.y = cfg.outer.y;
+  }
+  if (rect.bottom > cfg.outer.bottom) {
+    arrowAdjust = rect.bottom - cfg.outer.bottom;
+    rect.y -= arrowAdjust;
+  }
+  arrow.y += arrowAdjust;
+}
+
+/**
+ * Update popup and arrow positions to fit by minor axis.
+ * @param cfg - popup position config
+ * @param rect - popup position rect
+ * @param arrow - arrow position value
+ * */
+function fitByMinorAxis(cfg: PopupPositionConfig, rect: Rect, arrow: ArrowPositionValue) {
+  if (cfg.behavior !== 'fit') return;
+
+  if (['left', 'right'].includes(cfg.position)) {
+    fitByMinorAxisVertical(cfg, rect, arrow);
+  } else {
+    fitByMinorAxisHorizontal(cfg, rect, arrow);
+  }
+}
+
+/**
+ * Calculate popup and arrow popup positions.
+ * @param cfg - popup position config
+ * */
+export function calcPopupPosition(cfg: PopupPositionConfig): PopupPositionValue {
+  const rect = calcPopupBasicRect(cfg);
+  const arrow = {
+    x: cfg.element.width / 2,
+    y: cfg.element.height / 2,
+    position: cfg.position
+  };
+
+  fitByMajorAxis(cfg, rect, arrow);
+  fitByMinorAxis(cfg, rect, arrow);
+
+  return {
+    x: rect.x,
+    y: rect.y,
+    arrow
+  };
+}
diff --git a/src/modules/esl-popup/core/esl-popup.less b/src/modules/esl-popup/core/esl-popup.less
index 8c9bb1611..e1a1d0c62 100644
--- a/src/modules/esl-popup/core/esl-popup.less
+++ b/src/modules/esl-popup/core/esl-popup.less
@@ -40,6 +40,7 @@
     top: (-@POPUP_ARROW_HALF - 1);
     border-left: 1px solid @POPUP_BORDER_COLOR;
     border-top: 1px solid @POPUP_BORDER_COLOR;
+    z-index: -1;
 
     &[position=top] {
       bottom: (-@POPUP_ARROW_HALF - 1);
@@ -50,12 +51,14 @@
     &[position=left] {
       right: (-@POPUP_ARROW_HALF - 1);
       left: auto;
+      margin-top: -@POPUP_ARROW_HALF;
       transform: rotate(135deg);
     }
 
     &[position=right] {
-      left: (-@POPUP_ARROW_HALF - 1);
+      left: auto;
       right: auto;
+      margin-top: -@POPUP_ARROW_HALF;
       transform: rotate(315deg);
     }
   }
diff --git a/src/modules/esl-popup/core/esl-popup.ts b/src/modules/esl-popup/core/esl-popup.ts
index 397c9d210..3a51c6104 100644
--- a/src/modules/esl-popup/core/esl-popup.ts
+++ b/src/modules/esl-popup/core/esl-popup.ts
@@ -1,14 +1,23 @@
 import {ExportNs} from '../../esl-utils/environment/export-ns';
 import {attr, jsonAttr} from '../../esl-base-element/core';
+import {bind} from '../../esl-utils/decorators/bind';
+import {ready} from '../../esl-utils/decorators/ready';
 import {prop} from '../../esl-utils/decorators/prop';
 import {rafDecorator} from '../../esl-utils/async/raf';
 import {ESLToggleable} from '../../esl-toggleable/core';
+import {Rect} from '../../esl-utils/dom/rect';
+import {getListScrollParents} from '../../esl-utils/dom/scroll';
+import {getWindowRect} from '../../esl-utils/dom/window';
+import {calcPopupPosition} from './esl-popup-position';
 
 import type {ToggleableActionParams} from '../../esl-toggleable/core';
+import type {PositionType, IntersectionRatioRect} from './esl-popup-position';
+
+const INTERSECTION_LIMIT_FOR_ADJACENT_AXIS = 0.7;
 
 export interface PopupActionParams extends ToggleableActionParams {
   /** popup position relative to trigger */
-  position?: string;
+  position?: PositionType;
   /** popup behavior if it does not fit in the window */
   behavior?: string;
   /** offset in pixels from trigger element */
@@ -17,6 +26,11 @@ export interface PopupActionParams extends ToggleableActionParams {
   offsetWindow?: number;
 }
 
+export interface ActivatorObserver {
+  unsubscribers?: (() => void)[];
+  observer?: IntersectionObserver;
+}
+
 @ExportNs('Popup')
 export class ESLPopup extends ESLToggleable {
   public static is = 'esl-popup';
@@ -26,8 +40,10 @@ export class ESLPopup extends ESLToggleable {
   protected _offsetTrigger: number;
   protected _offsetWindow: number;
   protected _deferredUpdatePosition = rafDecorator(() => this._updatePosition());
+  protected _activatorObserver: ActivatorObserver;
+  protected _intersectionRatio: IntersectionRatioRect = {};
 
-  @attr({defaultValue: 'top'}) public position: string;
+  @attr({defaultValue: 'top'}) public position: PositionType;
   @attr({defaultValue: 'fit'}) public behavior: string;
 
   /** Default params to merge into passed action params */
@@ -40,25 +56,30 @@ export class ESLPopup extends ESLToggleable {
   @prop() public closeOnEsc = true;
   @prop() public closeOnOutsideAction = true;
 
+  @ready
   public connectedCallback() {
-    this.$arrow = this.querySelector('.esl-popup-arrow');
-
     super.connectedCallback();
+    this.$arrow = this.querySelector('span.esl-popup-arrow');
   }
 
   protected bindEvents() {
     super.bindEvents();
     window.addEventListener('resize', this._deferredUpdatePosition);
+    window.addEventListener('scroll', this._deferredUpdatePosition);
   }
 
   protected unbindEvents() {
     super.unbindEvents();
     window.removeEventListener('resize', this._deferredUpdatePosition);
+    window.removeEventListener('scroll', this._deferredUpdatePosition);
+  }
+
+  protected get _isPositioningAlongHorizontal() {
+    return ['left', 'right'].includes(this.position);
   }
 
-  // TODO: move to utilities
-  protected get _windowWidth() {
-    return document.documentElement.clientWidth || document.body.clientWidth;
+  protected get _isPositioningAlongVertical() {
+    return ['top', 'bottom'].includes(this.position);
   }
 
   public onShow(params: PopupActionParams) {
@@ -74,88 +95,121 @@ export class ESLPopup extends ESLToggleable {
     this._offsetWindow = params.offsetWindow || 0;
 
     this._updatePosition();
+    this.activator && this._addActivatorObserver(this.activator);
   }
 
-  protected set _arrowPosition(value: string) {
-    if (!this.$arrow) return;
+  public onHide(params: PopupActionParams) {
+    super.onHide(params);
 
-    this.$arrow.setAttribute('position', value);
+    this.activator && this._removeActivatorObserver(this.activator);
   }
 
-  protected _updatePosition() {
-    if (!this.activator) return;
-
-    const {left, top, arrowLeft, arrowTop, position} = this._calculatePosition(this.activator);
+  protected _checkIntersectionForAdjacentAxis(isAdjacentAxis: boolean, intersectionRatio: number) {
+    if (isAdjacentAxis && intersectionRatio < INTERSECTION_LIMIT_FOR_ADJACENT_AXIS) {
+      this.hide();
+    }
+  }
 
-    // set popup position
-    this.style.left = `${left}px`;
-    this.style.top = `${top}px`;
+  @bind
+  protected onActivatorIntersection(entries: IntersectionObserverEntry[], observer: IntersectionObserver) {
+    const entry = entries[0];
+    this._intersectionRatio = {};
+    if (!entry.isIntersecting) {
+      this.hide();
+      return;
+    }
 
-    // set arrow position
-    if (this.$arrow) {
-      this.$arrow.style.left = `${arrowLeft}px`;
-      this._arrowPosition = position;
+    if (entry.intersectionRect.y !== entry.boundingClientRect.y) {
+      this._intersectionRatio.top = entry.intersectionRect.height / entry.boundingClientRect.height;
+      this._checkIntersectionForAdjacentAxis(this._isPositioningAlongHorizontal, this._intersectionRatio.top);
+    }
+    if (entry.intersectionRect.bottom !== entry.boundingClientRect.bottom) {
+      this._intersectionRatio.bottom = entry.intersectionRect.height / entry.boundingClientRect.height;
+      this._checkIntersectionForAdjacentAxis(this._isPositioningAlongHorizontal, this._intersectionRatio.bottom);
+    }
+    if (entry.intersectionRect.x !== entry.boundingClientRect.x) {
+      this._intersectionRatio.left = entry.intersectionRect.width / entry.boundingClientRect.width;
+      this._checkIntersectionForAdjacentAxis(this._isPositioningAlongVertical, this._intersectionRatio.left);
+    }
+    if (entry.intersectionRect.right !== entry.boundingClientRect.right) {
+      this._intersectionRatio.right = entry.intersectionRect.width / entry.boundingClientRect.width;
+      this._checkIntersectionForAdjacentAxis(this._isPositioningAlongVertical, this._intersectionRatio.right);
     }
   }
 
-  protected _calculatePosition($activator: HTMLElement) {
-    const {left, arrowLeft} = this._calculateLeft($activator);
-    const {top, arrowTop, position} = this._calculateTop($activator);
-
-    return {
-      left,
-      top,
-      arrowLeft,
-      arrowTop,
-      position
-    };
+  @bind
+  protected onActivatorScroll(e: Event) {
+    this._updatePosition();
   }
 
-  protected _calculateLeft($activator: HTMLElement) {
-    const triggerRect = $activator.getBoundingClientRect();
-    const triggerPosX = triggerRect.left + window.pageXOffset;
-    const centerX = triggerPosX + triggerRect.width / 2;
+  protected _addActivatorObserver(target: HTMLElement) {
+    const scrollParents = getListScrollParents(target);
 
-    let arrowAdjust = 0;
-    let left = centerX - this.offsetWidth / 2;
+    const unsubscribers = scrollParents.map(($root) => {
+      const options = {passive: true} as EventListenerOptions;
+      $root.addEventListener('scroll', this.onActivatorScroll, options);
+      return () => {
+        $root && $root.removeEventListener('scroll', this.onActivatorScroll, options);
+      };
+    });
 
-    if (this.behavior === 'fit' && left < this._offsetWindow) {
-      arrowAdjust += left - this._offsetWindow;
-      left = this._offsetWindow;
-    }
+    const options = {
+      rootMargin: '0px',
+      threshold: [...Array(9).keys()].map((x) => x/8)
+    } as IntersectionObserverInit;
 
-    const right = this._windowWidth - (left + this.offsetWidth);
-    if (this.behavior === 'fit' && right < this._offsetWindow) {
-      arrowAdjust -= right - this._offsetWindow;
-      left += right - this._offsetWindow;
-    }
-    const arrowLeft = this.clientWidth / 2 + arrowAdjust;
+    const observer = new IntersectionObserver(this.onActivatorIntersection, options);
+    observer.observe(target);
 
-    return {
-      left,
-      arrowLeft
+    this._activatorObserver = {
+      unsubscribers,
+      observer
     };
   }
 
-  protected _calculateTop($activator: HTMLElement) {
-    const arrowRect = this.$arrow ? this.$arrow.getBoundingClientRect() : new DOMRect();
-    const triggerRect = $activator.getBoundingClientRect();
-    const triggerPosY = triggerRect.top + window.pageYOffset;
-    const arrowHeight = arrowRect.height / 2;
-
-    let arrowTop = triggerPosY - this._offsetTrigger - arrowHeight;
-    let top = arrowTop - this.offsetHeight;
-    let position = 'top';
-    if (this.behavior === 'fit' && window.pageYOffset > top) {  /* show popup at the bottom of trigger */
-      arrowTop = triggerPosY + triggerRect.height + this._offsetTrigger;
-      top = arrowTop + arrowHeight + this._offsetTrigger;
-      position = 'bottom';
-    }
+  protected _removeActivatorObserver(target: HTMLElement) {
+    this._activatorObserver.observer?.disconnect();
+    this._activatorObserver.observer = undefined;
+    this._activatorObserver.unsubscribers?.forEach((cb) => cb());
+    this._activatorObserver.unsubscribers = [];
+  }
+
+  protected set _arrowPosition(value: string) {
+    if (!this.$arrow) return;
+
+    this.$arrow.setAttribute('position', value);
+  }
 
-    return {
-      top,
-      arrowTop,
-      position
+  protected _updatePosition() {
+    if (!this.activator) return;
+
+    const triggerRect = this.activator.getBoundingClientRect();
+    const popupRect = this.getBoundingClientRect();
+    const arrowRect = this.$arrow ? this.$arrow.getBoundingClientRect() : new DOMRect();
+    const trigger = new Rect(triggerRect.x, triggerRect.y + window.pageYOffset, triggerRect.width, triggerRect.height);
+    const innerMargin = this._offsetTrigger + arrowRect.width / 2;
+
+    const config = {
+      position: this.position,
+      behavior: this.behavior,
+      intersectionRatio: this._intersectionRatio,
+      element: popupRect,
+      trigger,
+      inner: Rect.from(trigger).grow(innerMargin),
+      outer: getWindowRect().shrink(this._offsetWindow)
     };
+
+    const {x, y, arrow} = calcPopupPosition(config);
+
+    // set popup position
+    this.style.left = `${x}px`;
+    this.style.top = `${y}px`;
+
+    // set arrow position
+    if (this.$arrow) {
+      this.$arrow.style.left = ['top', 'bottom'].includes(arrow.position) ? `${arrow.x}px` : 'none';
+      this.$arrow.style.top = ['left', 'right'].includes(arrow.position) ? `${arrow.y}px` : 'none';
+      this._arrowPosition = arrow.position;
+    }
   }
 }
diff --git a/src/modules/esl-scrollbar/core/esl-scrollbar.ts b/src/modules/esl-scrollbar/core/esl-scrollbar.ts
index 5e40b84aa..a061c55c8 100644
--- a/src/modules/esl-scrollbar/core/esl-scrollbar.ts
+++ b/src/modules/esl-scrollbar/core/esl-scrollbar.ts
@@ -6,6 +6,7 @@ import {rafDecorator} from '../../esl-utils/async/raf';
 import {EventUtils} from '../../esl-utils/dom/events';
 import {TraversingUtils} from '../../esl-utils/dom/traversing';
 import {TraversingQuery} from '../../esl-traversing-query/core';
+import {RTLUtils} from '../../esl-utils/dom/rtl';
 
 /**
  * ESL Scrollbar component
@@ -175,16 +176,22 @@ export class ESLScrollbar extends ESLBaseElement {
   /** Relative position value (between 0.0 and 1.0) */
   public get position() {
     if (!this.$target) return 0;
-    const scrollOffset = this.horizontal ? this.$target.scrollLeft : this.$target.scrollTop;
+    const scrollOffset = this.horizontal ? RTLUtils.normalizeScrollLeft(this.$target) : this.$target.scrollTop;
     return this.scrollableSize ? (scrollOffset / this.scrollableSize) : 0;
   }
 
   public set position(position) {
-    const normalizedPosition = Math.min(1, Math.max(0, position));
-    this.scrollTargetTo(this.scrollableSize * normalizedPosition);
+    this.scrollTargetTo(this.scrollableSize * this.normalizePosition(position));
     this.update();
   }
 
+  /** Normalize position value (between 0.0 and 1.0) */
+  protected normalizePosition(position: number) {
+    const relativePosition = Math.min(1, Math.max(0, position));
+    if (this.$target && !RTLUtils.isRtl(this.$target)) return relativePosition;
+    return RTLUtils.scrollType === 'negative' ? (relativePosition - 1) : (1 - relativePosition);
+  }
+
   /** Scroll target element to passed position */
   protected scrollTargetTo(pos: number) {
     if (!this.$target) return;
diff --git a/src/modules/esl-tab/core/esl-tabs.ts b/src/modules/esl-tab/core/esl-tabs.ts
index 04da9c172..f92f33eea 100644
--- a/src/modules/esl-tab/core/esl-tabs.ts
+++ b/src/modules/esl-tab/core/esl-tabs.ts
@@ -45,7 +45,7 @@ export class ESLTabs extends ESLBaseElement {
   protected attributeChangedCallback(attrName: string, oldVal: string, newVal: string) {
     if (!this.connected || oldVal === newVal) return;
     if (attrName === 'scrollable') {
-      this.scrollableTypeRules = ESLMediaRuleList.parse<string>(newVal, ESLMediaRuleList.STRING_PARSER);
+      this.scrollableTypeRules = ESLMediaRuleList.parse(newVal);
       this.updateScrollableType();
     }
   }
@@ -206,7 +206,7 @@ export class ESLTabs extends ESLBaseElement {
   /** ESLMediaRuleList instance of the scrollable type mapping */
   public get scrollableTypeRules() {
     if (!this._scrollableTypeRules) {
-      this.scrollableTypeRules = ESLMediaRuleList.parse<string>(this.scrollable, ESLMediaRuleList.STRING_PARSER);
+      this.scrollableTypeRules = ESLMediaRuleList.parse(this.scrollable);
     }
     return this._scrollableTypeRules;
   }
diff --git a/src/modules/esl-tooltip/core/esl-tooltip.ts b/src/modules/esl-tooltip/core/esl-tooltip.ts
index ff2abe809..ef38a7f97 100644
--- a/src/modules/esl-tooltip/core/esl-tooltip.ts
+++ b/src/modules/esl-tooltip/core/esl-tooltip.ts
@@ -4,6 +4,7 @@ import {ESLPopup} from '../../esl-popup/core';
 import {memoize} from '../../esl-utils/decorators/memoize';
 
 import type {PopupActionParams} from '../../esl-popup/core';
+import type {PositionType} from '../../esl-popup/core/esl-popup-position';
 
 export interface TooltipActionParams extends PopupActionParams {
   /** text to be shown */
@@ -18,7 +19,7 @@ export interface TooltipActionParams extends PopupActionParams {
 export class ESLTooltip extends ESLPopup {
   static is = 'esl-tooltip';
 
-  @attr({defaultValue: 'top'}) public position: string;
+  @attr({defaultValue: 'top'}) public position: PositionType;
   @attr({defaultValue: 'fit'}) public behavior: string;
   @boolAttr() public disableArrow: boolean;
 
diff --git a/src/modules/esl-utils/abstract/observable.ts b/src/modules/esl-utils/abstract/observable.ts
index ebe4f68ee..2d22bb8dc 100644
--- a/src/modules/esl-utils/abstract/observable.ts
+++ b/src/modules/esl-utils/abstract/observable.ts
@@ -1,20 +1,20 @@
+import type {AnyToVoidFnSignature} from '../misc/functions';
+
 /**
  * Abstract Observable implementation
  * @author Yuliya Adamskaya
  */
-export type ObserverCallback = (...args: any) => void;
-
-export abstract class Observable {
-  private _listeners = new Set<ObserverCallback>();
+export abstract class Observable <Callback extends AnyToVoidFnSignature = AnyToVoidFnSignature>{
+  protected _listeners = new Set<Callback>();
 
-  public addListener(listener: ObserverCallback) {
+  public addListener(listener: Callback) {
     this._listeners.add(listener);
   }
-  public removeListener(listener: ObserverCallback) {
+  public removeListener(listener: Callback) {
     this._listeners.delete(listener);
   }
 
-  protected fire(...args: any) {
+  protected fire(...args: Parameters<Callback>) {
     this._listeners.forEach((listener) => {
       try {
         listener.apply(this, args);
diff --git a/src/modules/esl-utils/decorators/memoize.ts b/src/modules/esl-utils/decorators/memoize.ts
index 7daac8846..540903fbe 100644
--- a/src/modules/esl-utils/decorators/memoize.ts
+++ b/src/modules/esl-utils/decorators/memoize.ts
@@ -1,9 +1,9 @@
-import {getPropertyDescriptor} from '../misc/object';
 import {defaultArgsHashFn, memoizeFn} from '../misc/memoize';
+import {isPrototype, getPropertyDescriptor} from '../misc/object';
+
 import type {MemoHashFn} from '../misc/memoize';
 import type {MethodTypedDecorator} from '../misc/functions';
 
-
 export function memoize(): MethodDecorator;
 export function memoize<H extends MemoHashFn>(hashFn: H): MethodTypedDecorator<(...args: Parameters<H>) => any>;
 /**
@@ -12,32 +12,46 @@ export function memoize<H extends MemoHashFn>(hashFn: H): MethodTypedDecorator<(
  */
 export function memoize(hashFn: MemoHashFn = defaultArgsHashFn) {
   return function (target: any, prop: string, descriptor: TypedPropertyDescriptor<any>) {
-    const isPrototype = Object.hasOwnProperty.call(target, 'constructor');
-    if (descriptor && typeof descriptor.value === 'function') {
-      descriptor.value = isPrototype ?
-        memoizeMember(descriptor.value, prop, false, hashFn) :
-        memoizeFn(descriptor.value, hashFn);
-    } else if (descriptor && typeof descriptor.get === 'function') {
-      descriptor.get = isPrototype ?
-        memoizeMember(descriptor.get, prop, true, hashFn) :
-        memoizeFn(descriptor.get, hashFn);
-    } else {
+    if (!descriptor || typeof (descriptor.value || descriptor.get) !== 'function') {
       throw new TypeError('Only get accessors or class methods can be decorated via @memoize');
     }
+
+    if (isPrototype(target)) {
+      // Object members
+      (typeof descriptor.get === 'function') && (descriptor.get = memoizeGetter(descriptor.get, prop));
+      (typeof descriptor.value === 'function') && (descriptor.value = memoizeMethod(descriptor.value, prop, hashFn));
+    } else {
+      // Static members
+      (typeof descriptor.get === 'function') && (descriptor.get = memoizeFn(descriptor.get));
+      (typeof descriptor.value === 'function') && (descriptor.value = memoizeFn(descriptor.value, hashFn));
+    }
   };
 }
 
 // Lock storage to prevent cache logic for some key
 const locks = new WeakMap();
+const defineOwnKeySafe = (obj: any, prop: string, value: any) => {
+  locks.set(obj, prop); // IE try to get key with the prototype instance call, so we lock it
+  Object.defineProperty(obj, prop, {value, writable: true, configurable: true});
+  locks.delete(obj); // Free property key
+};
+
+/** Cache getter result as an object own property */
+function memoizeGetter(originalMethod: any, prop: string) {
+  return function (this: any, ...args: any[]): any {
+    if (locks.get(this) === prop) return originalMethod;
+    const value = originalMethod.call(this);
+    defineOwnKeySafe(this, prop, value);
+    return value;
+  };
+}
 
-/** Cache memo function in the current context on call */
-function memoizeMember(originalMethod: any, prop: string, isGetter: boolean, hashFn: MemoHashFn) {
+/** Cache method memo function in the current context on call */
+function memoizeMethod(originalMethod: any, prop: string, hashFn: MemoHashFn) {
   return function (this: any, ...args: any[]): any {
     if (locks.get(this) === prop) return originalMethod;
     const memo = memoizeFn(originalMethod, hashFn);
-    locks.set(this, prop); // IE try to get key with the prototype instance call, so we lock it
-    Object.defineProperty(this, prop, isGetter ? {get: memo} : {value: memo});
-    locks.delete(this); // Free property key
+    defineOwnKeySafe(this, prop, memo);
     return memo.apply(this, args);
   };
 }
@@ -45,11 +59,13 @@ function memoizeMember(originalMethod: any, prop: string, isGetter: boolean, has
 /**
  * Clear memoization cache for passed target and property.
  * Accepts not own properties.
+ * Note: be sure that you targeting memoized property or function.
+ * Clear utility has no 100% check to prevent modifying incorrect (not memoized) property keys
  */
 memoize.clear = function (target: any, property: string) {
-  let clearFn = null;
   const desc = getPropertyDescriptor(target, property);
-  if (desc && typeof desc.value === 'function') clearFn = desc.value.clear;
-  if (desc && typeof desc.get === 'function') clearFn = (desc.get as any).clear;
-  (typeof clearFn === 'function') && clearFn();
+  if (!desc) return;
+  if (typeof desc.get === 'function' && typeof (desc.get as any).clear === 'function') return (desc.get as any).clear();
+  if (typeof desc.value === 'function' && typeof desc.value.clear === 'function') return desc.value.clear();
+  if (Object.hasOwnProperty.call(target, property)) delete target[property];
 };
diff --git a/src/modules/esl-utils/dom/element.ts b/src/modules/esl-utils/dom/element.ts
new file mode 100644
index 000000000..5924134c8
--- /dev/null
+++ b/src/modules/esl-utils/dom/element.ts
@@ -0,0 +1,31 @@
+/**
+ * Get the Element that is the root element of the document.
+ * @param element - element for which to get the document element
+ * */
+export function getDocumentElement(element: Element | Window): Element {
+  return ((element instanceof Window
+    ? element.document
+    : element.ownerDocument) || window.document).documentElement;
+}
+
+/**
+ * Get the name of node.
+ * @param element - element for which to get the name
+ */
+export function getNodeName(element?: Node | Window): string {
+  return element && !(element instanceof Window)? (element.nodeName).toLowerCase() : '';
+}
+
+/**
+ * Get the parent of the specified element in the DOM tree.
+ * @param element - element for which to get the parent
+ */
+export function getParentNode(element: Element | ShadowRoot): Node {
+  if (getNodeName(element) === 'html') {
+    return element;
+  }
+
+  return (element instanceof ShadowRoot
+    ? element.host
+    : element.assignedSlot || element.parentNode) || getDocumentElement(element as Element);
+}
diff --git a/src/modules/esl-utils/dom/rect.ts b/src/modules/esl-utils/dom/rect.ts
new file mode 100644
index 000000000..baf6e3744
--- /dev/null
+++ b/src/modules/esl-utils/dom/rect.ts
@@ -0,0 +1,92 @@
+/**
+ * A Rect describes the size and position of a rectangle.
+ * */
+export class Rect {
+  /** The X coordinate of the Rect's origin (top-left corner of the rectangle). */
+  public x = 0;
+  /** The Y coordinate of the Rect's origin (top-left corner of the rectangle). */
+  public y = 0;
+  /** The width of the Rect. */
+  public width = 0;
+  /** The height of the Rect. */
+  public height = 0;
+
+  /**
+   * The static method creates a new Rect instance from a rect-like object.
+   * @param rect - rect-like object
+   * */
+  public static from(rect: {x?: number; y?: number; width?: number; height?: number} = {}): Rect {
+    return new this(rect.x, rect.y, rect.width, rect.height);
+  }
+
+  public constructor(x?: number, y?: number, width?: number, height?: number) {
+    this.x = x || 0;
+    this.y = y || 0;
+    this.width= width || 0;
+    this.height = height || 0;
+  }
+
+  /**
+   * Get the top coordinate value of the Rect (has the same value as y).
+   * */
+  public get top(): number {
+    return this.y;
+  }
+
+  /**
+   * Get the left coordinate value of the Rect (has the same value as x).
+   * */
+  public get left(): number {
+    return this.x;
+  }
+
+  /**
+   * Get the right coordinate value of the DOMRect.
+   * */
+  public get right(): number {
+    return this.x + this.width;
+  }
+
+  /**
+   * Get the bottom coordinate value of the Rect.
+   * */
+  public get bottom(): number {
+    return this.y + this.height;
+  }
+
+  /**
+   * Get the center X coordinate value of the Rect.
+   * */
+  public get cx(): number {
+    return this.x + this.width / 2;
+  }
+
+  /**
+   * Get the center Y coordinate value of the Rect.
+   * */
+  public get cy(): number {
+    return this.y + this.height / 2;
+  }
+
+  /**
+   * Grow the Rect by the specified increment in pixels.
+   * It increases the size of the Rect by moving each point on the edge of the Rect to a certain distance further away from the center of the Rect.
+   * @param increment - distance to grow in pixels
+   * */
+  public grow(increment: number): Rect {
+    this.y -= increment;
+    this.x -= increment;
+    this.height += 2 * increment;
+    this.width += 2 * increment;
+    return this;
+  }
+
+  /**
+   * Shrink the Rect by the specified decrement in pixels.
+   * It reduces the size of the Rect by moving each point on the edge of the Rect to a certain distance closer to the center of the Rect.
+   * @param decrement - distance to shrink in pixels
+   * */
+  public shrink(decrement: number): Rect {
+    return this.grow(-decrement);
+  }
+}
diff --git a/src/modules/esl-utils/dom/rtl.ts b/src/modules/esl-utils/dom/rtl.ts
index be3261c01..a38912e02 100644
--- a/src/modules/esl-utils/dom/rtl.ts
+++ b/src/modules/esl-utils/dom/rtl.ts
@@ -27,18 +27,17 @@ export abstract class RTLUtils {
     return scrollType;
   }
 
-  // Potentially can be useful in future
-  // static normalizeScrollLeft(el: HTMLElement, value: number | null = null, isRtl: boolean = RTLUtils.isRtl(el)): number {
-  //   value = (value === null) ? el.scrollLeft: value;
-  //   switch (isRtl ? RTLUtils.scrollType : '') {
-  //     case 'negative':
-  //       return el.scrollWidth - el.clientWidth + value;
-  //     case 'reverse':
-  //       return el.scrollWidth - el.clientWidth - value;
-  //     default:
-  //       return value;
-  //   }
-  // }
+  static normalizeScrollLeft(el: HTMLElement, value: number | null = null, isRtl: boolean = RTLUtils.isRtl(el)): number {
+    value = (value === null) ? el.scrollLeft: value;
+    switch (isRtl ? RTLUtils.scrollType : '') {
+      case 'negative':
+        return el.scrollWidth - el.clientWidth + value;
+      case 'reverse':
+        return el.scrollWidth - el.clientWidth - value;
+      default:
+        return value;
+    }
+  }
 }
 
 /** Creates the dummy test element with a horizontal scroll presented */
diff --git a/src/modules/esl-utils/dom/scroll.ts b/src/modules/esl-utils/dom/scroll.ts
index 6b5556138..6fdc3f3de 100644
--- a/src/modules/esl-utils/dom/scroll.ts
+++ b/src/modules/esl-utils/dom/scroll.ts
@@ -1,3 +1,5 @@
+import {getNodeName, getParentNode} from './element';
+
 export type ScrollStrategy = 'none' | 'native' | 'pseudo';
 
 const $html = document.documentElement;
@@ -52,3 +54,47 @@ export abstract class ScrollUtils {
     (initiatorSet.size === 0) && ScrollUtils.unlock();
   }
 }
+
+/**
+ * Get the list of all scroll parents, up the list of ancestors until we get to the top window object.
+ * @param element - element for which you want to get the list of all scroll parents
+ * Optional @param [list] - array of elements to concatenate with the list of all scroll parents of element
+ * */
+export function getListScrollParents(element: Node, list: Element[] = []): Element[] {
+  const scrollParent = getScrollParent(element);
+  const isBody = scrollParent === element.ownerDocument?.body;
+  const target = isBody
+    ? isScrollParent(scrollParent) ? scrollParent : []
+    : scrollParent;
+
+  const updatedList = list.concat(target);
+  return isBody
+    ? updatedList
+    : updatedList.concat(getListScrollParents(getParentNode(scrollParent)));
+}
+
+/**
+ * Get the scroll parent of the specified element in the DOM tree.
+ * @param node - element for which to get the scroll parent
+ */
+export function getScrollParent(node: Node): Element {
+  if (['html', 'body', '#document'].indexOf(getNodeName(node)) >= 0) {
+    return node.ownerDocument?.body as Element;
+  }
+
+  if (node instanceof HTMLElement && isScrollParent(node as Element)) {
+    return node as Element;
+  }
+
+  return getScrollParent(getParentNode(node as Element));
+}
+
+/**
+ * Check that element is scroll parent.
+ * @param element - element for checking
+ * */
+export function isScrollParent(element: Element): boolean {
+  // Firefox wants us to check `-x` and `-y` variations as well
+  const {overflow, overflowX, overflowY} = getComputedStyle(element);
+  return /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX);
+}
diff --git a/src/modules/esl-utils/dom/window.ts b/src/modules/esl-utils/dom/window.ts
new file mode 100644
index 000000000..4e3f3adf5
--- /dev/null
+++ b/src/modules/esl-utils/dom/window.ts
@@ -0,0 +1,73 @@
+import {Rect} from './rect';
+
+/**
+ * Get the window object associated with a document of the specified element.
+ * @param node - element for which to get window
+ * */
+export function getWindow(node: Node | Window): Window {
+  if (node == null) {
+    return window;
+  }
+
+  if (node instanceof Window) {
+    return node;
+  }
+
+  const ownerDocument = node.ownerDocument;
+  return ownerDocument ? ownerDocument.defaultView || window : window;
+}
+
+/**
+ * Get the bottom coordinate value of the window.
+ * */
+export function getWindowBottom(): number {
+  return window.pageYOffset + getWindowHeight();
+}
+
+/**
+ * Get the left coordinate value of the window.
+ * */
+export function getWindowLeft(): number {
+  return window.pageXOffset;
+}
+
+/**
+ * Get the right coordinate value of the window.
+ * */
+export function getWindowRight(): number {
+  return window.pageXOffset + getWindowWidth();
+}
+
+/**
+ * Get the top coordinate value of the window.
+ * */
+export function getWindowTop(): number {
+  return window.pageYOffset;
+}
+
+/**
+ * Get the width of the window.
+ * */
+export function getWindowHeight(): number {
+  return window.innerHeight || window.document.documentElement.clientHeight;
+}
+
+/**
+ * Get the height of the window.
+ * */
+export function getWindowWidth(): number {
+  return window.innerWidth || window.document.documentElement.clientWidth;
+}
+
+/**
+ * Get the size and position of the window.
+ * @returns
+ */
+export function getWindowRect(): Rect {
+  return Rect.from({
+    x: getWindowLeft(),
+    y: getWindowTop(),
+    height: getWindowHeight(),
+    width: getWindowWidth()
+  });
+}
diff --git a/src/modules/esl-utils/misc/object.ts b/src/modules/esl-utils/misc/object.ts
index 1c0399215..b95af567a 100644
--- a/src/modules/esl-utils/misc/object.ts
+++ b/src/modules/esl-utils/misc/object.ts
@@ -3,6 +3,8 @@ export const isObjectLike = (obj: any) => isObject(obj) || typeof obj === 'funct
 export const isPrimitive = (obj: any): obj is string | number | boolean =>
   typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean';
 
+export const isPrototype = (obj: any) => Object.hasOwnProperty.call(obj, 'constructor');
+
 export type CopyPredicate = (key: string, value: any) => boolean;
 
 /** Deep object compare */
@@ -87,3 +89,28 @@ export const get = (data: any, path: string, defaultValue?: any): any => {
   }, data);
   return typeof result === 'undefined' ? defaultValue : result;
 };
+
+/**
+ * Performs a deep merge of objects and returns new object.
+ * Does not modify objects (immutable)
+ * @param objects to merge
+ * @returns new object with merged key/values
+ */
+export function deepMerge(...objects: any[]): any {
+  return objects.reduce((res: any, obj: any) => {
+    isObject(obj) && Object.keys(obj).forEach((key) => {
+      const resultVal = res[key];
+      const objectVal = obj[key];
+
+      if (Array.isArray(objectVal)) {
+        res[key] = objectVal.slice();
+      } else if (isObject(resultVal) && isObject(objectVal)) {
+        res[key] = deepMerge(resultVal, objectVal);
+      } else {
+        res[key] = objectVal;
+      }
+    });
+
+    return res;
+  }, {});
+}
diff --git a/src/modules/lib.ts b/src/modules/lib.ts
index 472924014..6cfd7329b 100644
--- a/src/modules/lib.ts
+++ b/src/modules/lib.ts
@@ -1,26 +1,37 @@
 import type {DeviceDetector} from './esl-utils/environment/device-detector';
+import type {TraversingQuery} from './esl-traversing-query/core/esl-traversing-query';
 
-import type {ESLMediaBreakpoints} from './esl-media-query/core';
+import type {
+  ESLScreenDPR,
+  ESLScreenBreakpoint,
+  ESLEnvShortcuts,
+  ESLMediaQuery,
+  ESLMediaRuleList
+} from './esl-media-query/core';
 
 import type {ESLImage} from './esl-image/core/esl-image';
 import type {ESLMedia} from './esl-media/core/esl-media';
 
 import type {ESLScrollbar} from './esl-scrollbar/core/esl-scrollbar';
 
-import type {ESLA11yGroup} from './esl-a11y-group/core/esl-a11y-group';
 import type {ESLToggleableDispatcher} from './esl-toggleable/core/esl-toggleable-dispatcher';
 import type {ESLToggleable} from './esl-toggleable/core/esl-toggleable';
+import type {ESLPanel, ESLPanelGroup} from './esl-panel/core';
 import type {ESLPopup} from './esl-popup/core/esl-popup';
+
+import type {ESLA11yGroup} from './esl-a11y-group/core/esl-a11y-group';
 import type {ESLTrigger} from './esl-trigger/core/esl-trigger';
-import type {ESLTab} from './esl-tab/core/esl-tab';
-import type {ESLPanel} from './esl-panel/core/esl-panel';
-import type {ESLPanelGroup} from './esl-panel/core/esl-panel-group';
-import type {ESLTabs} from './esl-tab/core/esl-tabs';
+import type {ESLTab, ESLTabs} from './esl-tab/core';
+
 import type {ESLNote} from './esl-note/core/esl-note';
 import type {ESLFootnotes} from './esl-footnotes/core/esl-footnotes';
 import type {ESLTooltip} from './esl-tooltip/core/esl-tooltip';
 
-import type {ESLSelect, ESLSelectList, ESLSelectItem} from './esl-forms/all';
+import type {
+  ESLSelect,
+  ESLSelectList,
+  ESLSelectItem
+} from './esl-forms/all';
 
 import type {ESLCarousel} from './draft/esl-carousel/core/esl-carousel';
 import type {ESLCarouselDotsPlugin} from './draft/esl-carousel/plugin/esl-carousel-dots.plugin';
@@ -37,11 +48,19 @@ declare global {
   const ESL: ESLLibrary;
 
   export interface ESLLibrary {
+    // ESL Utils
     DeviceDetector?: typeof DeviceDetector;
-    MediaBreakpoints?: typeof ESLMediaBreakpoints;
+    TraversingQuery?: typeof TraversingQuery;
+    ScreenDPR?: typeof ESLScreenDPR;
+    ScreenBreakpoint?: typeof ESLScreenBreakpoint;
+    EnvShortcuts?: typeof ESLEnvShortcuts;
+    MediaQuery?: typeof ESLMediaQuery;
+    MediaRuleList?: typeof ESLMediaRuleList;
 
+    // ESL Components
     Image?: typeof ESLImage;
     Media?: typeof ESLMedia;
+
     Scrollbar?: typeof ESLScrollbar;
 
     A11yGroup?: typeof ESLA11yGroup;
@@ -54,10 +73,12 @@ declare global {
     PanelGroup?: typeof ESLPanelGroup;
     TabsContainer?: typeof ESLTabs;
 
+    // ESL Forms
     Select?: typeof ESLSelect;
     SelectList?: typeof ESLSelectList;
     SelectItem?: typeof ESLSelectItem;
 
+    // ESL Drafts
     Carousel?: typeof ESLCarousel;
     CarouselPlugins: {
       Dots?: typeof ESLCarouselDotsPlugin;


Test patch:
diff --git a/src/modules/esl-media-query/test/esl-media-breakpoints.test.ts b/src/modules/esl-media-query/test/esl-media-breakpoints.test.ts
deleted file mode 100644
index ce0d3f222..000000000
--- a/src/modules/esl-media-query/test/esl-media-breakpoints.test.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import {ESLMediaBreakpoints} from '../core/esl-media-breakpoints';
-
-describe('esl breakpoints test', () => {
-  test('Get all breakpoints', () => {
-    expect(new Set(ESLMediaBreakpoints.getAllBreakpointsNames())).toEqual(new Set(['xs', 'sm', 'md', 'lg', 'xl']));
-  })
-
-  test('add and get custom breakpoints tests', () => {
-    const getBp = ESLMediaBreakpoints.getBreakpoint;
-    const addBp = ESLMediaBreakpoints.addCustomBreakpoint;
-    expect(addBp('SMALL', 300, 400)).toBeUndefined();
-    expect(getBp('SMALL').mediaQuery).toBe('(min-width: 300px) and (max-width: 400px)');
-    expect(getBp('SMALL').mediaQueryGE).toBe('(min-width: 300px)');
-    expect(getBp('SMALL').mediaQueryLE).toBe('(max-width: 400px)');
-    expect(addBp('Small', 400, 500).mediaQuery).toBe('(min-width: 300px) and (max-width: 400px)');
-    expect(getBp('small').mediaQuery).toBe('(min-width: 400px) and (max-width: 500px)');
-    addBp('xs', 11, 22);
-    expect(getBp('xs').mediaQuery).toBe('(min-width: 11px) and (max-width: 22px)');
-  });
-
-  test('Get all breakpoints after adding custom', () => {
-    expect(new Set(ESLMediaBreakpoints.getAllBreakpointsNames())).toEqual(new Set(['xs', 'sm', 'md', 'lg', 'xl', 'small']));
-  })
-
-  test('Error tests', () => {
-    const testFn = ESLMediaBreakpoints.addCustomBreakpoint;
-    expect(() => testFn('небольшой', 200, 300)).toThrowError();
-    expect(() => testFn('@XS', 200, 300)).toThrowError();
-    expect(() => testFn('', 200, 300)).toThrowError();
-    expect(() => testFn('234', 200, 300)).toThrowError();
-    expect(() => testFn('XS2', 200, 300)).not.toThrowError();
-    expect(ESLMediaBreakpoints.getBreakpoint('some')).toBeUndefined();
-  });
-
-  test.each([
-    {query: '@md', queryString: '(min-width: 992px) and (max-width: 1199px)'},
-    {
-      query: '@lg, @sm',
-      queryString: '(min-width: 1200px) and (max-width: 1599px), (min-width: 768px) and (max-width: 991px)'
-    },
-    {query: '@+sm,@-md', queryString: '(min-width: 768px),(max-width: 1199px)'},
-    {query: '@mobile', queryString: '@mobile'},
-    {query: '', queryString: ''},
-    {query: '@MOBILE', queryString: '@MOBILE'},
-    {query: '@XL', queryString: '(min-width: 1600px) and (max-width: 999999px)'},
-    {query: '@small', queryString: '(min-width: 400px) and (max-width: 500px)'},
-    {query: '@+small', queryString: '(min-width: 400px)'},
-    {query: '@-small', queryString: '(max-width: 500px)'},
-    {query: '@Mobile, @xl', queryString: '@Mobile, (min-width: 1600px) and (max-width: 999999px)'},
-    {query: '@Xl, @MOBILE', queryString: '(min-width: 1600px) and (max-width: 999999px), @MOBILE'}
-  ])('Apply tests for %p breakpoint', ({query, queryString}) => {
-    expect(ESLMediaBreakpoints.apply(query)).toBe(queryString);
-  })
-});
diff --git a/src/modules/esl-media-query/test/esl-media-query.test.ts b/src/modules/esl-media-query/test/esl-media-query.test.ts
index 454861ed5..e65ffa8d7 100644
--- a/src/modules/esl-media-query/test/esl-media-query.test.ts
+++ b/src/modules/esl-media-query/test/esl-media-query.test.ts
@@ -1,9 +1,10 @@
+import {DDMock} from '../../esl-utils/test/deviceDetector.mock';
+import {ESLMediaQuery, ESLScreenBreakpoints, ESLScreenDPR} from '../core';
+
 /**
  * ESL Media Query tests
  *
  * Require mocked DeviceDetector and matchMedia
- * Constructor tests compare expected results with called args of mocked matchMedia func
- * ESL Media Query cashes queries, so make sure all queries come to matchMedia are different
  *
  * Realization of ESL Media Query implies next query parsing cases
  * (examples for desktop device):
@@ -14,51 +15,144 @@
  * - '@desktop, @1x' --> ', (min-resolution: 96.0dpi)'
  * - '@1x, @desktop, @sm' --> '(min-resolution: 96.0dpi), , (min-width: 768px) and (max-width: 991px)'
  */
+describe('ESLMediaQuery', () => {
+  beforeAll(() => {
+    ESLScreenBreakpoints.add('small', 100, 200);
+  });
 
-import {mmMock} from "../../esl-utils/test/matchMedia.mock";
-import {DDMock} from "../../esl-utils/test/deviceDetector.mock";
+  describe('Trivial cases', () => {
+    test.each([
+      ['', ''],
+      ['all', 'all'],
+      ['not all', 'not all']
+    ])('Apply tests for %p breakpoint', (query, expected) => {
+      expect(ESLMediaQuery.from(query).toString()).toBe(expected);
+    });
 
-import {ESLMediaBreakpoints} from '../core/esl-media-breakpoints';
-import {ESLMediaQuery} from '../core/esl-media-query';
+    test('Const query instances', () => {
+      expect(ESLMediaQuery.from('all')).toBe(ESLMediaQuery.ALL);
+      expect(ESLMediaQuery.from('not all')).toBe(ESLMediaQuery.NOT_ALL);
+    });
+  });
 
-describe('ESLMediaQuery tests', () => {
-  beforeAll(() => {
-    ESLMediaBreakpoints.addCustomBreakpoint('small', 100, 200);
+  describe('Breakpoint shortcuts', () => {
+    test.each([
+      ['@xs', '(min-width: 1px) and (max-width: 767px)'],
+      ['@sm', '(min-width: 768px) and (max-width: 991px)'],
+      ['@md', '(min-width: 992px) and (max-width: 1199px)'],
+      ['@lg', '(min-width: 1200px) and (max-width: 1599px)'],
+      ['@xl', '(min-width: 1600px) and (max-width: 999999px)'],
+      ['@XL', '(min-width: 1600px) and (max-width: 999999px)'],
+      ['@small', '(min-width: 100px) and (max-width: 200px)'],
+      ['@+sm', '(min-width: 768px)'],
+      ['@-md', '(max-width: 1199px)']
+    ])('%s -> %s', (query, expected) => {
+      expect(ESLMediaQuery.from(query).toString()).toBe(expected);
+    });
+
+    test.each([
+      ['not @small', 'not ((min-width: 100px) and (max-width: 200px))'],
+      ['not @+sm', 'not (min-width: 768px)'],
+      ['not @-md', 'not (max-width: 1199px)'],
+    ])('Inverted replacement %p', (query, expected) => {
+      expect(ESLMediaQuery.from(query).toString()).toBe(expected);
+    });
+
+    test.each([
+      ['@+sm, @-md', '(min-width: 768px), (max-width: 1199px)'],
+      ['@+sm or @-md', '(min-width: 768px), (max-width: 1199px)'],
+      ['@+sm and @-md', '(min-width: 768px) and (max-width: 1199px)'],
+    ])('Inverted replacement %p', (query, expected) => {
+      expect(ESLMediaQuery.from(query).toString()).toBe(expected);
+    });
   });
 
-  describe('Constructor tests', () => {
+  describe('DPR shortcuts', () => {
     test.each([
-      ['@0x', false, '(min-resolution: 0.0dpi)'],
-      ['@1x', false, '(min-resolution: 96.0dpi)'],
-      ['@0.3x', false, '(min-resolution: 28.8dpi)'],
-      ['@1.6x', false, '(min-resolution: 153.6dpi)'],
-      ['@1x', true, '(-webkit-min-device-pixel-ratio: 1)'],
-      ['@2.3x', true, '(-webkit-min-device-pixel-ratio: 2.3)'],
-    ])('Dpr %s replacement', (query, isSafari, expected) => {
-      DDMock.isSafari = isSafari;
-      const mq = new ESLMediaQuery(query);
-      expect(mmMock).toHaveBeenLastCalledWith(expected);
+      ['@0x', '(min-resolution: 0.0dpi)'],
+      ['@1x', '(min-resolution: 96.0dpi)'],
+      ['@01x', '(min-resolution: 96.0dpi)'],
+      ['@.3x', '(min-resolution: 28.8dpi)'],
+      ['@0.3x', '(min-resolution: 28.8dpi)'],
+      ['@1.6x', '(min-resolution: 153.6dpi)'],
+    ])('Dpr %s replacement', (query, expected) => {
+      expect(ESLMediaQuery.from(query).toString()).toBe(expected);
+    });
+
+    test.each([
+      ['@1x', '(-webkit-min-device-pixel-ratio: 1)'],
+      ['@2.3x', '(-webkit-min-device-pixel-ratio: 2.3)'],
+    ])('Dpr %s replacement', (query, expected) => {
+      DDMock.isSafari = true;
+      expect(ESLMediaQuery.from(query).toString()).toBe(expected);
       DDMock.isSafari = false;
     });
 
+    test('Bot DPR override test', () => {
+      DDMock.isBot = true;
+      ESLScreenDPR.ignoreBotsDpr = true;
+
+      expect(ESLMediaQuery.from('@1x').toString()).toBe('(min-resolution: 96.0dpi)');
+      expect(ESLMediaQuery.from('@3x, @2.4x').toString()).toBe('not all');
+
+      DDMock.isBot = false;
+      ESLScreenDPR.ignoreBotsDpr = false;
+    });
+  });
+
+  describe('Device type shortcut', () => {
     test.each([
-      ['@xs', '(min-width: 1px) and (max-width: 767px)'],
-      ['@small', '(min-width: 100px) and (max-width: 200px)'],
-    ])('Bp %s replacement', (query, expected) => {
-      const mq = new ESLMediaQuery(query);
-      expect(mmMock).toHaveBeenLastCalledWith(expected);
+      ['@mobile', 'not all'],
+      ['@Mobile', 'not all'],
+      ['@MOBILE', 'not all'],
+      ['@desktop', 'all'],
+      ['@Desktop', 'all'],
+      ['@DESKTOP', 'all'],
+    ])('Query check for %p', (query, expected) => {
+      expect(ESLMediaQuery.from(query).toString()).toBe(expected);
     });
 
-    test('Device replacement', () => {
-      const mqMobile = new ESLMediaQuery('@mobile');
-      expect(mmMock).toHaveBeenLastCalledWith('not all');
-      const mqDesktop = new ESLMediaQuery('@desktop');
-      expect(mmMock).toHaveBeenLastCalledWith('all');
+    test.each([
+      ['not @mobile', 'all'],
+      ['not @desktop', 'not all']
+    ])('Inverted query check for %p', (query, expected) => {
+      expect(ESLMediaQuery.from(query).toString()).toBe(expected);
     });
-  })
+  });
 
-  describe('Complex tests', () => {
-    describe('And combination', () => {
+  describe('Browser check shortcut', () => {
+    test.each([
+      ['@ie', 'not all'],
+      ['@edge', 'not all'],
+      ['@gecko', 'not all'],
+      ['@blink', 'not all'],
+      ['@safari', 'not all']
+    ])('Query check for %p', (query, expected) => {
+      expect(ESLMediaQuery.from(query).toString()).toBe(expected);
+    });
+
+    test.each([
+      ['not @ie', 'all']
+    ])('Inverted query check for %p', (query, expected) => {
+      expect(ESLMediaQuery.from(query).toString()).toBe(expected);
+    });
+  });
+
+  describe('Edge cases', () => {
+    test.each([
+      ['@smnot', '@smnot'],
+      ['@+smnot', '@+smnot'],
+      ['2x', '2x'],
+      ['@-2x', '@-2x'],
+      ['@1.5', '@1.5'],
+      ['not valid', 'not valid']
+    ])('Apply tests for %p breakpoint', (query, expected) => {
+      expect(ESLMediaQuery.from(query).toString()).toBe(expected);
+    });
+  });
+
+  describe('Logical operations', () => {
+    describe('Conjunction', () => {
       test.each([
         ['@desktop and @2x', '(min-resolution: 192.0dpi)'],
         ['@1x and @+sm', '(min-resolution: 96.0dpi) and (min-width: 768px)'],
@@ -66,76 +160,42 @@ describe('ESLMediaQuery tests', () => {
         ['@2.5x and @desktop and @+sm', '(min-resolution: 240.0dpi) and (min-width: 768px)'],
         ['@desktop and @+sm and @2.5x', '(min-width: 768px) and (min-resolution: 240.0dpi)'],
         ['@+sm and @2.6x and @desktop', '(min-width: 768px) and (min-resolution: 249.6dpi)'],
-      ])('Combination %s', (query, expected) => {
-        const mq = new ESLMediaQuery(query);
-        expect(mmMock).toHaveBeenLastCalledWith(expected);
+      ])('%s to %s', (query, expected) => {
+        expect(ESLMediaQuery.from(query).toString()).toBe(expected);
       });
-    })
+    });
 
-    describe('Or combination', () => {
+    describe('Disjunction', (() => {
       test.each([
+        ['@+xs, @+sm', '(min-width: 1px), (min-width: 768px)'],
+        ['@1x, @2x', '(min-resolution: 96.0dpi), (min-resolution: 192.0dpi)'],
         ['@1x, @+sm', '(min-resolution: 96.0dpi), (min-width: 768px)'],
         ['@+sm, @1x', '(min-width: 768px), (min-resolution: 96.0dpi)'],
-      ])('Combination %s', (query, expected) => {
-        const mq = new ESLMediaQuery(query);
-        expect(mmMock).toHaveBeenLastCalledWith(expected);
-      })
-    })
+      ])('%s to %s', (query, expected) => {
+        expect(ESLMediaQuery.from(query).toString()).toBe(expected);
+      });
+    }));
+  });
 
-    describe('Multiple basis terms', (() => {
+  describe('Optimization', () => {
+    describe('Conjunction', () => {
       test.each([
-        ['@+xs, @+sm', '(min-width: 1px), (min-width: 768px)'],
-        ['@1x, @2x', '(min-resolution: 96.0dpi), (min-resolution: 192.0dpi)'],
-      ])('%s', (query, expected) => {
-        const mq = new ESLMediaQuery(query);
-        expect(mmMock).toHaveBeenLastCalledWith(expected);
-      })
-    }))
-  })
-
-  describe('Breaking tests', () => {
-    test.each([
-      ['2x', '2x'],
-      ['@.2x', '@.2x'],
-      ['@-2x', '@-2x'],
-      ['@02x', '@02x'],
-      ['@1.5', '@1.5'],
-      ['@1.23x', '@1.23x'],
-      ['not valid', 'not valid']
-    ])('%s', (query, expected) => {
-      const mq = new ESLMediaQuery(query);
-      expect(mmMock).toHaveBeenLastCalledWith(expected);
-    })
-  })
-
-  describe('API tests', () => {
-    test('Listeners', () => {
-      const mq = new ESLMediaQuery('@xs');
-      const listener = jest.fn();
-
-      mq.addListener(listener);
-      mq.removeListener(listener);
-
-      expect(mq.query.addEventListener).toHaveBeenCalled();
-      expect(mq.query.removeEventListener).toHaveBeenCalled();
-
-      expect(mq.query.addListener).not.toBeCalled();
-      expect(mq.query.removeListener).not.toBeCalled();
+        ['@1x and @+sm and all', '(min-resolution: 96.0dpi) and (min-width: 768px)'],
+        ['@1x and @+sm, not all', '(min-resolution: 96.0dpi) and (min-width: 768px)'],
+      ])('Combination %s', (query, expected) => {
+        expect(ESLMediaQuery.from(query).toString()).toBe(expected);
+      });
     });
+  });
 
-    test('toString', () => {
-      expect(new ESLMediaQuery('@1x, @+sm').toString()).toContain('(min-resolution: 96.0dpi), (min-width: 768px)')
+  describe('Cache test', () => {
+    test.each([
+      ['all'],
+      ['not all'],
+      ['@xs'],
+      ['@xs or @xl']
+    ])('Apply tests for %p breakpoint', (query) => {
+      expect(ESLMediaQuery.for(query)).toBe(ESLMediaQuery.for(query));
     });
-  })
-
-  test('Bot DPR override test', () => {
-    ESLMediaQuery.ignoreBotsDpr = true;
-    DDMock.isBot = true;
-
-    const mq = new ESLMediaQuery('@3x, @2.4x');
-    expect(mmMock).toHaveBeenLastCalledWith('not all, not all');
-
-    DDMock.isBot = false;
-    ESLMediaQuery.ignoreBotsDpr = false;
-  })
-})
+  });
+});
diff --git a/src/modules/esl-media-query/test/esl-media-rule-list.test.ts b/src/modules/esl-media-query/test/esl-media-rule-list.test.ts
new file mode 100644
index 000000000..cd5a57fb6
--- /dev/null
+++ b/src/modules/esl-media-query/test/esl-media-rule-list.test.ts
@@ -0,0 +1,142 @@
+import {ESLMediaRuleList} from '../core/esl-media-rule-list';
+import {getMatchMediaMock} from '../../esl-utils/test/matchMedia.mock';
+import {ESLScreenBreakpoints} from '../core/common/screen-breakpoint';
+
+describe('ESLMediaRuleList', () => {
+  const mockSmMatchMedia = getMatchMediaMock(ESLScreenBreakpoints.get('sm')!.mediaQuery);
+  const mockMdMatchMedia = getMatchMediaMock(ESLScreenBreakpoints.get('md')!.mediaQuery);
+
+  test('123 (default)', () => {
+    const mrl = ESLMediaRuleList.parse('123');
+    expect(mrl.rules.length).toBe(1);
+    expect(mrl.default).toBeDefined();
+    expect(mrl.default.default).toBe(true);
+    expect(mrl.default.payload).toBe('123');
+
+    expect(mrl.active).toBeDefined();
+    expect(mrl.activeValue).toBe('123');
+  });
+
+  test('@sm => 1', () => {
+    const mrl = ESLMediaRuleList.parse('@sm => 1');
+    expect(mrl.rules.length).toBe(1);
+    expect(mrl.default).not.toBeDefined();
+
+    mockSmMatchMedia.matches = false;
+    expect(mrl.active).toBeDefined();
+    expect(mrl.activeValue).toBe(undefined);
+
+    mockSmMatchMedia.matches = true;
+    expect(mrl.active).toBeDefined();
+    expect(mrl.activeValue).toBe('1');
+
+    mockSmMatchMedia.matches = false;
+    expect(mrl.active).toBeDefined();
+    expect(mrl.activeValue).toBe(undefined);
+  });
+
+  test('1 | @sm => 2 | @md => 3', () => {
+    const mrl = ESLMediaRuleList.parse('1 | @sm => 2 | @md => 3');
+    expect(mrl.rules.length).toBe(3);
+    expect(mrl.default).toBeDefined();
+
+    mockSmMatchMedia.matches = false;
+    mockMdMatchMedia.matches = false;
+    expect(mrl.active).toBeDefined();
+    expect(mrl.activeValue).toBe('1');
+
+    mockSmMatchMedia.matches = true;
+    mockMdMatchMedia.matches = false;
+    expect(mrl.activeValue).toBe('2');
+
+    mockSmMatchMedia.matches = false;
+    mockMdMatchMedia.matches = true;
+    expect(mrl.activeValue).toBe('3');
+
+    mockSmMatchMedia.matches = true;
+    mockMdMatchMedia.matches = true;
+    expect(mrl.activeValue).toBe('3');
+  });
+
+  test('1 | @sm and @md => 2', () => {
+    const mrl = ESLMediaRuleList.parse('1 | @sm and @md => 2');
+    const listener = jest.fn();
+
+    expect(mrl.rules.length).toBe(2);
+
+    mrl.addListener(listener);
+
+    mockSmMatchMedia.matches = false;
+    mockMdMatchMedia.matches = false;
+    expect(mrl.activeValue).toBe('1');
+
+    mockSmMatchMedia.matches = true;
+    mockMdMatchMedia.matches = false;
+    expect(mrl.activeValue).toBe('1');
+
+    mockSmMatchMedia.matches = false;
+    mockMdMatchMedia.matches = true;
+    expect(mrl.activeValue).toBe('1');
+    expect(listener).not.toBeCalled();
+
+    mockSmMatchMedia.matches = true;
+    mockMdMatchMedia.matches = true;
+    expect(mrl.activeValue).toBe('2');
+    expect(listener).toBeCalled();
+  });
+
+  test('1 | @sm or @md => 2', () => {
+    const mrl = ESLMediaRuleList.parse('1 | @sm or @md => 2');
+    const listener = jest.fn();
+
+    expect(mrl.rules.length).toBe(2);
+
+    mrl.addListener(listener);
+
+    mockSmMatchMedia.matches = false;
+    mockMdMatchMedia.matches = false;
+    expect(mrl.activeValue).toBe('1');
+    expect(listener).not.toBeCalled();
+
+    mockSmMatchMedia.matches = true;
+    mockMdMatchMedia.matches = false;
+    expect(mrl.activeValue).toBe('2');
+    expect(listener).toBeCalled();
+
+    mockSmMatchMedia.matches = false;
+    mockMdMatchMedia.matches = true;
+    expect(mrl.activeValue).toBe('2');
+
+    mockSmMatchMedia.matches = true;
+    mockMdMatchMedia.matches = true;
+    expect(mrl.activeValue).toBe('2');
+  });
+
+  test('1|2|3 with all|@sm|@md mask', () => {
+    const mrl = ESLMediaRuleList.parseTuple('1|2|3', 'all|@sm|@md');
+    expect(mrl.rules.length).toBe(3);
+    expect(mrl.default).not.toBeDefined();
+
+    mockSmMatchMedia.matches = false;
+    mockMdMatchMedia.matches = false;
+    expect(mrl.active).toBeDefined();
+    expect(mrl.activeValue).toBe('1');
+
+    mockSmMatchMedia.matches = true;
+    mockMdMatchMedia.matches = false;
+    expect(mrl.activeValue).toBe('2');
+
+    mockSmMatchMedia.matches = false;
+    mockMdMatchMedia.matches = true;
+    expect(mrl.activeValue).toBe('3');
+
+    mockSmMatchMedia.matches = true;
+    mockMdMatchMedia.matches = true;
+    expect(mrl.activeValue).toBe('3');
+  });
+
+  afterEach(() => {
+    mockSmMatchMedia.matches = false;
+    mockMdMatchMedia.matches = false;
+  });
+});
diff --git a/src/modules/esl-media-query/test/esl-media-rule.test.ts b/src/modules/esl-media-query/test/esl-media-rule.test.ts
new file mode 100644
index 000000000..f51bdd9a7
--- /dev/null
+++ b/src/modules/esl-media-query/test/esl-media-rule.test.ts
@@ -0,0 +1,102 @@
+import {ESLMediaRuleList} from '../core/esl-media-rule-list';
+import {ESLMediaRule} from '../core/esl-media-rule';
+import {ALL} from '../core/conditions/media-query-base';
+
+describe('ESLMediaRule', () => {
+
+  describe('parse', () => {
+    test('all => 1', () => {
+      const serialized = 'all => 1';
+      const rule = ESLMediaRule.parse(serialized, ESLMediaRuleList.STRING_PARSER);
+      expect(rule).toBeDefined();
+      expect(rule!.default).toBe(false);
+      expect(rule!.matches).toBe(true);
+      expect(rule!.payload).toBe('1');
+      expect(rule!.toString()).toEqual(serialized);
+    });
+
+    test('not all => 1', () => {
+      const serialized = 'not all => 1';
+      const rule = ESLMediaRule.parse(serialized, ESLMediaRuleList.STRING_PARSER);
+      expect(rule).toBeDefined();
+      expect(rule!.default).toBe(false);
+      expect(rule!.matches).toBe(false);
+      expect(rule!.payload).toBe('1');
+      expect(rule!.toString()).toEqual(serialized);
+    });
+
+    test('@sm => {a: 1}', () => {
+      const serialized = '@sm => {a: 1}';
+      const rule = ESLMediaRule.parse(serialized, ESLMediaRuleList.OBJECT_PARSER);
+      expect(rule).toBeDefined();
+      expect(rule!.default).toBe(false);
+      expect(rule!.payload).toEqual({a: 1});
+    });
+
+    test('@sm => {a: 1', () => {
+      const spy = jest.spyOn(console, 'warn').mockImplementationOnce(() => void 0);
+      const serialized = '@sm => {a: 1';
+      const rule = ESLMediaRule.parse(serialized, ESLMediaRuleList.OBJECT_PARSER);
+      expect(rule).not.toBeDefined();
+      expect(spy).toBeCalled();
+    });
+
+    test('hello', () => {
+      const serialized = 'hello';
+      const rule = ESLMediaRule.parse(serialized, ESLMediaRuleList.STRING_PARSER);
+      expect(rule).toBeDefined();
+      expect(rule!.default).toBe(true);
+      expect(rule!.payload).toEqual('hello');
+    });
+
+    test('123 (custom parser)', () => {
+      const serialized = '123';
+      const rule = ESLMediaRule.parse(serialized, (str) => +str);
+      expect(rule).toBeDefined();
+      expect(rule!.default).toBe(true);
+      expect(rule!.payload).toEqual(123);
+    });
+  });
+
+  test('empty', () => {
+    const rule = ESLMediaRule.empty();
+    expect(rule).toBeDefined();
+    expect(rule.default).toBe(false);
+    expect(rule.matches).toBe(true);
+    expect(rule.payload).toEqual(undefined);
+  });
+
+  test('all', () => {
+    const obj = {};
+    const rule = ESLMediaRule.all(obj);
+    expect(rule).toBeDefined();
+    expect(rule.default).toBe(false);
+    expect(rule.matches).toBe(true);
+    expect(rule.payload).toEqual(obj);
+  });
+
+  test('default', () => {
+    const obj = {};
+    const rule = ESLMediaRule.default(obj);
+    expect(rule).toBeDefined();
+    expect(rule.default).toBe(true);
+    expect(rule.matches).toBe(true);
+    expect(rule.payload).toEqual(obj);
+  });
+
+  describe('subscription', () => {
+    const callback = () => void 0;
+    const testRule = ESLMediaRule.parse('all => 1', ESLMediaRuleList.STRING_PARSER) as ESLMediaRule<string>;
+
+    test('addListener', () => {
+      const spyAdd = jest.spyOn(ALL, 'addListener');
+      testRule.addListener(callback);
+      expect(spyAdd).toBeCalled();
+    });
+    test('removeListener', () => {
+      const spyRemove = jest.spyOn(ALL, 'removeListener');
+      testRule.removeListener(callback);
+      expect(spyRemove).toBeCalled();
+    });
+  });
+});
diff --git a/src/modules/esl-media-query/test/esl-screen-breakpoint.test.ts b/src/modules/esl-media-query/test/esl-screen-breakpoint.test.ts
new file mode 100644
index 000000000..9f595eb68
--- /dev/null
+++ b/src/modules/esl-media-query/test/esl-screen-breakpoint.test.ts
@@ -0,0 +1,34 @@
+import {ESLScreenBreakpoints} from '../core';
+
+describe('ESLScreenBreakpoint tests', () => {
+  test('Get all breakpoints (default)', () => {
+    expect(new Set(ESLScreenBreakpoints.names)).toEqual(new Set(['xs', 'sm', 'md', 'lg', 'xl']));
+  });
+
+  test('add and get custom breakpoints tests', () => {
+    expect(ESLScreenBreakpoints.add('SMALL', 300, 400)).toBeUndefined();
+    expect(ESLScreenBreakpoints.get('SMALL')?.mediaQuery).toBe('(min-width: 300px) and (max-width: 400px)');
+    expect(ESLScreenBreakpoints.get('SMALL')?.mediaQueryGE).toBe('(min-width: 300px)');
+    expect(ESLScreenBreakpoints.get('SMALL')?.mediaQueryLE).toBe('(max-width: 400px)');
+    expect(ESLScreenBreakpoints.add('Small', 400, 500)?.mediaQuery)
+      .toBe('(min-width: 300px) and (max-width: 400px)');
+    expect(ESLScreenBreakpoints.get('small')?.mediaQuery).toBe('(min-width: 400px) and (max-width: 500px)');
+    ESLScreenBreakpoints.add('xs', 11, 22);
+    expect(ESLScreenBreakpoints.get('xs')?.mediaQuery).toBe('(min-width: 11px) and (max-width: 22px)');
+  });
+
+  test('Get all breakpoints after adding custom', () => {
+    expect(new Set(ESLScreenBreakpoints.names))
+      .toEqual(new Set(['xs', 'sm', 'md', 'lg', 'xl', 'small']));
+  });
+
+  test('Error tests', () => {
+    const testFn = ESLScreenBreakpoints.add;
+    expect(() => testFn('небольшой', 200, 300)).toThrowError();
+    expect(() => testFn('@XS', 200, 300)).toThrowError();
+    expect(() => testFn('', 200, 300)).toThrowError();
+    expect(() => testFn('234', 200, 300)).toThrowError();
+    expect(() => testFn('XS2', 200, 300)).not.toThrowError();
+    expect(ESLScreenBreakpoints.get('some')).toBeUndefined();
+  });
+});
diff --git a/src/modules/esl-utils/misc/test/object.test.ts b/src/modules/esl-utils/misc/test/object.test.ts
index b6a9b0e66..711ed84d4 100644
--- a/src/modules/esl-utils/misc/test/object.test.ts
+++ b/src/modules/esl-utils/misc/test/object.test.ts
@@ -1,4 +1,4 @@
-import {defined, deepCompare, getPropertyDescriptor, get, set, copyDefinedKeys, copy, omit} from '../object';
+import {defined, deepCompare, getPropertyDescriptor, get, set, copyDefinedKeys, copy, omit, deepMerge} from '../object';
 
 describe('misc/object', () => {
   describe('deepCompare', () => {
@@ -144,7 +144,6 @@ describe('misc/object', () => {
   });
 
   describe('omit', () => {
-    const predicate = (key: string) => !key.startsWith('_');
     test.each([
       [undefined, ['prop'], {}],
       [null, ['prop'], {}],
@@ -189,4 +188,20 @@ describe('misc/object', () => {
       expect(targ).toEqual(expVal)
     });
   });
+
+  describe('deepMerge', () => {
+    test.each([
+      [{}, {}, {}],
+      [{a: [1]}, {a: 1}, {a: 1}],
+      [{a: 1}, {a: ['2']}, {a: ['2']}],
+      [{a: 1, b: 1}, {a: 2, c: 2}, {a: 2, b: 1, c: 2}],
+      [{a: {a: 1, b: 1}, b: 1}, {a: {a: 3, c: 3}}, {a: {a: 3, b: 1, c: 3}, b: 1}],
+      [{a: 1}, {b: 2}, {c: 3}, {a: 1, b: 2, c: 3}],
+      [{a: 'value', b: 'value'}, {a: null, b: ''}, {a: null, b: ''}],
+      [{a: [1, 2, {a: 1, b: 1}]}, {a: [3, 4, {a: 2, b: 2}]}, {a: [3, 4, {a: 2, b: 2}]}]
+    ])('get key "%s" from %p', (...args: any[]) => {
+      const result = args.pop();
+      expect(deepMerge(...args)).toEqual(result);
+    });
+  });
 });
diff --git a/src/modules/esl-utils/test/matchMedia.mock.ts b/src/modules/esl-utils/test/matchMedia.mock.ts
index e1ac125e5..ab7630953 100644
--- a/src/modules/esl-utils/test/matchMedia.mock.ts
+++ b/src/modules/esl-utils/test/matchMedia.mock.ts
@@ -1,15 +1,45 @@
-export const mmMock = jest.fn(query => ({
-  matches: true,
-  media: query,
-  onchange: null,
-  addListener: jest.fn(), // deprecated
-  removeListener: jest.fn(), // deprecated
-  addEventListener: jest.fn(),
-  removeEventListener: jest.fn(),
-  dispatchEvent: jest.fn(),
-}));
-
-Object.defineProperty(window, 'matchMedia', {
-  writable: true,
-  value: mmMock,
-});
+import {memoizeFn} from '../misc/memoize';
+
+export class MatchMediaMock implements MediaQueryList {
+  private _matches;
+  private _listeners = new Set<VoidFunction>();
+
+  constructor(
+    public readonly media: string
+  ) {
+    this._matches = (media === '' || media === 'all');
+  }
+
+  get matches() { return this._matches; }
+  set matches(match) { this.set(match); }
+  set(matches: boolean, notify = (matches === this._matches)) {
+    this._matches = matches;
+    if (!notify) return;
+    this._listeners.forEach((cb) => cb());
+  }
+
+  onchange() {}
+  dispatchEvent(event: Event) {
+    this._listeners.forEach((cb) => cb());
+    return false;
+  }
+  addListener(cb: any) {
+    this._listeners.add(cb);
+  }
+  removeListener(cb: any) {
+    this._listeners.delete(cb);
+  }
+  addEventListener(event: string, cb: any) {
+    this.addListener(cb);
+  }
+  removeEventListener(event: string, cb: any) {
+    this.removeListener(cb);
+  }
+}
+
+export const getMatchMediaMock = memoizeFn(
+  (query: string) => new MatchMediaMock(query)
+);
+export const matchMediaMock = jest.fn((q) => getMatchMediaMock(q.trim()));
+
+Object.defineProperty(window, 'matchMedia', { writable: true, value: matchMediaMock });