Repository: microsoft/griffel
Pull request #98 (2022-04-21 10:22:54)

feat(Devtools): UI to display monolithic css rules per makeStyle slot 
Add real UI for griffel devtools.
<img width="422" alt="Screenshot 2022-04-21 at 11 35 28" src="https://user-images.githubusercontent.com/28751745/164433695-fca36090-3f65-4a39-ba29-d77cde16a9f7.png">

- show all makeStyles calls with slot name and monolithic css rules
    - atomic css rules from devtools result are processed into monolithic css objects using `stylis`
- displayed overridden css rules; clicking on them highlights the overriding rules


TODOs on devtools UI
- Show code highlights for css rules
- Add search function
- Click slot name expand/collapse rules within
- Show className for each rule
- Collapse shorthands in devtools UI

TODOs on devtools data:
- currently devtools is always disabled from @griffel/core, find out a way to enable it that is compatible with SSR
- Add sourcemap to devtools data; jump to source code from devtools UI

github-actions[bot]: ## 📊 Bundle size report

<sub>🤖 This report was generated against <a href='https://github.com/microsoft/griffel/commit/09eb2760eabcb6abc3d21b7333d0ec14f6c4b5d2'>09eb2760eabcb6abc3d21b7333d0ec14f6c4b5d2</a></sub>

<!-- Sticky Pull Request Comment -->

Patch:
diff --git a/apps/website/docs/react/api/make-styles.md b/apps/website/docs/react/api/make-styles.md
index 5577977..f341ba6 100644
--- a/apps/website/docs/react/api/make-styles.md
+++ b/apps/website/docs/react/api/make-styles.md
@@ -122,7 +122,7 @@ html[data-whatintent='mouse'] .f1xz3i88 {
 
 ### At-rules
 
-`@media` & `@supports` queries are also supported:
+`@media`, `@supports` & `@layer` queries are also supported:
 
 ```js
 import { makeStyles } from '@griffel/react';
@@ -131,6 +131,7 @@ const useClasses = makeStyles({
   root: {
     '@media screen and (max-width: 992px)': { color: 'orange' },
     '@supports (display: grid)': { color: 'red' },
+    '@layer utility': { marginBottom: '1em' }
   },
 });
 ```
@@ -148,6 +149,12 @@ const useClasses = makeStyles({
     color: red;
   }
 }
+
+@layer utility {
+  .f2d3jla {
+    margin-bottom: 1em;
+  }
+}
 ```
 
 ### `@keyframes` (animations)
diff --git a/beachball.config.js b/beachball.config.js
index 1472bfb..cd0b916 100644
--- a/beachball.config.js
+++ b/beachball.config.js
@@ -5,5 +5,14 @@
  */
 module.exports = {
   gitTags: false,
+  ignorePatterns: [
+    '**/__fixtures__/**',
+    '**/*.test.{ts,tsx}',
+    '**/*.stories.tsx',
+    '**/.eslintrc.json',
+    '**/jest.config.js',
+    '**/project.json',
+    '**/README.md',
+  ],
   hooks: require('./beachball.hooks'),
 };
diff --git a/change/@griffel-babel-preset-41b92af0-6e5b-4e53-b341-0bd5b8407b76.json b/change/@griffel-babel-preset-41b92af0-6e5b-4e53-b341-0bd5b8407b76.json
new file mode 100644
index 0000000..28279be
--- /dev/null
+++ b/change/@griffel-babel-preset-41b92af0-6e5b-4e53-b341-0bd5b8407b76.json
@@ -0,0 +1,7 @@
+{
+  "type": "patch",
+  "comment": "fix: handle duplicated import sources properly",
+  "packageName": "@griffel/babel-preset",
+  "email": "rezha@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/change/@griffel-core-8a3b4de2-ff52-4c79-aa40-84cdf0bb669e.json b/change/@griffel-core-8a3b4de2-ff52-4c79-aa40-84cdf0bb669e.json
new file mode 100644
index 0000000..f13a16d
--- /dev/null
+++ b/change/@griffel-core-8a3b4de2-ff52-4c79-aa40-84cdf0bb669e.json
@@ -0,0 +1,7 @@
+{
+  "type": "none",
+  "comment": "add a comment to exports",
+  "packageName": "@griffel/core",
+  "email": "olfedias@microsoft.com",
+  "dependentChangeType": "none"
+}
diff --git a/change/@griffel-core-9132796e-b0a7-46a9-b70f-72bef61024b9.json b/change/@griffel-core-9132796e-b0a7-46a9-b70f-72bef61024b9.json
new file mode 100644
index 0000000..27a4ba3
--- /dev/null
+++ b/change/@griffel-core-9132796e-b0a7-46a9-b70f-72bef61024b9.json
@@ -0,0 +1,7 @@
+{
+  "type": "minor",
+  "comment": "feat: add support for @layer",
+  "packageName": "@griffel/core",
+  "email": "mgodbolt@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/change/@griffel-core-e9730721-98ad-4971-8976-6f934c35cf1f.json b/change/@griffel-core-e9730721-98ad-4971-8976-6f934c35cf1f.json
new file mode 100644
index 0000000..54e01ad
--- /dev/null
+++ b/change/@griffel-core-e9730721-98ad-4971-8976-6f934c35cf1f.json
@@ -0,0 +1,7 @@
+{
+  "type": "minor",
+  "comment": "Adds inset shorthand",
+  "packageName": "@griffel/core",
+  "email": "bsunderhus@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/change/@griffel-eslint-plugin-0f2d3131-9acc-46f2-b1a1-b7c23c1e852a.json b/change/@griffel-eslint-plugin-0f2d3131-9acc-46f2-b1a1-b7c23c1e852a.json
new file mode 100644
index 0000000..4d9bbb5
--- /dev/null
+++ b/change/@griffel-eslint-plugin-0f2d3131-9acc-46f2-b1a1-b7c23c1e852a.json
@@ -0,0 +1,7 @@
+{
+  "type": "major",
+  "comment": "initial release",
+  "packageName": "@griffel/eslint-plugin",
+  "email": "olfedias@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/change/@griffel-react-6d0f6f43-4864-4ba4-b292-8e46b9d1d20f.json b/change/@griffel-react-6d0f6f43-4864-4ba4-b292-8e46b9d1d20f.json
new file mode 100644
index 0000000..02b672d
--- /dev/null
+++ b/change/@griffel-react-6d0f6f43-4864-4ba4-b292-8e46b9d1d20f.json
@@ -0,0 +1,7 @@
+{
+  "type": "none",
+  "comment": "docs: add support for @layer",
+  "packageName": "@griffel/react",
+  "email": "mgodbolt@microsoft.com",
+  "dependentChangeType": "none"
+}
diff --git a/package.json b/package.json
index 71d985e..0cfdb04 100644
--- a/package.json
+++ b/package.json
@@ -103,6 +103,7 @@
     "@emotion/hash": "^0.8.0",
     "@linaria/babel-preset": "^3.0.0-beta.14",
     "@linaria/shaker": "^3.0.0-beta.14",
+    "@typescript-eslint/utils": "^5.20.0",
     "ajv": "^8.4.0",
     "csstype": "^3.0.10",
     "enhanced-resolve": "^5.8.2",
diff --git a/packages/babel-preset/__fixtures__/duplicated-imports/code.ts b/packages/babel-preset/__fixtures__/duplicated-imports/code.ts
new file mode 100644
index 0000000..8c4eb21
--- /dev/null
+++ b/packages/babel-preset/__fixtures__/duplicated-imports/code.ts
@@ -0,0 +1,12 @@
+// @ts-expect-error This module does not exist, but will be resolved via aliases
+import { createStylesA } from 'custom-package';
+// @ts-expect-error This module does not exist, but will be resolved via aliases
+import { createStylesB } from 'custom-package';
+
+export const useClassesA = createStylesA({
+  root: { color: 'red' },
+});
+
+export const useClassesB = createStylesB({
+  root: { color: 'yellow' },
+});
diff --git a/packages/babel-preset/__fixtures__/duplicated-imports/options.json b/packages/babel-preset/__fixtures__/duplicated-imports/options.json
new file mode 100644
index 0000000..d8906ae
--- /dev/null
+++ b/packages/babel-preset/__fixtures__/duplicated-imports/options.json
@@ -0,0 +1,3 @@
+{
+  "modules": [{ "moduleSource": "custom-package", "importName": "createStylesA" }]
+}
diff --git a/packages/babel-preset/__fixtures__/duplicated-imports/output.ts b/packages/babel-preset/__fixtures__/duplicated-imports/output.ts
new file mode 100644
index 0000000..16a685f
--- /dev/null
+++ b/packages/babel-preset/__fixtures__/duplicated-imports/output.ts
@@ -0,0 +1,19 @@
+// @ts-expect-error This module does not exist, but will be resolved via aliases
+import { __styles } from 'custom-package'; // @ts-expect-error This module does not exist, but will be resolved via aliases
+
+import { createStylesB } from 'custom-package';
+export const useClassesA = __styles(
+  {
+    root: {
+      sj55zd: 'fe3e8s9',
+    },
+  },
+  {
+    d: ['.fe3e8s9{color:red;}'],
+  },
+);
+export const useClassesB = createStylesB({
+  root: {
+    color: 'yellow',
+  },
+});
diff --git a/packages/babel-preset/src/transformPlugin.ts b/packages/babel-preset/src/transformPlugin.ts
index 0d7d5ba..bdf3dae 100644
--- a/packages/babel-preset/src/transformPlugin.ts
+++ b/packages/babel-preset/src/transformPlugin.ts
@@ -10,7 +10,7 @@ import { BabelPluginOptions } from './types';
 import { validateOptions } from './validateOptions';
 
 type BabelPluginState = PluginPass & {
-  importDeclarationPath?: NodePath<t.ImportDeclaration>;
+  importDeclarationPaths?: NodePath<t.ImportDeclaration>[];
   requireDeclarationPath?: NodePath<t.VariableDeclarator>;
 
   definitionPaths?: NodePath<t.ObjectExpression>[];
@@ -130,6 +130,7 @@ export const transformPlugin = declare<Partial<BabelPluginOptions>, PluginObj<Ba
     name: '@griffel/babel-plugin-transform',
 
     pre() {
+      this.importDeclarationPaths = [];
       this.definitionPaths = [];
       this.calleePaths = [];
     },
@@ -142,7 +143,7 @@ export const transformPlugin = declare<Partial<BabelPluginOptions>, PluginObj<Ba
         },
 
         exit(path, state) {
-          if (!state.importDeclarationPath && !state.requireDeclarationPath) {
+          if (state.importDeclarationPaths!.length === 0 && !state.requireDeclarationPath) {
             return;
           }
 
@@ -172,9 +173,9 @@ export const transformPlugin = declare<Partial<BabelPluginOptions>, PluginObj<Ba
             });
           }
 
-          if (state.importDeclarationPath) {
-            const specifiers = state.importDeclarationPath.get('specifiers');
-            const source = state.importDeclarationPath.get('source');
+          state.importDeclarationPaths!.forEach(importDeclarationPath => {
+            const specifiers = importDeclarationPath.get('specifiers');
+            const source = importDeclarationPath.get('source');
 
             specifiers.forEach(specifier => {
               if (specifier.isImportSpecifier()) {
@@ -194,7 +195,7 @@ export const transformPlugin = declare<Partial<BabelPluginOptions>, PluginObj<Ba
                 }
               }
             });
-          }
+          });
 
           if (state.calleePaths) {
             state.calleePaths.forEach(calleePath => {
@@ -207,7 +208,7 @@ export const transformPlugin = declare<Partial<BabelPluginOptions>, PluginObj<Ba
       // eslint-disable-next-line @typescript-eslint/naming-convention
       ImportDeclaration(path, state) {
         if (hasMakeStylesImport(path, pluginOptions.modules)) {
-          state.importDeclarationPath = path;
+          state.importDeclarationPaths!.push(path);
         }
       },
 
@@ -225,7 +226,7 @@ export const transformPlugin = declare<Partial<BabelPluginOptions>, PluginObj<Ba
          *
          * @example makeStyles({})
          */
-        if (!state.importDeclarationPath) {
+        if (state.importDeclarationPaths!.length === 0) {
           return;
         }
 
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index bd186d1..4eab908 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -14,6 +14,7 @@ import {
   margin,
   padding,
   overflow,
+  inset,
 } from './shorthands/index';
 
 export const shorthands = {
@@ -30,6 +31,7 @@ export const shorthands = {
   margin,
   padding,
   overflow,
+  inset,
 };
 
 export { createDOMRenderer } from './renderer/createDOMRenderer';
@@ -65,4 +67,5 @@ export type {
   GriffelRenderer,
 } from './types';
 
+// Private exports, are used by devtools
 export type { DebugCSSRules, DebugSequence, DebugResult } from './devtools';
diff --git a/packages/core/src/renderer/rehydrateRendererCache.ts b/packages/core/src/renderer/rehydrateRendererCache.ts
index 9dd34f5..5ba2986 100644
--- a/packages/core/src/renderer/rehydrateRendererCache.ts
+++ b/packages/core/src/renderer/rehydrateRendererCache.ts
@@ -3,7 +3,7 @@ import { GriffelRenderer, StyleBucketName } from '../types';
 // Regexps to extract names of classes and animations
 // https://github.com/styletron/styletron/blob/e0fcae826744eb00ce679ac613a1b10d44256660/packages/styletron-engine-atomic/src/client/client.js#L8
 const KEYFRAMES_HYDRATOR = /@(-webkit-)?keyframes ([^{]+){((?:(?:from|to|(?:\d+\.?\d*%))\{(?:[^}])*})*)}/g;
-const AT_RULES_HYDRATOR = /@(media|supports)[^{]+\{([\s\S]+?})\s*}/g;
+const AT_RULES_HYDRATOR = /@(media|supports|layer)[^{]+\{([\s\S]+?})\s*}/g;
 const STYLES_HYDRATOR = /\.([^{:]+)(:[^{]+)?{(?:[^}]*;)?([^}]*?)}/g;
 
 const regexps: Partial<Record<StyleBucketName, RegExp>> = {
diff --git a/packages/core/src/runtime/compileCSS.ts b/packages/core/src/runtime/compileCSS.ts
index 8dfcb5b..11adaeb 100644
--- a/packages/core/src/runtime/compileCSS.ts
+++ b/packages/core/src/runtime/compileCSS.ts
@@ -8,6 +8,7 @@ export interface CompileCSSOptions {
 
   pseudo: string;
   media: string;
+  layer: string;
   support: string;
 
   property: string;
@@ -60,7 +61,7 @@ export function compileCSSRules(cssRules: string): string[] {
 }
 
 export function compileCSS(options: CompileCSSOptions): [string /* ltr definition */, string? /* rtl definition */] {
-  const { className, media, pseudo, support, property, rtlClassName, rtlProperty, rtlValue, value } = options;
+  const { className, media, layer, pseudo, support, property, rtlClassName, rtlProperty, rtlValue, value } = options;
 
   const classNameSelector = `.${className}`;
   const cssDeclaration = Array.isArray(value)
@@ -109,6 +110,10 @@ export function compileCSS(options: CompileCSSOptions): [string /* ltr definitio
     cssRule = `@media ${media} { ${cssRule} }`;
   }
 
+  if (layer) {
+    cssRule = `@layer ${layer} { ${cssRule} }`;
+  }
+
   if (support) {
     cssRule = `@supports ${support} { ${cssRule} }`;
   }
diff --git a/packages/core/src/runtime/getStyleBucketName.ts b/packages/core/src/runtime/getStyleBucketName.ts
index ac4bfa3..1bab581 100644
--- a/packages/core/src/runtime/getStyleBucketName.ts
+++ b/packages/core/src/runtime/getStyleBucketName.ts
@@ -40,9 +40,9 @@ const pseudosMap: Record<string, StyleBucketName | undefined> = {
  * "f"
  * ```
  */
-export function getStyleBucketName(pseudo: string, media: string, support: string): StyleBucketName {
+export function getStyleBucketName(pseudo: string, layer: string, media: string, support: string): StyleBucketName {
   // We are grouping all the at-rules like @media, @supports etc under `t` bucket.
-  if (media || support) {
+  if (media || layer || support) {
     return 't';
   }
 
diff --git a/packages/core/src/runtime/resolveStyleRules.ts b/packages/core/src/runtime/resolveStyleRules.ts
index 588c27c..6a368db 100644
--- a/packages/core/src/runtime/resolveStyleRules.ts
+++ b/packages/core/src/runtime/resolveStyleRules.ts
@@ -7,6 +7,7 @@ import { compileCSS, CompileCSSOptions } from './compileCSS';
 import { compileKeyframeRule, compileKeyframesCSS } from './compileKeyframeCSS';
 import { generateCombinedQuery } from './utils/generateCombinedMediaQuery';
 import { isMediaQuerySelector } from './utils/isMediaQuerySelector';
+import { isLayerSelector } from './utils/isLayerSelector';
 import { isNestedSelector } from './utils/isNestedSelector';
 import { isSupportQuerySelector } from './utils/isSupportQuerySelector';
 import { normalizeNestedProperty } from './utils/normalizeNestedProperty';
@@ -48,6 +49,7 @@ export function resolveStyleRules(
   styles: GriffelStyle,
   pseudo = '',
   media = '',
+  layer = '',
   support = '',
   cssClassesMap: CSSClassesMap = {},
   cssRulesByBucket: CSSRulesByBucket = {},
@@ -85,6 +87,7 @@ export function resolveStyleRules(
       const key = hashPropertyKey(pseudo, media, support, property);
       const className = hashClassName({
         media,
+        layer,
         value: value.toString(),
         support,
         pseudo,
@@ -100,6 +103,7 @@ export function resolveStyleRules(
             property: rtlDefinition.key,
             pseudo,
             media,
+            layer,
             support,
           })
         : undefined;
@@ -111,10 +115,11 @@ export function resolveStyleRules(
           }
         : undefined;
 
-      const styleBucketName = getStyleBucketName(pseudo, media, support);
+      const styleBucketName = getStyleBucketName(pseudo, layer, media, support);
       const [ltrCSS, rtlCSS] = compileCSS({
         className,
         media,
+        layer,
         pseudo,
         property,
         support,
@@ -166,6 +171,7 @@ export function resolveStyleRules(
         { animationName: animationNames.join(', ') },
         pseudo,
         media,
+        layer,
         support,
         cssClassesMap,
         cssRulesByBucket,
@@ -185,6 +191,7 @@ export function resolveStyleRules(
       const key = hashPropertyKey(pseudo, media, support, property);
       const className = hashClassName({
         media,
+        layer,
         value: value.map(v => (v ?? '').toString()).join(';'),
         support,
         pseudo,
@@ -211,6 +218,7 @@ export function resolveStyleRules(
             value: rtlDefinitions.map(v => (v?.value ?? '').toString()).join(';'),
             property: rtlDefinitions[0].key,
             pseudo,
+            layer,
             media,
             support,
           })
@@ -224,10 +232,11 @@ export function resolveStyleRules(
           }
         : undefined;
 
-      const styleBucketName = getStyleBucketName(pseudo, media, support);
+      const styleBucketName = getStyleBucketName(pseudo, layer, media, support);
       const [ltrCSS, rtlCSS] = compileCSS({
         className,
         media,
+        layer,
         pseudo,
         property,
         support,
@@ -243,6 +252,7 @@ export function resolveStyleRules(
           value as GriffelStyle,
           pseudo + normalizeNestedProperty(property),
           media,
+          layer,
           support,
           cssClassesMap,
           cssRulesByBucket,
@@ -250,11 +260,39 @@ export function resolveStyleRules(
       } else if (isMediaQuerySelector(property)) {
         const combinedMediaQuery = generateCombinedQuery(media, property.slice(6).trim());
 
-        resolveStyleRules(value as GriffelStyle, pseudo, combinedMediaQuery, support, cssClassesMap, cssRulesByBucket);
+        resolveStyleRules(
+          value as GriffelStyle,
+          pseudo,
+          combinedMediaQuery,
+          layer,
+          support,
+          cssClassesMap,
+          cssRulesByBucket,
+        );
+      } else if (isLayerSelector(property)) {
+        const combinedLayerQuery = (layer ? `${layer}.` : '') + property.slice(6).trim();
+
+        resolveStyleRules(
+          value as GriffelStyle,
+          pseudo,
+          media,
+          combinedLayerQuery,
+          support,
+          cssClassesMap,
+          cssRulesByBucket,
+        );
       } else if (isSupportQuerySelector(property)) {
         const combinedSupportQuery = generateCombinedQuery(support, property.slice(9).trim());
 
-        resolveStyleRules(value as GriffelStyle, pseudo, media, combinedSupportQuery, cssClassesMap, cssRulesByBucket);
+        resolveStyleRules(
+          value as GriffelStyle,
+          pseudo,
+          media,
+          layer,
+          combinedSupportQuery,
+          cssClassesMap,
+          cssRulesByBucket,
+        );
       } else {
         if (process.env.NODE_ENV !== 'production') {
           // eslint-disable-next-line no-console
diff --git a/packages/core/src/runtime/utils/hashClassName.ts b/packages/core/src/runtime/utils/hashClassName.ts
index c2cf369..70a602a 100644
--- a/packages/core/src/runtime/utils/hashClassName.ts
+++ b/packages/core/src/runtime/utils/hashClassName.ts
@@ -6,12 +6,13 @@ export interface HashedClassNameParts {
   value: string;
   pseudo: string;
   media: string;
+  layer: string;
   support: string;
 }
 
-export function hashClassName({ media, property, pseudo, support, value }: HashedClassNameParts): string {
+export function hashClassName({ media, layer, property, pseudo, support, value }: HashedClassNameParts): string {
   // Trimming of value is required to generate consistent hashes
-  const classNameHash = hashString(pseudo + media + support + property + value.trim());
+  const classNameHash = hashString(pseudo + media + layer + support + property + value.trim());
 
   return HASH_PREFIX + classNameHash;
 }
diff --git a/packages/core/src/runtime/utils/isLayerSelector.ts b/packages/core/src/runtime/utils/isLayerSelector.ts
new file mode 100644
index 0000000..85c531a
--- /dev/null
+++ b/packages/core/src/runtime/utils/isLayerSelector.ts
@@ -0,0 +1,3 @@
+export function isLayerSelector(property: string): boolean {
+  return property.substr(0, 6) === '@layer';
+}
diff --git a/packages/core/src/shorthands/index.ts b/packages/core/src/shorthands/index.ts
index 00d26fa..b398d95 100644
--- a/packages/core/src/shorthands/index.ts
+++ b/packages/core/src/shorthands/index.ts
@@ -11,3 +11,4 @@ export { gap } from './gap';
 export { margin } from './margin';
 export { padding } from './padding';
 export { overflow } from './overflow';
+export { inset } from './inset';
diff --git a/packages/core/src/shorthands/inset.ts b/packages/core/src/shorthands/inset.ts
new file mode 100644
index 0000000..1f8247b
--- /dev/null
+++ b/packages/core/src/shorthands/inset.ts
@@ -0,0 +1,30 @@
+import type { GriffelStylesStrictCSSObject } from '../types';
+import { InsetInput } from './types';
+
+type InsetStyle = Pick<GriffelStylesStrictCSSObject, 'top' | 'right' | 'bottom' | 'left'>;
+
+export function inset(all: InsetInput): InsetStyle;
+export function inset(vertical: InsetInput, horizontal: InsetInput): InsetStyle;
+export function inset(top: InsetInput, horizontal: InsetInput, bottom: InsetInput): InsetStyle;
+export function inset(top: InsetInput, right: InsetInput, bottom: InsetInput, left: InsetInput): InsetStyle;
+
+/**
+ * A function that implements CSS spec conformant expansion for "inset"
+ *
+ * @example
+ *   inset('10px')
+ *   inset('10px', '5px')
+ *   inset('2px', '4px', '8px')
+ *   inset('1px', 0, '3px', '4px')
+ *
+ * See https://developer.mozilla.org/en-US/docs/Web/CSS/inset
+ */
+export function inset(...values: InsetInput[]): InsetStyle {
+  const [firstValue, secondValue = firstValue, thirdValue = firstValue, fourthValue = secondValue] = values;
+  return {
+    top: firstValue,
+    right: secondValue,
+    bottom: thirdValue,
+    left: fourthValue,
+  };
+}
diff --git a/packages/core/src/shorthands/types.ts b/packages/core/src/shorthands/types.ts
index 3b79852..2ac2eb6 100644
--- a/packages/core/src/shorthands/types.ts
+++ b/packages/core/src/shorthands/types.ts
@@ -14,3 +14,5 @@ export type MarginInput = ValueOrArray<CSS.Property.Margin<GriffelStylesCSSValue
 export type OverflowInput = ValueOrArray<CSS.Property.Overflow>;
 
 export type PaddingInput = ValueOrArray<CSS.Property.Padding<GriffelStylesCSSValue>>;
+
+export type InsetInput = ValueOrArray<CSS.Property.Inset<GriffelStylesCSSValue>>;
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index 401a054..ec71957 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -126,7 +126,7 @@ export type StyleBucketName =
   | 'a'
   // @keyframes definitions
   | 'k'
-  // at-rules (@media, @support)
+  // at-rules (@media, @support, @layer)
   | 't';
 
 export type SequenceHash = string;
diff --git a/packages/devtools/package.json b/packages/devtools/package.json
index 719bb00..7b5200a 100644
--- a/packages/devtools/package.json
+++ b/packages/devtools/package.json
@@ -11,7 +11,9 @@
   "sideEffects": false,
   "dependencies": {
     "@griffel/react": "^1.0.3",
+    "js-beautify": "^1.14.0",
     "react": ">=16.8.0 <18.0.0",
-    "react-dom": ">=16.8.0 <18.0.0"
+    "react-dom": ">=16.8.0 <18.0.0",
+    "stylis": "^4.0.13"
   }
 }
diff --git a/packages/devtools/src/FlattenView.tsx b/packages/devtools/src/FlattenView.tsx
new file mode 100644
index 0000000..5cc86dc
--- /dev/null
+++ b/packages/devtools/src/FlattenView.tsx
@@ -0,0 +1,51 @@
+import * as React from 'react';
+import { makeStaticStyles, makeStyles, shorthands } from '@griffel/react';
+
+import { SlotCSSRules } from './SlotCSSRules';
+import { getRulesBySlots } from './utils';
+import { ViewContext } from './ViewContext';
+
+import type { DebugResult } from '@griffel/core';
+
+const useStyles = makeStyles({
+  root: {
+    paddingBottom: '10px',
+  },
+  info: {
+    ...shorthands.margin(0, '5px'),
+  },
+});
+
+const useStaticStyles = makeStaticStyles({
+  pre: {
+    fontFamily: 'Menlo, Consolas, "dejavu sans mono", monospace',
+    margin: 'initial',
+    whiteSpace: 'normal',
+    fontSize: '11px',
+    lineHeight: '14px',
+  },
+});
+
+type FlattenViewProps = { debugResultRoot: DebugResult };
+
+export const FlattenView: React.FC<FlattenViewProps> = props => {
+  const { debugResultRoot } = props;
+  const slots = React.useMemo(() => getRulesBySlots(debugResultRoot), [debugResultRoot]);
+
+  useStaticStyles();
+  const classes = useStyles();
+
+  const [highlightedClass, setHighlightedClass] = React.useState('');
+  const contextValue = React.useMemo(() => ({ highlightedClass, setHighlightedClass }), [highlightedClass]);
+
+  return (
+    <div className={classes.root}>
+      <div className={classes.info}>direction: {debugResultRoot.direction}</div>
+      <ViewContext.Provider value={contextValue}>
+        {slots.map(({ slot, rules }) => (
+          <SlotCSSRules key={slot} slot={slot} atomicRules={rules} />
+        ))}
+      </ViewContext.Provider>
+    </div>
+  );
+};
diff --git a/packages/devtools/src/MonolithicRulesView.tsx b/packages/devtools/src/MonolithicRulesView.tsx
new file mode 100644
index 0000000..960422a
--- /dev/null
+++ b/packages/devtools/src/MonolithicRulesView.tsx
@@ -0,0 +1,96 @@
+import beautify from 'js-beautify';
+import * as React from 'react';
+import { makeStyles, mergeClasses, shorthands } from '@griffel/react';
+
+import { useViewContext } from './ViewContext';
+import type { MonolithicRules, RuleDetail } from './types';
+
+const formatCSS = (css: string) => {
+  const formatted = beautify.css_beautify(`{${css}}`); // add {} for formatting
+  return formatted.slice(1, -1).trim(); // remove {}
+};
+
+const INDENT = '10px';
+
+const useSingleRuleStyles = makeStyles({
+  overriden: {
+    textDecorationLine: 'line-through',
+  },
+  indent: {
+    marginLeft: INDENT,
+  },
+  highlighted: {
+    outlineColor: 'orangered',
+    outlineStyle: 'dashed',
+    outlineWidth: '1px',
+  },
+});
+
+const SingleRuleView: React.FC<{ rule: RuleDetail; indent?: boolean }> = ({ rule, indent }) => {
+  const { highlightedClass, setHighlightedClass } = useViewContext();
+
+  const handleClick = (e: React.SyntheticEvent) => {
+    e.stopPropagation();
+    if (rule.overriddenBy) {
+      setHighlightedClass(highlighted => (highlighted === rule.overriddenBy ? '' : rule.overriddenBy!));
+    } else {
+      setHighlightedClass('');
+    }
+  };
+
+  const classes = useSingleRuleStyles();
+  const className = mergeClasses(
+    rule.overriddenBy && classes.overriden,
+    indent && classes.indent,
+    highlightedClass === rule.className && classes.highlighted,
+  );
+
+  return (
+    <div onClick={handleClick} className={className}>
+      <pre>{formatCSS(rule.css)}</pre>
+    </div>
+  );
+};
+
+const useMonolithicRulesStyles = makeStyles({
+  atRulesIndent: {
+    marginLeft: INDENT,
+  },
+  lineSpacing: {
+    ...shorthands.margin('5px', 0),
+  },
+  noLineSpacing: {
+    ...shorthands.margin(0),
+  },
+});
+
+type MonolithicRulesViewProps = {
+  rules: MonolithicRules;
+  // specify if there should be spacing between rules with different selectors
+  noLineSpacing?: boolean;
+};
+
+export const MonolithicRulesView: React.FC<MonolithicRulesViewProps> = props => {
+  const { rules, noLineSpacing } = props;
+  const classes = useMonolithicRulesStyles();
+
+  return (
+    <>
+      {Object.entries(rules).map(([selector, rules]) => {
+        return (
+          <div key={selector} className={noLineSpacing ? classes.noLineSpacing : classes.lineSpacing}>
+            {selector && <pre>{`${selector} {`} </pre>}
+            {Array.isArray(rules) ? (
+              rules.map(rule => <SingleRuleView key={rule.css} rule={rule} indent={!!selector} />)
+            ) : (
+              <div className={classes.atRulesIndent}>
+                <MonolithicRulesView rules={rules} noLineSpacing />
+              </div>
+            )}
+            {selector && <pre>{`}`} </pre>}
+          </div>
+        );
+      })}
+    </>
+  );
+};
diff --git a/packages/devtools/src/SlotCSSRules.tsx b/packages/devtools/src/SlotCSSRules.tsx
new file mode 100644
index 0000000..6637b8b
--- /dev/null
+++ b/packages/devtools/src/SlotCSSRules.tsx
@@ -0,0 +1,38 @@
+import * as React from 'react';
+import { makeStyles, shorthands } from '@griffel/react';
+
+import { getMonolithicCSSRules } from './getMonolithicCSSRules';
+import { MonolithicRulesView } from './MonolithicRulesView';
+import { useViewContext } from './ViewContext';
+
+import type { AtomicRules } from './types';
+
+const useStyles = makeStyles({
+  slotName: {
+    ...shorthands.padding('2px', '5px'),
+    ...shorthands.margin('5px', 0),
+    ...shorthands.borderTop('1px', 'solid', 'grey'),
+    ...shorthands.borderBottom('1px', 'solid', 'grey'),
+  },
+  rules: {
+    ...shorthands.padding(0, '5px'),
+  },
+});
+
+export const SlotCSSRules: React.FC<{ slot: string; atomicRules: AtomicRules[] }> = ({ slot, atomicRules }) => {
+  const rules = React.useMemo(() => getMonolithicCSSRules(atomicRules), [atomicRules]);
+
+  const classes = useStyles();
+
+  const { setHighlightedClass } = useViewContext();
+  const undoHighlight = () => setHighlightedClass('');
+
+  return (
+    <>
+      <pre className={classes.slotName}>{slot}</pre>
+      <div className={classes.rules} onClick={undoHighlight}>
+        <MonolithicRulesView rules={rules} />
+      </div>
+    </>
+  );
+};
diff --git a/packages/devtools/src/ViewContext.ts b/packages/devtools/src/ViewContext.ts
new file mode 100644
index 0000000..b796c3b
--- /dev/null
+++ b/packages/devtools/src/ViewContext.ts
@@ -0,0 +1,16 @@
+import * as React from 'react';
+
+export type ViewContextType = {
+  highlightedClass: string;
+  setHighlightedClass: React.Dispatch<React.SetStateAction<string>>;
+};
+
+export const ViewContext = React.createContext<ViewContextType>({
+  highlightedClass: '',
+  // eslint-disable-next-line @typescript-eslint/no-empty-function
+  setHighlightedClass: () => {},
+});
+
+export function useViewContext() {
+  return React.useContext(ViewContext);
+}
diff --git a/packages/devtools/src/getMonolithicCSSRules.ts b/packages/devtools/src/getMonolithicCSSRules.ts
new file mode 100644
index 0000000..ca4fcc6
--- /dev/null
+++ b/packages/devtools/src/getMonolithicCSSRules.ts
@@ -0,0 +1,76 @@
+import * as stylis from 'stylis';
+import type { AtomicRules, MonolithicAtRules, MonolithicRules, RuleDetail } from './types';
+
+function parseRuleElement(element: stylis.Element, overriddenBy?: string) {
+  // example of `value`: `.f3xbvq9:hover`
+  // `children` contains all CSS rules under the `value` selector
+  const { value: classNameSelector, children } = element;
+
+  if (Array.isArray(children)) {
+    const selector = stylis.tokenize(classNameSelector).slice(1).join('');
+    const rules: RuleDetail[] = children.map(child => ({
+      css: child.value,
+      className: stylis.tokenize(classNameSelector)[0].slice(1),
+      overriddenBy,
+    }));
+    return {
+      selector,
+      rules,
+    };
+  }
+  throw new Error('Unsupported input from "element.children"');
+}
+
+function parseAtElement(element: stylis.Element, overriddenBy?: string) {
+  const { value: atSelector, children } = element;
+
+  if (Array.isArray(children)) {
+    const rules: MonolithicAtRules = {};
+    children.forEach(child => {
+      const { selector: childSelector, rules: childRules } = parseRuleElement(child, overriddenBy);
+      rules[childSelector] = [...(rules[childSelector] || []), ...childRules];
+    });
+
+    return {
+      selector: atSelector,
+      rules,
+    };
+  }
+  throw new Error('Unsupported input from "element.children"');
+}
+
+function parseElement(element: stylis.Element, overriddenBy?: string) {
+  const { type } = element;
+
+  if (type.startsWith('@')) {
+    return parseAtElement(element, overriddenBy);
+  }
+
+  if (type === 'rule') {
+    return parseRuleElement(element, overriddenBy);
+  }
+
+  throw new Error('Unsupported type of entries');
+}
+
+export function getMonolithicCSSRules(rules: AtomicRules[]): MonolithicRules {
+  return rules.reduce((acc, entry) => {
+    const compiled = stylis.compile(entry.cssRule);
+
+    compiled.forEach(element => {
+      const { selector, rules } = parseElement(element, entry.overriddenBy);
+
+      if (Array.isArray(rules)) {
+        acc[selector] = [...((acc[selector] as RuleDetail[]) || []), ...rules];
+      } else {
+        acc[selector] = acc[selector] ?? {};
+        const childRules = acc[selector] as MonolithicAtRules;
+        Object.entries(rules).forEach(([nestedSelector, nestedRules]) => {
+          childRules[nestedSelector] = [...(childRules[nestedSelector] || []), ...nestedRules];
+        });
+      }
+    });
+
+    return acc;
+  }, {} as MonolithicRules);
+}
diff --git a/packages/devtools/src/index.tsx b/packages/devtools/src/index.tsx
index 3584d98..06f63b0 100644
--- a/packages/devtools/src/index.tsx
+++ b/packages/devtools/src/index.tsx
@@ -2,13 +2,14 @@ import * as React from 'react';
 import * as ReactDOM from 'react-dom';
 
 import { DefaultMessage } from './DefaultMessage';
+import { FlattenView } from './FlattenView';
 import { useMakeStylesState } from './useMakeStylesState';
 
 const DevTools: React.FC = () => {
   const state = useMakeStylesState();
 
-  if (state) {
-    console.log(state);
+  if (state && Object.keys(state).length > 0) {
+    return <FlattenView debugResultRoot={state} />;
   }
 
   return <DefaultMessage />;
diff --git a/packages/devtools/src/stories/FlattenView.stories.tsx b/packages/devtools/src/stories/FlattenView.stories.tsx
new file mode 100644
index 0000000..3d17e90
--- /dev/null
+++ b/packages/devtools/src/stories/FlattenView.stories.tsx
@@ -0,0 +1,104 @@
+import * as React from 'react';
+import { FlattenView } from '../FlattenView';
+import type { DebugResult } from '@griffel/core';
+
+const debugResultRoot: DebugResult = {
+  sequenceHash: '___29aowz0_1rg0tlg',
+  direction: 'ltr',
+  children: [
+    {
+      sequenceHash: '___12k6f67_1qq0ij4',
+      direction: 'ltr',
+      children: [
+        {
+          sequenceHash: '___1cxgqvx_0000000',
+          direction: 'ltr',
+          children: [],
+          debugClassNames: [
+            { className: 'f3rmtva', overriddenBy: 'fdmssx0' },
+            { className: 'f1s6fcnf' },
+            { className: 'f8491dx' },
+            { className: 'f139dqjh', overriddenBy: 'f19s2fr6' },
+          ],
+          rules: {
+            f3rmtva: '.f3rmtva{background-color:transparent;}',
+            f1s6fcnf: '.f1s6fcnf{outline-style:none;}',
+            f8491dx: '.f8491dx:hover{cursor:pointer;}',
+            f139dqjh: '.f139dqjh:hover{background-color:#ddd;}',
+          },
+          slot: 'root',
+        },
+        {
+          sequenceHash: '___1wgw9g0_0000000',
+          direction: 'ltr',
+          children: [],
+          debugClassNames: [
+            { className: 'fdmssx0', overriddenBy: 'f3xbvq9' },
+            { className: 'f183xr6j' },
+            { className: 'f19s2fr6' },
+            { className: 'f15639b', overriddenBy: 'fy4xd57' },
+          ],
+          rules: {
+            fdmssx0: '.fdmssx0{background-color:#106ebe;}',
+            f183xr6j: '.f183xr6j{color:#eff6fc;}',
+            f19s2fr6: '.f19s2fr6:hover{background-color:#2899f5;}',
+            f15639b: '@media screen and (max-width: 992px){.f15639b:hover{background-color:green;}}',
+          },
+          slot: 'primary',
+        },
+      ],
+      debugClassNames: [
+        { className: 'fdmssx0', overriddenBy: 'f3xbvq9' },
+        { className: 'f1s6fcnf' },
+        { className: 'f8491dx' },
+        { className: 'f19s2fr6' },
+        { className: 'f183xr6j' },
+        { className: 'f15639b', overriddenBy: 'fy4xd57' },
+      ],
+    },
+    {
+      sequenceHash: '___b844lz0_0000000',
+      direction: 'ltr',
+      children: [],
+      debugClassNames: [
+        { className: 'f3xbvq9' },
+        { className: 'f7sr9i2' },
+        { className: 'fg103nd' },
+        { className: 'fsz8qd4' },
+        { className: 'f7wpa5l' },
+        { className: 'fy4xd57' },
+      ],
+      rules: {
+        f3xbvq9: '.f3xbvq9{background-color:red;}',
+        f7sr9i2: '.f7sr9i2>div{color:pink;}',
+        fg103nd: '.fg103nd>div:hover{border-top-color:#2899f5;}',
+        fsz8qd4: '@media (forced-colors: active):{.fsz8qd4{color:purple;}}',
+        f7wpa5l: '@media screen and (max-width: 992px){.f7wpa5l:hover{color:red;}}',
+        fy4xd57: '@media screen and (max-width: 992px){.fy4xd57:hover{background-color:blue;}}',
+      },
+      slot: 'primaryRed',
+    },
+  ],
+  debugClassNames: [
+    { className: 'f3xbvq9' },
+    { className: 'f1s6fcnf' },
+    { className: 'f8491dx' },
+    { className: 'f19s2fr6' },
+    { className: 'f183xr6j' },
+    { className: 'fy4xd57' },
+    { className: 'f7sr9i2' },
+    { className: 'fg103nd' },
+    { className: 'fsz8qd4' },
+    { className: 'f7wpa5l' },
+  ],
+};
+
+export const Default = () => (
+  <div style={{ border: '3px solid gray', width: 400 }}>
+    <FlattenView debugResultRoot={debugResultRoot} />
+  </div>
+);
+
+export default {
+  title: 'FlattenView',
+};
diff --git a/packages/devtools/src/types.ts b/packages/devtools/src/types.ts
new file mode 100644
index 0000000..d8a4796
--- /dev/null
+++ b/packages/devtools/src/types.ts
@@ -0,0 +1,11 @@
+export type SlotInfo = { slot: string; rules: AtomicRules[] };
+
+export type AtomicRules = { cssRule: string; overriddenBy?: string };
+
+export type RuleDetail = {
+  className: string;
+  css: string;
+  overriddenBy?: string;
+};
+export type MonolithicAtRules = Record<string /** selector */, RuleDetail[]>;
+export type MonolithicRules = Record<string /** selector */, RuleDetail[] | MonolithicAtRules>;
diff --git a/packages/devtools/src/utils.ts b/packages/devtools/src/utils.ts
new file mode 100644
index 0000000..9b68c35
--- /dev/null
+++ b/packages/devtools/src/utils.ts
@@ -0,0 +1,25 @@
+import type { DebugResult } from '@griffel/core';
+import { SlotInfo } from './types';
+
+export function getRulesBySlots(node: DebugResult, result: SlotInfo[] = []): SlotInfo[] {
+  if (node.children.length === 0 && node.slot) {
+    const { debugClassNames, rules, slot } = node;
+
+    return [
+      ...result,
+      {
+        slot,
+        rules: debugClassNames.map(({ className, overriddenBy }) => {
+          return {
+            cssRule: rules![className],
+            overriddenBy,
+          };
+        }),
+      },
+    ];
+  }
+
+  return node.children.reverse().reduce((acc, child) => {
+    return [...acc, ...getRulesBySlots(child)];
+  }, result);
+}
diff --git a/packages/eslint-plugin/.babelrc b/packages/eslint-plugin/.babelrc
new file mode 100644
index 0000000..cf7ddd9
--- /dev/null
+++ b/packages/eslint-plugin/.babelrc
@@ -0,0 +1,3 @@
+{
+  "presets": [["@nrwl/web/babel", { "useBuiltIns": "usage" }]]
+}
diff --git a/packages/eslint-plugin/.eslintrc.json b/packages/eslint-plugin/.eslintrc.json
new file mode 100644
index 0000000..9d9c0db
--- /dev/null
+++ b/packages/eslint-plugin/.eslintrc.json
@@ -0,0 +1,18 @@
+{
+  "extends": ["../../.eslintrc.json"],
+  "ignorePatterns": ["!**/*"],
+  "overrides": [
+    {
+      "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
+      "rules": {}
+    },
+    {
+      "files": ["*.ts", "*.tsx"],
+      "rules": {}
+    },
+    {
+      "files": ["*.js", "*.jsx"],
+      "rules": {}
+    }
+  ]
+}
diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md
new file mode 100644
index 0000000..f353207
--- /dev/null
+++ b/packages/eslint-plugin/README.md
@@ -0,0 +1,44 @@
+# ESLint plugin for Griffel
+
+ESLint plugin to follow best practices and anticipate common mistakes when writing styles with Griffel
+
+## Install
+
+```bash
+yarn add --dev @griffel/eslint-plugin
+# or
+npm install --dev @griffel/eslint-plugin
+```
+
+## Usage
+
+Add `@griffel/eslint-plugin` to the extends section of your `.eslintrc` configuration file:
+
+```json
+{
+  "extends": ["@griffel"]
+}
+```
+
+## Shareable configurations
+
+This plugin exports recommended configuration that enforce good practices, but you can use only uses that are required for your use case:
+
+```json
+{
+  "plugins": ["@griffel"],
+  "rules": {
+    "@griffel/no-shorthands": "warn"
+  }
+}
+```
+
+You can find more info about enabled rules in the [Supported Rules section](#supported-rules) below.
+
+## Supported Rules
+
+**Key**: 🔧 = fixable
+
+| Name                                                     | Description                    | 🔧  |
+| -------------------------------------------------------- | ------------------------------ | --- |
+| [`@griffel/no-shorthands`](./src/rules/no-shorthands.md) | Enforce usage of CSS longhands |     |
diff --git a/packages/eslint-plugin/jest.config.js b/packages/eslint-plugin/jest.config.js
new file mode 100644
index 0000000..81437d4
--- /dev/null
+++ b/packages/eslint-plugin/jest.config.js
@@ -0,0 +1,15 @@
+module.exports = {
+  displayName: 'eslint-plugin',
+  preset: '../../jest.preset.js',
+  globals: {
+    'ts-jest': {
+      tsconfig: '<rootDir>/tsconfig.spec.json',
+    },
+  },
+  testEnvironment: 'node',
+  transform: {
+    '^.+\\.[tj]sx?$': 'ts-jest',
+  },
+  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
+  coverageDirectory: '../../coverage/packages/eslint-plugin',
+};
diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json
new file mode 100644
index 0000000..c030a69
--- /dev/null
+++ b/packages/eslint-plugin/package.json
@@ -0,0 +1,17 @@
+{
+  "name": "@griffel/eslint-plugin",
+  "version": "0.0.1",
+  "description": "ESLint plugin with lint rules for Griffel",
+  "license": "MIT",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/microsoft/griffel"
+  },
+  "dependencies": {
+    "csstype": "^3.0.10",
+    "@typescript-eslint/utils": "^5.20.0"
+  },
+  "peerDependencies": {
+    "eslint": "^7.5.0 || ^8.0.0"
+  }
+}
diff --git a/packages/eslint-plugin/project.json b/packages/eslint-plugin/project.json
new file mode 100644
index 0000000..f64f373
--- /dev/null
+++ b/packages/eslint-plugin/project.json
@@ -0,0 +1,49 @@
+{
+  "root": "packages/eslint-plugin",
+  "sourceRoot": "packages/eslint-plugin/src",
+  "projectType": "library",
+  "targets": {
+    "lint": {
+      "executor": "@nrwl/linter:eslint",
+      "outputs": ["{options.outputFile}"],
+      "options": {
+        "lintFilePatterns": ["packages/eslint-plugin/**/*.ts"]
+      }
+    },
+    "test": {
+      "executor": "@nrwl/jest:jest",
+      "outputs": ["coverage/packages/eslint-plugin"],
+      "options": {
+        "jestConfig": "packages/eslint-plugin/jest.config.js",
+        "passWithNoTests": true
+      }
+    },
+    "build": {
+      "executor": "@nrwl/node:package",
+      "outputs": ["{options.outputPath}"],
+      "options": {
+        "outputPath": "dist/packages/eslint-plugin",
+        "tsConfig": "packages/eslint-plugin/tsconfig.lib.json",
+        "packageJson": "packages/eslint-plugin/package.json",
+        "main": "packages/eslint-plugin/src/index.ts",
+        "assets": [
+          "packages/eslint-plugin/README.md",
+          {
+            "glob": "LICENSE.md",
+            "input": ".",
+            "output": "."
+          }
+        ]
+      }
+    },
+    "type-check": {
+      "executor": "@nrwl/workspace:run-commands",
+      "options": {
+        "cwd": "packages/babel-preset",
+        "commands": [{ "command": "tsc -b --pretty" }],
+        "outputPath": []
+      }
+    }
+  },
+  "tags": []
+}
diff --git a/packages/eslint-plugin/src/configs/recommended.ts b/packages/eslint-plugin/src/configs/recommended.ts
new file mode 100644
index 0000000..5691dc4
--- /dev/null
+++ b/packages/eslint-plugin/src/configs/recommended.ts
@@ -0,0 +1,6 @@
+export const recommendedConfig = {
+  plugins: ['@griffel'],
+  rules: {
+    '@griffel/no-shorthands': 'error',
+  },
+};
diff --git a/packages/eslint-plugin/src/index.ts b/packages/eslint-plugin/src/index.ts
new file mode 100644
index 0000000..7bf7b16
--- /dev/null
+++ b/packages/eslint-plugin/src/index.ts
@@ -0,0 +1,11 @@
+import { recommendedConfig } from './configs/recommended';
+import { noShorthandsRule } from './rules/no-shorthands';
+
+export = {
+  configs: {
+    recommended: recommendedConfig,
+  },
+  rules: {
+    'no-shorthands': noShorthandsRule,
+  },
+};
diff --git a/packages/eslint-plugin/src/rules/no-shorthands.md b/packages/eslint-plugin/src/rules/no-shorthands.md
new file mode 100644
index 0000000..89b2485
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/no-shorthands.md
@@ -0,0 +1,34 @@
+# Enforce usage of CSS longhands (`@griffel/no-shorthands`)
+
+Ensure that rules passed to `makeStyles()` function do not contain [CSS shorthands](https://developer.mozilla.org/en-US/docs/Web/CSS/Shorthand_properties).
+
+## Rule Details
+
+Griffel does not support usage of CSS shorthands, [microsoft/griffel#30](https://github.com/microsoft/griffel/issues/30#issuecomment-1045959297).
+This is enforced by typings, but there are cases when they don't work as expected [microsoft/griffel#78](https://github.com/microsoft/griffel/issues/78).
+
+Examples of **incorrect** code for this rule:
+
+```js
+import { makeStyles } from '@griffel/react';
+
+export const useStyles = makeStyles({
+  root: {
+    background: 'red',
+    padding: '10px 20px',
+  },
+});
+```
+
+Examples of **correct** code for this rule:
+
+```js
+import { makeStyles } from '@griffel/react';
+
+export const useStyles = makeStyles({
+  root: {
+    backgroundColor: 'red',
+    ...shorthands.padding('10px', '20px'),
+  },
+});
+```
diff --git a/packages/eslint-plugin/src/rules/no-shorthands.ts b/packages/eslint-plugin/src/rules/no-shorthands.ts
new file mode 100644
index 0000000..ed1b1bc
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/no-shorthands.ts
@@ -0,0 +1,122 @@
+import { ESLintUtils, TSESTree } from '@typescript-eslint/utils';
+import * as CSS from 'csstype';
+
+import { createRule } from '../utils/createRule';
+import { isIdentifier, isObjectExpression, isProperty } from '../utils/helpers';
+
+export const RULE_NAME = 'no-shorthands';
+
+// This collection is a map simply for faster access when checking if a CSS property is unsupported
+// @griffel/core has the same definition, but ESLint plugin should not depend on it
+const UNSUPPORTED_CSS_PROPERTIES: Record<keyof CSS.StandardShorthandProperties, true> = {
+  all: true,
+  animation: true,
+  background: true,
+  backgroundPosition: true,
+  border: true,
+  borderBlock: true,
+  borderBlockEnd: true,
+  borderBlockStart: true,
+  borderBottom: true,
+  borderColor: true,
+  borderImage: true,
+  borderInline: true,
+  borderInlineEnd: true,
+  borderInlineStart: true,
+  borderLeft: true,
+  borderRadius: true,
+  borderRight: true,
+  borderStyle: true,
+  borderTop: true,
+  borderWidth: true,
+  columns: true,
+  columnRule: true,
+  flex: true,
+  flexFlow: true,
+  font: true,
+  gap: true,
+  grid: true,
+  gridArea: true,
+  gridColumn: true,
+  gridRow: true,
+  gridTemplate: true,
+  lineClamp: true,
+  listStyle: true,
+  margin: true,
+  mask: true,
+  maskBorder: true,
+  motion: true,
+  offset: true,
+  outline: true,
+  overflow: true,
+  overscrollBehavior: true,
+  padding: true,
+  placeItems: true,
+  placeSelf: true,
+  textDecoration: true,
+  textEmphasis: true,
+  transition: true,
+};
+
+function findShorthandProperties(
+  node: TSESTree.ObjectExpression,
+  isRoot = false,
+  result: TSESTree.Identifier[] = [],
+): TSESTree.Identifier[] {
+  for (const propertyNode of node.properties) {
+    if (isProperty(propertyNode)) {
+      if (isIdentifier(propertyNode.key) && !isRoot) {
+        if (Object.prototype.hasOwnProperty.call(UNSUPPORTED_CSS_PROPERTIES, propertyNode.key.name)) {
+          result.push(propertyNode.key);
+        }
+      }
+
+      if (isObjectExpression(propertyNode.value)) {
+        findShorthandProperties(propertyNode.value, false, result);
+      }
+    }
+  }
+
+  return result;
+}
+
+export const noShorthandsRule: ReturnType<ReturnType<typeof ESLintUtils.RuleCreator>> = createRule({
+  name: RULE_NAME,
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'Disallow using CSS shorthands in makeStyles() calls',
+      recommended: 'error',
+    },
+    messages: {
+      shorthandFound: 'Usage of shorthands is prohibited',
+    },
+    schema: [
+      {
+        type: 'string',
+      },
+    ],
+  },
+  defaultOptions: [],
+
+  create(context) {
+    return {
+      CallExpression(node) {
+        if (isIdentifier(node.callee) && node.callee.name === 'makeStyles') {
+          const argument = node.arguments[0];
+
+          if (isObjectExpression(argument)) {
+            const shorthandProperties = findShorthandProperties(argument, true);
+
+            shorthandProperties.forEach(shorthandProperty => {
+              context.report({
+                node: shorthandProperty,
+                messageId: 'shorthandFound',
+              });
+            });
+          }
+        }
+      },
+    };
+  },
+});
diff --git a/packages/eslint-plugin/src/utils/createRule.ts b/packages/eslint-plugin/src/utils/createRule.ts
new file mode 100644
index 0000000..8614b4b
--- /dev/null
+++ b/packages/eslint-plugin/src/utils/createRule.ts
@@ -0,0 +1,7 @@
+import { ESLintUtils } from '@typescript-eslint/utils';
+
+const getDocsUrl = (ruleName: string): string =>
+  `https://github.com/microsoft/griffel/tree/main/packages/eslint-plugin/src/rules/${ruleName}.md`;
+
+export const createRule: ReturnType<typeof ESLintUtils.RuleCreator> = (...args) =>
+  ESLintUtils.RuleCreator(getDocsUrl)(...args);
diff --git a/packages/eslint-plugin/src/utils/helpers.ts b/packages/eslint-plugin/src/utils/helpers.ts
new file mode 100644
index 0000000..64babcb
--- /dev/null
+++ b/packages/eslint-plugin/src/utils/helpers.ts
@@ -0,0 +1,11 @@
+import { AST_NODE_TYPES, ASTUtils, TSESTree } from '@typescript-eslint/utils';
+
+type IsHelper<NodeType extends AST_NODE_TYPES> = (node: TSESTree.Node | null | undefined) => node is TSESTree.Node & {
+  type: NodeType;
+};
+
+export const isIdentifier: IsHelper<AST_NODE_TYPES.Identifier> = ASTUtils.isIdentifier;
+export const isObjectExpression: IsHelper<AST_NODE_TYPES.ObjectExpression> = ASTUtils.isNodeOfType(
+  AST_NODE_TYPES.ObjectExpression,
+);
+export const isProperty: IsHelper<AST_NODE_TYPES.Property> = ASTUtils.isNodeOfType(AST_NODE_TYPES.Property);
diff --git a/packages/eslint-plugin/tsconfig.json b/packages/eslint-plugin/tsconfig.json
new file mode 100644
index 0000000..e464c9e
--- /dev/null
+++ b/packages/eslint-plugin/tsconfig.json
@@ -0,0 +1,22 @@
+{
+  "extends": "../../tsconfig.base.json",
+  "compilerOptions": {
+    "allowJs": true,
+    "forceConsistentCasingInFileNames": true,
+    "strict": true,
+    "noImplicitOverride": true,
+    "noPropertyAccessFromIndexSignature": true,
+    "noImplicitReturns": true,
+    "noFallthroughCasesInSwitch": true
+  },
+  "files": [],
+  "include": [],
+  "references": [
+    {
+      "path": "./tsconfig.lib.json"
+    },
+    {
+      "path": "./tsconfig.spec.json"
+    }
+  ]
+}
diff --git a/packages/eslint-plugin/tsconfig.lib.json b/packages/eslint-plugin/tsconfig.lib.json
new file mode 100644
index 0000000..8d27d94
--- /dev/null
+++ b/packages/eslint-plugin/tsconfig.lib.json
@@ -0,0 +1,11 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "module": "commonjs",
+    "outDir": "../../dist/out-tsc",
+    "declaration": true,
+    "types": ["node", "environment"]
+  },
+  "exclude": ["**/*.spec.ts", "**/*.test.ts"],
+  "include": ["**/*.ts"]
+}
diff --git a/packages/eslint-plugin/tsconfig.spec.json b/packages/eslint-plugin/tsconfig.spec.json
new file mode 100644
index 0000000..25c45ce
--- /dev/null
+++ b/packages/eslint-plugin/tsconfig.spec.json
@@ -0,0 +1,19 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../../dist/out-tsc",
+    "module": "commonjs",
+    "types": ["jest", "node", "environment"]
+  },
+  "include": [
+    "**/*.test.ts",
+    "**/*.spec.ts",
+    "**/*.test.tsx",
+    "**/*.spec.tsx",
+    "**/*.test.js",
+    "**/*.spec.js",
+    "**/*.test.jsx",
+    "**/*.spec.jsx",
+    "**/*.d.ts"
+  ]
+}
diff --git a/packages/react/README.md b/packages/react/README.md
index b7534c3..8f1d3db 100644
--- a/packages/react/README.md
+++ b/packages/react/README.md
@@ -187,6 +187,7 @@ const useClasses = makeStyles({
 
     '@media screen and (max-width: 992px)': { color: 'orange' },
     '@supports (display: grid)': { color: 'red' },
+    '@layer utility': { marginBottom: '1em' }
   },
 });
 ```
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 38d39e6..0bfcb9c 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -15,10 +15,11 @@
     "skipDefaultLibCheck": true,
     "baseUrl": ".",
     "paths": {
-      "@griffel/e2e-typescript": ["e2e/typescript/src/index.ts"],
       "@griffel/babel-preset": ["packages/babel-preset/src/index.ts"],
       "@griffel/core": ["packages/core/src/index.ts"],
       "@griffel/devtools": ["packages/devtools/src/index.ts"],
+      "@griffel/e2e-typescript": ["e2e/typescript/src/index.ts"],
+      "@griffel/eslint-plugin": ["packages/eslint-plugin/src/index.ts"],
       "@griffel/jest-serializer": ["packages/jest-serializer/src/index.ts"],
       "@griffel/react": ["packages/react/src/index.ts"],
       "@griffel/webpack-loader": ["packages/webpack-loader/src/index.ts"]
diff --git a/workspace.json b/workspace.json
index 9ba29c7..0696a83 100644
--- a/workspace.json
+++ b/workspace.json
@@ -1,13 +1,14 @@
 {
   "version": 2,
   "projects": {
-    "@griffel/e2e-typescript": "e2e/typescript",
     "@griffel/babel-preset": "packages/babel-preset",
     "@griffel/core": "packages/core",
+    "@griffel/devtools": "packages/devtools",
+    "@griffel/e2e-typescript": "e2e/typescript",
+    "@griffel/eslint-plugin": "packages/eslint-plugin",
     "@griffel/jest-serializer": "packages/jest-serializer",
     "@griffel/react": "packages/react",
     "@griffel/webpack-loader": "packages/webpack-loader",
-    "@griffel/website": "apps/website",
-    "@griffel/devtools": "packages/devtools"
+    "@griffel/website": "apps/website"
   }
 }
diff --git a/yarn.lock b/yarn.lock
index 16b0d76..3ad9a33 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6623,6 +6623,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@typescript-eslint/scope-manager@npm:5.20.0":
+  version: 5.20.0
+  resolution: "@typescript-eslint/scope-manager@npm:5.20.0"
+  dependencies:
+    "@typescript-eslint/types": 5.20.0
+    "@typescript-eslint/visitor-keys": 5.20.0
+  checksum: 904fd43f559dc2579958496ffad837eca124940b4a172666f0ea54ed606074d9ec7d2bec0f2141c3f9a8b894dd2644817cb86809e79a7a73ecba2b7babcdb5c9
+  languageName: node
+  linkType: hard
+
 "@typescript-eslint/scope-manager@npm:5.3.1":
   version: 5.3.1
   resolution: "@typescript-eslint/scope-manager@npm:5.3.1"
@@ -6633,6 +6643,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@typescript-eslint/types@npm:5.20.0":
+  version: 5.20.0
+  resolution: "@typescript-eslint/types@npm:5.20.0"
+  checksum: d7f6e51e23f59feee8857340828c47a98a0dd5eaa1b045e936dc11199b55754cf78ae5cd8d56c1fafb1b5a40a6f472c1ac921072951217caffe3f06a717fa61c
+  languageName: node
+  linkType: hard
+
 "@typescript-eslint/types@npm:5.3.1":
   version: 5.3.1
   resolution: "@typescript-eslint/types@npm:5.3.1"
@@ -6640,6 +6657,24 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@typescript-eslint/typescript-estree@npm:5.20.0":
+  version: 5.20.0
+  resolution: "@typescript-eslint/typescript-estree@npm:5.20.0"
+  dependencies:
+    "@typescript-eslint/types": 5.20.0
+    "@typescript-eslint/visitor-keys": 5.20.0
+    debug: ^4.3.2
+    globby: ^11.0.4
+    is-glob: ^4.0.3
+    semver: ^7.3.5
+    tsutils: ^3.21.0
+  peerDependenciesMeta:
+    typescript:
+      optional: true
+  checksum: 2b709292b7df3675d1f8eaf2f4e1ecf491f70fc525012c6a0fb5164aa893c165317b0a419022b8b00aaed502864d5b5b84092b58a9950d2633248e8d7627abd8
+  languageName: node
+  linkType: hard
+
 "@typescript-eslint/typescript-estree@npm:5.3.1":
   version: 5.3.1
   resolution: "@typescript-eslint/typescript-estree@npm:5.3.1"
@@ -6658,6 +6693,32 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@typescript-eslint/utils@npm:^5.20.0":
+  version: 5.20.0
+  resolution: "@typescript-eslint/utils@npm:5.20.0"
+  dependencies:
+    "@types/json-schema": ^7.0.9
+    "@typescript-eslint/scope-manager": 5.20.0
+    "@typescript-eslint/types": 5.20.0
+    "@typescript-eslint/typescript-estree": 5.20.0
+    eslint-scope: ^5.1.1
+    eslint-utils: ^3.0.0
+  peerDependencies:
+    eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
+  checksum: e387cf96124e34d079804220c5cb9134148fb3efc68d852a344453e285e3016e0b7e37b11308ef58c0e7afc638f145002cebc27c5da0fd03e0c074ff97d8210e
+  languageName: node
+  linkType: hard
+
+"@typescript-eslint/visitor-keys@npm:5.20.0":
+  version: 5.20.0
+  resolution: "@typescript-eslint/visitor-keys@npm:5.20.0"
+  dependencies:
+    "@typescript-eslint/types": 5.20.0
+    eslint-visitor-keys: ^3.0.0
+  checksum: 1e1aa5f14fd60f1846ee26947d571953898dc82eb635a7eab3984c6b7db9bb8897743416713a129cc95c8cd63325cc0c64b3935d264f73100911fc5da76fc65f
+  languageName: node
+  linkType: hard
+
 "@typescript-eslint/visitor-keys@npm:5.3.1":
   version: 5.3.1
   resolution: "@typescript-eslint/visitor-keys@npm:5.3.1"
@@ -13239,6 +13300,7 @@ __metadata:
     "@types/tmp": 0.2.3
     "@typescript-eslint/eslint-plugin": ~5.3.0
     "@typescript-eslint/parser": ~5.3.0
+    "@typescript-eslint/utils": ^5.20.0
     ajv: ^8.4.0
     babel-jest: 27.2.3
     babel-loader: 8.1.0


Test patch:
diff --git a/packages/core/src/runtime/compileCSS.test.ts b/packages/core/src/runtime/compileCSS.test.ts
index ea4bc06..386a986 100644
--- a/packages/core/src/runtime/compileCSS.test.ts
+++ b/packages/core/src/runtime/compileCSS.test.ts
@@ -1,12 +1,14 @@
 import { compileCSS, CompileCSSOptions, normalizePseudoSelector } from './compileCSS';
 
-const defaultOptions: Pick<CompileCSSOptions, 'rtlClassName' | 'className' | 'media' | 'pseudo' | 'support'> = {
-  className: 'foo',
-  rtlClassName: 'rtl-foo',
-  media: '',
-  pseudo: '',
-  support: '',
-};
+const defaultOptions: Pick<CompileCSSOptions, 'rtlClassName' | 'className' | 'media' | 'pseudo' | 'support' | 'layer'> =
+  {
+    className: 'foo',
+    rtlClassName: 'rtl-foo',
+    media: '',
+    pseudo: '',
+    support: '',
+    layer: '',
+  };
 
 describe('compileCSS', () => {
   it('handles pseudo', () => {
diff --git a/packages/core/src/runtime/getStyleBucketName.test.ts b/packages/core/src/runtime/getStyleBucketName.test.ts
index e18f439..93239d6 100644
--- a/packages/core/src/runtime/getStyleBucketName.test.ts
+++ b/packages/core/src/runtime/getStyleBucketName.test.ts
@@ -2,18 +2,19 @@ import { getStyleBucketName } from './getStyleBucketName';
 
 describe('getStyleBucketName', () => {
   it('returns bucketName based on mapping', () => {
-    expect(getStyleBucketName(' :link', '', '')).toBe('l');
-    expect(getStyleBucketName(' :hover ', '', '')).toBe('h');
+    expect(getStyleBucketName(' :link', '', '', '')).toBe('l');
+    expect(getStyleBucketName(' :hover ', '', '', '')).toBe('h');
 
-    expect(getStyleBucketName(':link', '', '')).toBe('l');
-    expect(getStyleBucketName(':visited', '', '')).toBe('v');
-    expect(getStyleBucketName(':focus-within', '', '')).toBe('w');
-    expect(getStyleBucketName(':focus', '', '')).toBe('f');
-    expect(getStyleBucketName(':focus-visible', '', '')).toBe('i');
-    expect(getStyleBucketName(':hover', '', '')).toBe('h');
-    expect(getStyleBucketName(':active', '', '')).toBe('a');
+    expect(getStyleBucketName(':link', '', '', '')).toBe('l');
+    expect(getStyleBucketName(':visited', '', '', '')).toBe('v');
+    expect(getStyleBucketName(':focus-within', '', '', '')).toBe('w');
+    expect(getStyleBucketName(':focus', '', '', '')).toBe('f');
+    expect(getStyleBucketName(':focus-visible', '', '', '')).toBe('i');
+    expect(getStyleBucketName(':hover', '', '', '')).toBe('h');
+    expect(getStyleBucketName(':active', '', '', '')).toBe('a');
 
-    expect(getStyleBucketName(':active', '(max-width: 100px)', '')).toBe('t');
-    expect(getStyleBucketName(':active', '', '(display: table-cell)')).toBe('t');
+    expect(getStyleBucketName(':active', 'theme', '', '')).toBe('t');
+    expect(getStyleBucketName(':active', '', '(max-width: 100px)', '')).toBe('t');
+    expect(getStyleBucketName(':active', '', '', '(display: table-cell)')).toBe('t');
   });
 });
diff --git a/packages/core/src/runtime/resolveStyleRules.test.ts b/packages/core/src/runtime/resolveStyleRules.test.ts
index 97938ba..3ceaa78 100644
--- a/packages/core/src/runtime/resolveStyleRules.test.ts
+++ b/packages/core/src/runtime/resolveStyleRules.test.ts
@@ -345,7 +345,7 @@ describe('resolveStyleRules', () => {
       `);
     });
 
-    it('handles media queries with preudo selectors', () => {
+    it('handles media queries with pseudo selectors', () => {
       expect(
         resolveStyleRules({
           color: 'green',
@@ -393,6 +393,85 @@ describe('resolveStyleRules', () => {
       `);
     });
 
+    it('handles layer queries', () => {
+      expect(
+        resolveStyleRules({
+          color: 'green',
+          '@layer color': { color: 'red' },
+        }),
+      ).toMatchInlineSnapshot(`
+        .fka9v86 {
+          color: green;
+        }
+        @layer color {
+          .f1hjcal7 {
+            color: red;
+          }
+        }
+      `);
+    });
+
+    it('handles layer queries with dots', () => {
+      expect(
+        resolveStyleRules({
+          '@layer framework.utilities': { color: 'red' },
+        }),
+      ).toMatchInlineSnapshot(`
+        @layer framework.utilities {
+          .faxdetk {
+            color: red;
+          }
+        }
+      `);
+    });
+    it('handles layer queries with pseudo selectors', () => {
+      expect(
+        resolveStyleRules({
+          color: 'green',
+          '@layer color': {
+            ':hover': {
+              color: 'red ',
+            },
+          },
+        }),
+      ).toMatchInlineSnapshot(`
+        .fka9v86 {
+          color: green;
+        }
+        @layer color {
+          .f1jv0g3z:hover {
+            color: red;
+          }
+        }
+      `);
+    });
+
+    it('handles nested layer queries', () => {
+      expect(
+        resolveStyleRules({
+          color: 'red',
+          '@layer color': {
+            color: 'red',
+            '@layer theme': { color: 'red' },
+          },
+        }),
+      ).toMatchInlineSnapshot(`
+        .fe3e8s9 {
+          color: red;
+        }
+        @layer color {
+          .f1hjcal7 {
+            color: red;
+          }
+        }
+        @layer color.theme {
+          .f132c7g3 {
+            color: red;
+          }
+        }
+      `);
+    });
+
     it('handles supports queries', () => {
       expect(
         resolveStyleRules({
diff --git a/packages/core/src/shorthands/inset.test.ts b/packages/core/src/shorthands/inset.test.ts
new file mode 100644
index 0000000..75aa674
--- /dev/null
+++ b/packages/core/src/shorthands/inset.test.ts
@@ -0,0 +1,57 @@
+import { inset } from './inset';
+
+describe('inset', () => {
+  it('for a given value', () => {
+    expect(inset('12px')).toEqual({
+      bottom: '12px',
+      left: '12px',
+      right: '12px',
+      top: '12px',
+    });
+  });
+
+  it('for given vertical and horizontal values', () => {
+    expect(inset('12px', '24px')).toEqual({
+      bottom: '12px',
+      left: '24px',
+      right: '24px',
+      top: '12px',
+    });
+  });
+
+  it('for given top, horizontal and bottom values', () => {
+    expect(inset('12px', '24px', '36px')).toEqual({
+      bottom: '36px',
+      left: '24px',
+      right: '24px',
+      top: '12px',
+    });
+  });
+
+  it('for given top, right, bottom and left values', () => {
+    expect(inset('12px', '24px', '36px', '48px')).toEqual({
+      bottom: '36px',
+      left: '48px',
+      right: '24px',
+      top: '12px',
+    });
+  });
+
+  it('for a given zero value', () => {
+    expect(inset(0)).toEqual({
+      bottom: 0,
+      left: 0,
+      right: 0,
+      top: 0,
+    });
+  });
+
+  it('for fallback value arrays', () => {
+    expect(inset([0, '12px'], ['auto', '-2px'])).toEqual({
+      bottom: [0, '12px'],
+      left: ['auto', '-2px'],
+      right: ['auto', '-2px'],
+      top: [0, '12px'],
+    });
+  });
+});
diff --git a/packages/devtools/src/getMonolithicCSSRules.test.ts b/packages/devtools/src/getMonolithicCSSRules.test.ts
new file mode 100644
index 0000000..be80a8c
--- /dev/null
+++ b/packages/devtools/src/getMonolithicCSSRules.test.ts
@@ -0,0 +1,148 @@
+import { getMonolithicCSSRules } from './getMonolithicCSSRules';
+
+describe('getMonolithicCSSRules', () => {
+  it('group atomic css with class names selector', () => {
+    const rules = ['.abcdef0{background-color:red;}', '.abcdef1{color:red;}'].map(cssRule => ({ cssRule }));
+    expect(getMonolithicCSSRules(rules)).toEqual({
+      '': [
+        {
+          className: 'abcdef0',
+          css: 'background-color:red;',
+        },
+        {
+          className: 'abcdef1',
+          css: 'color:red;',
+        },
+      ],
+    });
+  });
+
+  it('separate atomic css with class names selector and pseudo selector', () => {
+    const rules = [
+      '.abcdef0{background-color:red;}',
+      '.abcdef1{display:block;}',
+      '.abcdef2:hover{display:none;}',
+      '.abcdef3:hover{color:red;}',
+    ].map(cssRule => ({ cssRule }));
+    expect(getMonolithicCSSRules(rules)).toEqual({
+      '': [
+        {
+          className: 'abcdef0',
+          css: 'background-color:red;',
+        },
+        {
+          className: 'abcdef1',
+          css: 'display:block;',
+        },
+      ],
+      ':hover': [
+        {
+          className: 'abcdef2',
+          css: 'display:none;',
+        },
+        {
+          className: 'abcdef3',
+          css: 'color:red;',
+        },
+      ],
+    });
+  });
+
+  it('group atomic css with media selector', () => {
+    const rules = [
+      '.abcdef0{background-color:red;}',
+      '.abcdef1{display:block;}',
+      '.abcdef2:hover{display:none;}',
+      '.abcdef3:hover{color:red;}',
+    ].map(cssRule => ({ cssRule: `@media screen and (max-width: 992px){${cssRule}}` }));
+    expect(getMonolithicCSSRules(rules)).toEqual({
+      '@media screen and (max-width: 992px)': {
+        '': [
+          {
+            className: 'abcdef0',
+            css: 'background-color:red;',
+          },
+          {
+            className: 'abcdef1',
+            css: 'display:block;',
+          },
+        ],
+        ':hover': [
+          {
+            className: 'abcdef2',
+            css: 'display:none;',
+          },
+          {
+            className: 'abcdef3',
+            css: 'color:red;',
+          },
+        ],
+      },
+    });
+  });
+
+  it('separate atomic css with different media selector', () => {
+    const rules = [
+      '.abcdef0{background-color:red;}',
+      '@media screen and (max-width: 992px){.abcdef0{background-color:blue;}}',
+      '@media screen and (max-width: 1024px){.abcdef0{background-color:green;}}',
+    ].map(cssRule => ({ cssRule }));
+    expect(getMonolithicCSSRules(rules)).toEqual({
+      '': [
+        {
+          className: 'abcdef0',
+          css: 'background-color:red;',
+        },
+      ],
+      '@media screen and (max-width: 1024px)': {
+        '': [
+          {
+            className: 'abcdef0',
+            css: 'background-color:green;',
+          },
+        ],
+      },
+      '@media screen and (max-width: 992px)': {
+        '': [
+          {
+            className: 'abcdef0',
+            css: 'background-color:blue;',
+          },
+        ],
+      },
+    });
+  });
+
+  it('pass on overriddenBy information from atomic css to monolithic css', () => {
+    const rules = [
+      { cssRule: '.abcdef0{background-color:red;}', overriddenBy: 'bcdefg0' },
+      { cssRule: '.abcdef0>div{color:green;}', overriddenBy: 'bcdefg1' },
+      { cssRule: '@media screen and (max-width: 992px){.abcdef0{background-color:blue;}}', overriddenBy: 'bcdefg2' },
+    ];
+    expect(getMonolithicCSSRules(rules)).toEqual({
+      '': [
+        {
+          className: 'abcdef0',
+          css: 'background-color:red;',
+          overriddenBy: 'bcdefg0',
+        },
+      ],
+      '>div': [
+        {
+          className: 'abcdef0',
+          css: 'color:green;',
+          overriddenBy: 'bcdefg1',
+        },
+      ],
+      '@media screen and (max-width: 992px)': {
+        '': [
+          {
+            className: 'abcdef0',
+            css: 'background-color:blue;',
+            overriddenBy: 'bcdefg2',
+          },
+        ],
+      },
+    });
+  });
+});
diff --git a/packages/devtools/src/utils.test.ts b/packages/devtools/src/utils.test.ts
new file mode 100644
index 0000000..b01a293
--- /dev/null
+++ b/packages/devtools/src/utils.test.ts
@@ -0,0 +1,74 @@
+import { getRulesBySlots } from './utils';
+import type { DebugResult } from '@griffel/core';
+
+describe('getRulesBySlots', () => {
+  it('traverse debug result, gather and return all makeStyles slot name and rules', () => {
+    const makeStylesNodes: DebugResult[] = [
+      {
+        sequenceHash: '___abcdef0_0000000',
+        direction: 'ltr',
+        children: [],
+        debugClassNames: [{ className: 'fabcde0' }],
+        rules: {
+          fabcde0: '.fabcde0{background-color:transparent;}',
+        },
+        slot: 'root',
+      },
+      {
+        sequenceHash: '___abcdef1_0000000',
+        direction: 'ltr',
+        children: [],
+        debugClassNames: [{ className: 'fabcde1', overriddenBy: 'fabcde2' }],
+        rules: {
+          fabcde1: '.fabcde1{display:block;}',
+        },
+        slot: 'slot1',
+      },
+      {
+        sequenceHash: '___abcdef2_0000000',
+        direction: 'ltr',
+        children: [],
+        debugClassNames: [{ className: 'fabcde2' }],
+        rules: {
+          fabcde2: '.fabcde2{display:none;}',
+        },
+        slot: 'slot2',
+      },
+    ];
+    const debugResultRoot = {
+      children: [
+        makeStylesNodes[0],
+        {
+          children: [makeStylesNodes[1], makeStylesNodes[2]],
+        },
+      ],
+    };
+    expect(getRulesBySlots(debugResultRoot as DebugResult)).toEqual([
+      {
+        slot: 'slot2',
+        rules: [
+          {
+            cssRule: '.fabcde2{display:none;}',
+          },
+        ],
+      },
+      {
+        slot: 'slot1',
+        rules: [
+          {
+            cssRule: '.fabcde1{display:block;}',
+            overriddenBy: 'fabcde2',
+          },
+        ],
+      },
+      {
+        slot: 'root',
+        rules: [
+          {
+            cssRule: '.fabcde0{background-color:transparent;}',
+          },
+        ],
+      },
+    ]);
+  });
+});
diff --git a/packages/eslint-plugin/src/rules/no-shorthands.test.ts b/packages/eslint-plugin/src/rules/no-shorthands.test.ts
new file mode 100644
index 0000000..36a39c3
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/no-shorthands.test.ts
@@ -0,0 +1,64 @@
+import { TSESLint } from '@typescript-eslint/utils';
+import * as path from 'path';
+
+import { noShorthandsRule, RULE_NAME } from './no-shorthands';
+
+const ruleTester = new TSESLint.RuleTester({
+  parser: path.resolve('./node_modules/@typescript-eslint/parser'),
+  parserOptions: {
+    ecmaVersion: 2018,
+    sourceType: 'module',
+  },
+});
+
+ruleTester.run(RULE_NAME, noShorthandsRule, {
+  valid: [
+    {
+      name: 'without CSS shorthands',
+      code: `
+import { makeStyles } from '@griffel/react';
+
+export const useStyles = makeStyles({
+  root: { backgroundColor: 'red' },
+});
+`,
+    },
+    {
+      name: 'CSS shorthands can be used as slot names',
+      code: `
+import { makeStyles } from '@griffel/react';
+
+export const useStyles = makeStyles({
+  background: { backgroundColor: 'red' },
+});
+`,
+    },
+  ],
+
+  invalid: [
+    {
+      name: 'CSS shorthand or root level',
+      code: `
+import { makeStyles } from '@griffel/react';
+
+export const useStyles = makeStyles({
+  root: { background: 'red' },
+});
+`,
+      errors: [{ messageId: 'shorthandFound' }],
+    },
+    {
+      name: 'CSS shorthand in a selector',
+      code: `
+import { makeStyles } from '@griffel/react';
+
+export const useStyles = makeStyles({
+  root: {
+    ':hover': { background: 'red' }
+  },
+});
+`,
+      errors: [{ messageId: 'shorthandFound' }],
+    },
+  ],
+});