diff --git a/.eslintrc.js b/.eslintrc.js
index 62b27799a..16fc10361 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -43,6 +43,7 @@ module.exports = {
'**/__mocks__/snapshot*',
'**/storybook-static/**',
'**/examples/example-cli/src/**',
+ '**/devtools-extension/extension/**',
],
globals: {
$Call: 'readonly',
@@ -142,7 +143,7 @@ module.exports = {
'no-unexpected-multiline': 2,
'no-unmodified-loop-condition': 2,
'no-unneeded-ternary': [2, { defaultAssignment: false }],
- 'no-unreachable': 2,
+ 'no-unreachable': 0,
'no-unsafe-finally': 2,
'no-unused-vars': [
2,
diff --git a/.flowconfig b/.flowconfig
index 002210a78..476b2cdf7 100644
--- a/.flowconfig
+++ b/.flowconfig
@@ -11,6 +11,7 @@
[options]
experimental.pattern_matching=true
+component_syntax=true
enums=true
emoji=true
casting_syntax=as
diff --git a/.prettierignore b/.prettierignore
index 2cfbe37fa..3a2fee7da 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -12,4 +12,5 @@ node_modules
packages/benchmarks/size/fixtures/lotsOfStyles.js
flow-typed
examples/example-storybook/storybook-static
-examples/example-cli/src
\ No newline at end of file
+examples/example-cli/src
+devtools-extension/extension
\ No newline at end of file
diff --git a/examples/example-redwoodsdk/src/app/components/Copy.tsx b/examples/example-redwoodsdk/src/app/components/Copy.tsx
index d06537957..ec431fe37 100644
--- a/examples/example-redwoodsdk/src/app/components/Copy.tsx
+++ b/examples/example-redwoodsdk/src/app/components/Copy.tsx
@@ -22,7 +22,16 @@ export function Copy({ textToCopy }: { textToCopy: string }) {
const styles = stylex.create({
copyButton: {
- backgroundColor: 'transparent',
+ backgroundColor: {
+ default: 'transparent',
+ ':hover': 'rgba(255,255,255,0.1)',
+ ':focus-visible': 'rgba(255,255,255,0.1)',
+ '@media not (hover: hover)': {
+ default: 'rgba(255,255,255,0.1)',
+ ':hover': 'rgba(255,255,255,0.3)',
+ ':focus-visible': 'rgba(255,255,255,0.3)',
+ },
+ },
color: '#ffad48',
borderWidth: 0,
borderRadius: 4,
@@ -31,6 +40,5 @@ const styles = stylex.create({
cursor: 'pointer',
fontSize: 16,
fontWeight: 700,
- ':hover': { backgroundColor: 'rgba(255,255,255,0.1)' },
},
});
diff --git a/package-lock.json b/package-lock.json
index 27399de79..f16bc0d04 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -60,7 +60,6 @@
"integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -80,7 +79,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -588,7 +586,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -903,7 +900,6 @@
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -1158,7 +1154,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -1218,7 +1213,6 @@
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
"dev": true,
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -1286,7 +1280,6 @@
"version": "19.2.6",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -1509,7 +1502,6 @@
"version": "4.0.3",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -1553,7 +1545,6 @@
"examples/example-react-router/node_modules/react": {
"version": "19.2.0",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -1561,7 +1552,6 @@
"examples/example-react-router/node_modules/react-dom": {
"version": "19.2.0",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -1649,7 +1639,6 @@
"version": "7.2.4",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -1746,7 +1735,6 @@
"examples/example-redwoodsdk/node_modules/@cloudflare/vite-plugin": {
"version": "1.15.2",
"license": "MIT",
- "peer": true,
"dependencies": {
"@cloudflare/unenv-preset": "2.7.11",
"@remix-run/node-fetch-server": "^0.8.0",
@@ -1824,7 +1812,6 @@
"version": "19.2.6",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -2142,7 +2129,6 @@
"examples/example-redwoodsdk/node_modules/picomatch": {
"version": "4.0.3",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -2153,7 +2139,6 @@
"examples/example-redwoodsdk/node_modules/react": {
"version": "19.3.0-canary-561ee24d-20251101",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -2161,7 +2146,6 @@
"examples/example-redwoodsdk/node_modules/react-dom": {
"version": "19.3.0-canary-561ee24d-20251101",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "0.28.0-canary-561ee24d-20251101"
},
@@ -2274,7 +2258,6 @@
"examples/example-redwoodsdk/node_modules/rwsdk/node_modules/@types/react": {
"version": "19.1.17",
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -2416,7 +2399,6 @@
"examples/example-redwoodsdk/node_modules/vite": {
"version": "7.2.4",
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -2539,7 +2521,6 @@
"examples/example-rollup/node_modules/react": {
"version": "19.2.0",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -2609,7 +2590,6 @@
"examples/example-rspack/node_modules/react": {
"version": "19.2.0",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -2826,7 +2806,6 @@
"version": "8.44.0",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.44.0",
"@typescript-eslint/types": "8.44.0",
@@ -3048,7 +3027,6 @@
"version": "9.36.0",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -3150,7 +3128,6 @@
"version": "3.0.3",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/estree": "^1.0.0"
}
@@ -3259,7 +3236,6 @@
"version": "4.0.3",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -3368,7 +3344,6 @@
"integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -3675,7 +3650,6 @@
"version": "19.2.6",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -3700,7 +3674,6 @@
"version": "9.39.1",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -3862,7 +3835,6 @@
"version": "4.0.3",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -3873,7 +3845,6 @@
"examples/example-vite-react/node_modules/react": {
"version": "19.2.0",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -3996,7 +3967,6 @@
"version": "19.2.6",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -4050,7 +4020,6 @@
"version": "4.0.3",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -4061,7 +4030,6 @@
"examples/example-vite-rsc/node_modules/react": {
"version": "19.2.0",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -4094,7 +4062,6 @@
"version": "7.1.12",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -4212,7 +4179,6 @@
"version": "4.0.3",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -4223,7 +4189,6 @@
"examples/example-vite/node_modules/react": {
"version": "19.2.0",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -4256,7 +4221,6 @@
"version": "7.2.4",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -4353,7 +4317,6 @@
"version": "19.2.6",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -4424,7 +4387,6 @@
"examples/example-waku/node_modules/picomatch": {
"version": "4.0.3",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -4435,7 +4397,6 @@
"examples/example-waku/node_modules/react": {
"version": "19.2.0",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -4443,7 +4404,6 @@
"examples/example-waku/node_modules/react-dom": {
"version": "19.2.0",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -4465,7 +4425,6 @@
"examples/example-waku/node_modules/vite": {
"version": "7.2.2",
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -4627,7 +4586,6 @@
"version": "8.17.1",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -4790,7 +4748,6 @@
"examples/example-webpack/node_modules/react": {
"version": "19.1.1",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -5109,7 +5066,6 @@
"node_modules/@algolia/client-search": {
"version": "5.34.0",
"license": "MIT",
- "peer": true,
"dependencies": {
"@algolia/client-common": "5.34.0",
"@algolia/requester-browser-xhr": "5.34.0",
@@ -5418,7 +5374,6 @@
"node_modules/@babel/core": {
"version": "7.28.5",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -7423,8 +7378,7 @@
"node_modules/@cloudflare/workers-types": {
"version": "4.20251121.0",
"devOptional": true,
- "license": "MIT OR Apache-2.0",
- "peer": true
+ "license": "MIT OR Apache-2.0"
},
"node_modules/@codemirror/autocomplete": {
"version": "6.20.0",
@@ -7732,7 +7686,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18"
},
@@ -7753,7 +7706,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -9037,7 +8989,6 @@
"node_modules/@fortawesome/fontawesome-svg-core": {
"version": "6.7.2",
"license": "MIT",
- "peer": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
},
@@ -10590,7 +10541,6 @@
"node_modules/@mdx-js/mdx/node_modules/@babel/core": {
"version": "7.12.9",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/generator": "^7.12.5",
@@ -11173,7 +11123,6 @@
"version": "3.6.0",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@octokit/auth-token": "^2.4.4",
"@octokit/graphql": "^4.5.8",
@@ -11376,7 +11325,6 @@
"version": "8.17.1",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -11734,7 +11682,6 @@
"version": "5.2.0",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
@@ -12047,7 +11994,8 @@
"optional": true,
"os": [
"darwin"
- ]
+ ],
+ "peer": true
},
"node_modules/@rspack/cli": {
"version": "1.6.5",
@@ -12072,7 +12020,6 @@
"version": "1.6.4",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@module-federation/runtime-tools": "0.21.4",
"@rspack/binding": "1.6.4",
@@ -12128,7 +12075,6 @@
"version": "8.17.1",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -12379,7 +12325,6 @@
"version": "8.13.0",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"json-schema-traverse": "^1.0.0",
@@ -12852,6 +12797,10 @@
"resolved": "packages/@stylexjs/cli",
"link": true
},
+ "node_modules/@stylexjs/devtools-extension": {
+ "resolved": "packages/@stylexjs/devtools-extension",
+ "link": true
+ },
"node_modules/@stylexjs/eslint-plugin": {
"resolved": "packages/@stylexjs/eslint-plugin",
"link": true
@@ -13019,7 +12968,6 @@
"node_modules/@svgr/core": {
"version": "6.5.1",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/core": "^7.19.6",
"@svgr/babel-preset": "^6.5.1",
@@ -13841,7 +13789,6 @@
"node_modules/@types/react": {
"version": "18.3.23",
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -13960,7 +13907,8 @@
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
- "optional": true
+ "optional": true,
+ "peer": true
},
"node_modules/@types/unist": {
"version": "2.0.11",
@@ -14671,7 +14619,6 @@
"version": "3.2.4",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@testing-library/dom": "^10.4.0",
"@testing-library/user-event": "^14.6.1",
@@ -14867,6 +14814,7 @@
"version": "3.2.4",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@vitest/pretty-format": "3.2.4",
"magic-string": "^0.30.17",
@@ -15202,7 +15150,6 @@
"node_modules/acorn": {
"version": "8.15.0",
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -15275,7 +15222,6 @@
"node_modules/ajv": {
"version": "6.12.6",
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -15330,7 +15276,6 @@
"node_modules/algoliasearch": {
"version": "4.25.2",
"license": "MIT",
- "peer": true,
"dependencies": {
"@algolia/cache-browser-local-storage": "4.25.2",
"@algolia/cache-common": "4.25.2",
@@ -15787,7 +15732,6 @@
"node_modules/astring": {
"version": "1.9.0",
"license": "MIT",
- "peer": true,
"bin": {
"astring": "bin/astring"
}
@@ -16460,7 +16404,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001726",
"electron-to-chromium": "^1.5.173",
@@ -16555,6 +16498,7 @@
"version": "6.7.14",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=8"
}
@@ -17215,8 +17159,7 @@
},
"node_modules/codemirror": {
"version": "5.65.19",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/collapse-white-space": {
"version": "1.0.6",
@@ -17496,7 +17439,6 @@
"node_modules/copy-webpack-plugin/node_modules/ajv": {
"version": "8.17.1",
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -17792,7 +17734,6 @@
"node_modules/css-minimizer-webpack-plugin/node_modules/ajv": {
"version": "8.17.1",
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -18669,8 +18610,7 @@
},
"node_modules/devtools-protocol": {
"version": "0.0.1495869",
- "license": "BSD-3-Clause",
- "peer": true
+ "license": "BSD-3-Clause"
},
"node_modules/dir-glob": {
"version": "3.0.1",
@@ -18768,6 +18708,7 @@
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
"license": "(MPL-2.0 OR Apache-2.0)",
+ "peer": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
@@ -19343,7 +19284,6 @@
"node_modules/eslint": {
"version": "8.57.1",
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -19565,7 +19505,6 @@
"version": "2.32.0",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -19617,7 +19556,6 @@
"version": "6.10.2",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"aria-query": "^5.3.2",
"array-includes": "^3.1.8",
@@ -19646,7 +19584,6 @@
"version": "7.37.5",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"array-includes": "^3.1.8",
"array.prototype.findlast": "^1.2.5",
@@ -19678,7 +19615,6 @@
"version": "4.6.2",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=10"
},
@@ -20858,7 +20794,6 @@
"node_modules/flow-api-translator/node_modules/typescript": {
"version": "5.3.2",
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -22416,7 +22351,6 @@
"node_modules/hermes-eslint": {
"version": "0.32.1",
"license": "MIT",
- "peer": true,
"dependencies": {
"esrecurse": "^4.3.0",
"hermes-estree": "0.32.1",
@@ -22531,7 +22465,6 @@
"node_modules/hono": {
"version": "4.10.6",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=16.9.0"
}
@@ -25068,7 +25001,6 @@
"version": "26.1.0",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
@@ -25201,7 +25133,6 @@
"node_modules/kysely": {
"version": "0.28.8",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=20.0.0"
}
@@ -25268,7 +25199,6 @@
"node_modules/lightningcss": {
"version": "1.30.2",
"license": "MPL-2.0",
- "peer": true,
"dependencies": {
"detect-libc": "^2.0.3"
},
@@ -26017,6 +25947,7 @@
"resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
"integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
"license": "MIT",
+ "peer": true,
"bin": {
"marked": "bin/marked.js"
},
@@ -27326,7 +27257,6 @@
"node_modules/mini-css-extract-plugin/node_modules/ajv": {
"version": "8.17.1",
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -28741,7 +28671,6 @@
"integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"playwright-core": "1.57.0"
},
@@ -28804,7 +28733,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -29212,7 +29140,6 @@
"version": "7.1.0",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -29414,7 +29341,6 @@
"node_modules/postcss-selector-parser": {
"version": "6.1.2",
"license": "MIT",
- "peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -29494,7 +29420,6 @@
"node_modules/prettier": {
"version": "3.5.3",
"license": "MIT",
- "peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -29509,7 +29434,6 @@
"version": "0.32.0",
"devOptional": true,
"license": "MIT",
- "peer": true,
"peerDependencies": {
"prettier": "^3.0.0"
}
@@ -29967,7 +29891,6 @@
"node_modules/react": {
"version": "17.0.2",
"license": "MIT",
- "peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
@@ -30104,7 +30027,6 @@
"node_modules/react-dom": {
"version": "17.0.2",
"license": "MIT",
- "peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
@@ -30164,7 +30086,6 @@
"name": "@docusaurus/react-loadable",
"version": "5.5.2",
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/react": "*",
"prop-types": "^15.6.2"
@@ -30190,7 +30111,6 @@
"node_modules/react-refresh": {
"version": "0.17.0",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -30198,7 +30118,6 @@
"node_modules/react-router": {
"version": "5.3.4",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
@@ -30250,7 +30169,6 @@
"resolved": "https://registry.npmjs.org/react-server-dom-webpack/-/react-server-dom-webpack-19.2.1.tgz",
"integrity": "sha512-6z3FuEtZ7wVWyPYRhKKRo4TF7IyQ7XrQZRW1fTjzK2mLVmcv7xJtoANSixaUJ+EFjCh2ekNOaBSoI9spiMSnNA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"acorn-loose": "^8.3.0",
"neo-async": "^2.6.1",
@@ -31071,7 +30989,6 @@
"node_modules/remark-mdx/node_modules/@babel/core": {
"version": "7.12.9",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/generator": "^7.12.5",
@@ -31702,7 +31619,6 @@
"node_modules/rollup": {
"version": "4.45.1",
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -33218,7 +33134,6 @@
"integrity": "sha512-kfr6kxQAjA96ADlH6FMALJwJ+eM80UqXy106yVHNgdsAP/CdzkkicglRAhZAvUycXK9AeadF6KZ00CWLtVMN4w==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@storybook/global": "^5.0.0",
"@testing-library/jest-dom": "^6.6.3",
@@ -34181,7 +34096,6 @@
"node_modules/terser-webpack-plugin/node_modules/ajv": {
"version": "8.17.1",
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -34394,7 +34308,6 @@
"node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.3",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -34406,6 +34319,7 @@
"version": "1.1.1",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": "^18.0.0 || >=20.0.0"
}
@@ -34644,8 +34558,7 @@
},
"node_modules/tslib": {
"version": "2.8.1",
- "license": "0BSD",
- "peer": true
+ "license": "0BSD"
},
"node_modules/turbo-stream": {
"version": "3.1.0",
@@ -34781,7 +34694,6 @@
"node_modules/typescript": {
"version": "5.9.3",
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -34849,7 +34761,6 @@
"version": "8.46.3",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.46.3",
"@typescript-eslint/types": "8.46.3",
@@ -35155,7 +35066,6 @@
"node_modules/unenv": {
"version": "2.0.0-rc.24",
"license": "MIT",
- "peer": true,
"dependencies": {
"pathe": "^2.0.3"
}
@@ -35382,6 +35292,7 @@
"node_modules/unplugin": {
"version": "2.3.11",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@jridgewell/remapping": "^2.3.5",
"acorn": "^8.15.0",
@@ -35395,6 +35306,7 @@
"node_modules/unplugin/node_modules/picomatch": {
"version": "4.0.3",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -35947,6 +35859,7 @@
"version": "3.2.4",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"cac": "^6.7.14",
"debug": "^4.4.1",
@@ -35968,6 +35881,7 @@
"version": "6.5.0",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12.0.0"
},
@@ -35996,6 +35910,7 @@
"version": "7.1.6",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -36200,6 +36115,7 @@
"version": "3.2.4",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@vitest/spy": "3.2.4",
"estree-walker": "^3.0.3",
@@ -36225,6 +36141,7 @@
"version": "3.0.3",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/estree": "^1.0.0"
}
@@ -36233,6 +36150,7 @@
"version": "6.5.0",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12.0.0"
},
@@ -36421,7 +36339,6 @@
"node_modules/webpack": {
"version": "5.101.3",
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8",
@@ -36647,7 +36564,6 @@
"node_modules/webpack-dev-middleware/node_modules/ajv": {
"version": "8.17.1",
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -36757,7 +36673,6 @@
"node_modules/webpack-dev-server/node_modules/ajv": {
"version": "8.17.1",
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -36857,7 +36772,6 @@
"node_modules/webpack/node_modules/ajv": {
"version": "8.17.1",
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -37123,7 +37037,6 @@
"version": "1.20251118.0",
"hasInstallScript": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"workerd": "bin/workerd"
},
@@ -37141,7 +37054,6 @@
"node_modules/wrangler": {
"version": "4.50.0",
"license": "MIT OR Apache-2.0",
- "peer": true,
"dependencies": {
"@cloudflare/kv-asset-handler": "0.4.0",
"@cloudflare/unenv-preset": "2.7.11",
@@ -37815,7 +37727,6 @@
"version": "2.3.1",
"devOptional": true,
"license": "ISC",
- "peer": true,
"engines": {
"node": ">= 14"
}
@@ -38089,7 +38000,6 @@
"version": "4.0.3",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -38119,6 +38029,186 @@
"scripts": "0.17.4"
}
},
+ "packages/@stylexjs/devtools-extension": {
+ "version": "0.17.3",
+ "license": "MIT",
+ "dependencies": {
+ "@stylexjs/stylex": "0.17.3",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3"
+ },
+ "devDependencies": {
+ "@babel/plugin-transform-flow-strip-types": "^7.27.1",
+ "@rollup/plugin-babel": "^6.0.4",
+ "@rollup/plugin-commonjs": "^28.0.1",
+ "@rollup/plugin-json": "^6.1.0",
+ "@rollup/plugin-node-resolve": "^15.3.0",
+ "@rollup/plugin-replace": "^6.0.1",
+ "@stylexjs/unplugin": "0.17.3",
+ "rollup": "^4.24.0"
+ }
+ },
+ "packages/@stylexjs/devtools-extension/node_modules/@rollup/plugin-commonjs": {
+ "version": "28.0.9",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.9.tgz",
+ "integrity": "sha512-PIR4/OHZ79romx0BVVll/PkwWpJ7e5lsqFa3gFfcrFPWwLXLV39JVUzQV9RKjWerE7B845Hqjj9VYlQeieZ2dA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rollup/pluginutils": "^5.0.1",
+ "commondir": "^1.0.1",
+ "estree-walker": "^2.0.2",
+ "fdir": "^6.2.0",
+ "is-reference": "1.2.1",
+ "magic-string": "^0.30.3",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=16.0.0 || 14 >= 14.17"
+ },
+ "peerDependencies": {
+ "rollup": "^2.68.0||^3.0.0||^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "packages/@stylexjs/devtools-extension/node_modules/@rollup/plugin-replace": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.3.tgz",
+ "integrity": "sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rollup/pluginutils": "^5.0.1",
+ "magic-string": "^0.30.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "packages/@stylexjs/devtools-extension/node_modules/@stylexjs/babel-plugin": {
+ "version": "0.17.3",
+ "resolved": "https://registry.npmjs.org/@stylexjs/babel-plugin/-/babel-plugin-0.17.3.tgz",
+ "integrity": "sha512-C9KOQUa+PS7Ef6KGLfEuevzP5lA183g5iu+xito14tN1Tx4OfQe5oEo6O+ntHSVdyZGsnLj6aCr9elVg3yLoOQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.26.8",
+ "@babel/helper-module-imports": "^7.25.9",
+ "@babel/traverse": "^7.26.8",
+ "@babel/types": "^7.26.8",
+ "@dual-bundle/import-meta-resolve": "^4.1.0",
+ "@stylexjs/shared": "0.17.3",
+ "@stylexjs/stylex": "0.17.3",
+ "postcss-value-parser": "^4.1.0"
+ }
+ },
+ "packages/@stylexjs/devtools-extension/node_modules/@stylexjs/shared": {
+ "version": "0.17.3",
+ "resolved": "https://registry.npmjs.org/@stylexjs/shared/-/shared-0.17.3.tgz",
+ "integrity": "sha512-cnGYrskfEa8QGFFJG1JYCqPAstxFqe/aU7xuTDIIbL+38DQ4MDqSJOQFOWrqqma72UZXuGHAcJ7NqqnCVTKXTA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "packages/@stylexjs/devtools-extension/node_modules/@stylexjs/stylex": {
+ "version": "0.17.3",
+ "resolved": "https://registry.npmjs.org/@stylexjs/stylex/-/stylex-0.17.3.tgz",
+ "integrity": "sha512-uJKi6sWvsZo7A0Hku0SjnnxsYkQVyi/e1g34/Uik3PXLBVnGMZ+6N9NiwVGdfGQ+/ulqUqEHktROG02279Ug0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "css-mediaquery": "^0.1.2",
+ "invariant": "^2.2.4",
+ "styleq": "0.2.1"
+ }
+ },
+ "packages/@stylexjs/devtools-extension/node_modules/@stylexjs/unplugin": {
+ "version": "0.17.3",
+ "resolved": "https://registry.npmjs.org/@stylexjs/unplugin/-/unplugin-0.17.3.tgz",
+ "integrity": "sha512-v0b7CaLq1qD1qtOQtwly0ERPH+B9dAXwrNUpEFP4XupPRXAhcusmDnDtKddP2tOgS/aCsKgb0ElxOJ9ufJ2a/Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.26.8",
+ "@babel/plugin-syntax-flow": "^7.26.0",
+ "@babel/plugin-syntax-jsx": "^7.25.9",
+ "@babel/plugin-syntax-typescript": "^7.25.9",
+ "@stylexjs/babel-plugin": "0.17.3",
+ "browserslist": "^4.24.0",
+ "lightningcss": "^1.29.1"
+ },
+ "peerDependencies": {
+ "unplugin": "^2.3.11"
+ }
+ },
+ "packages/@stylexjs/devtools-extension/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "packages/@stylexjs/devtools-extension/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "packages/@stylexjs/devtools-extension/node_modules/react": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
+ "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "packages/@stylexjs/devtools-extension/node_modules/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.3"
+ }
+ },
+ "packages/@stylexjs/devtools-extension/node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
"packages/@stylexjs/eslint-plugin": {
"version": "0.17.4",
"license": "MIT",
@@ -38201,7 +38291,6 @@
"version": "4.0.3",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -38310,7 +38399,6 @@
"version": "4.0.3",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
diff --git a/packages/@stylexjs/devtools-extension/.babelrc.js b/packages/@stylexjs/devtools-extension/.babelrc.js
new file mode 100644
index 000000000..58429fc00
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/.babelrc.js
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+const BABEL_ENV = process.env['BABEL_ENV'];
+
+module.exports = {
+ assumptions: {
+ iterableIsArray: true,
+ },
+ presets: [
+ // [
+ // '@babel/preset-env',
+ // {
+ // exclude: ['@babel/plugin-transform-typeof-symbol'],
+ // targets: 'defaults',
+ // // Convert files to cjs for jest testing
+ // modules: BABEL_ENV === 'test' ? 'cjs' : false,
+ // },
+ // ],
+ '@babel/preset-flow',
+ '@babel/preset-react',
+ ],
+ plugins: [['babel-plugin-syntax-hermes-parser', { flow: 'detect' }]],
+};
diff --git a/packages/@stylexjs/devtools-extension/.gitignore b/packages/@stylexjs/devtools-extension/.gitignore
new file mode 100644
index 000000000..6f722e973
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/.gitignore
@@ -0,0 +1,3 @@
+
+extension/assets
+extension/*.html
diff --git a/packages/@stylexjs/devtools-extension/README.md b/packages/@stylexjs/devtools-extension/README.md
new file mode 100644
index 000000000..734577798
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/README.md
@@ -0,0 +1,35 @@
+# @stylexjs/devtools-extension
+
+Chrome DevTools extension for debugging StyleX in development.
+
+## Develop / Build
+
+Source lives in `src/` (React + StyleX). The loadable Chrome extension is the
+build output in `extension/`.
+
+```sh
+npm run build -w @stylexjs/devtools-extension
+```
+
+## Load in Chrome
+
+1. Open `chrome://extensions`
+2. Enable **Developer mode**
+3. Click **Load unpacked**
+4. Select `packages/@stylexjs/devtools-extension/extension`
+
+## Use
+
+1. Open DevTools → **Elements**
+2. Select an element that has `data-style-src`
+3. Open the **StyleX** tab in the Elements sidebar
+
+For best results, enable StyleX dev mode so the DOM includes `data-style-src`
+and dev marker classNames:
+
+```js
+// @stylexjs/babel-plugin
+{
+ dev: true,
+}
+```
diff --git a/packages/@stylexjs/devtools-extension/devtools.html b/packages/@stylexjs/devtools-extension/devtools.html
new file mode 100644
index 000000000..b6a6982ba
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/devtools.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ StyleX DevTools
+
+
+
+
+
diff --git a/packages/@stylexjs/devtools-extension/extension/manifest.json b/packages/@stylexjs/devtools-extension/extension/manifest.json
new file mode 100644
index 000000000..515e97937
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/extension/manifest.json
@@ -0,0 +1,8 @@
+{
+ "manifest_version": 3,
+ "name": "StyleX DevTools",
+ "version": "0.1.0",
+ "description": "Inspect StyleX-applied styles and their sources in the Elements panel.",
+ "devtools_page": "devtools.html"
+}
+
diff --git a/packages/@stylexjs/devtools-extension/flow-types/chrome.js b/packages/@stylexjs/devtools-extension/flow-types/chrome.js
new file mode 100644
index 000000000..e57ef677e
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/flow-types/chrome.js
@@ -0,0 +1,81 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict
+ */
+
+type Devtools = {
+ inspectedWindow: InspectedWindow,
+ panels: Panels,
+ network: Network,
+ ...
+};
+
+export type InspectedWindowEvalOptions = {
+ includeCommandLineAPI?: boolean,
+ ...
+};
+
+export type ExceptionInfo = {
+ isException: boolean,
+ value?: mixed,
+ ...
+};
+
+export type Resource = {
+ url: string,
+ getContent: (
+ callback: (content: ?string, encoding: ?string) => mixed,
+ ) => void,
+ ...
+};
+
+export type InspectedWindow = {
+ eval: (
+ expression: string,
+ options: InspectedWindowEvalOptions,
+ callback: (result: mixed, exceptionInfo?: ExceptionInfo) => mixed,
+ ) => void,
+ getResources: (callback: (resources: Array) => mixed) => void,
+ ...
+};
+
+export type DevtoolsEvent = {
+ addListener: (callback: (...args: any[]) => mixed) => void,
+ removeListener: (callback: (...args: any[]) => mixed) => void,
+ ...
+};
+
+export type SidebarPane = {
+ setPage: (page: string) => void,
+ setHeight: (height: number) => void,
+ ...
+};
+
+export type Panels = {
+ elements: {
+ createSidebarPane: (
+ title: string,
+ callback: (pane: SidebarPane) => mixed,
+ ) => void,
+ onSelectionChanged: DevtoolsEvent,
+ ...
+ },
+ openResource: (url: string, lineNumber?: number) => void,
+ ...
+};
+
+export type Network = {
+ onNavigated: DevtoolsEvent,
+ ...
+};
+
+declare var chrome: {
+ devtools: Devtools,
+ ...
+};
+
+export const devtools: Devtools = chrome.devtools;
diff --git a/packages/@stylexjs/devtools-extension/package.json b/packages/@stylexjs/devtools-extension/package.json
new file mode 100644
index 000000000..29d983fd7
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "@stylexjs/devtools-extension",
+ "version": "0.17.3",
+ "private": true,
+ "description": "Chrome DevTools extension for debugging StyleX.",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/facebook/stylex.git"
+ },
+ "license": "MIT",
+ "scripts": {
+ "dev": "rollup -c --watch",
+ "build": "rollup -c"
+ },
+ "files": [
+ "extension/**"
+ ],
+ "dependencies": {
+ "@stylexjs/stylex": "0.17.3",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3"
+ },
+ "devDependencies": {
+ "@babel/plugin-transform-flow-strip-types": "^7.27.1",
+ "@stylexjs/unplugin": "0.17.3",
+ "@rollup/plugin-babel": "^6.0.4",
+ "@rollup/plugin-commonjs": "^28.0.1",
+ "@rollup/plugin-json": "^6.1.0",
+ "@rollup/plugin-node-resolve": "^15.3.0",
+ "@rollup/plugin-replace": "^6.0.1",
+ "rollup": "^4.24.0"
+ }
+}
diff --git a/packages/@stylexjs/devtools-extension/panel.html b/packages/@stylexjs/devtools-extension/panel.html
new file mode 100644
index 000000000..3a5f86030
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/panel.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ StyleX
+
+
+
+
+
+
+
diff --git a/packages/@stylexjs/devtools-extension/public/manifest.json b/packages/@stylexjs/devtools-extension/public/manifest.json
new file mode 100644
index 000000000..515e97937
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/public/manifest.json
@@ -0,0 +1,8 @@
+{
+ "manifest_version": 3,
+ "name": "StyleX DevTools",
+ "version": "0.1.0",
+ "description": "Inspect StyleX-applied styles and their sources in the Elements panel.",
+ "devtools_page": "devtools.html"
+}
+
diff --git a/packages/@stylexjs/devtools-extension/rollup.config.mjs b/packages/@stylexjs/devtools-extension/rollup.config.mjs
new file mode 100644
index 000000000..676d0c877
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/rollup.config.mjs
@@ -0,0 +1,142 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import fs from 'node:fs/promises';
+import { babel } from '@rollup/plugin-babel';
+import commonjs from '@rollup/plugin-commonjs';
+import json from '@rollup/plugin-json';
+import { nodeResolve } from '@rollup/plugin-node-resolve';
+import replace from '@rollup/plugin-replace';
+import { browserslistToTargets } from 'lightningcss';
+import browserslist from 'browserslist';
+import stylex from '@stylexjs/unplugin';
+
+const rootDir = path.dirname(fileURLToPath(import.meta.url));
+const outDir = path.resolve(rootDir, 'extension');
+const extensions = ['.js', '.jsx'];
+const isWatch = Boolean(process.env.ROLLUP_WATCH);
+
+function cssBundle({ fileName = 'assets/style.css' } = {}) {
+ let styles = new Map();
+ return {
+ name: 'css-bundle',
+ buildStart() {
+ styles = new Map();
+ },
+ resolveId(source, importer) {
+ if (!source.endsWith('.css')) return null;
+ const resolved = importer
+ ? path.resolve(path.dirname(importer), source)
+ : path.resolve(source);
+ return { id: resolved, moduleSideEffects: true };
+ },
+ async load(id) {
+ if (!id.endsWith('.css')) return null;
+ const css = await fs.readFile(id, 'utf8');
+ styles.set(id, css);
+ this.addWatchFile(id);
+ return 'export default ""';
+ },
+ generateBundle() {
+ if (styles.size === 0) return;
+ const combined = Array.from(styles.values()).join('\n');
+ this.emitFile({
+ type: 'asset',
+ fileName,
+ source: combined,
+ });
+ },
+ };
+}
+
+function copyStatic({ outDir, targets }) {
+ const resolved = targets.map(({ src, dest }) => ({
+ src: path.resolve(rootDir, src),
+ dest: path.resolve(outDir, dest),
+ }));
+ async function copyAll() {
+ await fs.mkdir(outDir, { recursive: true });
+ await Promise.all(
+ resolved.map(async ({ src, dest }) => {
+ await fs.mkdir(path.dirname(dest), { recursive: true });
+ await fs.copyFile(src, dest);
+ }),
+ );
+ }
+ return {
+ name: 'copy-static',
+ buildStart() {
+ for (const { src } of resolved) {
+ this.addWatchFile(src);
+ }
+ },
+ async generateBundle() {
+ await copyAll();
+ },
+ };
+}
+
+export default {
+ input: {
+ devtools: path.resolve(rootDir, 'src/devtools/main.js'),
+ panel: path.resolve(rootDir, 'src/panel/main.js'),
+ },
+ output: {
+ dir: outDir,
+ format: 'es',
+ sourcemap: true,
+ entryFileNames: 'assets/[name].js',
+ chunkFileNames: 'assets/[name]-[hash].js',
+ assetFileNames: 'assets/[name][extname]',
+ },
+ plugins: [
+ cssBundle(),
+ replace({
+ preventAssignment: true,
+ values: {
+ 'process.env.NODE_ENV': JSON.stringify(
+ isWatch ? 'development' : 'production',
+ ),
+ },
+ }),
+ stylex.rollup({
+ devMode: 'off',
+ useCSSLayers: true,
+ lightningcssOptions: {
+ targets: browserslistToTargets(browserslist('>= 2%')),
+ },
+ }),
+ babel({
+ babelHelpers: 'bundled',
+ extensions,
+ babelrc: true,
+ configFile: path.resolve(rootDir, '.babelrc.js'),
+ include: [
+ path.resolve(rootDir, 'src/**/*'),
+ path.resolve(rootDir, 'flow-types/**/*'),
+ ],
+ exclude: ['**/node_modules/**'],
+ }),
+ nodeResolve({
+ browser: true,
+ extensions,
+ preferBuiltins: false,
+ }),
+ json(),
+ commonjs({ include: /node_modules/ }),
+ copyStatic({
+ outDir,
+ targets: [
+ { src: 'devtools.html', dest: 'devtools.html' },
+ { src: 'panel.html', dest: 'panel.html' },
+ { src: 'public/manifest.json', dest: 'manifest.json' },
+ ],
+ }),
+ ],
+};
diff --git a/packages/@stylexjs/devtools-extension/src/devtools/api.js b/packages/@stylexjs/devtools-extension/src/devtools/api.js
new file mode 100644
index 000000000..784494325
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/src/devtools/api.js
@@ -0,0 +1,106 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict
+ */
+
+'use strict';
+
+import { devtools } from '../../flow-types/chrome.js';
+import type {
+ ExceptionInfo,
+ InspectedWindowEvalOptions,
+ Resource,
+} from '../../flow-types/chrome.js';
+
+export { devtools };
+export type { ExceptionInfo, InspectedWindowEvalOptions, Resource };
+
+export function evalInInspectedWindow(
+ fn: () => T,
+ options?: InspectedWindowEvalOptions,
+): Promise {
+ const expression = `(${fn.toString()})()`;
+ const mergedOptions = {
+ // includeCommandLineAPI: true,
+ ...options,
+ };
+
+ return new Promise((resolve, reject) => {
+ devtools.inspectedWindow.eval(
+ expression,
+ mergedOptions as any,
+ (result, exceptionInfo) => {
+ if (exceptionInfo && exceptionInfo.isException) {
+ const msg =
+ exceptionInfo.value != null
+ ? `Error: ${String(exceptionInfo.value)}`
+ : 'Error evaluating in inspected window.';
+ reject(new Error(msg));
+ return;
+ }
+ resolve(result as any as T);
+ },
+ );
+ });
+}
+
+export function evalInInspectedWindowWithArgs(
+ fn: (args: any) => T,
+ args: mixed,
+ options?: InspectedWindowEvalOptions,
+): Promise {
+ const serializedArgs = JSON.stringify(args);
+ const expression = `(${fn.toString()})(${serializedArgs ?? ''})`;
+ const mergedOptions = {
+ // includeCommandLineAPI: true,
+ ...options,
+ };
+
+ return new Promise((resolve, reject) => {
+ devtools.inspectedWindow.eval(
+ expression,
+ mergedOptions as any,
+ (result, exceptionInfo) => {
+ if (exceptionInfo && exceptionInfo.isException) {
+ const msg =
+ exceptionInfo.value != null
+ ? `Error: ${String(exceptionInfo.value)}`
+ : 'Error evaluating in inspected window.';
+ reject(new Error(msg));
+ return;
+ }
+ resolve(result as any as T);
+ },
+ );
+ });
+}
+
+export function getResources(): Promise> {
+ return new Promise((resolve) => {
+ devtools.inspectedWindow.getResources((resources) => resolve(resources));
+ });
+}
+
+export function getResourceText(resource: Resource): Promise {
+ return new Promise((resolve) => {
+ resource.getContent((content, encoding) => {
+ if (encoding === 'base64' && typeof content === 'string') {
+ try {
+ resolve(atob(content));
+ } catch {
+ resolve(null);
+ }
+ return;
+ }
+ resolve(content);
+ });
+ });
+}
+
+export function openResource(url: string, lineZeroBased?: number): void {
+ devtools.panels.openResource(url, lineZeroBased);
+}
diff --git a/packages/@stylexjs/devtools-extension/src/devtools/createSidebarPane.js b/packages/@stylexjs/devtools-extension/src/devtools/createSidebarPane.js
new file mode 100644
index 000000000..7bf958dd8
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/src/devtools/createSidebarPane.js
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict
+ */
+
+'use strict';
+
+import { devtools } from '../../flow-types/chrome.js';
+
+export function createStylexSidebarPane(): void {
+ devtools.panels.elements.createSidebarPane('StyleX', (pane) => {
+ pane.setPage('panel.html');
+ pane.setHeight(400);
+ });
+}
diff --git a/packages/@stylexjs/devtools-extension/src/devtools/events.js b/packages/@stylexjs/devtools-extension/src/devtools/events.js
new file mode 100644
index 000000000..8fbcc95ae
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/src/devtools/events.js
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict
+ */
+
+'use strict';
+
+import { devtools } from './api.js';
+
+export function subscribeToSelectionAndNavigation(
+ callback: () => mixed,
+): () => void {
+ devtools.panels.elements.onSelectionChanged.addListener(callback);
+ devtools.network.onNavigated.addListener(callback);
+
+ return () => {
+ devtools.panels.elements.onSelectionChanged.removeListener(callback);
+ devtools.network.onNavigated.removeListener(callback);
+ };
+}
diff --git a/packages/@stylexjs/devtools-extension/src/devtools/main.js b/packages/@stylexjs/devtools-extension/src/devtools/main.js
new file mode 100644
index 000000000..35a6be9af
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/src/devtools/main.js
@@ -0,0 +1,14 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict
+ */
+
+'use strict';
+
+import { createStylexSidebarPane } from './createSidebarPane.js';
+
+createStylexSidebarPane();
diff --git a/packages/@stylexjs/devtools-extension/src/devtools/overrides.js b/packages/@stylexjs/devtools-extension/src/devtools/overrides.js
new file mode 100644
index 000000000..4d1c42884
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/src/devtools/overrides.js
@@ -0,0 +1,166 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict
+ */
+
+'use strict';
+
+import { evalInInspectedWindowWithArgs } from './api.js';
+import type { StylexOverride } from '../types.js';
+
+type SwapClassArgs = {
+ from: string,
+ to: string,
+};
+
+type InlineStyleArgs = {
+ property: string,
+ value: string,
+ important?: boolean,
+};
+
+type ClearInlineArgs = {
+ property: string,
+};
+
+type SetOverridesArgs = {
+ overrides: Array,
+};
+
+declare const window: any;
+
+function swapClassNameInInspectedWindow({ from, to }: SwapClassArgs): boolean {
+ const overrideElementKey = '__stylexDevtoolsOverrideElement__';
+ // $FlowExpectedError[cannot-resolve-name]
+ const current = typeof $0 !== 'undefined' ? $0 : null;
+ const stored = window[overrideElementKey];
+ const sameNode =
+ stored &&
+ current &&
+ typeof stored.isSameNode === 'function' &&
+ stored.isSameNode(current);
+ if (!sameNode && current) {
+ window[overrideElementKey] = current;
+ }
+ const element = sameNode
+ ? stored
+ : current ||
+ (stored && typeof stored.isSameNode === 'function' ? stored : null);
+ if (!element || !from || !to) return false;
+ element.classList.remove(from);
+ element.classList.add(to);
+ return true;
+}
+
+function setInlineStyleInInspectedWindow({
+ property,
+ value,
+ important,
+}: InlineStyleArgs): boolean {
+ const overrideElementKey = '__stylexDevtoolsOverrideElement__';
+ // $FlowExpectedError[cannot-resolve-name]
+ const current = typeof $0 !== 'undefined' ? $0 : null;
+ const stored = window[overrideElementKey];
+ const sameNode =
+ stored &&
+ current &&
+ typeof stored.isSameNode === 'function' &&
+ stored.isSameNode(current);
+ if (!sameNode && current) {
+ window[overrideElementKey] = current;
+ }
+ const element = sameNode
+ ? stored
+ : current ||
+ (stored && typeof stored.isSameNode === 'function' ? stored : null);
+ if (!element || !property) return false;
+ element.style.setProperty(property, value, important ? 'important' : '');
+ return true;
+}
+
+function clearInlineStyleInInspectedWindow({
+ property,
+}: ClearInlineArgs): boolean {
+ const overrideElementKey = '__stylexDevtoolsOverrideElement__';
+ // $FlowExpectedError[cannot-resolve-name]
+ const current = typeof $0 !== 'undefined' ? $0 : null;
+ const stored = window[overrideElementKey];
+ const sameNode =
+ stored &&
+ current &&
+ typeof stored.isSameNode === 'function' &&
+ stored.isSameNode(current);
+ if (!sameNode && current) {
+ window[overrideElementKey] = current;
+ }
+ const element = sameNode
+ ? stored
+ : current ||
+ (stored && typeof stored.isSameNode === 'function' ? stored : null);
+ if (!element || !property) return false;
+ element.style.removeProperty(property);
+ return true;
+}
+
+function setStylexOverridesInInspectedWindow({
+ overrides,
+}: SetOverridesArgs): boolean {
+ try {
+ const overrideElementKey = '__stylexDevtoolsOverrideElement__';
+ const overrideStoreKey = '__stylexDevtoolsOverrides__';
+ // $FlowExpectedError[cannot-resolve-name]
+ const current = typeof $0 !== 'undefined' ? $0 : null;
+ const stored = window[overrideElementKey];
+ const sameNode =
+ stored &&
+ current &&
+ typeof stored.isSameNode === 'function' &&
+ stored.isSameNode(current);
+ if (!sameNode && current) {
+ window[overrideElementKey] = current;
+ }
+ const element = sameNode
+ ? stored
+ : current ||
+ (stored && typeof stored.isSameNode === 'function' ? stored : null);
+ if (!element) return false;
+ const existing = window[overrideStoreKey];
+ const store: WeakMap =
+ existing && typeof existing.get === 'function' ? existing : new WeakMap();
+ if (existing == null || existing !== store) {
+ window[overrideStoreKey] = store;
+ }
+ if (!Array.isArray(overrides) || overrides.length === 0) {
+ store.delete(element);
+ } else {
+ store.set(element, overrides);
+ }
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+export function swapClassName(args: SwapClassArgs): Promise {
+ return evalInInspectedWindowWithArgs(swapClassNameInInspectedWindow, args);
+}
+
+export function setInlineStyle(args: InlineStyleArgs): Promise {
+ return evalInInspectedWindowWithArgs(setInlineStyleInInspectedWindow, args);
+}
+
+export function clearInlineStyle(args: ClearInlineArgs): Promise {
+ return evalInInspectedWindowWithArgs(clearInlineStyleInInspectedWindow, args);
+}
+
+export function setStylexOverrides(
+ overrides: Array,
+): Promise {
+ return evalInInspectedWindowWithArgs(setStylexOverridesInInspectedWindow, {
+ overrides,
+ });
+}
diff --git a/packages/@stylexjs/devtools-extension/src/devtools/resources.js b/packages/@stylexjs/devtools-extension/src/devtools/resources.js
new file mode 100644
index 000000000..efea22f69
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/src/devtools/resources.js
@@ -0,0 +1,66 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict
+ */
+
+'use strict';
+
+import { getResourceText, getResources, openResource } from './api.js';
+import type { Resource } from './api.js';
+import { findBestMatchingResource } from '../utils/resourceMatching.js';
+import { formatSourceSnippet } from '../utils/sourceSnippet.js';
+import type { SourcePreview } from '../types.js';
+
+function normalizeLineToZeroBased(line: ?number | null): number {
+ if (typeof line !== 'number') return 0;
+ return Math.max(line - 1, 0);
+}
+
+export async function findBestResourceForFile(
+ file: string,
+): Promise {
+ const resources = await getResources();
+ return findBestMatchingResource(resources, file);
+}
+
+export async function openSourceBestEffort(
+ file: string,
+ line: number | null,
+): Promise {
+ const best = await findBestResourceForFile(file);
+ if (!best) {
+ throw new Error(`Could not find a loaded resource matching: ${file}`);
+ }
+ openResource(best.url, normalizeLineToZeroBased(line));
+}
+
+export async function getSourcePreview(
+ file: string,
+ line: number | null,
+): Promise {
+ const best = await findBestResourceForFile(file);
+ if (!best) {
+ return {
+ url: '',
+ snippet: `Could not find a DevTools resource matching:\n${file}`,
+ };
+ }
+
+ const content = await getResourceText(best);
+ if (typeof content !== 'string' || content.length === 0) {
+ return {
+ url: best.url,
+ snippet:
+ 'DevTools did not provide file contents for this resource.\nTry opening it in the Sources panel once, then retry.',
+ };
+ }
+
+ return {
+ url: best.url,
+ snippet: formatSourceSnippet(content, line),
+ };
+}
diff --git a/packages/@stylexjs/devtools-extension/src/inspected/collectStylexDebugData.js b/packages/@stylexjs/devtools-extension/src/inspected/collectStylexDebugData.js
new file mode 100644
index 000000000..4d4cc3428
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/src/inspected/collectStylexDebugData.js
@@ -0,0 +1,533 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict
+ */
+
+'use strict';
+
+import type { AtomicStyleRule, StylexDebugData } from '../types.js';
+
+type RuleData = $ReadOnly<{
+ selectorText: string,
+ classNames: Array,
+ conditions: Array,
+ cssText: string,
+ order: number,
+}>;
+
+declare const window: any;
+
+// NOTE:
+// This function is stringified and used using `evalInInspectedWindow` in the panel.
+// So it must be a completely self-contained function that doesn't rely on any external variables or functions.
+export function collectStylexDebugData(): StylexDebugData {
+ function safeString(value: mixed): string {
+ if (typeof value === 'string') return value;
+ if (value == null) return '';
+ return String(value);
+ }
+
+ function isCSSStyleRule(rule: CSSRule): implies rule is CSSStyleRule {
+ // $FlowExpectedError[incompatible-type-guard]
+ return rule.type === 1;
+ }
+
+ function parseDataStyleSrc(raw: string): Array {
+ if (typeof raw !== 'string' || raw.trim() === '') return [];
+ return raw
+ .split(';')
+ .map((s) => s.trim())
+ .filter(Boolean);
+ }
+
+ const OVERRIDE_STORE_KEY = '__stylexDevtoolsOverrides__';
+ const OVERRIDE_ELEMENT_KEY = '__stylexDevtoolsOverrideElement__';
+
+ function getOverridesForElement(element: ?HTMLElement): Array {
+ if (!element) return [];
+ const store = window[OVERRIDE_STORE_KEY];
+ if (!store || typeof store.get !== 'function') {
+ return [];
+ }
+ const stored = window[OVERRIDE_ELEMENT_KEY];
+ const hasStored = stored && typeof stored.isSameNode === 'function';
+ const sameNode = hasStored && stored.isSameNode(element);
+ const elementKey = sameNode ? stored : element;
+ if (!sameNode) {
+ window[OVERRIDE_ELEMENT_KEY] = element;
+ }
+ const value = store.get(elementKey);
+ if (!Array.isArray(value)) return [];
+ return value.map((item) =>
+ item && typeof item === 'object' ? { ...item } : item,
+ );
+ }
+
+ function parseSourceEntry(raw: mixed): {
+ raw: string,
+ file: string,
+ line: number | null,
+ } {
+ const text = safeString(raw).trim();
+ const match = text.match(/:(\d+)\s*$/);
+ if (!match || match.index == null) {
+ return { raw: text, file: text, line: null };
+ }
+ const line = Number(match[1]);
+ const file = text.slice(0, match.index);
+ return { raw: text, file, line: Number.isFinite(line) ? line : null };
+ }
+
+ function splitSelectors(selectorText: string): Array {
+ return selectorText
+ .split(',')
+ .map((s) => s.trim())
+ .filter(Boolean);
+ }
+
+ const LAYER_POLYFILL_RE = /:not\(#\\#\)/g;
+
+ function stripLayerPolyfill(selectorText: string): {
+ cleaned: string,
+ } {
+ if (!selectorText) {
+ return { cleaned: selectorText };
+ }
+ const cleaned = selectorText.replace(LAYER_POLYFILL_RE, '');
+ return {
+ cleaned,
+ };
+ }
+
+ const SIMPLE_CLASS_SELECTOR = /^\.[_a-zA-Z0-9-]+$/;
+
+ function getAtRuleCondition(rule: CSSRule): string | null {
+ if (!rule || typeof rule.cssText !== 'string') return null;
+ const braceIndex = rule.cssText.indexOf('{');
+ if (braceIndex === -1) return null;
+ const prelude = rule.cssText.slice(0, braceIndex).trim();
+ if (!prelude.startsWith('@')) return null;
+ if (prelude.startsWith('@layer')) return null;
+ return prelude;
+ }
+
+ function parseSelectorCondition(selectorText: string): null | {
+ baseSelector: string,
+ pseudoCondition: string | null,
+ pseudoElementKey: string | null,
+ } {
+ const trimmed = selectorText.trim();
+ if (!trimmed || trimmed[0] !== '.') return null;
+ const { cleaned } = stripLayerPolyfill(trimmed);
+ const firstColonIndex = cleaned.indexOf(':');
+ if (firstColonIndex === -1) {
+ return {
+ baseSelector: cleaned,
+ pseudoCondition: null,
+ pseudoElementKey: null,
+ };
+ }
+ const baseSelector = cleaned.slice(0, firstColonIndex).trim();
+ const suffix = cleaned.slice(firstColonIndex).trim();
+ const pseudoElementIndex = suffix.indexOf('::');
+ if (pseudoElementIndex !== -1) {
+ return {
+ baseSelector,
+ pseudoCondition: null,
+ pseudoElementKey: suffix,
+ };
+ }
+ return {
+ baseSelector,
+ pseudoCondition: suffix || null,
+ pseudoElementKey: null,
+ };
+ }
+
+ function isSimpleClassSelector(baseSelector: string): boolean {
+ return SIMPLE_CLASS_SELECTOR.test(baseSelector);
+ }
+
+ function extractClassNames(selectorText: string): Array {
+ const out = [];
+ const re = /\.([_a-zA-Z0-9-]+)/g;
+ let m = re.exec(selectorText);
+ while (m != null) {
+ out.push(m[1]);
+ m = re.exec(selectorText);
+ }
+ return out;
+ }
+
+ function collectStyleRulesFromSheet(
+ sheet: CSSStyleSheet,
+ elementClassSet: $ReadOnlySet,
+ out: Array,
+ state: { ruleOrder: number, skippedSheets: number },
+ ) {
+ let rules: ?CSSRuleList;
+ try {
+ rules = sheet.cssRules;
+ } catch {
+ state.skippedSheets += 1;
+ return;
+ }
+ if (!rules) return;
+
+ function walkRules(ruleList: CSSRuleList, conditions: Array) {
+ for (let i = 0; i < ruleList.length; i += 1) {
+ const rule = ruleList[i];
+ if (!rule) continue;
+
+ // CSSRule.STYLE_RULE === 1
+ if (isCSSStyleRule(rule) && rule.selectorText && rule.cssText) {
+ const selectorText = rule.selectorText;
+ const classNames = extractClassNames(selectorText);
+ if (classNames.length === 0) continue;
+
+ let hasIntersection = false;
+ for (const cls of classNames) {
+ if (elementClassSet.has(cls)) {
+ hasIntersection = true;
+ break;
+ }
+ }
+ if (!hasIntersection) continue;
+
+ out.push({
+ selectorText,
+ classNames,
+ conditions,
+ cssText: rule.cssText,
+ order: state.ruleOrder++,
+ });
+ continue;
+ }
+
+ if ('cssRules' in rule) {
+ const atCondition = getAtRuleCondition(rule);
+ const nextConditions = atCondition
+ ? [...conditions, atCondition]
+ : conditions;
+ // $FlowFixMe[prop-missing]
+ walkRules(rule.cssRules, nextConditions);
+ }
+ }
+ }
+
+ walkRules(rules, []);
+ }
+
+ function collectAtomicRulesFromSheet(
+ sheet: CSSStyleSheet,
+ out: Array,
+ state: { skippedSheets: number },
+ ) {
+ let rules: ?CSSRuleList;
+ try {
+ rules = sheet.cssRules;
+ } catch {
+ state.skippedSheets += 1;
+ return;
+ }
+ if (!rules) return;
+
+ function walkRules(ruleList: CSSRuleList, conditions: Array) {
+ for (let i = 0; i < ruleList.length; i += 1) {
+ const rule = ruleList[i];
+ if (!rule) continue;
+
+ if (isCSSStyleRule(rule) && rule.selectorText && rule.cssText) {
+ const decls = parseDeclarationsFromRuleCssText(rule.cssText);
+ if (decls.length !== 1) continue;
+ const [decl] = decls;
+ const selectors = splitSelectors(rule.selectorText);
+ for (const selector of selectors) {
+ const selectorInfo = parseSelectorCondition(selector);
+ if (!selectorInfo) continue;
+ const { baseSelector, pseudoCondition, pseudoElementKey } =
+ selectorInfo;
+ if (!isSimpleClassSelector(baseSelector)) continue;
+ const className = baseSelector.slice(1);
+
+ const conditionParts: Array = [];
+ for (const entry of conditions) {
+ if (!conditionParts.includes(entry)) conditionParts.push(entry);
+ }
+ if (pseudoCondition && !conditionParts.includes(pseudoCondition)) {
+ conditionParts.push(pseudoCondition);
+ }
+
+ out.push({
+ className,
+ property: decl.property,
+ value: decl.value,
+ important: decl.important,
+ conditions: conditionParts,
+ ...(pseudoElementKey ? { pseudoElement: pseudoElementKey } : {}),
+ });
+ }
+ continue;
+ }
+
+ if ('cssRules' in rule) {
+ const atCondition = getAtRuleCondition(rule);
+ const nextConditions = atCondition
+ ? [...conditions, atCondition]
+ : conditions;
+ // $FlowFixMe[prop-missing]
+ walkRules(rule.cssRules, nextConditions);
+ }
+ }
+ }
+
+ walkRules(rules, []);
+ }
+
+ function matchesSelector(element: HTMLElement, selectorText: string) {
+ try {
+ return element.matches(selectorText);
+ } catch {
+ // ignore invalid selectors (e.g. some pseudo-elements)
+ return false;
+ }
+ }
+
+ function stripCssComments(cssText: string) {
+ let out = '';
+ let i = 0;
+ let inString = false;
+ let quote = '';
+ while (i < cssText.length) {
+ const ch = cssText[i];
+
+ if (!inString && ch === '/' && cssText[i + 1] === '*') {
+ const end = cssText.indexOf('*/', i + 2);
+ if (end === -1) {
+ break;
+ }
+ i = end + 2;
+ continue;
+ }
+
+ out += ch;
+ if (inString) {
+ if (ch === quote) {
+ inString = false;
+ quote = '';
+ } else if (ch === '\\\\') {
+ // skip escaped char
+ i += 1;
+ if (i < cssText.length) out += cssText[i];
+ }
+ } else if (ch === '"' || ch === "'") {
+ inString = true;
+ quote = ch;
+ }
+
+ i += 1;
+ }
+ return out;
+ }
+
+ function splitTopLevel(text: string, delimiterChar: string) {
+ const parts = [];
+ let current = '';
+ let depth = 0;
+ let inString = false;
+ let quote = '';
+
+ for (let i = 0; i < text.length; i += 1) {
+ const ch = text[i];
+
+ if (inString) {
+ current += ch;
+ if (ch === quote) {
+ inString = false;
+ quote = '';
+ } else if (ch === '\\\\') {
+ i += 1;
+ if (i < text.length) current += text[i];
+ }
+ continue;
+ }
+
+ if (ch === '"' || ch === "'") {
+ inString = true;
+ quote = ch;
+ current += ch;
+ continue;
+ }
+
+ if (ch === '(') depth += 1;
+ if (ch === ')') depth = Math.max(depth - 1, 0);
+
+ if (ch === delimiterChar && depth === 0) {
+ parts.push(current);
+ current = '';
+ continue;
+ }
+
+ current += ch;
+ }
+
+ if (current) parts.push(current);
+ return parts;
+ }
+
+ function parseDeclarationsFromRuleCssText(ruleCssText: unknown) {
+ if (typeof ruleCssText !== 'string') return [];
+ const cleaned = stripCssComments(ruleCssText);
+ const start = cleaned.indexOf('{');
+ const end = cleaned.lastIndexOf('}');
+ if (start === -1 || end === -1 || end <= start) return [];
+ const body = cleaned.slice(start + 1, end).trim();
+ if (!body) return [];
+
+ const decls = [];
+ const statements = splitTopLevel(body, ';')
+ .map((s) => s.trim())
+ .filter(Boolean);
+
+ for (const stmt of statements) {
+ const [propPart, ...rest] = splitTopLevel(stmt, ':');
+ if (!propPart || rest.length === 0) continue;
+ const property = propPart.trim();
+ const valueRaw = rest.join(':').trim();
+ if (!property || !valueRaw) continue;
+
+ let value = valueRaw;
+ let important = false;
+ if (/\s!important\s*$/i.test(value)) {
+ important = true;
+ value = value.replace(/\s!important\s*$/i, '').trim();
+ }
+ decls.push({ property, value, important });
+ }
+
+ return decls;
+ }
+
+ // $FlowExpectedError[cannot-resolve-name] - $0 helps get the currently selected item
+ const element = typeof $0 !== 'undefined' ? $0 : null;
+ if (!element) {
+ return {
+ element: { tagName: '—' },
+ sources: [],
+ computed: {},
+ atomicRules: [],
+ overrides: [],
+ applied: { classes: [] },
+ };
+ }
+
+ const tagName = safeString(element.tagName).toLowerCase();
+ const computedStyle = window.getComputedStyle(element);
+ const computed: { [string]: string } = {};
+ for (let i = 0; i < computedStyle.length; i += 1) {
+ const prop = computedStyle[i];
+ if (!prop) continue;
+ const value = computedStyle.getPropertyValue(prop);
+ computed[prop] = value ? value.trim() : '';
+ }
+ const classAttr: string = safeString(element.getAttribute('class'));
+ const classesOrdered = classAttr.trim() ? classAttr.trim().split(/\s+/) : [];
+ const elementClassSet = new Set(classesOrdered);
+
+ const dataStyleSrcRaw = safeString(element.getAttribute('data-style-src'));
+ const sourcesRaw = parseDataStyleSrc(dataStyleSrcRaw);
+ const sources = sourcesRaw.map(parseSourceEntry);
+ const overrides = getOverridesForElement(element);
+
+ const state = { ruleOrder: 0, skippedSheets: 0 };
+ const rules: Array = [];
+ const atomicRules: Array = [];
+ const atomicState = { skippedSheets: 0 };
+
+ const sheets: Array = Array.from(
+ document.styleSheets,
+ ) as $FlowFixMe;
+
+ for (const sheet of sheets) {
+ collectStyleRulesFromSheet(sheet, elementClassSet, rules, state);
+ collectAtomicRulesFromSheet(sheet, atomicRules, atomicState);
+ }
+
+ const classToDecls = new Map>();
+ for (const rule of rules) {
+ const decls = parseDeclarationsFromRuleCssText(rule.cssText);
+ if (decls.length === 0) continue;
+
+ const selectors = splitSelectors(rule.selectorText);
+ for (const selector of selectors) {
+ const selectorInfo = parseSelectorCondition(selector);
+ if (!selectorInfo) continue;
+ const { baseSelector, pseudoCondition, pseudoElementKey } = selectorInfo;
+
+ const matchedClasses = extractClassNames(baseSelector).filter(
+ (cls: string) => elementClassSet.has(cls),
+ );
+ const uniqueMatchedClasses = Array.from(new Set(matchedClasses));
+ if (uniqueMatchedClasses.length === 0) continue;
+
+ if (!baseSelector || !matchesSelector(element, baseSelector)) continue;
+
+ const conditionParts: Array = [];
+ for (const entry of rule.conditions) {
+ if (!conditionParts.includes(entry)) conditionParts.push(entry);
+ }
+ if (pseudoCondition && !conditionParts.includes(pseudoCondition)) {
+ conditionParts.push(pseudoCondition);
+ }
+ const condition =
+ conditionParts.length > 0 ? conditionParts.join(', ') : 'default';
+
+ for (const cls of uniqueMatchedClasses) {
+ const pseudoElementValue = pseudoElementKey || undefined;
+ const declsWithCondition = decls.map((decl): $FlowFixMe => ({
+ ...decl,
+ condition,
+ className: cls,
+ ...((conditionParts.length > 0
+ ? { conditions: conditionParts }
+ : {}) as $FlowFixMe),
+ ...((pseudoElementValue
+ ? { pseudoElement: pseudoElementValue }
+ : {}) as $FlowFixMe),
+ }));
+ const declList = classToDecls.get(cls);
+ if (declList == null) {
+ classToDecls.set(cls, [...declsWithCondition]);
+ } else {
+ declList.push(...declsWithCondition);
+ }
+ for (const decl of decls) {
+ if (computed[decl.property] == null) {
+ const value = computedStyle.getPropertyValue(decl.property);
+ computed[decl.property] = value ? value.trim() : '';
+ }
+ }
+ }
+ }
+ }
+
+ const classes = [];
+ for (const cls of classesOrdered) {
+ const decls = classToDecls.get(cls);
+ if (!decls) continue;
+ classes.push({ name: cls, declarations: decls });
+ }
+
+ return {
+ element: { tagName },
+ sources,
+ computed,
+ atomicRules,
+ overrides,
+ applied: { classes },
+ };
+}
diff --git a/packages/@stylexjs/devtools-extension/src/panel/App.jsx b/packages/@stylexjs/devtools-extension/src/panel/App.jsx
new file mode 100644
index 000000000..8f5286808
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/src/panel/App.jsx
@@ -0,0 +1,254 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict
+ */
+
+'use strict';
+
+import * as React from 'react';
+import {
+ useState,
+ useCallback,
+ useEffect,
+ use,
+ startTransition,
+ Suspense,
+} from 'react';
+import * as stylex from '@stylexjs/stylex';
+
+import type { StylexDebugData } from '../types.js';
+import { subscribeToSelectionAndNavigation } from '../devtools/events.js';
+import { evalInInspectedWindow } from '../devtools/api.js';
+import { collectStylexDebugData } from '../inspected/collectStylexDebugData.js';
+import { Button } from './components/Button';
+import { DeclarationsList } from './components/DeclarationsList';
+import { SourcesList } from './components/SourcesList';
+import { Section } from './components/Section';
+import { colors } from './theme.stylex';
+import Logo from './components/Logo';
+import { ErrorBoundary } from './components/ErrorBoundary';
+
+export function App(): React.Node {
+ const [count, setCount] = useState(0);
+
+ const refresh = useCallback(() => {
+ startTransition(async () => {
+ setCount((x) => x + 1);
+ });
+ }, []);
+
+ const handleSelectionChange = useCallback(() => {
+ refresh();
+ }, [refresh]);
+
+ useEffect(
+ () => subscribeToSelectionAndNavigation(handleSelectionChange),
+ [handleSelectionChange],
+ );
+
+ return (
+ }>
+ (
+
+ )}
+ key={count}
+ >
+
+
+
+ );
+}
+
+function Loading() {
+ return (
+
+
+
+ );
+}
+
+function ErrorFallback({
+ errorMessage,
+ retry,
+}: {
+ errorMessage: string,
+ retry: () => void,
+}) {
+ return (
+
+
{errorMessage}
+
+
+ );
+}
+
+let cache: ?[number, Promise] = null;
+const debugDataPromise = (id: number): Promise => {
+ if (cache != null && cache[0] === id) {
+ return cache[1];
+ }
+ const promise = evalInInspectedWindow(collectStylexDebugData);
+ cache = [id, promise];
+ return promise;
+};
+
+function Panel({
+ id,
+ refresh,
+}: {
+ id: number,
+ refresh: () => void,
+}): React.Node {
+ const data = use(debugDataPromise(id));
+
+ const tagName = data?.element?.tagName ?? '—';
+
+ const classes = data?.applied?.classes ?? [];
+ const computed = data?.computed ?? {};
+ const atomicRules = data?.atomicRules ?? [];
+ const overrides = data?.overrides ?? [];
+
+ const hasSources = data?.sources?.length > 0;
+ const hasClasses = classes.length > 0;
+ const hasOverrides = overrides.length > 0;
+ const showAppliedSection = hasClasses || hasOverrides;
+ const showEmptyState = !hasSources && !hasClasses && !hasOverrides;
+
+ return (
+
+
+
+
+
+
+ {tagName}
+
+
+
+
+ {/*
+ {status.message}
+
*/}
+
+ {hasSources && (
+
+ {}} sources={data.sources} />
+
+ )}
+
+ {showAppliedSection && (
+
+ )}
+
+ {showEmptyState && (
+
No styles found
+ )}
+
+ );
+}
+
+const styles = stylex.create({
+ fallbackContainer: {
+ height: '100%',
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 16,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ errorMessage: {
+ boxSizing: 'border-box',
+ color: 'tomato',
+ fontSize: '0.8rem',
+ fontWeight: 400,
+ padding: 16,
+ width: '100%',
+ whiteSpace: 'pre-wrap',
+ },
+ emptyState: {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ textAlign: 'center',
+ color: colors.textMuted,
+ padding: 16,
+ },
+ root: {
+ backgroundColor: colors.bg,
+ color: colors.textPrimary,
+ fontFamily:
+ 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif',
+ fontSize: 12,
+ minHeight: '100%',
+ boxSizing: 'border-box',
+ display: 'flex',
+ flexDirection: 'column',
+ },
+ logo: {
+ height: '2rem',
+ color: colors.textPrimary,
+ },
+ mono: {
+ fontFamily:
+ 'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace',
+ },
+ header: {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ padding: 8,
+ },
+ title: {
+ display: 'flex',
+ alignItems: 'center',
+ gap: 8,
+ fontWeight: 600,
+ fontSize: 13,
+ },
+
+ status: {
+ color: colors.textMuted,
+ marginTop: 6,
+ marginBottom: 10,
+ },
+ statusError: {
+ color: '#cf222e',
+ },
+
+ pill: {
+ display: 'inline-block',
+ paddingTop: 1,
+ paddingRight: 6,
+ paddingBottom: 1,
+ paddingLeft: 6,
+ borderRadius: 999,
+ borderWidth: 1,
+ borderStyle: 'solid',
+ borderColor: colors.border,
+ backgroundColor: colors.bgRaised,
+ color: colors.textMuted,
+ fontSize: 11,
+ },
+});
diff --git a/packages/@stylexjs/devtools-extension/src/panel/components/Button.js b/packages/@stylexjs/devtools-extension/src/panel/components/Button.js
new file mode 100644
index 000000000..8ad2a60bf
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/src/panel/components/Button.js
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict
+ */
+'use strict';
+
+import * as React from 'react';
+import * as stylex from '@stylexjs/stylex';
+import { colors } from '../theme.stylex';
+
+export function Button({
+ onClick,
+ children,
+ title,
+ xstyle,
+}: {
+ onClick: (e: MouseEvent) => mixed,
+ children: React.Node,
+ title?: string,
+ xstyle?: stylex.StyleXStyles<>,
+}): React.Node {
+ return (
+
+ );
+}
+
+const styles = stylex.create({
+ button: {
+ borderWidth: 1,
+ borderStyle: 'solid',
+ borderColor: colors.border,
+ backgroundColor: {
+ default: colors.bgRaised,
+ ':hover': colors.bg,
+ ':active': colors.bg,
+ },
+ transform: { default: null, ':active': 'scale(0.95)' },
+ paddingBlock: 4,
+ paddingInline: 8,
+ borderRadius: 8,
+ },
+});
diff --git a/packages/@stylexjs/devtools-extension/src/panel/components/DeclarationsList.js b/packages/@stylexjs/devtools-extension/src/panel/components/DeclarationsList.js
new file mode 100644
index 000000000..82b016951
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/src/panel/components/DeclarationsList.js
@@ -0,0 +1,1880 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict
+ */
+
+'use strict';
+
+import * as React from 'react';
+import {
+ useState,
+ useMemo,
+ useEffect,
+ useCallback,
+ useRef,
+ useId,
+} from 'react';
+import * as stylex from '@stylexjs/stylex';
+import { colors } from '../theme.stylex';
+import type { AtomicStyleRule, StylexOverride } from '../../types.js';
+import {
+ swapClassName,
+ setInlineStyle,
+ clearInlineStyle,
+ setStylexOverrides,
+} from '../../devtools/overrides.js';
+
+type TDeclaration = $ReadOnly<{
+ property: string,
+ value: string,
+ important: boolean,
+ condition?: string,
+ conditions?: $ReadOnlyArray,
+ pseudoElement?: string,
+ className?: string,
+ ...
+}>;
+
+type TPropertyGroup = $ReadOnly<{
+ property: string,
+ entries: $ReadOnlyArray,
+}>;
+
+type TSection = $ReadOnly<{
+ key: string,
+ properties: $ReadOnlyArray,
+}>;
+
+type TConditionNode = {
+ key: string,
+ label: string,
+ entries: Array,
+ children: Array,
+};
+
+type TOverrideValue = {
+ value: string,
+ important: boolean,
+};
+
+type TOverridesByEntry = { [string]: TOverrideValue, ... };
+
+type TOverrideUpdater = (override: StylexOverride) => Promise;
+
+type TOverrideRemover = (id: string) => Promise;
+
+type TClassOverrideMap = { [string]: StylexOverride, ... };
+
+type TAtomicValueIndex = {
+ values: Array,
+ valueToClassName: { [string]: string, ... },
+};
+
+type TAtomicIndex = { [string]: TAtomicValueIndex, ... };
+
+function getConditionParts(entry: TDeclaration): Array {
+ if (entry.conditions && entry.conditions.length > 0) {
+ return [...entry.conditions];
+ }
+ if (!entry.condition || entry.condition === 'default') return [];
+ return entry.condition
+ .split(',')
+ .map((part) => part.trim())
+ .filter(Boolean);
+}
+
+function moveKeyToFront(list: Array, key: string): Array {
+ if (!list.includes(key)) {
+ return list.slice();
+ }
+ return [key, ...list.filter((item) => item !== key)];
+}
+
+function formatConditionLabel(label: string): string {
+ if (label === 'default') return 'default';
+ return `'${label}'`;
+}
+
+function isAtRuleLabel(label: string): boolean {
+ return label.startsWith('@');
+}
+
+function isDefaultLabel(label: string): boolean {
+ return label.trim() === 'default';
+}
+
+function collapseDefaultChild(node: TConditionNode): TConditionNode {
+ if (node.children.length !== 1) return node;
+ const child = node.children[0];
+ if (!isDefaultLabel(child.label)) return node;
+ return {
+ ...node,
+ entries: [...node.entries, ...child.entries],
+ children: child.children,
+ };
+}
+
+function formatValue(value: string, important: boolean): string {
+ return important ? `${value} !important` : value;
+}
+
+function parseValueInput(raw: string): TOverrideValue {
+ const trimmed = raw.trim();
+ if (trimmed === '') {
+ return { value: '', important: false };
+ }
+ if (!/\s!important\s*$/i.test(trimmed)) {
+ return { value: trimmed, important: false };
+ }
+ return {
+ value: trimmed.replace(/\s!important\s*$/i, '').trim(),
+ important: true,
+ };
+}
+
+function buildInlineOverrideId(
+ property: string,
+ pseudoElement?: string,
+): string {
+ return ['inline', property, pseudoElement ?? ''].join('::');
+}
+
+function buildClassOverrideId(originalClassName: string): string {
+ return `class::${originalClassName}`;
+}
+
+function createInlineOverride(
+ entry: TDeclaration,
+ override: TOverrideValue,
+ entryKey: string,
+): StylexOverride {
+ const conditions = getConditionParts(entry);
+ const base: StylexOverride = {
+ id: buildInlineOverrideId(entry.property, entry.pseudoElement),
+ kind: 'inline',
+ property: entry.property,
+ value: override.value,
+ important: override.important,
+ conditions,
+ sourceEntryKey: entryKey,
+ };
+ return entry.pseudoElement
+ ? { ...base, pseudoElement: entry.pseudoElement }
+ : base;
+}
+
+function createClassOverride(
+ entry: TDeclaration,
+ override: TOverrideValue,
+ entryKey: string,
+ nextClassName: string,
+ originalClassNameOverride?: string,
+): StylexOverride {
+ const originalClassName = originalClassNameOverride ?? entry.className ?? '';
+ const conditions = getConditionParts(entry);
+ const base: StylexOverride = {
+ id: buildClassOverrideId(originalClassName),
+ kind: 'class',
+ property: entry.property,
+ value: override.value,
+ important: override.important,
+ conditions,
+ className: nextClassName,
+ originalClassName,
+ sourceEntryKey: entryKey,
+ };
+ return entry.pseudoElement
+ ? { ...base, pseudoElement: entry.pseudoElement }
+ : base;
+}
+
+function upsertOverride(
+ overrides: $ReadOnlyArray,
+ nextOverride: StylexOverride,
+): Array {
+ const index = overrides.findIndex((item) => item.id === nextOverride.id);
+ if (index === -1) {
+ return [...overrides, nextOverride];
+ }
+ return overrides.map((item) =>
+ item.id === nextOverride.id ? nextOverride : item,
+ );
+}
+
+function removeOverride(
+ overrides: $ReadOnlyArray,
+ id: string,
+): Array {
+ return overrides.filter((item) => item.id !== id);
+}
+
+function overridesToEntryMap(
+ overrides: $ReadOnlyArray,
+): TOverridesByEntry {
+ return overrides.reduce(
+ (acc, override) =>
+ override.kind === 'inline' && override.sourceEntryKey
+ ? {
+ ...acc,
+ [override.sourceEntryKey]: {
+ value: override.value,
+ important: override.important,
+ },
+ }
+ : acc,
+ {},
+ );
+}
+
+function buildClassOverrideMap(
+ overrides: $ReadOnlyArray,
+): TClassOverrideMap {
+ return overrides.reduce(
+ (acc, override) =>
+ override.kind === 'class' && override.className
+ ? { ...acc, [override.className]: override }
+ : acc,
+ {},
+ );
+}
+
+function buildPropertyValues(rules: $ReadOnlyArray): {
+ [string]: Array,
+ ...
+} {
+ return rules.reduce<{ [string]: Array, ... }>((acc, rule) => {
+ const displayValue = formatValue(rule.value, rule.important);
+ const key = rule.property.toLowerCase();
+ const existing = acc[key] ?? [];
+ const values = existing.includes(displayValue)
+ ? existing
+ : [...existing, displayValue];
+ return { ...acc, [key]: values };
+ }, {});
+}
+
+function filterSuggestions(
+ values: $ReadOnlyArray,
+ query: string,
+): Array {
+ const normalizedQuery = query.trim().toLowerCase();
+ const filtered =
+ normalizedQuery === ''
+ ? values
+ : values.filter((value) => value.toLowerCase().includes(normalizedQuery));
+ return filtered.slice(0, 8);
+}
+
+function getNextIndex(current: number, delta: number, length: number): number {
+ if (length === 0) return -1;
+ if (current === -1) {
+ return delta > 0 ? 0 : length - 1;
+ }
+ return (current + delta + length) % length;
+}
+
+function normalizeConditions(conditions: Array): Array {
+ return conditions
+ .map((condition) => condition.trim())
+ .filter(Boolean)
+ .reduce(
+ (acc, condition) => (acc.includes(condition) ? acc : [...acc, condition]),
+ [],
+ );
+}
+
+function buildConditionKey(conditions: Array): string {
+ return normalizeConditions(conditions).join('||');
+}
+
+function buildAtomicKey(
+ property: string,
+ conditions: Array,
+ pseudoElement?: string,
+): string {
+ return [property, pseudoElement ?? '', buildConditionKey(conditions)].join(
+ '::',
+ );
+}
+
+function buildEntryKey(entry: TDeclaration): string {
+ const conditions = getConditionParts(entry);
+ return [
+ entry.className ?? '',
+ entry.property,
+ entry.pseudoElement ?? '',
+ buildConditionKey(conditions),
+ ].join('::');
+}
+
+function buildAtomicIndex(
+ rules: $ReadOnlyArray,
+): TAtomicIndex {
+ const empty: TAtomicIndex = {};
+ return rules.reduce((acc, rule) => {
+ const key = buildAtomicKey(
+ rule.property,
+ rule.conditions,
+ rule.pseudoElement,
+ );
+ const displayValue = formatValue(rule.value, rule.important);
+ const existing = acc[key];
+ const existingValues = existing?.values ?? [];
+ const existingMap = existing?.valueToClassName ?? {};
+ const values = existingValues.includes(displayValue)
+ ? existingValues
+ : [...existingValues, displayValue];
+ const valueToClassName = existingMap[displayValue]
+ ? existingMap
+ : { ...existingMap, [displayValue]: rule.className };
+ return {
+ ...acc,
+ [key]: {
+ values,
+ valueToClassName,
+ },
+ };
+ }, empty);
+}
+
+function getAtomicGroupForEntry(
+ atomicIndex: TAtomicIndex,
+ entry: TDeclaration,
+): ?TAtomicValueIndex {
+ const key = buildAtomicKey(
+ entry.property,
+ getConditionParts(entry),
+ entry.pseudoElement,
+ );
+ return atomicIndex[key];
+}
+
+function buildSections(
+ classes: $ReadOnlyArray<
+ $ReadOnly<{
+ name: string,
+ declarations: $ReadOnlyArray,
+ ...
+ }>,
+ >,
+): Array {
+ const sectionOrder: Array = [];
+ const sectionMap: Map<
+ string,
+ {
+ propertyOrder: Array,
+ propertyToEntries: Map>,
+ },
+ > = new Map();
+
+ for (const entry of classes) {
+ for (const decl of entry.declarations) {
+ const sectionKey = decl.pseudoElement ?? '';
+ let section = sectionMap.get(sectionKey);
+ if (section == null) {
+ section = {
+ propertyOrder: [],
+ propertyToEntries: new Map(),
+ };
+ sectionMap.set(sectionKey, section);
+ sectionOrder.push(sectionKey);
+ }
+ const bucket = section.propertyToEntries.get(decl.property);
+ if (bucket == null) {
+ section.propertyOrder.push(decl.property);
+ section.propertyToEntries.set(decl.property, [decl]);
+ } else {
+ bucket.push(decl);
+ }
+ }
+ }
+
+ const orderedSections = moveKeyToFront(sectionOrder, '');
+ const result: Array = [];
+ for (const sectionKey of orderedSections) {
+ const section = sectionMap.get(sectionKey);
+ if (!section) continue;
+ const properties = section.propertyOrder.map((property) => ({
+ property,
+ entries: section.propertyToEntries.get(property) ?? [],
+ }));
+ result.push({ key: sectionKey, properties });
+ }
+ return result;
+}
+
+type TConditionSplit = {
+ atRuleKey: string,
+ conditionKey: string,
+ entry: TDeclaration,
+};
+
+type TGroupData = {
+ atRuleKey: string,
+ conditionOrder: Array,
+ conditions: { [string]: TConditionNode, ... },
+};
+
+type TGroupState = {
+ atRuleOrder: Array,
+ groups: { [string]: TGroupData, ... },
+};
+
+function ensureUnique(list: Array, value: string): Array {
+ return list.includes(value) ? list : [...list, value];
+}
+
+function partitionParts(parts: Array): {
+ atRules: Array,
+ others: Array,
+} {
+ return parts.reduce(
+ (acc, part) =>
+ part.startsWith('@')
+ ? { atRules: [...acc.atRules, part], others: acc.others }
+ : part
+ ? { atRules: acc.atRules, others: [...acc.others, part] }
+ : acc,
+ { atRules: [], others: [] },
+ );
+}
+
+function splitConditions(entry: TDeclaration): TConditionSplit {
+ const parts = getConditionParts(entry);
+ const { atRules, others } = partitionParts(parts);
+ const atRuleKey = atRules.join(', ');
+ const conditionKey = others.length > 0 ? others.join(', ') : 'default';
+ return { atRuleKey, conditionKey, entry };
+}
+
+function createConditionNode(
+ atRuleKey: string,
+ conditionKey: string,
+): TConditionNode {
+ return {
+ key: `cond:${atRuleKey}:${conditionKey}`,
+ label: conditionKey,
+ entries: [],
+ children: [],
+ };
+}
+
+function updateGroupWithEntry(
+ group: TGroupData,
+ split: TConditionSplit,
+): TGroupData {
+ const existingNode = group.conditions[split.conditionKey];
+ const nextNode = {
+ ...(existingNode ??
+ createConditionNode(split.atRuleKey, split.conditionKey)),
+ entries: [...(existingNode?.entries ?? []), split.entry],
+ };
+
+ return {
+ ...group,
+ conditionOrder: ensureUnique(group.conditionOrder, split.conditionKey),
+ conditions: { ...group.conditions, [split.conditionKey]: nextNode },
+ };
+}
+
+function updateStateWithEntry(
+ state: TGroupState,
+ split: TConditionSplit,
+): TGroupState {
+ const existingGroup = state.groups[split.atRuleKey];
+ const baseGroup = existingGroup ?? {
+ atRuleKey: split.atRuleKey,
+ conditionOrder: [],
+ conditions: {},
+ };
+ const nextGroup = updateGroupWithEntry(baseGroup, split);
+
+ return {
+ atRuleOrder: ensureUnique(state.atRuleOrder, split.atRuleKey),
+ groups: { ...state.groups, [split.atRuleKey]: nextGroup },
+ };
+}
+
+function toConditionNodes(state: TGroupState): Array {
+ const orderedAtRules = moveKeyToFront(state.atRuleOrder, '');
+ // $FlowFixMe[incompatible-type]
+ return orderedAtRules.reduce(
+ (acc: Array, atRuleKey: string) => {
+ const group = state.groups[atRuleKey];
+ if (!group) return acc;
+ const orderedConditions = moveKeyToFront(group.conditionOrder, 'default');
+ const children = orderedConditions.reduce(
+ (childAcc: Array, conditionKey: string) => {
+ const node = group.conditions[conditionKey];
+ return node ? [...childAcc, node] : childAcc;
+ },
+ [],
+ );
+
+ if (atRuleKey === '') {
+ return [...acc, ...children];
+ }
+
+ return [
+ ...acc,
+ {
+ key: `at:${atRuleKey || 'base'}`,
+ label: atRuleKey,
+ entries: [],
+ children,
+ },
+ ];
+ },
+ [],
+ );
+}
+
+function buildConditionNodes(
+ entries: $ReadOnlyArray,
+): Array {
+ const splits = entries.map(splitConditions);
+ // $FlowFixMe[incompatible-type]
+ const grouped = splits.reduce(updateStateWithEntry, {
+ atRuleOrder: [],
+ groups: {},
+ });
+ return toConditionNodes(grouped);
+}
+
+export function DeclarationsList({
+ classes,
+ computed,
+ atomicRules,
+ onRefresh,
+ overrides,
+}: {
+ classes: $ReadOnlyArray<
+ $ReadOnly<{
+ name: string,
+ declarations: $ReadOnlyArray,
+ ...
+ }>,
+ >,
+ computed: { [string]: string, ... },
+ atomicRules: $ReadOnlyArray,
+ onRefresh: () => void,
+ overrides: $ReadOnlyArray,
+}): React.Node {
+ const atomicIndex = useMemo(
+ () => buildAtomicIndex(atomicRules),
+ [atomicRules],
+ );
+ const propertyValues = useMemo(
+ () => buildPropertyValues(atomicRules),
+ [atomicRules],
+ );
+ const overrideValues = useMemo(
+ () => overridesToEntryMap(overrides),
+ [overrides],
+ );
+ const classOverrides = useMemo(
+ () => buildClassOverrideMap(overrides),
+ [overrides],
+ );
+ const overridesRef = useRef(overrides);
+ overridesRef.current = overrides;
+
+ const persistOverrides = useCallback(
+ async (nextOverrides: Array) => {
+ overridesRef.current = nextOverrides;
+ await setStylexOverrides(nextOverrides);
+ },
+ [],
+ );
+
+ const upsertOverrideEntry = useCallback(
+ async (override: StylexOverride) => {
+ const nextOverrides = upsertOverride(overridesRef.current, override);
+ await persistOverrides(nextOverrides);
+ },
+ [persistOverrides],
+ );
+ const removeOverrideEntry = useCallback(
+ async (id: string) => {
+ const nextOverrides = removeOverride(overridesRef.current, id);
+ await persistOverrides(nextOverrides);
+ },
+ [persistOverrides],
+ );
+ const handleAddOverride = useCallback(
+ async (property: string, rawValue: string) => {
+ const normalizedProperty = property.trim();
+ if (!normalizedProperty) return;
+ const parsed = parseValueInput(rawValue);
+ if (!parsed.value) return;
+ let didMutate = false;
+ try {
+ await setInlineStyle({
+ property: normalizedProperty,
+ value: parsed.value,
+ important: parsed.important,
+ });
+ didMutate = true;
+ await upsertOverrideEntry({
+ id: buildInlineOverrideId(normalizedProperty),
+ kind: 'inline',
+ property: normalizedProperty,
+ value: parsed.value,
+ important: parsed.important,
+ conditions: [],
+ });
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error(error);
+ } finally {
+ if (didMutate) {
+ onRefresh();
+ }
+ }
+ },
+ [onRefresh, upsertOverrideEntry],
+ );
+ const handleRemoveOverride = useCallback(
+ async (override: StylexOverride) => {
+ let didMutate = false;
+ try {
+ if (override.kind === 'inline') {
+ await clearInlineStyle({ property: override.property });
+ didMutate = true;
+ }
+ if (
+ override.kind === 'class' &&
+ override.className &&
+ override.originalClassName
+ ) {
+ await swapClassName({
+ from: override.className,
+ to: override.originalClassName,
+ });
+ didMutate = true;
+ }
+ await removeOverrideEntry(override.id);
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error(error);
+ } finally {
+ if (didMutate) {
+ onRefresh();
+ }
+ }
+ },
+ [onRefresh, removeOverrideEntry],
+ );
+
+ const sections = buildSections(classes);
+ const hasSections = sections.length > 0;
+
+ return (
+
+ {hasSections ? (
+ sections.map((section) => (
+
+ ))
+ ) : (
+
+ No matching StyleX CSS rules found for the selected element.
+
+ )}
+
+
+ );
+}
+
+function PseudoSection({
+ atomicIndex,
+ classOverrides,
+ computed,
+ onOverrideRemove,
+ onOverrideUpsert,
+ onRefresh,
+ overrideValues,
+ section,
+}: {
+ atomicIndex: TAtomicIndex,
+ classOverrides: TClassOverrideMap,
+ computed: { [string]: string, ... },
+ onOverrideRemove: TOverrideRemover,
+ onOverrideUpsert: TOverrideUpdater,
+ onRefresh: () => void,
+ overrideValues: TOverridesByEntry,
+ section: TSection,
+}): React.Node {
+ if (section.key === '') {
+ return (
+
+ );
+ }
+ return (
+
+ );
+}
+
+function PropertyList({
+ atomicIndex,
+ classOverrides,
+ computed,
+ onOverrideRemove,
+ onOverrideUpsert,
+ onRefresh,
+ overrideValues,
+ properties,
+}: {
+ atomicIndex: TAtomicIndex,
+ classOverrides: TClassOverrideMap,
+ computed: { [string]: string, ... },
+ onOverrideRemove: TOverrideRemover,
+ onOverrideUpsert: TOverrideUpdater,
+ onRefresh: () => void,
+ overrideValues: TOverridesByEntry,
+ properties: $ReadOnlyArray,
+}): React.Node {
+ return (
+
+ {properties.map((group) => (
+
+ ))}
+
+ );
+}
+
+function PropertyGroup({
+ atomicIndex,
+ classOverrides,
+ computedValue,
+ group,
+ onOverrideRemove,
+ onOverrideUpsert,
+ onRefresh,
+ overrideValues,
+}: {
+ atomicIndex: TAtomicIndex,
+ classOverrides: TClassOverrideMap,
+ computedValue?: string,
+ group: TPropertyGroup,
+ onOverrideRemove: TOverrideRemover,
+ onOverrideUpsert: TOverrideUpdater,
+ onRefresh: () => void,
+ overrideValues: TOverridesByEntry,
+}): React.Node {
+ if (group.entries.length === 1) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
+
+function SingleDeclaration({
+ atomicIndex,
+ classOverrides,
+ computedValue,
+ entry,
+ onOverrideRemove,
+ onOverrideUpsert,
+ onRefresh,
+ overrideValues,
+ property,
+}: {
+ atomicIndex: TAtomicIndex,
+ classOverrides: TClassOverrideMap,
+ computedValue?: string,
+ entry: TDeclaration,
+ onOverrideRemove: TOverrideRemover,
+ onOverrideUpsert: TOverrideUpdater,
+ onRefresh: () => void,
+ overrideValues: TOverridesByEntry,
+ property: string,
+}): React.Node {
+ const computedTitle = computedValue ? computedValue.trim() : '';
+ const prefix: React.Node = (
+
+ {property}
+
+ );
+
+ return (
+
+ );
+}
+
+function GroupedDeclaration({
+ atomicIndex,
+ classOverrides,
+ computedValue,
+ entries,
+ onOverrideRemove,
+ onOverrideUpsert,
+ onRefresh,
+ overrideValues,
+ property,
+}: {
+ atomicIndex: TAtomicIndex,
+ classOverrides: TClassOverrideMap,
+ computedValue?: string,
+ entries: $ReadOnlyArray,
+ onOverrideRemove: TOverrideRemover,
+ onOverrideUpsert: TOverrideUpdater,
+ onRefresh: () => void,
+ overrideValues: TOverridesByEntry,
+ property: string,
+}): React.Node {
+ const nodes = buildConditionNodes(entries);
+ const computedTitle = computedValue ? computedValue.trim() : '';
+ const line: React.Node = (
+ <>
+
+ {property}
+
+ :
+ >
+ );
+
+ return (
+
+ );
+}
+
+function ConditionList({
+ atomicIndex,
+ classOverrides,
+ depth,
+ nodes,
+ onOverrideRemove,
+ onOverrideUpsert,
+ onRefresh,
+ overrideValues,
+}: {
+ atomicIndex: TAtomicIndex,
+ classOverrides: TClassOverrideMap,
+ depth: number,
+ nodes: $ReadOnlyArray,
+ onOverrideRemove: TOverrideRemover,
+ onOverrideUpsert: TOverrideUpdater,
+ onRefresh: () => void,
+ overrideValues: TOverridesByEntry,
+}): React.Node {
+ if (nodes.length === 0) return null;
+ const listStyle = depth === 0 ? styles.declSubList : styles.atRuleList;
+
+ return (
+
+ {nodes.map((node) => (
+
+ ))}
+
+ );
+}
+
+function ConditionNode({
+ atomicIndex,
+ classOverrides,
+ node,
+ onOverrideRemove,
+ onOverrideUpsert,
+ onRefresh,
+ overrideValues,
+ depth = 0,
+}: {
+ atomicIndex: TAtomicIndex,
+ classOverrides: TClassOverrideMap,
+ node: TConditionNode,
+ onOverrideRemove: TOverrideRemover,
+ onOverrideUpsert: TOverrideUpdater,
+ onRefresh: () => void,
+ overrideValues: TOverridesByEntry,
+ depth: number,
+}): React.Node {
+ const collapsedNode = collapseDefaultChild(node);
+ const displayNode =
+ isAtRuleLabel(collapsedNode.label) &&
+ collapsedNode.entries.length === 0 &&
+ collapsedNode.children.length === 1 &&
+ isDefaultLabel(collapsedNode.children[0].label)
+ ? {
+ ...collapsedNode,
+ entries: collapsedNode.children[0].entries,
+ children: collapsedNode.children[0].children,
+ }
+ : collapsedNode;
+ const label = displayNode.label;
+ const isAtRule = isAtRuleLabel(label);
+ const hasEntries = displayNode.entries.length > 0;
+ const formattedLabel = label !== '' ? formatConditionLabel(label) : null;
+ const shouldInlineAtRule = isAtRule && hasEntries;
+ const showLabel =
+ isAtRule && formattedLabel != null && displayNode.entries.length === 0;
+ const labelText = isAtRule
+ ? shouldInlineAtRule
+ ? formattedLabel
+ : null
+ : formattedLabel;
+
+ return (
+
+ {showLabel ? (
+
+ {formattedLabel}
+
+ ) : null}
+ {displayNode.entries.length > 0 ? (
+
+ ) : null}
+ {displayNode.children.length > 0 ? (
+
+ ) : null}
+
+ );
+}
+
+function DeclarationEntries({
+ atomicIndex,
+ classOverrides,
+ entries,
+ label,
+ onOverrideRemove,
+ onOverrideUpsert,
+ onRefresh,
+ overrideValues,
+}: {
+ atomicIndex: TAtomicIndex,
+ classOverrides: TClassOverrideMap,
+ entries: $ReadOnlyArray,
+ label: string | null,
+ onOverrideRemove: TOverrideRemover,
+ onOverrideUpsert: TOverrideUpdater,
+ onRefresh: () => void,
+ overrideValues: TOverridesByEntry,
+}): React.Node {
+ const prefix = label ? (
+ {label}
+ ) : null;
+ const showColon = label != null;
+
+ return entries.map((entry, index) => (
+
+ ));
+}
+
+function DeclarationEntryRow({
+ atomicIndex,
+ classOverrides,
+ entry,
+ isSubLine,
+ onOverrideRemove,
+ onOverrideUpsert,
+ onRefresh,
+ overrideValues,
+ prefix,
+ showColon,
+}: {
+ atomicIndex: TAtomicIndex,
+ classOverrides: TClassOverrideMap,
+ entry: TDeclaration,
+ isSubLine: boolean,
+ onOverrideRemove: TOverrideRemover,
+ onOverrideUpsert: TOverrideUpdater,
+ onRefresh: () => void,
+ overrideValues: TOverridesByEntry,
+ prefix: React.Node | null,
+ showColon: boolean,
+}): React.Node {
+ const entryKey = buildEntryKey(entry);
+ const override = overrideValues[entryKey];
+ const baseValue = formatValue(entry.value, entry.important);
+ const displayValue = override
+ ? formatValue(override.value, override.important)
+ : baseValue;
+ const existingClassOverride =
+ entry.className && classOverrides[entry.className]
+ ? classOverrides[entry.className]
+ : null;
+ const group = getAtomicGroupForEntry(atomicIndex, entry);
+ const suggestions = group?.values ?? [];
+ const valueToClassName = group?.valueToClassName ?? {};
+
+ const [isEditing, setIsEditing] = useState(false);
+ const [draftValue, setDraftValue] = useState('');
+ const [isPending, setIsPending] = useState(false);
+ const [activeIndex, setActiveIndex] = useState(-1);
+ const listId = useId();
+
+ // useEffect(() => {
+ // if (!isEditing) {
+ // setDraftValue(displayValue);
+ // }
+ // }, [displayValue, isEditing]);
+
+ const filteredSuggestions = useMemo(
+ () => filterSuggestions(suggestions, draftValue),
+ [draftValue, suggestions],
+ );
+
+ useEffect(() => {
+ if (!isEditing || filteredSuggestions.length === 0) {
+ setActiveIndex(-1);
+ return;
+ }
+ setActiveIndex((prev) =>
+ prev >= filteredSuggestions.length
+ ? filteredSuggestions.length - 1
+ : prev,
+ );
+ }, [filteredSuggestions, isEditing]);
+
+ const commitChange = useCallback(
+ async (nextValue?: string) => {
+ const rawValue = nextValue ?? draftValue;
+ if (isPending || rawValue === '') {
+ return;
+ }
+
+ const trimmed = rawValue.trim();
+ const current = displayValue.trim();
+ setIsEditing(false);
+ if (trimmed === current) {
+ return;
+ }
+
+ const parsed = parseValueInput(trimmed);
+ const nextFormatted = formatValue(parsed.value, parsed.important);
+ // $FlowFixMe[invalid-computed-prop]
+ const nextClassName = valueToClassName[nextFormatted];
+
+ setIsPending(true);
+ let didMutate = false;
+ try {
+ if (parsed.value === '') {
+ await clearInlineStyle({ property: entry.property });
+ didMutate = true;
+ await onOverrideRemove(
+ buildInlineOverrideId(entry.property, entry.pseudoElement),
+ );
+ return;
+ }
+
+ if (nextClassName && entry.className) {
+ const originalClassName =
+ existingClassOverride?.originalClassName ?? entry.className;
+ const shouldRemoveClassOverride =
+ existingClassOverride != null &&
+ nextClassName === originalClassName;
+
+ if (nextClassName !== entry.className) {
+ await swapClassName({ from: entry.className, to: nextClassName });
+ didMutate = true;
+ }
+ await clearInlineStyle({ property: entry.property });
+ didMutate = true;
+ await onOverrideRemove(
+ buildInlineOverrideId(entry.property, entry.pseudoElement),
+ );
+ if (shouldRemoveClassOverride && existingClassOverride) {
+ await onOverrideRemove(existingClassOverride.id);
+ return;
+ }
+ await onOverrideUpsert(
+ createClassOverride(
+ entry,
+ parsed,
+ entryKey,
+ nextClassName,
+ existingClassOverride?.originalClassName,
+ ),
+ );
+ return;
+ }
+
+ await setInlineStyle({
+ property: entry.property,
+ value: parsed.value,
+ important: parsed.important,
+ });
+ didMutate = true;
+ await onOverrideUpsert(createInlineOverride(entry, parsed, entryKey));
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error(error);
+ } finally {
+ setIsPending(false);
+ if (didMutate) {
+ onRefresh();
+ }
+ }
+ },
+ [
+ displayValue,
+ draftValue,
+ entry,
+ entryKey,
+ existingClassOverride,
+ isPending,
+ onOverrideRemove,
+ onOverrideUpsert,
+ onRefresh,
+ valueToClassName,
+ ],
+ );
+
+ const handleKeyDown = useCallback(
+ (
+ event: KeyboardEvent & {
+ currentTarget: HTMLInputElement | HTMLButtonElement,
+ },
+ ) => {
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
+ if (filteredSuggestions.length === 0) return;
+ event.preventDefault();
+ const delta = event.key === 'ArrowDown' ? 1 : -1;
+ setActiveIndex((prev) =>
+ getNextIndex(prev, delta, filteredSuggestions.length),
+ );
+ return;
+ }
+ if (event.key === 'Enter') {
+ const activeValue = filteredSuggestions[activeIndex];
+ if (activeValue) {
+ event.preventDefault();
+ commitChange(activeValue);
+ return;
+ }
+ event.preventDefault();
+ // $FlowFixMe[prop-missing]
+ event.currentTarget.blur();
+ }
+ if (event.key === 'Escape') {
+ event.preventDefault();
+ setIsEditing(false);
+ setDraftValue(displayValue);
+ setActiveIndex(-1);
+ }
+ },
+ [activeIndex, commitChange, displayValue, filteredSuggestions],
+ );
+
+ const prefixContent =
+ prefix && showColon ? (
+ <>
+ {prefix}
+ {': '}
+ >
+ ) : (
+ prefix
+ );
+ const hasSuggestions = filteredSuggestions.length > 0;
+ const activeDescendant =
+ activeIndex >= 0 && activeIndex < filteredSuggestions.length
+ ? `${listId}-option-${activeIndex}`
+ : undefined;
+
+ return (
+
+
+ {prefixContent}
+ {isEditing ? (
+
+ commitChange()}
+ onChange={(event) => setDraftValue(event.currentTarget.value)}
+ onKeyDown={handleKeyDown}
+ placeholder={draftValue}
+ role="combobox"
+ spellCheck={false}
+ />
+ {
+ commitChange(value);
+ }}
+ suggestions={filteredSuggestions}
+ />
+
+ ) : (
+
+ )}
+
+ {entry.className ? (
+
{entry.className}
+ ) : null}
+
+ );
+}
+
+function OverridesSection({
+ onAddOverride,
+ onRemoveOverride,
+ overrides,
+ propertyValues,
+}: {
+ onAddOverride: (property: string, rawValue: string) => Promise | void,
+ onRemoveOverride: (override: StylexOverride) => Promise | void,
+ overrides: $ReadOnlyArray,
+ propertyValues: { [string]: Array, ... },
+}): React.Node {
+ return (
+
+
Overrides
+ {overrides.length === 0 ? (
+
No overrides yet.
+ ) : (
+
+ {overrides.map((override) => (
+
+ ))}
+
+ )}
+
+
+ );
+}
+
+function OverrideRow({
+ onRemove,
+ override,
+}: {
+ onRemove: (override: StylexOverride) => Promise | void,
+ override: StylexOverride,
+}): React.Node {
+ const [isPending, setIsPending] = useState(false);
+
+ const handleRemove = useCallback(async () => {
+ if (isPending) return;
+ setIsPending(true);
+ try {
+ await onRemove(override);
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error(error);
+ } finally {
+ setIsPending(false);
+ }
+ }, [isPending, onRemove, override]);
+
+ const value = formatValue(override.value, override.important);
+ const line: React.Node = (
+ <>
+ {override.property}
+ {`: ${value}`}
+ >
+ );
+
+ return (
+
+
{line}
+
+ {override.className ? (
+ {override.className}
+ ) : null}
+
+
+
+ );
+}
+
+function OverrideComposer({
+ onAddOverride,
+ propertyValues,
+}: {
+ onAddOverride: (property: string, rawValue: string) => Promise | void,
+ propertyValues: { [string]: Array, ... },
+}): React.Node {
+ const [property, setProperty] = useState('');
+ const [value, setValue] = useState('');
+ const [isPending, setIsPending] = useState(false);
+ const [isValueFocused, setIsValueFocused] = useState(false);
+ const [activeIndex, setActiveIndex] = useState(-1);
+ const listId = useId();
+
+ const normalizedProperty = property.trim().toLowerCase();
+ const suggestions = normalizedProperty
+ ? (propertyValues[normalizedProperty] ?? [])
+ : [];
+ const filteredSuggestions = useMemo(
+ () => filterSuggestions(suggestions, value),
+ [suggestions, value],
+ );
+ const showSuggestions = isValueFocused && filteredSuggestions.length > 0;
+
+ useEffect(() => {
+ if (!showSuggestions) {
+ setActiveIndex(-1);
+ return;
+ }
+ setActiveIndex((prev) =>
+ prev >= filteredSuggestions.length
+ ? filteredSuggestions.length - 1
+ : prev,
+ );
+ }, [filteredSuggestions, showSuggestions]);
+
+ const commitAdd = useCallback(
+ async (nextValue?: string) => {
+ if (isPending) return;
+ const prop = property.trim();
+ const next = (nextValue ?? value).trim();
+ if (!prop || !next) return;
+ setIsPending(true);
+ try {
+ await onAddOverride(prop, nextValue ?? value);
+ setValue('');
+ } finally {
+ setIsPending(false);
+ }
+ },
+ [isPending, onAddOverride, property, value],
+ );
+
+ const handleKeyDown = useCallback(
+ (
+ event: KeyboardEvent & {
+ currentTarget: HTMLInputElement | HTMLButtonElement,
+ },
+ ) => {
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
+ if (!showSuggestions) return;
+ event.preventDefault();
+ const delta = event.key === 'ArrowDown' ? 1 : -1;
+ setActiveIndex((prev) =>
+ getNextIndex(prev, delta, filteredSuggestions.length),
+ );
+ return;
+ }
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ const activeValue = filteredSuggestions[activeIndex];
+ if (activeValue) {
+ commitAdd(activeValue);
+ return;
+ }
+ commitAdd();
+ }
+ if (event.key === 'Escape') {
+ setActiveIndex(-1);
+ }
+ },
+ [activeIndex, commitAdd, filteredSuggestions, showSuggestions],
+ );
+
+ return (
+
+ );
+}
+
+function SuggestionsList({
+ activeIndex,
+ listId,
+ onActiveIndexChange,
+ onSelect,
+ suggestions,
+}: {
+ activeIndex: number,
+ listId: string,
+ onActiveIndexChange?: (index: number) => void,
+ onSelect: (value: string) => void,
+ suggestions: $ReadOnlyArray,
+}): React.Node {
+ if (suggestions.length === 0) return null;
+ return (
+
+ {suggestions.map((value, index) => {
+ const isActive = index === activeIndex;
+ return (
+
{
+ event.preventDefault();
+ onSelect(value);
+ }}
+ onMouseEnter={() => {
+ if (onActiveIndexChange) {
+ onActiveIndexChange(index);
+ }
+ }}
+ >
+ {value}
+
+ );
+ })}
+
+ );
+}
+
+const styles = stylex.create({
+ muted: {
+ color: colors.textMuted,
+ },
+ declList: {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 8,
+ paddingBlock: 8,
+ },
+ declRow: {
+ display: 'flex',
+ alignItems: 'baseline',
+ justifyContent: 'space-between',
+ gap: 12,
+ },
+ declText: {
+ display: 'flex',
+ flex: 1,
+ minWidth: 0,
+ },
+ declLine: {
+ fontFamily:
+ 'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace',
+ whiteSpace: 'pre-wrap',
+ wordBreak: 'break-word',
+ },
+ declProperty: {
+ color: colors.textAccent,
+ },
+ declGroup: {
+ display: 'grid',
+ gap: 4,
+ },
+ declSubList: {
+ display: 'grid',
+ gap: 2,
+ paddingLeft: 12,
+ },
+ declSubLine: {
+ fontFamily:
+ 'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace',
+ whiteSpace: 'pre-wrap',
+ wordBreak: 'break-word',
+ },
+ overridesSection: {
+ display: 'grid',
+ gap: 6,
+ paddingTop: 12,
+ borderTopWidth: 1,
+ borderTopStyle: 'solid',
+ borderTopColor: colors.border,
+ },
+ overridesTitle: {
+ fontWeight: 600,
+ fontSize: 12,
+ },
+ overridesList: {
+ display: 'grid',
+ gap: 6,
+ },
+ overrideMeta: {
+ display: 'flex',
+ alignItems: 'center',
+ gap: 8,
+ flexShrink: 0,
+ },
+ overrideButton: {
+ appearance: 'none',
+ backgroundColor: colors.bgRaised,
+ borderWidth: 1,
+ borderStyle: 'solid',
+ borderColor: colors.border,
+ borderRadius: 6,
+ paddingInline: 6,
+ paddingBlock: 2,
+ cursor: 'pointer',
+ color: {
+ default: colors.textPrimary,
+ ':hover': colors.textAccent,
+ ':focus-visible': colors.textAccent,
+ },
+ ':disabled': {
+ opacity: 0.6,
+ cursor: 'default',
+ },
+ },
+ overrideButtonPending: {
+ opacity: 0.6,
+ cursor: 'default',
+ },
+ overrideComposer: {
+ display: 'flex',
+ alignItems: 'center',
+ gap: 8,
+ flexWrap: 'wrap',
+ flexGrow: 1,
+ },
+ overrideInput: {
+ fontFamily:
+ 'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace',
+ fontSize: 12,
+ lineHeight: '1.4',
+ color: 'inherit',
+ backgroundColor: colors.bgRaised,
+ borderWidth: 1,
+ borderStyle: 'solid',
+ borderColor: colors.border,
+ borderRadius: 4,
+ paddingInline: 6,
+ paddingBlock: 2,
+ minWidth: 0,
+ flexGrow: 1,
+ },
+ overrideValueWrap: {
+ position: 'relative',
+ flex: 1,
+ minWidth: 0,
+ },
+ valueButton: {
+ appearance: 'none',
+ backgroundColor: 'transparent',
+ borderStyle: 'none',
+ padding: 0,
+ margin: 0,
+ color: {
+ default: 'inherit',
+ ':hover': colors.textAccent,
+ ':focus-visible': colors.textAccent,
+ },
+ cursor: 'text',
+ textAlign: 'left',
+ fontFamily: 'inherit',
+ fontSize: 'inherit',
+ lineHeight: 'inherit',
+ },
+ valueInput: {
+ fontFamily: 'inherit',
+ fontSize: 'inherit',
+ lineHeight: 'inherit',
+ color: 'inherit',
+ backgroundColor: colors.bgRaised,
+ borderWidth: 1,
+ marginBlock: -2,
+ borderStyle: 'solid',
+ borderColor: colors.border,
+ borderRadius: 4,
+ paddingInline: 4,
+ paddingBlock: 1,
+ minWidth: 0,
+ flexGrow: 1,
+ boxSizing: 'border-box',
+ },
+ valuePending: {
+ opacity: 0.6,
+ },
+ suggestionWrap: {
+ display: 'flex',
+ position: 'relative',
+ flexGrow: 1,
+ },
+ suggestionList: {
+ position: 'absolute',
+ top: '100%',
+ left: 0,
+ zIndex: 2,
+ marginTop: 4,
+ backgroundColor: colors.bgRaised,
+ borderWidth: 1,
+ borderStyle: 'solid',
+ borderColor: colors.border,
+ borderRadius: 6,
+ paddingBlock: 4,
+ minWidth: '100%',
+ maxHeight: 160,
+ overflowY: 'auto',
+ boxShadow: '0 6px 16px rgba(0, 0, 0, 0.18)',
+ },
+ suggestionItem: {
+ appearance: 'none',
+ width: '100%',
+ textAlign: 'left',
+ borderStyle: 'none',
+ backgroundColor: 'transparent',
+ cursor: 'pointer',
+ paddingBlock: 4,
+ paddingInline: 8,
+ color: {
+ default: colors.textPrimary,
+ ':hover': colors.textAccent,
+ },
+ fontFamily: 'inherit',
+ fontSize: 'inherit',
+ },
+ suggestionItemActive: {
+ backgroundColor: colors.bg,
+ color: colors.textAccent,
+ },
+ declCondition: {
+ color: colors.secondaryAccent,
+ },
+ pseudoSection: {
+ display: 'grid',
+ gap: 6,
+ },
+ pseudoTitle: {
+ fontFamily:
+ 'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace',
+ color: colors.textMuted,
+ },
+ sectionList: {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 8,
+ },
+ atRuleGroup: {
+ display: 'grid',
+ gap: 2,
+ },
+ atRuleTitle: {
+ fontFamily:
+ 'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace',
+ },
+ atRuleList: {
+ display: 'grid',
+ gap: 2,
+ paddingLeft: 12,
+ },
+ className: {
+ color: colors.textMuted,
+ fontFamily:
+ 'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace',
+ '::before': {
+ content: '.',
+ },
+ },
+});
diff --git a/packages/@stylexjs/devtools-extension/src/panel/components/ErrorBoundary.js b/packages/@stylexjs/devtools-extension/src/panel/components/ErrorBoundary.js
new file mode 100644
index 000000000..3520f9556
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/src/panel/components/ErrorBoundary.js
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict
+ */
+
+'use strict';
+
+import * as React from 'react';
+
+type Props = {
+ children: React.Node,
+ fallback?: React.Node | ((error: Error) => React.Node),
+};
+
+type State = {
+ error: Error | null,
+};
+
+export class ErrorBoundary extends React.Component {
+ constructor(props: Props) {
+ super(props);
+ this.state = { error: null };
+ }
+
+ componentDidCatch(error: Error) {
+ this.setState({ error });
+ }
+
+ render(): React.Node {
+ const { fallback, children } = this.props;
+ if (this.state.error) {
+ return typeof fallback === 'function'
+ ? fallback(this.state.error)
+ : (fallback ?? null);
+ }
+ return children;
+ }
+}
diff --git a/packages/@stylexjs/devtools-extension/src/panel/components/EyeIcon.js b/packages/@stylexjs/devtools-extension/src/panel/components/EyeIcon.js
new file mode 100644
index 000000000..21f73545c
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/src/panel/components/EyeIcon.js
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict
+ */
+
+import * as React from 'react';
+import * as stylex from '@stylexjs/stylex';
+
+export function EyeIcon({
+ xstyle,
+}: {
+ xstyle?: stylex.StyleXStyles<>,
+}): React.Node {
+ return (
+
+ );
+}
diff --git a/packages/@stylexjs/devtools-extension/src/panel/components/Logo.js b/packages/@stylexjs/devtools-extension/src/panel/components/Logo.js
new file mode 100644
index 000000000..69b46e6c8
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/src/panel/components/Logo.js
@@ -0,0 +1,179 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict
+ */
+
+import * as React from 'react';
+import * as stylex from '@stylexjs/stylex';
+import type { StyleXStyles } from '@stylexjs/stylex';
+
+export const viewBox = '0 0 644 435';
+
+export function LogoText(): React.Node {
+ return (
+
+
+
+ );
+}
+
+export default function Logo({
+ xstyle,
+}: {
+ xstyle: StyleXStyles<>,
+}): React.Node {
+ const idA = 'a';
+ const idB = 'b';
+ const idC = 'c';
+ const idD = 'd';
+ const idE = 'e';
+ const idF = 'f';
+ const idG = 'g';
+ const idH = 'h';
+
+ return (
+
+ );
+}
diff --git a/packages/@stylexjs/devtools-extension/src/panel/components/Section.js b/packages/@stylexjs/devtools-extension/src/panel/components/Section.js
new file mode 100644
index 000000000..e4c6074fe
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/src/panel/components/Section.js
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict
+ */
+
+'use strict';
+
+import * as React from 'react';
+import * as stylex from '@stylexjs/stylex';
+
+export function Section({
+ title,
+ children,
+}: {
+ title: string,
+ children: React.Node,
+}): React.Node {
+ return (
+
+ );
+}
+
+const styles = stylex.create({
+ section: {
+ marginTop: 16,
+ padding: 8,
+ },
+ sectionTitle: {
+ marginTop: 0,
+ marginBottom: 8,
+ fontSize: '1rem',
+ fontWeight: 800,
+ },
+});
diff --git a/packages/@stylexjs/devtools-extension/src/panel/components/SourceRow.js b/packages/@stylexjs/devtools-extension/src/panel/components/SourceRow.js
new file mode 100644
index 000000000..76bc56972
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/src/panel/components/SourceRow.js
@@ -0,0 +1,215 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict
+ */
+
+'use strict';
+
+import * as React from 'react';
+import { useState, useCallback, use, Suspense, useTransition } from 'react';
+import * as stylex from '@stylexjs/stylex';
+import type { SourcePreview } from '../../types';
+// import { openInVsCodeFromStylexSource } from '../../utils/vscode';
+
+import {
+ getSourcePreview,
+ openSourceBestEffort,
+} from '../../devtools/resources';
+// import { Button } from './Button';
+import { colors } from '../theme.stylex';
+import { EyeIcon } from './EyeIcon';
+
+export function SourceRow({
+ src,
+ onError,
+}: {
+ src: { raw: string, file: string, line: number | null, ... },
+ onError: (message: string) => void,
+}): React.Node {
+ const [isPreviewOpen, setIsPreviewOpen] = useState(false);
+ const [isPending, startTransition] = useTransition();
+
+ const togglePreview = useCallback(() => {
+ startTransition(() => {
+ setIsPreviewOpen((open) => !open);
+ });
+ }, []);
+
+ return (
+
+
+
+
+ {/* */}
+ {/* */}
+
+
+ {isPreviewOpen ? (
+
+
+
+ ) : null}
+
+ );
+}
+
+const cache: { [string]: Promise } = {};
+function getSourcePreviewPromise(file: string, line: number) {
+ const cacheKey = `${file}:${line}`;
+ if (cache[cacheKey]) {
+ return cache[cacheKey];
+ }
+ const promise = getSourcePreview(file, line);
+ cache[cacheKey] = promise;
+ return promise;
+}
+
+function SourceSnippet({ file, line }: { file: string, line: number }) {
+ const preview = use(getSourcePreviewPromise(file, line));
+
+ return (
+
+ {preview?.snippet ?? 'no source found'}
+
+ );
+}
+
+function SourceSnippetSuspense({ file, line }: { file: string, line: number }) {
+ return (
+ }>
+
+
+ );
+}
+
+function SourceSnippetFallback() {
+ return (
+
+ Loading…
+
+ );
+}
+
+const styles = stylex.create({
+ icon: {
+ width: 16,
+ height: 16,
+ },
+ pill: {
+ appearance: 'none',
+ backgroundColor: {
+ default: 'transparent',
+ ':hover': colors.bgRaised,
+ ':focus-visible': colors.bgRaised,
+ },
+ transform: {
+ default: null,
+ ':active': 'scale(0.95)',
+ },
+ display: 'inline-block',
+ paddingTop: 8,
+ paddingBottom: 2,
+ paddingInline: 4,
+ borderRadius: 8,
+ borderWidth: 1,
+ borderStyle: 'none',
+ },
+ pillActive: {
+ color: colors.textAccent,
+ },
+ loading: {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ color: colors.textMuted,
+ },
+ sourceEntry: {
+ width: '100%',
+ maxWidth: '100%',
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 4,
+ },
+ sourceRow: {
+ width: '100%',
+ display: 'flex',
+ alignItems: 'center',
+ },
+ sourcePath: {
+ appearance: 'none',
+ textAlign: 'start',
+ backgroundColor: 'transparent',
+ display: 'inline',
+ borderStyle: 'none',
+ cursor: 'pointer',
+ flexGrow: 1,
+ fontFamily:
+ 'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace',
+ color: {
+ default: colors.textPrimary,
+ ':hover': colors.textAccent,
+ ':focus-visible': colors.textAccent,
+ },
+ textDecoration: {
+ default: 'none',
+ ':hover': 'underline',
+ ':focus-visible': 'underline',
+ },
+ wordBreak: 'break-word',
+ },
+ buttonPending: {
+ opacity: 0.5,
+ },
+ sourcePreview: {},
+ sourcePreviewCode: {
+ fontFamily:
+ 'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace',
+ whiteSpace: 'pre',
+ width: '100%',
+ overflow: 'auto',
+ backgroundColor: colors.bgRaised,
+ borderWidth: 1,
+ borderStyle: 'solid',
+ borderColor: colors.border,
+ borderRadius: 6,
+ margin: 0,
+ padding: 8,
+ },
+});
diff --git a/packages/@stylexjs/devtools-extension/src/panel/components/SourcesList.js b/packages/@stylexjs/devtools-extension/src/panel/components/SourcesList.js
new file mode 100644
index 000000000..13fbb150e
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/src/panel/components/SourcesList.js
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict
+ */
+
+import * as React from 'react';
+import * as stylex from '@stylexjs/stylex';
+import { SourceRow } from './SourceRow';
+
+export function SourcesList({
+ sources,
+ onError,
+}: {
+ sources: $ReadOnlyArray<{
+ raw: string,
+ file: string,
+ line: number | null,
+ ...
+ }>,
+ onError: (message: string) => void,
+}): React.Node {
+ return (
+
+ {sources.map((src, index) => (
+
+ ))}
+
+ );
+}
+
+const styles = stylex.create({
+ sourcesList: {
+ width: '100%',
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 6,
+ },
+});
diff --git a/packages/@stylexjs/devtools-extension/src/panel/index.css b/packages/@stylexjs/devtools-extension/src/panel/index.css
new file mode 100644
index 000000000..f710d7ade
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/src/panel/index.css
@@ -0,0 +1,16 @@
+@layer reset {
+ html,
+ body,
+ #root {
+ height: 100%;
+ color-scheme: light dark;
+ }
+
+ body {
+ margin: 0;
+ }
+
+ * {
+ box-sizing: border-box;
+ }
+}
diff --git a/packages/@stylexjs/devtools-extension/src/panel/main.js b/packages/@stylexjs/devtools-extension/src/panel/main.js
new file mode 100644
index 000000000..6e7da8fa8
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/src/panel/main.js
@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict
+ */
+
+'use strict';
+
+import * as React from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App.jsx';
+
+import './index.css';
+
+const rootEl = document.getElementById('root');
+if (rootEl) {
+ createRoot(rootEl).render();
+}
diff --git a/packages/@stylexjs/devtools-extension/src/panel/theme.stylex.js b/packages/@stylexjs/devtools-extension/src/panel/theme.stylex.js
new file mode 100644
index 000000000..9ada3340b
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/src/panel/theme.stylex.js
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict
+ */
+
+'use strict';
+
+import * as stylex from '@stylexjs/stylex';
+
+const colorsValue = {
+ bg: 'light-dark(#ffffff, #282828)',
+ bgRaised: 'light-dark(#f6f8fa, #282828)',
+ textPrimary: 'light-dark(#000000, #ffffff)',
+ textMuted: 'light-dark(#757575, #999999)',
+ textAccent: 'light-dark(#dc362e, rgb(92 213 251))',
+ secondaryAccent: 'light-dark(#0F7913, #73C89C)',
+ border: 'light-dark(#d3e3fd, #5e5e5eff)',
+};
+
+export const colors: typeof colorsValue = stylex.defineConsts(colorsValue);
diff --git a/packages/@stylexjs/devtools-extension/src/types.js b/packages/@stylexjs/devtools-extension/src/types.js
new file mode 100644
index 000000000..a13ee7add
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/src/types.js
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict
+ */
+
+'use strict';
+
+export type StatusKind = 'info' | 'error';
+
+export type StatusState = {
+ message: string,
+ kind: StatusKind,
+};
+
+export type StylexSource = {
+ raw: string,
+ file: string,
+ line: number | null,
+};
+
+export type StylexDeclaration = {
+ property: string,
+ value: string,
+ important: boolean,
+ condition?: string,
+ conditions?: $ReadOnlyArray,
+ pseudoElement?: string,
+ className?: string,
+ ...
+};
+
+export type AppliedStylexClass = {
+ name: string,
+ declarations: Array,
+};
+
+export type AtomicStyleRule = {
+ className: string,
+ property: string,
+ value: string,
+ important: boolean,
+ conditions: Array,
+ pseudoElement?: string,
+};
+
+export type StylexDebugData = $ReadOnly<{
+ element: {
+ tagName: string,
+ },
+ sources: Array,
+ computed: { [string]: string, ... },
+ atomicRules: Array,
+ overrides: Array,
+ applied: {
+ classes: Array,
+ },
+}>;
+
+export type SourcePreview = {
+ url: string,
+ snippet: string,
+};
+
+export type StylexOverride = {
+ id: string,
+ kind: 'inline' | 'class',
+ property: string,
+ value: string,
+ important: boolean,
+ conditions: Array,
+ pseudoElement?: string,
+ className?: string,
+ originalClassName?: string,
+ sourceEntryKey?: string,
+};
diff --git a/packages/@stylexjs/devtools-extension/src/utils/resourceMatching.js b/packages/@stylexjs/devtools-extension/src/utils/resourceMatching.js
new file mode 100644
index 000000000..3fe8ce27c
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/src/utils/resourceMatching.js
@@ -0,0 +1,117 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict
+ */
+
+'use strict';
+
+function isProbablySourceMappedUrl(url: string): boolean {
+ return (
+ url.startsWith('webpack://') ||
+ url.startsWith('vite://') ||
+ url.startsWith('rollup://') ||
+ url.startsWith('parcel://') ||
+ url.startsWith('ng://')
+ );
+}
+
+function normalizeForMatching(text: string): string {
+ const noHash = text.split('#')[0];
+ const noQuery = noHash.split('?')[0];
+ let decoded = noQuery;
+ try {
+ decoded = decodeURIComponent(noQuery);
+ } catch {
+ // ignore
+ }
+ return decoded.replace(/\\\\/g, '/');
+}
+
+function buildFileMatchCandidates(file: string): Array {
+ const candidates = new Set();
+ const raw = normalizeForMatching(file).trim();
+ if (!raw) return [];
+ candidates.add(raw);
+
+ const colonIndex = raw.indexOf(':');
+ const possiblePrefix = colonIndex !== -1 ? raw.slice(0, colonIndex) : '';
+ const possiblePath = colonIndex !== -1 ? raw.slice(colonIndex + 1) : '';
+ const looksLikePackagePrefix =
+ colonIndex !== -1 &&
+ possiblePrefix &&
+ !possiblePrefix.includes('/') &&
+ !/^[a-zA-Z]$/.test(possiblePrefix);
+
+ if (looksLikePackagePrefix) {
+ const withoutPrefix = possiblePath.replace(/^\.?\//, '');
+ if (withoutPrefix) candidates.add(withoutPrefix);
+ candidates.add(`${possiblePrefix}/${withoutPrefix}`);
+ candidates.add(
+ `packages/${possiblePrefix.replace(/\/+$/, '')}/${withoutPrefix}`,
+ );
+ candidates.add(
+ `node_modules/${possiblePrefix.replace(/\/+$/, '')}/${withoutPrefix}`,
+ );
+ }
+
+ const parts = raw.split('/').filter(Boolean);
+ if (parts.length >= 3) {
+ candidates.add(parts.slice(-3).join('/'));
+ }
+ if (parts.length >= 2) {
+ candidates.add(parts.slice(-2).join('/'));
+ }
+ if (parts.length >= 1) {
+ candidates.add(parts.slice(-1)[0]);
+ }
+
+ return Array.from(candidates)
+ .map((s) => normalizeForMatching(s).replace(/^\/+/, ''))
+ .filter(Boolean);
+}
+
+export function findBestMatchingResource(
+ resources: $ReadOnlyArray,
+ file: string,
+): T | null {
+ const suffixes = buildFileMatchCandidates(file);
+ if (suffixes.length === 0) return null;
+
+ function scoreResourceUrl(url: string): number | null {
+ const u = normalizeForMatching(url);
+ if (!u) return null;
+
+ let matchScore: number | null = null;
+ for (const s of suffixes) {
+ if (u === s) matchScore = Math.max(matchScore ?? -1, 10_000);
+ else if (u.endsWith(`/${s}`))
+ matchScore = Math.max(matchScore ?? -1, 9_000);
+ else if (u.endsWith(s)) matchScore = Math.max(matchScore ?? -1, 8_000);
+ else if (u.includes(`/${s}`))
+ matchScore = Math.max(matchScore ?? -1, 7_000);
+ else if (u.includes(s)) matchScore = Math.max(matchScore ?? -1, 6_000);
+ }
+ if (matchScore == null) return null;
+
+ const schemeBonus = isProbablySourceMappedUrl(url) ? 500 : 0;
+ const hasQuery = url.split('#')[0].includes('?');
+ const queryPenalty = hasQuery ? -250 : 0;
+ return matchScore + schemeBonus + queryPenalty;
+ }
+
+ let best: T | null = null;
+ let bestScore: number | null = null;
+ for (const r of resources) {
+ const score = scoreResourceUrl(r.url);
+ if (score == null) continue;
+ if (bestScore == null || score > bestScore) {
+ best = r;
+ bestScore = score;
+ }
+ }
+ return best;
+}
diff --git a/packages/@stylexjs/devtools-extension/src/utils/sourceSnippet.js b/packages/@stylexjs/devtools-extension/src/utils/sourceSnippet.js
new file mode 100644
index 000000000..19b3c95cb
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/src/utils/sourceSnippet.js
@@ -0,0 +1,213 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict
+ */
+
+'use strict';
+
+function findMatchingClosingCurlyBraceLine(
+ lines: Array,
+ startLine: number,
+): number | null {
+ const startIndex = Math.max(0, startLine - 1);
+ const firstLine = lines[startIndex] ?? '';
+ const startColumn = Math.max(0, firstLine.lastIndexOf('{'));
+
+ let depth = 0;
+ let started = false;
+ let inSingle = false;
+ let inDouble = false;
+ let inLineComment = false;
+ let inBlockComment = false;
+ let escapeNext = false;
+ const templateStack = [];
+
+ for (let li = startIndex; li < lines.length; li += 1) {
+ const text = lines[li] ?? '';
+ inLineComment = false;
+ const colStart = li === startIndex ? startColumn : 0;
+
+ for (let ci = colStart; ci < text.length; ci += 1) {
+ const ch = text[ci];
+ const next = text[ci + 1];
+
+ if (inLineComment) break;
+
+ if (inBlockComment) {
+ if (ch === '*' && next === '/') {
+ inBlockComment = false;
+ ci += 1;
+ }
+ continue;
+ }
+
+ if (inSingle) {
+ if (escapeNext) {
+ escapeNext = false;
+ continue;
+ }
+ if (ch === '\\') {
+ escapeNext = true;
+ continue;
+ }
+ if (ch === "'") {
+ inSingle = false;
+ }
+ continue;
+ }
+
+ if (inDouble) {
+ if (escapeNext) {
+ escapeNext = false;
+ continue;
+ }
+ if (ch === '\\') {
+ escapeNext = true;
+ continue;
+ }
+ if (ch === '"') {
+ inDouble = false;
+ }
+ continue;
+ }
+
+ const templateTop =
+ templateStack.length > 0
+ ? templateStack[templateStack.length - 1]
+ : null;
+ const inTemplateText = templateTop && templateTop.inExpression === false;
+
+ if (inTemplateText) {
+ if (escapeNext) {
+ escapeNext = false;
+ continue;
+ }
+ if (ch === '\\') {
+ escapeNext = true;
+ continue;
+ }
+ if (ch === '`') {
+ templateStack.pop();
+ continue;
+ }
+ if (ch === '$' && next === '{' && templateTop != null) {
+ templateTop.inExpression = true;
+ templateTop.exprDepth = 1;
+
+ if (!started) {
+ started = true;
+ depth = 1;
+ } else {
+ depth += 1;
+ }
+
+ ci += 1;
+ continue;
+ }
+ continue;
+ }
+
+ if (ch === '/' && next === '/') {
+ inLineComment = true;
+ ci += 1;
+ continue;
+ }
+ if (ch === '/' && next === '*') {
+ inBlockComment = true;
+ ci += 1;
+ continue;
+ }
+ if (ch === "'") {
+ inSingle = true;
+ escapeNext = false;
+ continue;
+ }
+ if (ch === '"') {
+ inDouble = true;
+ escapeNext = false;
+ continue;
+ }
+ if (ch === '`') {
+ templateStack.push({ inExpression: false, exprDepth: 0 });
+ escapeNext = false;
+ continue;
+ }
+
+ if (ch === '{') {
+ if (!started) {
+ started = true;
+ depth = 1;
+ } else {
+ depth += 1;
+ }
+ if (templateTop && templateTop.inExpression) {
+ templateTop.exprDepth += 1;
+ }
+ continue;
+ }
+
+ if (ch === '}') {
+ if (!started) continue;
+ depth -= 1;
+ if (templateTop && templateTop.inExpression) {
+ templateTop.exprDepth -= 1;
+ if (templateTop.exprDepth === 0) {
+ templateTop.inExpression = false;
+ templateTop.exprDepth = 0;
+ }
+ }
+ if (depth === 0) {
+ return li + 1;
+ }
+ }
+ }
+ }
+
+ return null;
+}
+
+export function formatSourceSnippet(
+ content: string,
+ line: number | null,
+): string {
+ const normalized = content.replace(/\r\n/g, '\n');
+ const lines = normalized.split('\n');
+ if (lines.length === 0) return '';
+
+ const targetLine =
+ typeof line === 'number' && Number.isFinite(line) ? line : null;
+ if (targetLine == null) {
+ return lines.slice(0, Math.min(40, lines.length)).join('\n');
+ }
+
+ const start = Math.max(targetLine, 1);
+ const startLineText = lines[targetLine - 1] ?? '';
+ const hasOpeningBrace = startLineText.includes('{');
+ const braceEnd = hasOpeningBrace
+ ? findMatchingClosingCurlyBraceLine(lines, targetLine)
+ : null;
+
+ let end =
+ braceEnd != null ? braceEnd : Math.min(targetLine + 6, lines.length);
+ const maxPreviewLines = 200;
+ const truncated = end - start + 1 > maxPreviewLines;
+ if (truncated) {
+ end = Math.min(start + maxPreviewLines - 1, lines.length);
+ }
+
+ const width = String(end).length;
+ const out = [];
+ for (let i = start; i <= end; i += 1) {
+ const prefix = targetLine === i ? '>' : ' ';
+ const num = String(i).padStart(width, ' ');
+ out.push(`${prefix} ${num} | ${lines[i - 1] ?? ''}`);
+ }
+ if (truncated) {
+ out.push('… (preview truncated)');
+ }
+ return out.join('\n');
+}
diff --git a/packages/@stylexjs/devtools-extension/src/utils/vscode.js b/packages/@stylexjs/devtools-extension/src/utils/vscode.js
new file mode 100644
index 000000000..7416bc5a6
--- /dev/null
+++ b/packages/@stylexjs/devtools-extension/src/utils/vscode.js
@@ -0,0 +1,99 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict
+ */
+
+'use strict';
+
+function openExternalUrl(url: string): void {
+ const a = document.createElement('a');
+ a.href = url;
+ a.target = '_blank';
+ a.rel = 'noreferrer';
+ a.click();
+}
+
+function ensureLeadingSlash(path: string): string {
+ if (!path) return '/';
+ if (path.startsWith('/')) return path;
+ return `/${path}`;
+}
+
+function joinPaths(root: string, rel: string): string {
+ const left = root.replace(/\\\\/g, '/').replace(/\/+$/, '');
+ const right = rel.replace(/\\\\/g, '/').replace(/^\/+/, '');
+ return `${left}/${right}`;
+}
+
+function stylexFileToEditorRelativePath(file: string): string {
+ const normalized = file.replace(/\\\\/g, '/').replace(/^\.?\//, '');
+
+ const nmIndex = normalized.indexOf('node_modules/');
+ if (nmIndex !== -1) {
+ return normalized.slice(nmIndex);
+ }
+
+ const colonIndex = normalized.indexOf(':');
+ if (colonIndex !== -1 && colonIndex !== 1) {
+ const prefix = normalized.slice(0, colonIndex);
+ const rest = normalized.slice(colonIndex + 1).replace(/^\/+/, '');
+ if (prefix && rest) {
+ return `packages/${prefix.replace(/\/+$/, '')}/${rest}`;
+ }
+ }
+
+ return normalized;
+}
+
+function getOrPromptForVsCodeRoot(): string | null {
+ const key = 'stylex_vscode_root';
+ const existing = (localStorage.getItem(key) ?? '').trim();
+ if (existing) return existing;
+
+ // eslint-disable-next-line no-alert
+ const next = window.prompt(
+ 'VS Code root path (absolute). Example: /Users/you/project',
+ '',
+ );
+ if (!next) return null;
+ const value = next.trim();
+ if (!value) return null;
+ localStorage.setItem(key, value);
+ return value;
+}
+
+function resolveStylexSourceToAbsolutePath(file: string): string | null {
+ const normalized = file.replace(/\\\\/g, '/').trim();
+ if (!normalized) return null;
+
+ if (normalized.startsWith('/') || /^[a-zA-Z]:\//.test(normalized)) {
+ return normalized;
+ }
+
+ const root = getOrPromptForVsCodeRoot();
+ if (!root) return null;
+
+ const rel = stylexFileToEditorRelativePath(normalized);
+ return joinPaths(root, rel);
+}
+
+export function openInVsCodeFromStylexSource(
+ file: string,
+ line: number | null,
+): void {
+ const absPath = resolveStylexSourceToAbsolutePath(file);
+ if (!absPath) {
+ return;
+ }
+
+ const normalizedPath = absPath.replace(/\\\\/g, '/');
+ const lineSuffix = typeof line === 'number' ? `:${line}:1` : '';
+ const url = encodeURI(
+ `vscode://file${ensureLeadingSlash(normalizedPath)}${lineSuffix}`,
+ );
+ openExternalUrl(url);
+}