diff --git a/licenses/dependency-identity.txt b/licenses/dependency-identity.txt
index 6970693..5286423 100644
--- a/licenses/dependency-identity.txt
+++ b/licenses/dependency-identity.txt
@@ -39,6 +39,7 @@ mbedtls url=https://github.com/Mbed-TLS/mbedtls/archive/refs/tags/mbedtls-3.6.7.
sqlite url=https://sqlite.org/2026/sqlite-amalgamation-3530400.zip hash=N-V-__8AAGVtrgCcOcmjrOJnagmnRyMrcKaOo09KbU-vu8w8
[npm runtime closure]
+@stylexjs/stylex 0.19.0 MIT
@tanstack/history 1.162.0 MIT
@tanstack/query-core 5.101.4 MIT
@tanstack/react-query 5.101.4 MIT
@@ -47,12 +48,17 @@ sqlite url=https://sqlite.org/2026/sqlite-amalgamation-3530400.zip hash=N-V-__8A
@tanstack/router-core 1.171.15 MIT
@tanstack/store 0.9.3 MIT
cookie-es 3.1.1 MIT
+css-mediaquery 0.1.2 BSD
+invariant 2.2.4 MIT
isbot 5.2.1 Unlicense
+js-tokens 4.0.0 MIT
+loose-envify 1.4.0 MIT
react 19.2.8 MIT
react-dom 19.2.8 MIT
scheduler 0.27.0 MIT
seroval 1.5.6 MIT
seroval-plugins 1.5.6 MIT
+styleq 0.2.1 MIT
use-sync-external-store 1.6.0 MIT
[npm build-time generators whose output ships]
diff --git a/licenses/inventory.zon b/licenses/inventory.zon
index db4e670..ab7f018 100644
--- a/licenses/inventory.zon
+++ b/licenses/inventory.zon
@@ -39,6 +39,21 @@
// stay listed in licenses/dependency-identity.txt so that a future build which
// does pull them in trips the guard.
//
+// Four more joined that list with the StyleX build integration: css-mediaquery,
+// invariant, and loose-envify with js-tokens under it. They are @stylexjs/
+// stylex's own dependencies, they serve its compiler and not its runtime, and a
+// sourcemap build of a component calling stylex.props shows none of them in any
+// `sources` list.
+//
+// StyleX itself is inventoried below although nothing imports it yet, so no
+// chunk carries it today. The licence is carried now because the same sourcemap
+// build settles what the page conversion will land, and it is not the obvious
+// answer: only @stylexjs/stylex appears as a bundled package, and styleq
+// arrives inside it. The published lib/es/stylex.mjs vendors styleq's source
+// instead of importing it, so styleq's bytes ship while styleq never appears in
+// a `sources` list — no guard would raise it, which is why it is written down
+// here.
+//
// Of the remaining devDependencies, none puts a byte in web/dist: @vitejs/
// plugin-react and typescript only transform our own sources (react-refresh is
// dev-server only, and tslib is optional and unused), @tailwindcss/vite
@@ -94,6 +109,18 @@
.note = "Bundled into the admin UI JavaScript. Grouped because all five carry the identical MIT text and copyright line.",
.file = "tanstack-mit.txt",
},
+ .{
+ .component = "StyleX",
+ .version = "@stylexjs/stylex 0.19.0",
+ .note = "The style runtime for the admin UI. Its published dist vendors styleq's source, so the styleq entry below covers bytes that arrive inside this package. The npm tarball ships no licence file of its own; this text is the LICENSE of facebook/stylex at tag 0.19.0, which is byte-identical to the one on main.",
+ .file = "stylex-mit.txt",
+ },
+ .{
+ .component = "styleq",
+ .version = "styleq 0.2.1",
+ .note = "The class-name merger StyleX calls at runtime. Never a bundled package in its own right: @stylexjs/stylex vendors its source into lib/es/stylex.mjs, so it reaches web/dist without appearing in any sourcemap `sources` list. Separate entry from StyleX: same MIT text, different copyright line.",
+ .file = "styleq-mit.txt",
+ },
.{
.component = "TanStack Store",
.version = "@tanstack/store 0.9.3, @tanstack/react-store 0.9.3",
diff --git a/licenses/licenses.zig b/licenses/licenses.zig
index ca4555e..f1e3210 100644
--- a/licenses/licenses.zig
+++ b/licenses/licenses.zig
@@ -51,6 +51,8 @@ pub const texts: []const Text = &.{
.{ .name = "react-mit.txt", .body = @embedFile("react-mit.txt") },
.{ .name = "tanstack-mit.txt", .body = @embedFile("tanstack-mit.txt") },
.{ .name = "tanstack-store-mit.txt", .body = @embedFile("tanstack-store-mit.txt") },
+ .{ .name = "stylex-mit.txt", .body = @embedFile("stylex-mit.txt") },
+ .{ .name = "styleq-mit.txt", .body = @embedFile("styleq-mit.txt") },
.{ .name = "tailwindcss-mit.txt", .body = @embedFile("tailwindcss-mit.txt") },
.{ .name = "vite-mit.txt", .body = @embedFile("vite-mit.txt") },
.{ .name = "rolldown-mit.txt", .body = @embedFile("rolldown-mit.txt") },
diff --git a/licenses/styleq-mit.txt b/licenses/styleq-mit.txt
new file mode 100644
index 0000000..ca36bda
--- /dev/null
+++ b/licenses/styleq-mit.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) Nicolas Gallagher
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/licenses/stylex-mit.txt b/licenses/stylex-mit.txt
new file mode 100644
index 0000000..b93be90
--- /dev/null
+++ b/licenses/stylex-mit.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) Meta Platforms, Inc. and affiliates.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/src/licenses_drift_test.zig b/src/licenses_drift_test.zig
index 993ffc8..1e6729e 100644
--- a/src/licenses_drift_test.zig
+++ b/src/licenses_drift_test.zig
@@ -57,7 +57,16 @@ const npm_generators = [_][]const u8{ "rolldown", "tailwindcss", "vite" };
/// This list is a claim about the build, and the build is what settles it: a
/// name here that turns up in the recorded `web/dist` bundle fails, so a
/// package that starts shipping cannot stay on this list quietly.
-const npm_not_shipped = [_][]const u8{ "cookie-es", "isbot", "seroval", "seroval-plugins" };
+const npm_not_shipped = [_][]const u8{
+ "cookie-es",
+ "isbot",
+ "seroval",
+ "seroval-plugins",
+ "css-mediaquery",
+ "invariant",
+ "js-tokens",
+ "loose-envify",
+};
/// Components the shipped artifacts contain that no dependency file mentions,
/// so no automatic check would ever notice their disappearance from the
diff --git a/web/package-lock.json b/web/package-lock.json
index c5dbe33..b249f75 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -8,12 +8,14 @@
"name": "nxdns-web",
"version": "0.0.0",
"dependencies": {
+ "@stylexjs/stylex": "0.19.0",
"@tanstack/react-query": "5.101.4",
"@tanstack/react-router": "1.170.18",
"react": "19.2.8",
"react-dom": "19.2.8"
},
"devDependencies": {
+ "@stylexjs/unplugin": "0.19.0",
"@tailwindcss/vite": "4.3.3",
"@testing-library/dom": "10.4.1",
"@testing-library/react": "16.3.2",
@@ -99,6 +101,153 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz",
+ "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.8",
+ "@babel/types": "^7.29.8",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/helper-validator-identifier": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
@@ -109,6 +258,94 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
+ "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.8"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-flow": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.29.7.tgz",
+ "integrity": "sha512-ajMX6QPcyomotqwpzhkYGxcK2i/us0rs1Qo9QvUpa+Fca0FTmqrzKrctoIYLMxcOhGZldGT/BAVkRGTWBiR8gQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-jsx": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz",
+ "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-typescript": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz",
+ "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
"node_modules/@babel/runtime": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
@@ -119,6 +356,54 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@babel/template": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz",
+ "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.8",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.8",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.8",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
+ "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@bramus/specificity": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
@@ -272,6 +557,17 @@
"node": ">=20.19.0"
}
},
+ "node_modules/@dual-bundle/import-meta-resolve": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.2.1.tgz",
+ "integrity": "sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/JounQin"
+ }
+ },
"node_modules/@emnapi/core": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
@@ -1039,6 +1335,60 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@stylexjs/babel-plugin": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/@stylexjs/babel-plugin/-/babel-plugin-0.19.0.tgz",
+ "integrity": "sha512-2SnVZIl047TtqKW0nRTn7Irz0kmoot1bxdGDE/HwfwfF7dJYHqwkIFg92s87v3hj7zxJobrHWwDx1p3qvQXW4g==",
+ "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.19.0",
+ "@stylexjs/stylex": "0.19.0",
+ "postcss-value-parser": "^4.1.0"
+ }
+ },
+ "node_modules/@stylexjs/shared": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/@stylexjs/shared/-/shared-0.19.0.tgz",
+ "integrity": "sha512-7Yym70cQH5sPzJHgzJX3FkL9ZCptbC3IbiRijN9YLYU/5OywB/3c519Xv1DOzvxqjv8urSxh98/9HxLaPG7sDg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@stylexjs/stylex": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/@stylexjs/stylex/-/stylex-0.19.0.tgz",
+ "integrity": "sha512-CnUFp7YMaDLDeemsWOfJgoC/gKM5P/yBNMcpJaE6ChJmXr7s0DJwSeGTTlHJcqqwN9OW1qGtmARWLFhGZN1pTA==",
+ "license": "MIT",
+ "dependencies": {
+ "css-mediaquery": "^0.1.2",
+ "invariant": "^2.2.4",
+ "styleq": "0.2.1"
+ }
+ },
+ "node_modules/@stylexjs/unplugin": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/@stylexjs/unplugin/-/unplugin-0.19.0.tgz",
+ "integrity": "sha512-2QwNYovSwarC/M8DLD78857F6MEi4eQ3UYzgdmyg/K7562M/rV1KWptiATxcKgujlUT6ToNy778SOXf4bGK+Bw==",
+ "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.19.0",
+ "browserslist": "^4.24.0",
+ "lightningcss": "^1.29.1"
+ },
+ "peerDependencies": {
+ "unplugin": "^2.3.11"
+ }
+ },
"node_modules/@tailwindcss/node": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz",
@@ -2032,6 +2382,20 @@
"url": "https://opencollective.com/vitest"
}
},
+ "node_modules/acorn": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
+ "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -2075,6 +2439,19 @@
"node": ">=12"
}
},
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.11.12",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz",
+ "integrity": "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/bidi-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
@@ -2085,6 +2462,61 @@
"require-from-string": "^2.0.2"
}
},
+ "node_modules/browserslist": {
+ "version": "4.28.7",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz",
+ "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.44",
+ "caniuse-lite": "^1.0.30001806",
+ "electron-to-chromium": "^1.5.393",
+ "node-releases": "^2.0.51",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001806",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz",
+ "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
"node_modules/chai": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
@@ -2108,6 +2540,12 @@
"integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==",
"license": "MIT"
},
+ "node_modules/css-mediaquery": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz",
+ "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==",
+ "license": "BSD"
+ },
"node_modules/css-tree": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
@@ -2143,6 +2581,24 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
"node_modules/decimal.js": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
@@ -2177,6 +2633,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.401",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.401.tgz",
+ "integrity": "sha512-H6ViHN68nGYlChEvlIU67fn8O2/tpbWQPwck98yaJmh+08LSvHiydzDQ6oXNccLU3kNRVIRS9A4mA7CG+i6fLQ==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/enhanced-resolve": {
"version": "5.24.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.3.tgz",
@@ -2211,6 +2674,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
@@ -2264,6 +2737,16 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -2284,6 +2767,15 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
+ "node_modules/invariant": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+ "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.0.0"
+ }
+ },
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
@@ -2314,7 +2806,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/jsdom": {
@@ -2358,6 +2849,32 @@
}
}
},
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
@@ -2631,6 +3148,18 @@
"url": "https://opencollective.com/parcel"
}
},
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
"node_modules/lru-cache": {
"version": "11.5.2",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
@@ -2668,6 +3197,13 @@
"dev": true,
"license": "CC0-1.0"
},
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/nanoid": {
"version": "3.3.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
@@ -2687,6 +3223,16 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
+ "node_modules/node-releases": {
+ "version": "2.0.52",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.52.tgz",
+ "integrity": "sha512-MRlTqhAfoMx/4mhEbPo3Hi02g9LJZaJkka69V6h67Cb1gjrAG0jsTE4CZX1eptNx+VCAwJmfpnDIF4P0Nh1A7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/obug": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
@@ -2819,6 +3365,13 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/prettier": {
"version": "3.9.6",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz",
@@ -2951,6 +3504,16 @@
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT"
},
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
"node_modules/seroval": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.6.tgz",
@@ -3003,6 +3566,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/styleq": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/styleq/-/styleq-0.2.1.tgz",
+ "integrity": "sha512-L0TR0NQb+X4/ktDEKmjWyp27gla+LUYi/by5k5SjKXf6/pvZP7wbwEB5J+tqxdFVPgzbsuz+d4RTScO/QZquBw==",
+ "license": "MIT"
+ },
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@@ -3181,6 +3750,54 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/unplugin": {
+ "version": "2.3.11",
+ "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
+ "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "acorn": "^8.15.0",
+ "picomatch": "^4.0.3",
+ "webpack-virtual-modules": "^0.6.2"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
@@ -3381,6 +3998,14 @@
"node": ">=20"
}
},
+ "node_modules/webpack-virtual-modules": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
+ "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/whatwg-mimetype": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
@@ -3439,6 +4064,13 @@
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"dev": true,
"license": "MIT"
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
}
}
}
diff --git a/web/package.json b/web/package.json
index dc34663..de2c475 100644
--- a/web/package.json
+++ b/web/package.json
@@ -25,12 +25,14 @@
"trailingComma": "all"
},
"dependencies": {
+ "@stylexjs/stylex": "0.19.0",
"@tanstack/react-query": "5.101.4",
"@tanstack/react-router": "1.170.18",
"react": "19.2.8",
"react-dom": "19.2.8"
},
"devDependencies": {
+ "@stylexjs/unplugin": "0.19.0",
"@tailwindcss/vite": "4.3.3",
"@testing-library/dom": "10.4.1",
"@testing-library/react": "16.3.2",
diff --git a/web/src/ui/styles.test.tsx b/web/src/ui/styles.test.tsx
new file mode 100644
index 0000000..0136a15
--- /dev/null
+++ b/web/src/ui/styles.test.tsx
@@ -0,0 +1,28 @@
+import { render, screen } from "@testing-library/react";
+import * as stylex from "@stylexjs/stylex";
+import { styles } from "./styles";
+
+/**
+ * The StyleX compile-time transform must run in the vitest pipeline, not just
+ * in `vite build`. `runtimeInjection` is off, so an untransformed
+ * `stylex.props` call yields no class name at all — this probe fails loudly if
+ * the plugin ever drops out of the test config (milestone 23, ruling 1).
+ */
+describe("the StyleX build integration", () => {
+ it("compiles stylex.props into a class name", () => {
+ render(
+ ,
+ );
+ const probe = screen.getByRole("button", { name: "probe" });
+ expect(probe.className).not.toBe("");
+ });
+
+ it("keeps composed styles distinct from a single style", () => {
+ const one = stylex.props(styles.button).className;
+ const two = stylex.props(styles.button, styles.focusRing).className;
+ expect(one).toBeTruthy();
+ expect(two).not.toBe(one);
+ });
+});
diff --git a/web/src/ui/styles.ts b/web/src/ui/styles.ts
new file mode 100644
index 0000000..a0e3d05
--- /dev/null
+++ b/web/src/ui/styles.ts
@@ -0,0 +1,190 @@
+/**
+ * The shared style vocabulary (milestone 23, ruling 3; replaces the Tailwind
+ * class constants of milestone 18, ruling 10).
+ *
+ * Every interactive element must carry `focusRing` or `insetFocusRing`; that
+ * is the milestone-9 accessibility floor. Compose at the call site with
+ * `stylex.props(styles.button, extra)` instead of re-spelling a variant.
+ */
+
+import * as stylex from "@stylexjs/stylex";
+import { colors } from "./tokens.stylex";
+
+const FOCUS = ":focus-visible";
+const DISABLED = ":disabled";
+
+export const styles = stylex.create({
+ /** The ring drawn outside the element. */
+ focusRing: {
+ outlineWidth: { default: null, [FOCUS]: 2 },
+ outlineStyle: { default: null, [FOCUS]: "solid" },
+ outlineColor: { default: null, [FOCUS]: colors.focus },
+ outlineOffset: { default: null, [FOCUS]: 2 },
+ },
+ /** The ring drawn inside the element, for controls flush against a panel edge. */
+ insetFocusRing: {
+ outlineWidth: { default: null, [FOCUS]: 2 },
+ outlineStyle: { default: null, [FOCUS]: "solid" },
+ outlineColor: { default: null, [FOCUS]: colors.focus },
+ outlineOffset: { default: null, [FOCUS]: -2 },
+ },
+
+ input: {
+ marginTop: "0.25rem",
+ width: "100%",
+ borderRadius: "0.25rem",
+ borderWidth: 1,
+ borderStyle: "solid",
+ borderColor: colors.borderStrong,
+ backgroundColor: colors.surfaceRaised,
+ color: colors.text,
+ paddingInline: "0.75rem",
+ paddingBlock: "0.5rem",
+ },
+ smallInput: {
+ borderRadius: "0.25rem",
+ borderWidth: 1,
+ borderStyle: "solid",
+ borderColor: colors.borderStrong,
+ backgroundColor: colors.surfaceRaised,
+ color: colors.text,
+ paddingInline: "0.5rem",
+ paddingBlock: "0.375rem",
+ fontSize: "0.875rem",
+ lineHeight: "1.25rem",
+ },
+
+ button: {
+ borderRadius: "0.25rem",
+ borderWidth: 1,
+ borderStyle: "solid",
+ borderColor: colors.borderStrong,
+ paddingInline: "0.75rem",
+ paddingBlock: "0.375rem",
+ fontSize: "0.875rem",
+ lineHeight: "1.25rem",
+ },
+ smallButton: {
+ borderRadius: "0.25rem",
+ borderWidth: 1,
+ borderStyle: "solid",
+ borderColor: colors.borderStrong,
+ paddingInline: "0.5rem",
+ paddingBlock: "0.25rem",
+ fontSize: "0.875rem",
+ lineHeight: "1.25rem",
+ },
+ largeButton: {
+ borderRadius: "0.25rem",
+ borderWidth: 1,
+ borderStyle: "solid",
+ borderColor: colors.borderStrong,
+ paddingInline: "0.75rem",
+ paddingBlock: "0.5rem",
+ fontWeight: 500,
+ },
+
+ primaryButton: {
+ borderRadius: "0.25rem",
+ borderStyle: "none",
+ backgroundColor: colors.primary,
+ color: colors.primaryText,
+ paddingInline: "0.75rem",
+ paddingBlock: "0.375rem",
+ fontSize: "0.875rem",
+ lineHeight: "1.25rem",
+ fontWeight: 500,
+ opacity: { default: 1, [DISABLED]: 0.5 },
+ },
+ largePrimaryButton: {
+ borderRadius: "0.25rem",
+ borderStyle: "none",
+ backgroundColor: colors.primary,
+ color: colors.primaryText,
+ paddingInline: "0.75rem",
+ paddingBlock: "0.5rem",
+ fontWeight: 500,
+ opacity: { default: 1, [DISABLED]: 0.5 },
+ },
+
+ rowButton: {
+ borderRadius: "0.25rem",
+ borderStyle: "none",
+ backgroundColor: "transparent",
+ paddingInline: "0.5rem",
+ paddingBlock: "0.25rem",
+ fontSize: "0.875rem",
+ lineHeight: "1.25rem",
+ color: colors.primaryOnSurface,
+ },
+ linkButton: {
+ borderStyle: "none",
+ backgroundColor: "transparent",
+ padding: 0,
+ fontSize: "0.875rem",
+ lineHeight: "1.25rem",
+ fontWeight: 500,
+ color: colors.primaryOnSurface,
+ },
+ dangerLinkButton: {
+ borderStyle: "none",
+ backgroundColor: "transparent",
+ padding: 0,
+ fontSize: "0.875rem",
+ lineHeight: "1.25rem",
+ fontWeight: 500,
+ color: colors.danger,
+ opacity: { default: 1, [DISABLED]: 0.5 },
+ },
+ retryButton: {
+ marginTop: "0.75rem",
+ borderRadius: "0.25rem",
+ borderWidth: 1,
+ borderStyle: "solid",
+ borderColor: colors.dangerBorder,
+ backgroundColor: "transparent",
+ paddingInline: "0.75rem",
+ paddingBlock: "0.375rem",
+ fontSize: "0.875rem",
+ lineHeight: "1.25rem",
+ fontWeight: 500,
+ color: colors.dangerText,
+ },
+
+ th: {
+ borderBottomWidth: 1,
+ borderBottomStyle: "solid",
+ borderBottomColor: colors.borderStrong,
+ paddingInline: "0.75rem",
+ paddingBlock: "0.5rem",
+ textAlign: "left",
+ fontWeight: 500,
+ },
+ td: {
+ borderBottomWidth: 1,
+ borderBottomStyle: "solid",
+ borderBottomColor: colors.border,
+ paddingInline: "0.75rem",
+ paddingBlock: "0.5rem",
+ },
+ tableWrap: {
+ marginTop: "1rem",
+ overflowX: "auto",
+ },
+ /**
+ * `space-y-3` became a flex column with a gap: same rendered rhythm, and it
+ * does not depend on sibling margin collapsing.
+ */
+ formCard: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "0.75rem",
+ marginTop: "1rem",
+ maxWidth: "32rem",
+ borderRadius: "0.25rem",
+ borderWidth: 1,
+ borderStyle: "solid",
+ borderColor: colors.border,
+ padding: "1rem",
+ },
+});
diff --git a/web/src/ui/tokens.stylex.ts b/web/src/ui/tokens.stylex.ts
new file mode 100644
index 0000000..0a38859
--- /dev/null
+++ b/web/src/ui/tokens.stylex.ts
@@ -0,0 +1,40 @@
+/**
+ * The design tokens (milestone 23, ruling 2).
+ *
+ * Values are the Tailwind 4 zinc/blue/red ramps the app rendered before the
+ * StyleX conversion, carried over verbatim so the palette does not shift.
+ * Each token holds its dark value under `prefers-color-scheme: dark` — the
+ * same media strategy Tailwind 4 defaulted to. There is no theme toggle and
+ * no `data-theme` attribute; the scheme follows the operating system.
+ *
+ * Name tokens by role, never by shade: a call site asks for `border`, not for
+ * zinc-200.
+ */
+
+import * as stylex from "@stylexjs/stylex";
+
+const DARK = "@media (prefers-color-scheme: dark)";
+
+export const colors = stylex.defineVars({
+ /** The page ground. */
+ surface: { default: "oklch(98.5% 0 none)", [DARK]: "oklch(14.1% 0.005 285.823)" },
+ /** Cards, dialogs and inputs sitting on top of the ground. */
+ surfaceRaised: { default: "#fff", [DARK]: "oklch(21% 0.006 285.885)" },
+ /** Hairlines between rows and around cards. */
+ border: { default: "oklch(92% 0.004 286.32)", [DARK]: "oklch(27.4% 0.006 286.033)" },
+ /** Control outlines, which need more contrast than a row divider. */
+ borderStrong: { default: "oklch(87.1% 0.006 286.286)", [DARK]: "oklch(37% 0.013 285.805)" },
+ text: { default: "oklch(21% 0.006 285.885)", [DARK]: "oklch(96.7% 0.001 286.375)" },
+ textMuted: { default: "oklch(55.2% 0.016 285.938)", [DARK]: "oklch(70.5% 0.015 286.067)" },
+ /** Primary actions. Identical in both schemes, as before the conversion. */
+ primary: { default: "oklch(54.6% 0.245 262.881)", [DARK]: "oklch(54.6% 0.245 262.881)" },
+ primaryText: { default: "#fff", [DARK]: "#fff" },
+ /** Primary as foreground: lightened in dark so it clears the ground. */
+ primaryOnSurface: { default: "oklch(54.6% 0.245 262.881)", [DARK]: "oklch(70.7% 0.165 254.624)" },
+ danger: { default: "oklch(57.7% 0.245 27.325)", [DARK]: "oklch(70.4% 0.191 22.216)" },
+ dangerSurface: { default: "oklch(97.1% 0.013 17.38)", [DARK]: "oklch(25.8% 0.092 26.042)" },
+ dangerBorder: { default: "oklch(80.8% 0.114 19.571)", [DARK]: "oklch(44.4% 0.177 26.899)" },
+ dangerText: { default: "oklch(44.4% 0.177 26.899)", [DARK]: "oklch(88.5% 0.062 18.334)" },
+ /** The focus ring colour. The ring itself is a floor, not a variant. */
+ focus: { default: "oklch(54.6% 0.245 262.881)", [DARK]: "oklch(54.6% 0.245 262.881)" },
+});
diff --git a/web/vite.config.ts b/web/vite.config.ts
index 017e5f5..e02a502 100644
--- a/web/vite.config.ts
+++ b/web/vite.config.ts
@@ -1,11 +1,16 @@
///
import { fileURLToPath } from "node:url";
+import stylex from "@stylexjs/unplugin";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
-export default defineConfig({
- plugins: [react(), tailwindcss()],
+export default defineConfig(({ mode }) => ({
+ plugins: [
+ stylex.vite({ useCSSLayers: true, dev: mode === "development", runtimeInjection: false }),
+ react(),
+ tailwindcss(),
+ ],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
@@ -21,4 +26,4 @@ export default defineConfig({
environment: "jsdom",
globals: true,
},
-});
+}));