diff --git a/.changeset/strong-penguins-search.md b/.changeset/strong-penguins-search.md
new file mode 100644
index 0000000..69b4cd5
--- /dev/null
+++ b/.changeset/strong-penguins-search.md
@@ -0,0 +1,5 @@
+---
+"@pandacss/eslint-plugin": patch
+---
+
+Add `no-physical-properties` rule
diff --git a/README.md b/README.md
index 46db660..48dbe0f 100644
--- a/README.md
+++ b/README.md
@@ -89,6 +89,7 @@ Where rules are included in the configs `recommended`, or `all` it is indicated
| [`@pandacss/no-invalid-token-paths`](docs/rules/no-invalid-token-paths.md) | ✔️ |
| [`@pandacss/no-invalid-nesting`](docs/rules/no-invalid-nesting.md) | ✔️ |
| [`@pandacss/no-property-renaming`](docs/rules/no-property-renaming.md) | ✔️ |
+| [`@pandacss/no-physical-properties`](docs/rules/no-physical-properties.md) | |
| [`@pandacss/no-unsafe-token-fn-usage`](docs/rules/no-unsafe-token-fn-usage.md) | |
| [`@pandacss/prefer-longhand-properties`](docs/rules/prefer-longhand-properties.md) | |
| [`@pandacss/prefer-shorthand-properties`](docs/rules/prefer-shorthand-properties.md) | |
diff --git a/docs/rules/no-physical-properties.md b/docs/rules/no-physical-properties.md
new file mode 100644
index 0000000..d94d323
--- /dev/null
+++ b/docs/rules/no-physical-properties.md
@@ -0,0 +1,60 @@
+[//]: # (This file is generated by eslint-docgen. Do not edit it directly.)
+
+# no-physical-properties
+
+Encourage the use of [logical properties](https://mdn.io/logical-properties-basic-concepts) over physical proeprties, to foster a responsive and adaptable user interface.
+
+📋 This rule is enabled in `plugin:@pandacss/all`.
+
+## Rule details
+
+❌ Examples of **incorrect** code:
+```js
+import { css } from './panda/css';
+
+const styles = css({ left: '0' });
+```
+```js
+
+import { css } from './panda/css';
+
+function App(){
+ return
;
+};
+```
+```js
+
+import { Circle } from './panda/jsx';
+
+function App(){
+ return ;
+}
+```
+
+✔️ Examples of **correct** code:
+```js
+import { css } from './panda/css';
+
+const styles = css({ insetInlineStart: '0' });
+```
+```js
+
+import { css } from './panda/css';
+
+function App(){
+ return ;
+};
+```
+```js
+
+import { Circle } from './panda/jsx';
+
+function App(){
+ return ;
+}
+```
+
+## Resources
+
+* [Rule source](/plugin/src/rules/no-physical-properties.ts)
+* [Test source](/tests/no-physical-properties.test.ts)
diff --git a/plugin/src/rules/index.ts b/plugin/src/rules/index.ts
index 7d0e144..a7088b7 100644
--- a/plugin/src/rules/index.ts
+++ b/plugin/src/rules/index.ts
@@ -7,6 +7,7 @@ import noHardCodedColor, { RULE_NAME as NoHardCodedColor } from './no-hardcoded-
import noImportant, { RULE_NAME as NoImportant } from './no-important'
import noInvalidNesting, { RULE_NAME as NoInvalidNesting } from './no-invalid-nesting'
import noInvalidTokenPaths, { RULE_NAME as NoInvalidTokenPaths } from './no-invalid-token-paths'
+import noPhysicalProperties, { RULE_NAME as NoPhysicalProperties } from './no-physical-properties'
import noPropertyRenaming, { RULE_NAME as NoPropertyRenaming } from './no-property-renaming'
import noUnsafeTokenUsage, { RULE_NAME as NoUnsafeTokenUsage } from './no-unsafe-token-fn-usage'
import preferLonghandProperties, { RULE_NAME as PreferLonghandProperties } from './prefer-longhand-properties'
@@ -26,6 +27,7 @@ export const rules = {
[NoInvalidTokenPaths]: noInvalidTokenPaths,
[NoInvalidNesting]: noInvalidNesting,
[NoPropertyRenaming]: noPropertyRenaming,
+ [NoPhysicalProperties]: noPhysicalProperties,
[NoUnsafeTokenUsage]: noUnsafeTokenUsage,
[PreferLonghandProperties]: preferLonghandProperties,
[PreferShorthandProperties]: preferShorthandProperties,
diff --git a/plugin/src/rules/no-physical-properties.ts b/plugin/src/rules/no-physical-properties.ts
new file mode 100644
index 0000000..b8d687b
--- /dev/null
+++ b/plugin/src/rules/no-physical-properties.ts
@@ -0,0 +1,75 @@
+import { isPandaAttribute, isPandaProp, resolveLonghand } from '../utils/helpers'
+import { type Rule, createRule } from '../utils'
+import { isIdentifier, isJSXIdentifier } from '../utils/nodes'
+import { physicalProperties } from '../utils/physical-properties'
+
+export const RULE_NAME = 'no-physical-properties'
+
+const rule: Rule = createRule({
+ name: RULE_NAME,
+ meta: {
+ docs: {
+ description:
+ 'Encourage the use of [logical properties](https://mdn.io/logical-properties-basic-concepts) over physical proeprties, to foster a responsive and adaptable user interface.',
+ },
+ messages: {
+ physical: 'Use logical property of {{physical}} instead. Prefer `{{logical}}`',
+ replace: 'Replace `{{physical}}` with `{{logical}}`',
+ },
+ type: 'suggestion',
+ hasSuggestions: true,
+ schema: [],
+ },
+ defaultOptions: [],
+ create(context) {
+ const getLonghand = (name: string) => resolveLonghand(name, context) ?? name
+
+ const sendReport = (node: any, name: string) => {
+ const logical = physicalProperties[getLonghand(name)]
+ const longhand = resolveLonghand(name, context)
+
+ return context.report({
+ node,
+ messageId: 'physical',
+ data: {
+ physical: `\`${name}\`${longhand ? ` - \`${longhand}\`` : ''}`,
+ logical,
+ },
+ suggest: [
+ {
+ messageId: 'replace',
+ data: {
+ physical: name,
+ logical,
+ },
+ fix: (fixer) => {
+ return fixer.replaceTextRange(node.range, logical)
+ },
+ },
+ ],
+ })
+ }
+
+ return {
+ JSXAttribute(node) {
+ if (!isJSXIdentifier(node.name)) return
+ if (!isPandaProp(node, context)) return
+
+ if (getLonghand(node.name.name) in physicalProperties) {
+ sendReport(node.name, node.name.name)
+ }
+ },
+
+ Property(node) {
+ if (!isIdentifier(node.key)) return
+ if (!isPandaAttribute(node, context)) return
+
+ if (getLonghand(node.key.name) in physicalProperties) {
+ sendReport(node.key, node.key.name)
+ }
+ },
+ }
+ },
+})
+
+export default rule
diff --git a/plugin/src/rules/prefer-atomic-properties.ts b/plugin/src/rules/prefer-atomic-properties.ts
index 1186f09..58d712d 100644
--- a/plugin/src/rules/prefer-atomic-properties.ts
+++ b/plugin/src/rules/prefer-atomic-properties.ts
@@ -55,6 +55,7 @@ const rule: Rule = createRule({
Property(node) {
if (!isIdentifier(node.key)) return
if (!isPandaAttribute(node, context)) return
+
const cmp = resolveCompositeProperty(node.key.name)
if (!cmp) return
diff --git a/plugin/src/rules/prefer-composite-properties.ts b/plugin/src/rules/prefer-composite-properties.ts
index e1fc268..5dc024e 100644
--- a/plugin/src/rules/prefer-composite-properties.ts
+++ b/plugin/src/rules/prefer-composite-properties.ts
@@ -53,6 +53,7 @@ const rule: Rule = createRule({
Property(node) {
if (!isIdentifier(node.key)) return
if (!isPandaAttribute(node, context)) return
+
const atm = resolveCompositeProperty(node.key.name)
if (!atm) return
diff --git a/plugin/src/rules/prefer-longhand-properties.ts b/plugin/src/rules/prefer-longhand-properties.ts
index 4f71fe1..f33b7cf 100644
--- a/plugin/src/rules/prefer-longhand-properties.ts
+++ b/plugin/src/rules/prefer-longhand-properties.ts
@@ -59,6 +59,7 @@ const rule: Rule = createRule({
Property(node) {
if (!isIdentifier(node.key)) return
if (!isPandaAttribute(node, context)) return
+
const longhand = resolveLonghand(node.key.name, context)
if (!longhand) return
diff --git a/plugin/src/rules/prefer-shorthand-properties.ts b/plugin/src/rules/prefer-shorthand-properties.ts
index c85f8be..97a8f59 100644
--- a/plugin/src/rules/prefer-shorthand-properties.ts
+++ b/plugin/src/rules/prefer-shorthand-properties.ts
@@ -62,6 +62,7 @@ const rule: Rule = createRule({
Property(node) {
if (!isIdentifier(node.key)) return
if (!isPandaAttribute(node, context)) return
+
const longhand = resolveLonghand(node.key.name, context)
if (longhand) return
diff --git a/plugin/src/rules/prefer-unified-property-style.ts b/plugin/src/rules/prefer-unified-property-style.ts
index 46b90ce..cca0060 100644
--- a/plugin/src/rules/prefer-unified-property-style.ts
+++ b/plugin/src/rules/prefer-unified-property-style.ts
@@ -24,10 +24,10 @@ const rule: Rule = createRule({
const getLonghand = (name: string) => resolveLonghand(name, context) ?? name
const resolveCompositeProperty = (name: string) => {
- if (Object.hasOwn(compositeProperties, name)) return name
+ if (name in compositeProperties) return name
const longhand = getLonghand(name)
- if (isValidProperty(longhand, context) && Object.hasOwn(compositeProperties, longhand)) return longhand
+ if (isValidProperty(longhand, context) && longhand in compositeProperties) return longhand
}
const sendReport = (node: any, cmp: string, siblings: string[]) => {
@@ -54,6 +54,7 @@ const rule: Rule = createRule({
JSXAttribute(node) {
if (!isJSXIdentifier(node.name)) return
if (!isPandaProp(node, context)) return
+
const cmp = resolveCompositeProperty(node.name.name)
if (!cmp) return
if (!isJSXOpeningElement(node.parent)) return
@@ -65,6 +66,7 @@ const rule: Rule = createRule({
Property(node) {
if (!isIdentifier(node.key)) return
if (!isPandaAttribute(node, context)) return
+
const cmp = resolveCompositeProperty(node.key.name)
if (!cmp) return
if (!isObjectExpression(node.parent)) return
diff --git a/plugin/src/utils/physical-properties.ts b/plugin/src/utils/physical-properties.ts
new file mode 100644
index 0000000..f45052b
--- /dev/null
+++ b/plugin/src/utils/physical-properties.ts
@@ -0,0 +1,34 @@
+export const physicalProperties: Record = {
+ borderBottom: 'borderBlockEnd',
+ borderBottomColor: 'borderBlockEndColor',
+ borderBottomStyle: 'borderBlockEndStyle',
+ borderBottomWidth: 'borderBlockEndWidth',
+ borderTop: 'borderBlockStart',
+ borderTopColor: 'borderBlockStartColor',
+ borderTopStyle: 'borderBlockStartStyle',
+ borderTopWidth: 'borderBlockStartWidth',
+ borderRight: 'borderInlineEnd',
+ borderRightColor: 'borderInlineEndColor',
+ borderRightStyle: 'borderInlineEndStyle',
+ borderRightWidth: 'borderInlineEndWidth',
+ borderLeft: 'borderInlineStart',
+ borderLeftColor: 'borderInlineStartColor',
+ borderLeftStyle: 'borderInlineStartStyle',
+ borderLeftWidth: 'borderInlineStartWidth',
+ borderTopLeftRadius: 'borderStartStartRadius',
+ borderBottomLeftRadius: 'borderEndStartRadius',
+ borderTopRightRadius: 'borderStartEndRadius',
+ borderBottomRightRadius: 'borderEndEndRadius',
+ marginBottom: 'marginBlockEnd',
+ marginTop: 'marginBlockStart',
+ marginRight: 'marginInlineEnd',
+ marginLeft: 'marginInlineStart',
+ paddingBottom: 'paddingBlockEnd',
+ paddingTop: 'paddingBlockStart',
+ paddingRight: 'paddingInlineEnd',
+ paddingLeft: 'paddingInlineStart',
+ left: 'insetInlineStart',
+ right: 'insetInlineEnd',
+ top: 'insetBlockStart',
+ bottom: 'insetBlockEnd',
+}
diff --git a/plugin/tests/no-physical-proeprties.test.ts b/plugin/tests/no-physical-proeprties.test.ts
new file mode 100644
index 0000000..1a256ec
--- /dev/null
+++ b/plugin/tests/no-physical-proeprties.test.ts
@@ -0,0 +1,68 @@
+import { tester } from '../test-utils'
+import rule, { RULE_NAME } from '../src/rules/no-physical-properties'
+
+const javascript = String.raw
+
+const valids = [
+ {
+ code: javascript`
+import { css } from './panda/css';
+
+const styles = css({ insetInlineStart: '0' })`,
+ },
+
+ {
+ code: javascript`
+import { css } from './panda/css';
+
+function App(){
+ return ;
+}`,
+ },
+
+ {
+ code: javascript`
+import { Circle } from './panda/jsx';
+
+function App(){
+ return ;
+}`,
+ },
+]
+
+const invalids = [
+ {
+ code: javascript`
+import { css } from './panda/css';
+
+const styles = css({ left: '0' })`,
+ },
+
+ {
+ code: javascript`
+import { css } from './panda/css';
+
+function App(){
+ return ;
+}`,
+ },
+
+ {
+ code: javascript`
+import { Circle } from './panda/jsx';
+
+function App(){
+ return ;
+}`,
+ },
+]
+
+tester.run(RULE_NAME, rule, {
+ valid: valids.map(({ code }) => ({
+ code,
+ })),
+ invalid: invalids.map(({ code }) => ({
+ code,
+ errors: 1,
+ })),
+})
diff --git a/sandbox/src/App.tsx b/sandbox/src/App.tsx
index 3431690..ade9808 100644
--- a/sandbox/src/App.tsx
+++ b/sandbox/src/App.tsx
@@ -32,7 +32,7 @@ function App() {
fontSize: 'token(fontSizes.2xl, 4px)',
marginTop: '{spacings.4} token(spacing.600)',
margin: '4',
- paddingTop: token('sizes.4'),
+ pt: token('sizes.4'),
})
const color = 'red'