diff --git a/.changepacks/changepack_log_J7kZ5wcihsOAeZHW9L3kE.json b/.changepacks/changepack_log_J7kZ5wcihsOAeZHW9L3kE.json
new file mode 100644
index 00000000..f8526cbc
--- /dev/null
+++ b/.changepacks/changepack_log_J7kZ5wcihsOAeZHW9L3kE.json
@@ -0,0 +1 @@
+{"changes":{"bindings/devup-ui-wasm/package.json":"Patch"},"note":"Support variable","date":"2026-04-01T07:11:17.490320600Z"}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 04f7bf01..ebc6e373 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,3 +20,4 @@ storybook-static
.sisyphus
test-results
playwright-report
+.omc
diff --git a/AGENTS.md b/AGENTS.md
index fa4d498e..e116b645 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -36,7 +36,7 @@ devup-ui/
| Add CSS property | `libs/css/src/constant.rs` | Property mappings |
| Add pseudo selector | `packages/react/src/types/props/selector/` | TypeScript types |
| Modify extraction | `libs/extractor/src/lib.rs` | Core logic + tests |
-| Theme system | `libs/sheet/src/theme.rs` | Color/typography |
+| Theme system | `libs/sheet/src/theme.rs` | Color/typography/length/shadow |
| Plugin behavior | `packages/*-plugin/src/plugin.ts` | All follow same pattern |
| Component API | `packages/react/src/components/` | Box, Flex, Text... |
| WASM exports | `bindings/devup-ui-wasm/src/lib.rs` | JS-exposed functions |
@@ -49,7 +49,7 @@ devup-ui/
|--------|------|-------|------|
| extractor | `lib.rs` | 9,094 | Main extraction + tests |
| sheet | `lib.rs` | 1,821 | CSS output generation |
-| theme | `theme.rs` | 1,526 | Color/typography system |
+| theme | `theme.rs` | 1,526 | Color/typography/length/shadow system |
| css_utils | `css_utils.rs` | 1,239 | Template literal parsing |
| visit | `visit.rs` | 669 | AST visitor pattern |
@@ -105,11 +105,19 @@ All React components throw `Error('Cannot run on the runtime')` - they're compil
{
"theme": {
"colors": { "default": {...}, "dark": {...} },
- "typography": { "heading": {...} }
+ "typography": { "heading": {...} },
+ "length": { "default": { "containerX": ["1px", null, "2px"] } },
+ "shadow": { "default": { "card": ["0 1px 2px #0003", null, null, "0 4px 8px #0003"] } }
}
}
```
+### Length & Shadow Tokens
+- Defined responsively like typography (arrays with `null` for skipped breakpoints)
+- Used with `$` prefix: ``, ``
+- `"$token"` and `{"$token"}` both expand to multiple breakpoint classes
+- `{["$token"]}` inside a responsive array stays single class (array defines breakpoints)
+
### Plugin Pattern
All plugins wrap bundler config:
```ts
diff --git a/Cargo.lock b/Cargo.lock
index 3a17808e..c8b9991e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -53,6 +53,12 @@ version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
[[package]]
name = "arrayvec"
version = "0.7.6"
@@ -109,11 +115,11 @@ checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "block-buffer"
-version = "0.10.4"
+version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be"
dependencies = [
- "generic-array",
+ "hybrid-array",
]
[[package]]
@@ -300,9 +306,9 @@ dependencies = [
[[package]]
name = "cc"
-version = "1.2.56"
+version = "1.2.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
+checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
dependencies = [
"find-msvc-tools",
"shlex",
@@ -391,14 +397,13 @@ dependencies = [
[[package]]
name = "console"
-version = "0.15.11"
+version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
+checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87"
dependencies = [
"encode_unicode",
"libc",
- "once_cell",
- "windows-sys 0.59.0",
+ "windows-sys",
]
[[package]]
@@ -411,6 +416,12 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "const-oid"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
+
[[package]]
name = "cow-utils"
version = "0.1.3"
@@ -419,9 +430,9 @@ checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79"
[[package]]
name = "cpufeatures"
-version = "0.2.17"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
@@ -503,12 +514,11 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-common"
-version = "0.1.7"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710"
dependencies = [
- "generic-array",
- "typenum",
+ "hybrid-array",
]
[[package]]
@@ -555,7 +565,7 @@ dependencies = [
"console_error_panic_hook",
"css",
"extractor",
- "getrandom",
+ "getrandom 0.3.4",
"insta",
"js-sys",
"rstest",
@@ -570,11 +580,12 @@ dependencies = [
[[package]]
name = "digest"
-version = "0.10.7"
+version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c"
dependencies = [
"block-buffer",
+ "const-oid",
"crypto-common",
]
@@ -672,7 +683,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
- "windows-sys 0.61.2",
+ "windows-sys",
]
[[package]]
@@ -746,6 +757,12 @@ dependencies = [
"rustc_version 0.2.3",
]
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
[[package]]
name = "foldhash"
version = "0.2.0"
@@ -847,27 +864,30 @@ dependencies = [
]
[[package]]
-name = "generic-array"
-version = "0.14.7"
+name = "getrandom"
+version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
- "typenum",
- "version_check",
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+ "wasm-bindgen",
]
[[package]]
name = "getrandom"
-version = "0.3.4"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [
"cfg-if",
- "js-sys",
"libc",
- "r-efi",
+ "r-efi 6.0.0",
"wasip2",
- "wasm-bindgen",
+ "wasip3",
]
[[package]]
@@ -893,6 +913,15 @@ version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash 0.1.5",
+]
+
[[package]]
name = "hashbrown"
version = "0.16.1"
@@ -901,7 +930,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"allocator-api2",
"equivalent",
- "foldhash",
+ "foldhash 0.2.0",
]
[[package]]
@@ -910,6 +939,15 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+[[package]]
+name = "hybrid-array"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214"
+dependencies = [
+ "typenum",
+]
+
[[package]]
name = "icu_collections"
version = "2.0.0"
@@ -999,6 +1037,12 @@ dependencies = [
"zerovec",
]
+[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
[[package]]
name = "indexmap"
version = "2.13.0"
@@ -1007,13 +1051,15 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown 0.16.1",
+ "serde",
+ "serde_core",
]
[[package]]
name = "insta"
-version = "1.46.3"
+version = "1.47.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e82db8c87c7f1ccecb34ce0c24399b8a73081427f3c7c50a5d597925356115e4"
+checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e"
dependencies = [
"console",
"once_cell",
@@ -1056,10 +1102,12 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "js-sys"
-version = "0.3.91"
+version = "0.3.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
+checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
dependencies = [
+ "cfg-if",
+ "futures-util",
"once_cell",
"wasm-bindgen",
]
@@ -1070,6 +1118,12 @@ version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3c2a6c0b4b5637c41719973ef40c6a1cf564f9db6958350de6193fbee9c23f5"
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
[[package]]
name = "libc"
version = "0.2.183"
@@ -1084,9 +1138,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "linux-raw-sys"
-version = "0.11.0"
+version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "litemap"
@@ -1156,7 +1210,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
- "windows-sys 0.61.2",
+ "windows-sys",
]
[[package]]
@@ -1291,9 +1345,9 @@ dependencies = [
[[package]]
name = "oxc_allocator"
-version = "0.122.0"
+version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff805b88789451a080b3c4d49fa0ebcd02dc6c0e370ed7a37ef954fbaf79915f"
+checksum = "5e6fc6ce99f6a28fd477c6df500bbc9bf1c39db166952e15bea218459cc0db0c"
dependencies = [
"allocator-api2",
"hashbrown 0.16.1",
@@ -1303,9 +1357,9 @@ dependencies = [
[[package]]
name = "oxc_ast"
-version = "0.122.0"
+version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "addc03b644cd9f26996bb32883f5cf4f4e46a51d20f5fbdbf675c14b29d38e95"
+checksum = "49fa0813bf9fcff5a4e48fc186ee15a0d276b30b0b575389a34a530864567819"
dependencies = [
"bitflags",
"oxc_allocator",
@@ -1320,9 +1374,9 @@ dependencies = [
[[package]]
name = "oxc_ast_macros"
-version = "0.122.0"
+version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5950f9746248c26af04811e6db0523d354080637995be1dcc1c6bd3fca893bb2"
+checksum = "3a2b2a2e09ff0dd4790a5ceb4a93349e0ea769d4d98d778946de48decb763b18"
dependencies = [
"phf",
"proc-macro2",
@@ -1332,9 +1386,9 @@ dependencies = [
[[package]]
name = "oxc_ast_visit"
-version = "0.122.0"
+version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31da485219d7ca6810872ce84fbcc7d11d8492145012603ead79beaf1476dc92"
+checksum = "ef6d2304cb25dbbd028440591bf289ef16e3df98517930e79dcc304be64b3045"
dependencies = [
"oxc_allocator",
"oxc_ast",
@@ -1344,9 +1398,9 @@ dependencies = [
[[package]]
name = "oxc_codegen"
-version = "0.122.0"
+version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e8af47790edfd7cc2d35ff47b70a1746c73388cc498c7f470a9cdc35f89375c"
+checksum = "ce92b24319ee9fbfa14a5cc488a5ba91bb04bac070c4bad0ba18c772060d19c0"
dependencies = [
"bitflags",
"cow-utils",
@@ -1365,9 +1419,9 @@ dependencies = [
[[package]]
name = "oxc_compat"
-version = "0.122.0"
+version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3103453f49b58f20dfb5d0d7be109c44975b436ad056fdb046db03e971ee9f64"
+checksum = "bd5a3de8c67c960a20bc0177d54498d1a96275c38eb78a0975b4ffdc5a1fb13a"
dependencies = [
"cow-utils",
"oxc-browserslist",
@@ -1378,18 +1432,18 @@ dependencies = [
[[package]]
name = "oxc_data_structures"
-version = "0.122.0"
+version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "623bffc9732a0d39f248a2e7655d6d1704201790e5a8777aa188a678f1746fe8"
+checksum = "c8e8f59bed9522098da177d894dc8635fb3eae218ff97d9c695900cb11fd10a2"
dependencies = [
"ropey",
]
[[package]]
name = "oxc_diagnostics"
-version = "0.122.0"
+version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c612203fb402e998169c3e152a9fc8e736faafea0f13287c92144d4b8bc7b55"
+checksum = "e0476859d4319f2b063f7c4a3120ee5b7e3e48032865ca501f8545ff44badcff"
dependencies = [
"cow-utils",
"oxc-miette",
@@ -1398,9 +1452,9 @@ dependencies = [
[[package]]
name = "oxc_ecmascript"
-version = "0.122.0"
+version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04c62e45b93f4257f5ca6d00f441e669ad52d98d36332394abe9f5527cf461d6"
+checksum = "1bcf46e5b1a6f8ea3797e887a9db4c79ed15894ca8685eb628da462d4c4e913f"
dependencies = [
"cow-utils",
"num-bigint",
@@ -1414,9 +1468,9 @@ dependencies = [
[[package]]
name = "oxc_estree"
-version = "0.122.0"
+version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8794e3fbcd834e8ae4246dbd3121f9ee82c6ae60bc92615a276d42b6b62a2341"
+checksum = "2251e6b61eab7b96f0e9d140b68b0f0d8a851c7d260725433e18b1babdcb9430"
[[package]]
name = "oxc_index"
@@ -1430,9 +1484,9 @@ dependencies = [
[[package]]
name = "oxc_parser"
-version = "0.122.0"
+version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "041125897019b72d23e6549d95985fe379354cf004e69cb811803109375fa91b"
+checksum = "439d2580047b77faf6e60d358b48e5292e0e026b9cfc158d46ddd0175244bb26"
dependencies = [
"bitflags",
"cow-utils",
@@ -1453,9 +1507,9 @@ dependencies = [
[[package]]
name = "oxc_regular_expression"
-version = "0.122.0"
+version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "405e9515c3ae4c7227b3596219ec256dd883cb403db3a0d1c10146f82a894c93"
+checksum = "0fb5669d3298a92d440afec516943745794cb4cf977911728cd73e3438db87b9"
dependencies = [
"bitflags",
"oxc_allocator",
@@ -1469,9 +1523,9 @@ dependencies = [
[[package]]
name = "oxc_semantic"
-version = "0.122.0"
+version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebb0597a0132e69aaecb010753b7450ffaf46cf45a389a7babe0e5e5825a911c"
+checksum = "487e9ef54375b23b159eef73746a02b505c3ae70b9c302610680d3c68a3bb62c"
dependencies = [
"itertools 0.14.0",
"memchr",
@@ -1502,9 +1556,9 @@ dependencies = [
[[package]]
name = "oxc_span"
-version = "0.122.0"
+version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "894327633e5dcaef8baf34815d68100297f9776e20371502458ea3c42b8a710b"
+checksum = "b1d452f6a664627bdd0f1f1586f9258f81cd7edc5c83e9ef50019f701ef1722d"
dependencies = [
"compact_str",
"oxc-miette",
@@ -1516,9 +1570,9 @@ dependencies = [
[[package]]
name = "oxc_str"
-version = "0.122.0"
+version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50e0b900b4f66db7d5b46a454532464861f675d03e16994040484d2c04151490"
+checksum = "5c7a27c4371f69387f3d6f8fa56f70e4c6fa6aedc399285de6ec02bb9fd148d7"
dependencies = [
"compact_str",
"hashbrown 0.16.1",
@@ -1528,9 +1582,9 @@ dependencies = [
[[package]]
name = "oxc_syntax"
-version = "0.122.0"
+version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a5edd0173b4667e5a1775b5d37e06a78c796fab18ee095739186831f2c54400"
+checksum = "0d60d91023aafc256ab99c3dbf6181473e495695029c0152d2093e87df18ffe2"
dependencies = [
"bitflags",
"cow-utils",
@@ -1547,9 +1601,9 @@ dependencies = [
[[package]]
name = "oxc_transformer"
-version = "0.122.0"
+version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a216c0a1291fcb42f6be51ce32d928921cf2a6e232e43e6339c8e48d0e4048f"
+checksum = "226d77c70778860c4b4888e4aa52f1b4799bfc67093aa5070f367848ff8df09b"
dependencies = [
"base64",
"compact_str",
@@ -1576,9 +1630,9 @@ dependencies = [
[[package]]
name = "oxc_traverse"
-version = "0.122.0"
+version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e1d4f7d8539ccc032bf20a837b075a301a7846c6ded266a7a1889f0cfcae038"
+checksum = "c31aba1910999e2f9a1cc9c47a490caaed828bb119351abe20a2a7851d554963"
dependencies = [
"itoa",
"oxc_allocator",
@@ -1783,6 +1837,16 @@ dependencies = [
"zerocopy",
]
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
[[package]]
name = "proc-macro-crate"
version = "3.4.0"
@@ -1816,6 +1880,12 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
[[package]]
name = "rand"
version = "0.9.2"
@@ -1842,7 +1912,7 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
- "getrandom",
+ "getrandom 0.3.4",
]
[[package]]
@@ -1990,15 +2060,15 @@ dependencies = [
[[package]]
name = "rustix"
-version = "1.1.3"
+version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
- "windows-sys 0.61.2",
+ "windows-sys",
]
[[package]]
@@ -2164,9 +2234,9 @@ dependencies = [
[[package]]
name = "sha1"
-version = "0.10.6"
+version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
dependencies = [
"cfg-if",
"cpufeatures",
@@ -2186,6 +2256,7 @@ dependencies = [
"rustc-hash",
"serde",
"serde_json",
+ "serial_test",
]
[[package]]
@@ -2311,15 +2382,15 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tempfile"
-version = "3.24.0"
+version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
- "getrandom",
+ "getrandom 0.4.2",
"once_cell",
"rustix",
- "windows-sys 0.61.2",
+ "windows-sys",
]
[[package]]
@@ -2481,16 +2552,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
-name = "utf16_iter"
-version = "1.0.5"
+name = "unicode-xid"
+version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
-name = "version_check"
-version = "0.9.5"
+name = "utf16_iter"
+version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
[[package]]
name = "vsimd"
@@ -2517,11 +2588,20 @@ dependencies = [
"wit-bindgen",
]
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen",
+]
+
[[package]]
name = "wasm-bindgen"
-version = "0.2.114"
+version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
+checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0"
dependencies = [
"cfg-if",
"once_cell",
@@ -2532,23 +2612,19 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.64"
+version = "0.4.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8"
+checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e"
dependencies = [
- "cfg-if",
- "futures-util",
"js-sys",
- "once_cell",
"wasm-bindgen",
- "web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.114"
+version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
+checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -2556,9 +2632,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.114"
+version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
+checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2"
dependencies = [
"bumpalo",
"proc-macro2",
@@ -2569,18 +2645,18 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.114"
+version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
+checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-bindgen-test"
-version = "0.3.64"
+version = "0.3.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6311c867385cc7d5602463b31825d454d0837a3aba7cdb5e56d5201792a3f7fe"
+checksum = "941c102b3f0c15b6d72a53205e09e6646aafcf2991e18412cc331dbac1806bc0"
dependencies = [
"async-trait",
"cast",
@@ -2600,9 +2676,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-test-macro"
-version = "0.3.64"
+version = "0.3.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67008cdde4769831958536b0f11b3bdd0380bde882be17fff9c2f34bb4549abd"
+checksum = "a26bd6570f39bb1440fd8f01b63461faaf2a3f6078a508e4e54efa99363108d2"
dependencies = [
"proc-macro2",
"quote",
@@ -2611,15 +2687,49 @@ dependencies = [
[[package]]
name = "wasm-bindgen-test-shared"
-version = "0.2.114"
+version = "0.2.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c29582b14d5bf030b02fa232b9b57faf2afc322d2c61964dd80bad02bf76207"
+
+[[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cfe29135b180b72b04c74aa97b2b4a2ef275161eff9a6c7955ea9eaedc7e1d4e"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver 1.0.27",
+]
[[package]]
name = "web-sys"
-version = "0.3.91"
+version = "0.3.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9"
+checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -2647,7 +2757,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
- "windows-sys 0.61.2",
+ "windows-sys",
]
[[package]]
@@ -2662,15 +2772,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
-[[package]]
-name = "windows-sys"
-version = "0.59.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
-dependencies = [
- "windows-targets",
-]
-
[[package]]
name = "windows-sys"
version = "0.61.2"
@@ -2681,83 +2782,101 @@ dependencies = [
]
[[package]]
-name = "windows-targets"
-version = "0.52.6"
+name = "winnow"
+version = "0.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_gnullvm",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
+ "memchr",
]
[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
-
-[[package]]
-name = "windows_i686_gnu"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
-
-[[package]]
-name = "windows_i686_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.52.6"
+name = "wit-bindgen"
+version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
[[package]]
-name = "windows_x86_64_gnu"
-version = "0.52.6"
+name = "wit-bindgen-core"
+version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser",
+]
[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.52.6"
+name = "wit-bindgen-rust"
+version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "prettyplease",
+ "syn",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
[[package]]
-name = "windows_x86_64_msvc"
-version = "0.52.6"
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
[[package]]
-name = "winnow"
-version = "0.7.14"
+name = "wit-component"
+version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
- "memchr",
+ "anyhow",
+ "bitflags",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
]
[[package]]
-name = "wit-bindgen"
-version = "0.51.0"
+name = "wit-parser"
+version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver 1.0.27",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
[[package]]
name = "write16"
diff --git a/SKILL.md b/SKILL.md
index f1036d81..5219f836 100644
--- a/SKILL.md
+++ b/SKILL.md
@@ -6,12 +6,13 @@ description: |
TRIGGER WHEN:
- Writing/modifying Devup UI components (Box, Flex, Grid, Text, Button, etc.)
- Using styling APIs: css(), globalCss(), keyframes()
- - Configuring devup.json theme (colors, typography, extends)
+ - Configuring devup.json theme (colors, typography, length, shadow, extends)
- Setting up build plugins (Vite, Next.js, Webpack, Rsbuild, Bun)
- Debugging "Cannot run on the runtime" errors
- Working with responsive arrays, pseudo-selectors (_hover, _dark, etc.)
- Using polymorphic `as` prop or `selectors` prop
- Working with @devup-ui/components (Button, Input, Select, Toggle, etc.)
+ - Using responsive length tokens ($containerX, $gutter) or shadow tokens ($card, $sm)
---
# Devup UI
@@ -338,6 +339,18 @@ const spin = keyframes({ from: { transform: "rotate(0)" }, to: { transform: "rot
null,
{ "fontSize": "16px", "lineHeight": 1.6 }
]
+ },
+ "length": {
+ "default": {
+ "containerX": ["16px", null, "32px"],
+ "gutter": ["8px", null, "16px"]
+ }
+ },
+ "shadow": {
+ "default": {
+ "card": ["0 1px 2px #0003", null, null, "0 4px 8px #0003"],
+ "sm": "0 1px 2px rgba(0,0,0,0.05)"
+ }
}
}
}
@@ -345,8 +358,23 @@ const spin = keyframes({ from: { transform: "rotate(0)" }, to: { transform: "rot
- **Colors**: Use with `$` prefix in JSX props: ``
- **Typography**: Use with `$` prefix: ``
+- **Length**: Responsive length tokens: ``, ``
+- **Shadow**: Responsive shadow tokens: ``
- **extends**: Inherit from base config files (deep merge, last wins)
-- **Responsive typography**: Use arrays with `null` for unchanged breakpoints
+- **Responsive typography/length/shadow**: Use arrays with `null` for unchanged breakpoints
+
+### Length & Shadow Token Behavior
+
+Length and shadow tokens support responsive arrays like typography. The key distinction is how `$token` behaves depending on syntax:
+
+| Syntax | Behavior | Classes |
+|--------|----------|---------|
+| `px="$containerX"` | Expands to all defined breakpoints | Multiple |
+| `px={"$containerX"}` | Expands to all defined breakpoints | Multiple |
+| `px={["$containerX"]}` | Single value at index 0 only | 1 |
+| `px={["8px", null, "$containerX"]}` | `8px` at index 0, token at index 2 | 2 |
+
+Both `"$token"` and `{"$token"}` expand the responsive token. Only `{["$token"]}` inside a responsive array keeps it as a single class — because the array itself defines the breakpoint levels.
Theme types are auto-generated via module augmentation of `DevupTheme` and `DevupThemeTypography`.
@@ -415,20 +443,22 @@ DevupUI({
})
```
-## $color Token Scope
+## $token Scope
-`$color` tokens only work in **JSX props**. Use `var(--color)` in external objects.
+`$token` values (colors, length, shadow) only work in **JSX props**. Use `var(--token)` in external objects.
```tsx
-// CORRECT - $color in JSX prop
+// CORRECT - $token in JSX prop
+
+
-// WRONG - $color in external object (won't be transformed)
+// WRONG - $token in external object (won't be transformed)
const colors = { active: '$primary' }
// broken!
-// CORRECT - var(--color) in external object
+// CORRECT - var(--token) in external object
const colors = { active: 'var(--primary)' }
```
diff --git a/apps/landing/src/app/(detail)/docs/LeftMenu.tsx b/apps/landing/src/app/(detail)/docs/LeftMenu.tsx
index 63788c78..d365c9bf 100644
--- a/apps/landing/src/app/(detail)/docs/LeftMenu.tsx
+++ b/apps/landing/src/app/(detail)/docs/LeftMenu.tsx
@@ -122,6 +122,14 @@ export function LeftMenu() {
to: '/docs/devup/typography',
children: 'Typography',
},
+ {
+ to: '/docs/devup/length',
+ children: 'Length',
+ },
+ {
+ to: '/docs/devup/shadow',
+ children: 'Shadow',
+ },
{
to: '/docs/devup/breakpoints',
children: 'Breakpoints',
diff --git a/apps/landing/src/app/(detail)/docs/devup/devup-json/page.mdx b/apps/landing/src/app/(detail)/docs/devup/devup-json/page.mdx
index 584de35e..a739be8f 100644
--- a/apps/landing/src/app/(detail)/docs/devup/devup-json/page.mdx
+++ b/apps/landing/src/app/(detail)/docs/devup/devup-json/page.mdx
@@ -7,7 +7,7 @@ export const metadata = {
# devup.json
-The `devup.json` file is the configuration file for your Devup UI theme. Create it at the root of your project to define colors, typography, and other design tokens.
+The `devup.json` file is the configuration file for your Devup UI theme. Create it at the root of your project to define colors, typography, length, shadow, and other design tokens.
## Basic Structure
@@ -18,7 +18,13 @@ The `devup.json` file is the configuration file for your Devup UI theme. Create
"default": {},
"dark": {}
},
- "typography": {}
+ "typography": {},
+ "length": {
+ "default": {}
+ },
+ "shadow": {
+ "default": {}
+ }
}
}
```
@@ -155,6 +161,53 @@ Typography properties:
See [Typography](/docs/devup/typography) for more details.
+## Length Configuration
+
+Define responsive length tokens for spacing, sizing, and other CSS length properties:
+
+```json
+{
+ "theme": {
+ "length": {
+ "default": {
+ "containerX": ["16px", null, "32px", null, "64px"],
+ "gutter": ["8px", null, "16px"]
+ }
+ }
+ }
+}
+```
+
+Length tokens use responsive arrays just like typography. Use `null` to skip breakpoints and inherit from the previous value.
+
+See [Length](/docs/devup/length) for more details.
+
+## Shadow Configuration
+
+Define responsive shadow tokens for box-shadow values:
+
+```json
+{
+ "theme": {
+ "shadow": {
+ "default": {
+ "card": [
+ "0 1px 2px rgba(0,0,0,0.1)",
+ null,
+ null,
+ "0 4px 8px rgba(0,0,0,0.1)"
+ ],
+ "sm": "0 1px 2px rgba(0,0,0,0.05)"
+ }
+ }
+ }
+}
+```
+
+Shadow tokens also support responsive arrays. Non-array values define a single shadow for all breakpoints.
+
+See [Shadow](/docs/devup/shadow) for more details.
+
## Usage in Components
### Colors
@@ -170,6 +223,18 @@ const headingExample = Heading
const bodyExample = Body text
```
+### Length
+
+```tsx
+
+```
+
+### Shadow
+
+```tsx
+
+```
+
## File Location
The `devup.json` file should be placed at the root of your project:
@@ -215,7 +280,7 @@ module.exports = withDevupUI({
When your project builds, Devup UI automatically generates TypeScript types from your `devup.json`. This enables:
-- Autocomplete for color and typography tokens
+- Autocomplete for color, typography, length, and shadow tokens
- Type errors for invalid tokens
- Consistent usage across your codebase
@@ -227,6 +292,6 @@ Changes to `devup.json` trigger hot reload during development. You can:
1. Add new color tokens
2. Modify existing values
-3. Add typography styles
+3. Add typography, length, or shadow styles
And see changes immediately without restarting the dev server.
diff --git a/apps/landing/src/app/(detail)/docs/devup/length/page.mdx b/apps/landing/src/app/(detail)/docs/devup/length/page.mdx
new file mode 100644
index 00000000..e74decd9
--- /dev/null
+++ b/apps/landing/src/app/(detail)/docs/devup/length/page.mdx
@@ -0,0 +1,111 @@
+export const metadata = {
+ title: 'Length',
+ alternates: {
+ canonical: '/docs/devup/length',
+ },
+}
+
+# Length
+
+Devup UI supports responsive length tokens for spacing, sizing, border-radius, and other CSS length properties. Like typography, length tokens can be defined as responsive arrays in `devup.json`.
+
+## Defining Length Tokens
+
+Define length tokens in your `devup.json`:
+
+```json
+{
+ "theme": {
+ "length": {
+ "default": {
+ "containerX": ["16px", null, "32px", null, "64px"],
+ "gutter": ["8px", null, "16px"],
+ "borderRadiusMd": ["8px", null, "16px"]
+ }
+ }
+ }
+}
+```
+
+Each array index corresponds to a breakpoint. Use `null` to skip a breakpoint and inherit from the previous value.
+
+## Usage
+
+Use length tokens with the `$` prefix on any length-accepting prop:
+
+```tsx
+<>
+
+
+
+
+>
+```
+
+Both `"$token"` and `{"$token"}` expand the responsive token into multiple breakpoint-level classes.
+
+## Responsive Array Behavior
+
+When a `$token` appears inside a responsive array, it does **not** expand — the array itself defines the breakpoints:
+
+```tsx
+{
+ /* "$containerX" stays as a single value at breakpoint index 2 */
+}
+;
+```
+
+This is the key distinction:
+
+| Syntax | Behavior | Classes |
+| ----------------------------------- | ---------------------------------- | -------- |
+| `px="$containerX"` | Expands to all defined breakpoints | Multiple |
+| `px={"$containerX"}` | Expands to all defined breakpoints | Multiple |
+| `px={["$containerX"]}` | Single value at index 0 | 1 |
+| `px={["8px", null, "$containerX"]}` | `8px` at index 0, token at index 2 | 2 |
+
+## Theme Variants
+
+Length tokens support theme variants, similar to colors:
+
+```json
+{
+ "theme": {
+ "length": {
+ "default": {
+ "containerX": ["16px", null, "32px"]
+ },
+ "compact": {
+ "containerX": ["8px", null, "16px"]
+ }
+ }
+ }
+}
+```
+
+## Non-Responsive Tokens
+
+For a single value across all breakpoints, use a simple string:
+
+```json
+{
+ "theme": {
+ "length": {
+ "default": {
+ "borderRadiusSm": "4px",
+ "borderRadiusMd": "8px",
+ "borderRadiusLg": "16px"
+ }
+ }
+ }
+}
+```
+
+## Type Safety
+
+Length tokens are fully type-safe. TypeScript will autocomplete available tokens when you type `$`:
+
+```tsx
+// Autocomplete shows: $containerX, $gutter, $borderRadiusMd, etc.
+
+```
diff --git a/apps/landing/src/app/(detail)/docs/devup/shadow/page.mdx b/apps/landing/src/app/(detail)/docs/devup/shadow/page.mdx
new file mode 100644
index 00000000..72b2f349
--- /dev/null
+++ b/apps/landing/src/app/(detail)/docs/devup/shadow/page.mdx
@@ -0,0 +1,102 @@
+export const metadata = {
+ title: 'Shadow',
+ alternates: {
+ canonical: '/docs/devup/shadow',
+ },
+}
+
+# Shadow
+
+Devup UI supports responsive shadow tokens for `box-shadow` values. Like typography and length, shadow tokens can be defined as responsive arrays in `devup.json`.
+
+## Defining Shadow Tokens
+
+Define shadow tokens in your `devup.json`:
+
+```json
+{
+ "theme": {
+ "shadow": {
+ "default": {
+ "sm": "0 1px 2px rgba(0,0,0,0.05)",
+ "card": [
+ "0 1px 2px rgba(0,0,0,0.1)",
+ null,
+ null,
+ "0 4px 8px rgba(0,0,0,0.1)"
+ ],
+ "modal": [
+ "0 4px 12px rgba(0,0,0,0.15)",
+ null,
+ null,
+ "0 8px 24px rgba(0,0,0,0.15)"
+ ]
+ }
+ }
+ }
+}
+```
+
+Each array index corresponds to a breakpoint. Use `null` to skip a breakpoint and inherit from the previous value.
+
+## Usage
+
+Use shadow tokens with the `$` prefix on `boxShadow`:
+
+```tsx
+<>
+
+
+
+>
+```
+
+Both `"$token"` and `{"$token"}` expand the responsive token into multiple breakpoint-level classes.
+
+## Responsive Array Behavior
+
+When a `$token` appears inside a responsive array, it does **not** expand — the array itself defines the breakpoints:
+
+```tsx
+{
+ /* "$card" stays as a single value at breakpoint index 3 */
+}
+;
+```
+
+This is the key distinction:
+
+| Syntax | Behavior | Classes |
+| ------------------------------------------- | ----------------------------------- | -------- |
+| `boxShadow="$card"` | Expands to all defined breakpoints | Multiple |
+| `boxShadow={"$card"}` | Expands to all defined breakpoints | Multiple |
+| `boxShadow={["$card"]}` | Single value at index 0 | 1 |
+| `boxShadow={["none", null, null, "$card"]}` | `none` at index 0, token at index 3 | 2 |
+
+## Theme Variants
+
+Shadow tokens support theme variants, similar to colors:
+
+```json
+{
+ "theme": {
+ "shadow": {
+ "default": {
+ "card": "0 2px 4px rgba(0,0,0,0.1)"
+ },
+ "dark": {
+ "card": "0 2px 4px rgba(0,0,0,0.4)"
+ }
+ }
+ }
+}
+```
+
+## Type Safety
+
+Shadow tokens are fully type-safe. TypeScript will autocomplete available tokens when you type `$`:
+
+```tsx
+// Autocomplete shows: $sm, $card, $modal, etc.
+
+```
diff --git a/bindings/devup-ui-wasm/Cargo.toml b/bindings/devup-ui-wasm/Cargo.toml
index e33d9a27..61f9bdc5 100644
--- a/bindings/devup-ui-wasm/Cargo.toml
+++ b/bindings/devup-ui-wasm/Cargo.toml
@@ -15,7 +15,7 @@ crate-type = ["cdylib"]
default = []
[dependencies]
-wasm-bindgen = "0.2.114"
+wasm-bindgen = "0.2.117"
extractor = { path = "../../libs/extractor" }
sheet = { path = "../../libs/sheet" }
css = { path = "../../libs/css" }
@@ -27,15 +27,15 @@ rustc-hash = "2"
# code size when deploying.
console_error_panic_hook = { version = "0.1.7", optional = true }
bimap = { version = "0.6.3", features = ["serde"] }
-js-sys = "0.3.91"
+js-sys = "0.3.94"
serde_json = "1.0.149"
serde-wasm-bindgen = "0.6.5"
getrandom = { version = "0.3", features = ["wasm_js"] }
[dev-dependencies]
-wasm-bindgen-test = "0.3.64"
+wasm-bindgen-test = "0.3.67"
serial_test = "3.4.0"
-insta = "1.46.3"
+insta = "1.47.2"
rstest = "0.26.1"
[lints.rust]
diff --git a/e2e/helpers.ts b/e2e/helpers.ts
index 59f2147c..5d409ccf 100644
--- a/e2e/helpers.ts
+++ b/e2e/helpers.ts
@@ -1,12 +1,5 @@
import type { Page } from '@playwright/test'
-const CI_SETTLE_DELAY_MS = 250
-const LOCAL_SETTLE_DELAY_MS = 100
-
-function getSettleDelayMs(): number {
- return process.env.CI ? CI_SETTLE_DELAY_MS : LOCAL_SETTLE_DELAY_MS
-}
-
/**
* Wait for all fonts to be loaded and a rendering frame to complete.
* Replaces waitForTimeout(1000) after page.goto()
@@ -15,43 +8,14 @@ function getSettleDelayMs(): number {
* (page.evaluate is not available in JS-disabled contexts).
*/
export async function waitForFontsReady(page: Page): Promise {
- await page.waitForLoadState('load')
-
try {
- await page.evaluate(async (settleDelayMs) => {
- const wait = (ms: number) =>
- new Promise((resolve) => window.setTimeout(resolve, ms))
- const nextFrame = () =>
- new Promise((resolve) => requestAnimationFrame(() => resolve()))
-
- if ('fonts' in document) {
- await document.fonts.ready
- }
-
- const pendingImages = Array.from(document.images).filter(
- (image) => !image.complete,
- )
-
- await Promise.all(
- pendingImages.map(
- (image) =>
- new Promise((resolve) => {
- image.addEventListener('load', () => resolve(), {
- once: true,
- })
- image.addEventListener('error', () => resolve(), {
- once: true,
- })
- }),
- ),
- )
-
- await nextFrame()
- await nextFrame()
- await wait(settleDelayMs)
- }, getSettleDelayMs())
+ await page.evaluate(async () => {
+ await document.fonts.ready
+ await page.waitForLoadState('load')
+ })
} catch {
- await page.waitForTimeout(getSettleDelayMs())
+ // JS disabled — fall back to load event (fires after fonts in CSS are loaded)
+ await page.waitForLoadState('load')
}
}
@@ -62,44 +26,5 @@ export async function waitForFontsReady(page: Page): Promise {
* Falls back to waitForLoadState('load') when JavaScript is disabled.
*/
export async function waitForStyleSettle(page: Page): Promise {
- try {
- await page.waitForFunction(() => {
- return Array.from(
- document.querySelectorAll('link[rel="stylesheet"]'),
- ).every((link) => {
- if (!link.href) {
- return true
- }
-
- const { sheet } = link
- if (!sheet) {
- return false
- }
-
- try {
- void sheet.cssRules
- return true
- } catch {
- return false
- }
- })
- })
- } catch {
- await page.waitForLoadState('load')
- }
-
- try {
- await page.evaluate(async (settleDelayMs) => {
- const wait = (ms: number) =>
- new Promise((resolve) => window.setTimeout(resolve, ms))
- const nextFrame = () =>
- new Promise((resolve) => requestAnimationFrame(() => resolve()))
-
- await nextFrame()
- await nextFrame()
- await wait(settleDelayMs)
- }, getSettleDelayMs())
- } catch {
- await page.waitForTimeout(getSettleDelayMs())
- }
+ await page.waitForTimeout(100)
}
diff --git a/libs/css/src/lib.rs b/libs/css/src/lib.rs
index 6801cc46..4f542229 100644
--- a/libs/css/src/lib.rs
+++ b/libs/css/src/lib.rs
@@ -9,6 +9,7 @@ pub mod optimize_value;
pub mod rm_css_comment;
mod selector_separator;
pub mod style_selector;
+pub mod theme_tokens;
pub mod utils;
use std::collections::BTreeMap;
diff --git a/libs/css/src/theme_tokens.rs b/libs/css/src/theme_tokens.rs
new file mode 100644
index 00000000..f7a18619
--- /dev/null
+++ b/libs/css/src/theme_tokens.rs
@@ -0,0 +1,55 @@
+use std::collections::BTreeMap;
+use std::sync::{LazyLock, RwLock};
+
+#[derive(Default, Debug)]
+struct ThemeTokenRegistry {
+ length: BTreeMap>,
+ shadow: BTreeMap>,
+}
+
+static TOKEN_REGISTRY: LazyLock> =
+ LazyLock::new(|| RwLock::new(ThemeTokenRegistry::default()));
+
+pub fn set_theme_token_levels(
+ length: BTreeMap>,
+ shadow: BTreeMap>,
+) {
+ if let Ok(mut registry) = TOKEN_REGISTRY.write() {
+ registry.length = length;
+ registry.shadow = shadow;
+ }
+}
+
+/// Look up a `$token` in the length and shadow registries.
+/// Returns the responsive breakpoint levels if the token is defined
+/// with more than one level, regardless of which CSS property it's used on.
+pub fn get_responsive_theme_token(value: &str) -> Option> {
+ let token = value.strip_prefix('$')?;
+ let registry = TOKEN_REGISTRY.read().ok()?;
+
+ registry
+ .length
+ .get(token)
+ .or_else(|| registry.shadow.get(token))
+ .filter(|levels| levels.len() > 1)
+ .cloned()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_get_responsive_theme_token() {
+ let mut length = BTreeMap::new();
+ length.insert("containerX".to_string(), vec![0, 2]);
+ let mut shadow = BTreeMap::new();
+ shadow.insert("card".to_string(), vec![0, 3]);
+ set_theme_token_levels(length, shadow);
+
+ assert_eq!(get_responsive_theme_token("$containerX"), Some(vec![0, 2]));
+ assert_eq!(get_responsive_theme_token("$card"), Some(vec![0, 3]));
+ assert_eq!(get_responsive_theme_token("$unknown"), None);
+ assert_eq!(get_responsive_theme_token("noprefix"), None);
+ }
+}
diff --git a/libs/extractor/Cargo.toml b/libs/extractor/Cargo.toml
index 20610632..1947bb24 100644
--- a/libs/extractor/Cargo.toml
+++ b/libs/extractor/Cargo.toml
@@ -4,15 +4,15 @@ version = "0.1.0"
edition = "2024"
[dependencies]
-oxc_parser = "0.122.0"
-oxc_syntax = "0.122.0"
-oxc_span = "0.122.0"
-oxc_allocator = "0.122.0"
-oxc_ast = "0.122.0"
-oxc_ast_visit = "0.122.0"
-oxc_codegen = "0.122.0"
-oxc_transformer = "0.122.0"
-oxc_semantic = "0.122.0"
+oxc_parser = "0.123.0"
+oxc_syntax = "0.123.0"
+oxc_span = "0.123.0"
+oxc_allocator = "0.123.0"
+oxc_ast = "0.123.0"
+oxc_ast_visit = "0.123.0"
+oxc_codegen = "0.123.0"
+oxc_transformer = "0.123.0"
+oxc_semantic = "0.123.0"
css = { path = "../css" }
phf = "0.13"
strum = "0.28.0"
@@ -23,7 +23,7 @@ rustc-hash = "2"
smallvec = "1"
[dev-dependencies]
-insta = "1.46.3"
+insta = "1.47.2"
serial_test = "3.4.0"
rstest = "0.26.1"
criterion = { version = "0.8", features = ["html_reports"] }
diff --git a/libs/extractor/src/as_visit.rs b/libs/extractor/src/as_visit.rs
index d9a4fe61..0c988d59 100644
--- a/libs/extractor/src/as_visit.rs
+++ b/libs/extractor/src/as_visit.rs
@@ -22,7 +22,7 @@ impl<'a> AsVisitor<'a> {
}
fn change_element_name<'a>(ast: &AstBuilder<'a>, element: &mut JSXElement<'a>, element_name: &str) {
- let element_name = ast.jsx_element_name_identifier(SPAN, ast.atom(element_name));
+ let element_name = ast.jsx_element_name_identifier(SPAN, ast.str(element_name));
element.opening_element.name = element_name.clone_in(ast.allocator);
if let Some(el) = &mut element.closing_element {
el.name = element_name;
diff --git a/libs/extractor/src/css_utils.rs b/libs/extractor/src/css_utils.rs
index 574a7b86..eabd6cfa 100644
--- a/libs/extractor/src/css_utils.rs
+++ b/libs/extractor/src/css_utils.rs
@@ -155,7 +155,7 @@ pub fn css_to_style_literal<'a>(
expression_to_code(&wrap_direct_call(
&ast_builder,
expr,
- &[ast_builder.expression_identifier(SPAN, ast_builder.atom("rest"))],
+ &[ast_builder.expression_identifier(SPAN, ast_builder.str("rest"))],
))
} else {
expression_to_code(expr)
@@ -200,7 +200,7 @@ pub fn css_to_style_literal<'a>(
&ast_builder,
expr,
&[ast_builder
- .expression_identifier(SPAN, ast_builder.atom("rest"))],
+ .expression_identifier(SPAN, ast_builder.str("rest"))],
))
} else {
expression_to_code(expr)
diff --git a/libs/extractor/src/extract_style/extract_static_style.rs b/libs/extractor/src/extract_style/extract_static_style.rs
index abe853cb..940a7e51 100644
--- a/libs/extractor/src/extract_style/extract_static_style.rs
+++ b/libs/extractor/src/extract_style/extract_static_style.rs
@@ -1,3 +1,5 @@
+use std::fmt::{Debug, Formatter};
+
use css::{
optimize_multi_css_value::{check_multi_css_optimize, optimize_mutli_css_value},
optimize_value::optimize_value,
@@ -12,7 +14,14 @@ use crate::{
utils::{convert_value, gcd},
};
-#[derive(Debug, PartialEq, Clone, Eq, Hash, Ord, PartialOrd)]
+#[derive(Debug, PartialEq, Clone, Copy, Eq, Hash, Ord, PartialOrd, Default)]
+pub enum ThemeTokenResolution {
+ #[default]
+ CssVariable,
+ FirstValue,
+}
+
+#[derive(PartialEq, Clone, Eq, Hash, Ord, PartialOrd)]
pub struct ExtractStaticStyle {
/// property
pub property: String,
@@ -26,6 +35,21 @@ pub struct ExtractStaticStyle {
pub style_order: Option,
/// CSS layer name (from vanilla-extract layer())
pub layer: Option,
+ /// How theme tokens should be resolved when converting to CSS.
+ pub theme_token_resolution: ThemeTokenResolution,
+}
+
+impl Debug for ExtractStaticStyle {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("ExtractStaticStyle")
+ .field("property", &self.property)
+ .field("value", &self.value)
+ .field("level", &self.level)
+ .field("selector", &self.selector)
+ .field("style_order", &self.style_order)
+ .field("layer", &self.layer)
+ .finish()
+ }
}
impl ExtractStaticStyle {
@@ -55,6 +79,7 @@ impl ExtractStaticStyle {
selector: selector.map(optimize_selector),
style_order: None,
layer: None,
+ theme_token_resolution: ThemeTokenResolution::CssVariable,
}
}
@@ -88,9 +113,15 @@ impl ExtractStaticStyle {
selector,
style_order: Some(0),
layer: None,
+ theme_token_resolution: ThemeTokenResolution::CssVariable,
}
}
+ pub fn with_theme_token_resolution(mut self, resolution: ThemeTokenResolution) -> Self {
+ self.theme_token_resolution = resolution;
+ self
+ }
+
/// Get the layer name
pub fn layer(&self) -> Option<&str> {
self.layer.as_deref()
@@ -115,6 +146,10 @@ impl ExtractStaticStyle {
pub fn style_order(&self) -> Option {
self.style_order
}
+
+ pub fn theme_token_resolution(&self) -> ThemeTokenResolution {
+ self.theme_token_resolution
+ }
}
impl ExtractStyleProperty for ExtractStaticStyle {
diff --git a/libs/extractor/src/extract_style/mod.rs b/libs/extractor/src/extract_style/mod.rs
index 2d02c05b..05d4646d 100644
--- a/libs/extractor/src/extract_style/mod.rs
+++ b/libs/extractor/src/extract_style/mod.rs
@@ -4,7 +4,7 @@ pub(super) mod extract_dynamic_style;
pub(super) mod extract_font_face;
pub(super) mod extract_import;
pub(super) mod extract_keyframes;
-pub(super) mod extract_static_style;
+pub mod extract_static_style;
pub mod extract_style_value;
pub mod style_property;
diff --git a/libs/extractor/src/extractor/extract_global_style_from_expression.rs b/libs/extractor/src/extractor/extract_global_style_from_expression.rs
index 689cca78..3186e876 100644
--- a/libs/extractor/src/extractor/extract_global_style_from_expression.rs
+++ b/libs/extractor/src/extractor/extract_global_style_from_expression.rs
@@ -8,7 +8,8 @@ use crate::{
extract_style_value::ExtractStyleValue,
},
extractor::{
- GlobalExtractResult, extract_style_from_expression::extract_style_from_expression,
+ GlobalExtractResult,
+ extract_style_from_expression::{LiteralHandling, extract_style_from_expression},
},
utils::{get_string_by_literal_expression, get_string_by_property_key},
};
@@ -166,6 +167,7 @@ pub fn extract_global_style_from_expression<'a>(
},
file.to_string(),
)),
+ LiteralHandling::ExpandResponsiveThemeToken,
);
// Filter out @layer property from styles and set layer on remaining styles
diff --git a/libs/extractor/src/extractor/extract_keyframes_from_expression.rs b/libs/extractor/src/extractor/extract_keyframes_from_expression.rs
index 4d8f72d4..a13900ce 100644
--- a/libs/extractor/src/extractor/extract_keyframes_from_expression.rs
+++ b/libs/extractor/src/extractor/extract_keyframes_from_expression.rs
@@ -3,7 +3,7 @@ use crate::{
extract_style::{extract_keyframes::ExtractKeyframes, extract_style_value::ExtractStyleValue},
extractor::{
ExtractResult, KeyframesExtractResult,
- extract_style_from_expression::extract_style_from_expression,
+ extract_style_from_expression::{LiteralHandling, extract_style_from_expression},
},
utils::get_string_by_property_key,
};
@@ -23,8 +23,14 @@ pub fn extract_keyframes_from_expression<'a>(
if let ObjectPropertyKind::ObjectProperty(o) = p
&& let Some(name) = get_string_by_property_key(&o.key)
{
- let ExtractResult { styles, .. } =
- extract_style_from_expression(ast_builder, None, &mut o.value, 0, &None);
+ let ExtractResult { styles, .. } = extract_style_from_expression(
+ ast_builder,
+ None,
+ &mut o.value,
+ 0,
+ &None,
+ LiteralHandling::ExpandResponsiveThemeToken,
+ );
let mut styles = styles
.into_iter()
diff --git a/libs/extractor/src/extractor/extract_style_from_expression.rs b/libs/extractor/src/extractor/extract_style_from_expression.rs
index 32ab7f37..6f996dbc 100644
--- a/libs/extractor/src/extractor/extract_style_from_expression.rs
+++ b/libs/extractor/src/extractor/extract_style_from_expression.rs
@@ -2,7 +2,8 @@ use crate::{
ExtractStyleProp,
css_utils::{css_to_style, css_to_style_literal},
extract_style::{
- extract_dynamic_style::ExtractDynamicStyle, extract_static_style::ExtractStaticStyle,
+ extract_dynamic_style::ExtractDynamicStyle,
+ extract_static_style::{ExtractStaticStyle, ThemeTokenResolution},
extract_style_value::ExtractStyleValue,
},
extractor::{
@@ -15,7 +16,8 @@ use crate::{
};
use css::{
add_selector_params, disassemble_property, get_enum_property_map, get_enum_property_value,
- is_special_property::is_special_property, style_selector::StyleSelector, utils::to_kebab_case,
+ is_special_property::is_special_property, style_selector::StyleSelector,
+ theme_tokens::get_responsive_theme_token, utils::to_kebab_case,
};
use oxc_allocator::CloneIn;
use oxc_ast::{
@@ -29,12 +31,47 @@ use oxc_span::SPAN;
const IGNORED_IDENTIFIERS: [&str; 3] = ["undefined", "NaN", "Infinity"];
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum LiteralHandling {
+ ExpandResponsiveThemeToken,
+ KeepSingleClass,
+}
+
+fn create_static_styles<'a>(
+ name: &str,
+ value: &str,
+ levels: &[u8],
+ selector: &Option,
+ resolution: ThemeTokenResolution,
+) -> Vec> {
+ let mut styles = Vec::new();
+
+ for &level in levels {
+ if let Some(map) = get_enum_property_value(name, value) {
+ styles.extend(map.into_iter().map(|(k, v)| {
+ ExtractStyleProp::Static(ExtractStyleValue::Static(
+ ExtractStaticStyle::new(&k, &v, level, selector.clone())
+ .with_theme_token_resolution(resolution),
+ ))
+ }));
+ } else {
+ styles.push(ExtractStyleProp::Static(ExtractStyleValue::Static(
+ ExtractStaticStyle::new(name, value, level, selector.clone())
+ .with_theme_token_resolution(resolution),
+ )));
+ }
+ }
+
+ styles
+}
+
pub fn extract_style_from_expression<'a>(
ast_builder: &AstBuilder<'a>,
name: Option<&str>,
expression: &mut Expression<'a>,
level: u8,
selector: &Option,
+ literal_handling: LiteralHandling,
) -> ExtractResult<'a> {
let mut typo = false;
@@ -72,6 +109,7 @@ pub fn extract_style_from_expression<'a>(
&mut prop.value,
0,
&None,
+ LiteralHandling::ExpandResponsiveThemeToken,
);
props_styles.extend(styles);
tag = _tag.or(tag);
@@ -91,6 +129,7 @@ pub fn extract_style_from_expression<'a>(
&mut prop.argument,
0,
&None,
+ LiteralHandling::ExpandResponsiveThemeToken,
);
props_styles.extend(styles);
tag = _tag.or(tag);
@@ -119,6 +158,7 @@ pub fn extract_style_from_expression<'a>(
&mut conditional.consequent,
level,
&None,
+ literal_handling,
)
.styles,
))),
@@ -129,6 +169,7 @@ pub fn extract_style_from_expression<'a>(
&mut conditional.alternate,
level,
selector,
+ literal_handling,
)
.styles,
))),
@@ -143,6 +184,7 @@ pub fn extract_style_from_expression<'a>(
&mut parenthesized.expression,
level,
&None,
+ literal_handling,
),
Expression::TemplateLiteral(tmp) => ExtractResult {
styles: css_to_style_literal(tmp, level, selector)
@@ -227,6 +269,7 @@ pub fn extract_style_from_expression<'a>(
&mut o.value,
level,
&Some(StyleSelector::Selector(sel)),
+ literal_handling,
)
.styles,
);
@@ -265,6 +308,7 @@ pub fn extract_style_from_expression<'a>(
&mut o.value,
level,
&Some(at_selector),
+ literal_handling,
)
.styles,
);
@@ -287,6 +331,7 @@ pub fn extract_style_from_expression<'a>(
} else {
new_selector.into()
}),
+ literal_handling,
);
}
typo = name == "typography";
@@ -299,19 +344,43 @@ pub fn extract_style_from_expression<'a>(
value.to_string(),
))]
} else {
- // Create a new ExtractStaticStyle
- if let Some(map) = get_enum_property_value(name, &value) {
- map.into_iter()
- .map(|(k, v)| {
- ExtractStyleProp::Static(ExtractStyleValue::Static(
- ExtractStaticStyle::new(&k, &v, level, selector.clone()),
- ))
- })
- .collect()
+ if matches!(
+ literal_handling,
+ LiteralHandling::ExpandResponsiveThemeToken
+ ) {
+ if let Some(levels) = get_responsive_theme_token(&value) {
+ create_static_styles(
+ name,
+ &value,
+ &levels,
+ selector,
+ ThemeTokenResolution::CssVariable,
+ )
+ } else {
+ create_static_styles(
+ name,
+ &value,
+ &[level],
+ selector,
+ ThemeTokenResolution::CssVariable,
+ )
+ }
+ } else if get_responsive_theme_token(&value).is_some() {
+ create_static_styles(
+ name,
+ &value,
+ &[level],
+ selector,
+ ThemeTokenResolution::FirstValue,
+ )
} else {
- vec![ExtractStyleProp::Static(ExtractStyleValue::Static(
- ExtractStaticStyle::new(name, &value, level, selector.clone()),
- ))]
+ create_static_styles(
+ name,
+ &value,
+ &[level],
+ selector,
+ ThemeTokenResolution::CssVariable,
+ )
}
},
..ExtractResult::default()
@@ -359,6 +428,7 @@ pub fn extract_style_from_expression<'a>(
&mut exp.expression,
level,
selector,
+ literal_handling,
),
Expression::ComputedMemberExpression(mem) => {
extract_style_from_member_expression(ast_builder, name, mem, level, selector)
@@ -372,7 +442,7 @@ pub fn extract_style_from_expression<'a>(
ast_builder.template_element(
SPAN,
TemplateElementValue {
- raw: ast_builder.atom("typo-"),
+ raw: ast_builder.str("typo-"),
cooked: None,
},
false,
@@ -381,7 +451,7 @@ pub fn extract_style_from_expression<'a>(
ast_builder.template_element(
SPAN,
TemplateElementValue {
- raw: ast_builder.atom(""),
+ raw: ast_builder.str(""),
cooked: None,
},
true,
@@ -422,7 +492,7 @@ pub fn extract_style_from_expression<'a>(
ast_builder.template_element(
SPAN,
TemplateElementValue {
- raw: ast_builder.atom("typo-"),
+ raw: ast_builder.str("typo-"),
cooked: None,
},
false,
@@ -431,7 +501,7 @@ pub fn extract_style_from_expression<'a>(
ast_builder.template_element(
SPAN,
TemplateElementValue {
- raw: ast_builder.atom(""),
+ raw: ast_builder.str(""),
cooked: None,
},
true,
@@ -470,6 +540,7 @@ pub fn extract_style_from_expression<'a>(
&mut logical.right,
level,
selector,
+ literal_handling,
)
.styles,
)));
@@ -515,6 +586,7 @@ pub fn extract_style_from_expression<'a>(
&mut logical.left,
level,
selector,
+ literal_handling,
)
.styles,
))),
@@ -530,6 +602,7 @@ pub fn extract_style_from_expression<'a>(
&mut parenthesized.expression,
level,
selector,
+ literal_handling,
),
Expression::ArrayExpression(array) => {
let mut props = vec![];
@@ -543,6 +616,7 @@ pub fn extract_style_from_expression<'a>(
element,
idx as u8,
selector,
+ LiteralHandling::KeepSingleClass,
)
.styles,
);
@@ -564,6 +638,7 @@ pub fn extract_style_from_expression<'a>(
&mut conditional.consequent,
level,
selector,
+ literal_handling,
)
} else {
ExtractResult {
@@ -576,6 +651,7 @@ pub fn extract_style_from_expression<'a>(
&mut conditional.consequent,
level,
selector,
+ literal_handling,
)
.styles,
))),
@@ -586,6 +662,7 @@ pub fn extract_style_from_expression<'a>(
&mut conditional.alternate,
level,
selector,
+ literal_handling,
)
.styles,
))),
@@ -639,6 +716,7 @@ pub fn extract_style_from_expression<'a>(
&mut o.value,
level,
&selector,
+ literal_handling,
)
.styles,
);
diff --git a/libs/extractor/src/extractor/extract_style_from_jsx.rs b/libs/extractor/src/extractor/extract_style_from_jsx.rs
index b77b6d81..9bae9edd 100644
--- a/libs/extractor/src/extractor/extract_style_from_jsx.rs
+++ b/libs/extractor/src/extractor/extract_style_from_jsx.rs
@@ -1,5 +1,6 @@
use crate::extractor::{
- ExtractResult, extract_style_from_expression::extract_style_from_expression,
+ ExtractResult,
+ extract_style_from_expression::{LiteralHandling, extract_style_from_expression},
};
use oxc_allocator::CloneIn;
use oxc_ast::{
@@ -12,7 +13,7 @@ pub fn extract_style_from_jsx<'a>(
name: &str,
value: &mut JSXAttributeValue<'a>,
) -> ExtractResult<'a> {
- match value {
+ let expression = match value {
JSXAttributeValue::ExpressionContainer(expression) => expression
.expression
.as_expression()
@@ -21,9 +22,18 @@ pub fn extract_style_from_jsx<'a>(
literal.clone_in(ast_builder.allocator),
)),
_ => None,
- }
- .map(|mut expression| {
- extract_style_from_expression(ast_builder, Some(name), &mut expression, 0, &None)
- })
- .unwrap_or_default()
+ };
+
+ expression
+ .map(|mut expression| {
+ extract_style_from_expression(
+ ast_builder,
+ Some(name),
+ &mut expression,
+ 0,
+ &None,
+ LiteralHandling::ExpandResponsiveThemeToken,
+ )
+ })
+ .unwrap_or_default()
}
diff --git a/libs/extractor/src/extractor/extract_style_from_member_expression.rs b/libs/extractor/src/extractor/extract_style_from_member_expression.rs
index 066d4970..d2ca8966 100644
--- a/libs/extractor/src/extractor/extract_style_from_member_expression.rs
+++ b/libs/extractor/src/extractor/extract_style_from_member_expression.rs
@@ -2,7 +2,9 @@ use crate::{
ExtractStyleProp,
extractor::{
ExtractResult,
- extract_style_from_expression::{dynamic_style, extract_style_from_expression},
+ extract_style_from_expression::{
+ LiteralHandling, dynamic_style, extract_style_from_expression,
+ },
},
utils::{
get_number_by_literal_expression, get_string_by_literal_expression,
@@ -53,7 +55,14 @@ pub(super) fn extract_style_from_member_expression<'a>(
} else if idx as f64 == num
&& let Some(p) = p.as_expression_mut()
{
- return extract_style_from_expression(ast_builder, name, p, level, selector);
+ return extract_style_from_expression(
+ ast_builder,
+ name,
+ p,
+ level,
+ selector,
+ LiteralHandling::ExpandResponsiveThemeToken,
+ );
}
}
return ExtractResult {
@@ -106,7 +115,15 @@ pub(super) fn extract_style_from_member_expression<'a>(
map.insert(
idx.to_string(),
Box::new(ExtractStyleProp::StaticArray(
- extract_style_from_expression(ast_builder, name, p, level, selector).styles,
+ extract_style_from_expression(
+ ast_builder,
+ name,
+ p,
+ level,
+ selector,
+ LiteralHandling::ExpandResponsiveThemeToken,
+ )
+ .styles,
)),
);
}
@@ -134,6 +151,7 @@ pub(super) fn extract_style_from_member_expression<'a>(
&mut o.value,
level,
selector,
+ LiteralHandling::ExpandResponsiveThemeToken,
)
.styles,
..ExtractResult::default()
@@ -176,6 +194,7 @@ pub(super) fn extract_style_from_member_expression<'a>(
&mut o.value,
level,
selector,
+ LiteralHandling::ExpandResponsiveThemeToken,
)
.styles,
)),
diff --git a/libs/extractor/src/extractor/extract_style_from_styled.rs b/libs/extractor/src/extractor/extract_style_from_styled.rs
index b36c3d26..53dba85e 100644
--- a/libs/extractor/src/extractor/extract_style_from_styled.rs
+++ b/libs/extractor/src/extractor/extract_style_from_styled.rs
@@ -5,7 +5,10 @@ use crate::{
component::ExportVariableKind,
css_utils::css_to_style_literal,
extract_style::extract_style_value::ExtractStyleValue,
- extractor::{ExtractResult, extract_style_from_expression::extract_style_from_expression},
+ extractor::{
+ ExtractResult,
+ extract_style_from_expression::{LiteralHandling, extract_style_from_expression},
+ },
gen_class_name::gen_class_names,
gen_style::gen_styles,
utils::{merge_object_expressions, wrap_array_filter},
@@ -90,7 +93,7 @@ pub fn extract_style_from_styled<'a>(
styles: props_styles,
tag: Some(ast_builder.expression_string_literal(
SPAN,
- ast_builder.atom(&tag_name),
+ ast_builder.str(&tag_name),
None,
)),
style_order: None,
@@ -124,6 +127,7 @@ pub fn extract_style_from_styled<'a>(
},
0,
&None,
+ LiteralHandling::ExpandResponsiveThemeToken,
);
if let Some(default_class_name) = default_class_name {
styles.extend(default_class_name.into_iter().map(ExtractStyleProp::Static));
@@ -196,7 +200,7 @@ fn create_styled_component<'a>(
SPAN,
ast_builder.binding_pattern_binding_identifier(
SPAN,
- ast_builder.atom("rest"),
+ ast_builder.str("rest"),
),
),
),
@@ -222,20 +226,20 @@ fn create_styled_component<'a>(
SPAN,
ast_builder.alloc_jsx_opening_element(
SPAN,
- ast_builder.jsx_element_name_identifier(SPAN, ast_builder.atom(tag_name)),
+ ast_builder.jsx_element_name_identifier(SPAN, ast_builder.str(tag_name)),
None::>>,
oxc_allocator::Vec::from_iter_in(
vec![
ast_builder.jsx_attribute_item_spread_attribute(
SPAN,
ast_builder
- .expression_identifier(SPAN, ast_builder.atom("rest")),
+ .expression_identifier(SPAN, ast_builder.str("rest")),
),
ast_builder.jsx_attribute_item_attribute(
SPAN,
ast_builder.jsx_attribute_name_identifier(
SPAN,
- ast_builder.atom("className"),
+ ast_builder.str("className"),
),
Some(
ast_builder.jsx_attribute_value_expression_container(
@@ -251,7 +255,7 @@ fn create_styled_component<'a>(
),
ast_builder.expression_identifier(
SPAN,
- ast_builder.atom("className"),
+ ast_builder.str("className"),
),
],
)
@@ -260,7 +264,7 @@ fn create_styled_component<'a>(
.unwrap_or_else(|| {
ast_builder.expression_identifier(
SPAN,
- ast_builder.atom("className"),
+ ast_builder.str("className"),
)
})
.into(),
@@ -271,7 +275,7 @@ fn create_styled_component<'a>(
SPAN,
ast_builder.jsx_attribute_name_identifier(
SPAN,
- ast_builder.atom("style"),
+ ast_builder.str("style"),
),
Some(
ast_builder.jsx_attribute_value_expression_container(
@@ -287,7 +291,7 @@ fn create_styled_component<'a>(
),
ast_builder.expression_identifier(
SPAN,
- ast_builder.atom("style"),
+ ast_builder.str("style"),
),
],
)
@@ -296,7 +300,7 @@ fn create_styled_component<'a>(
.unwrap_or_else(|| {
ast_builder.expression_identifier(
SPAN,
- ast_builder.atom("style"),
+ ast_builder.str("style"),
)
})
.into(),
diff --git a/libs/extractor/src/extractor/extract_style_from_stylex.rs b/libs/extractor/src/extractor/extract_style_from_stylex.rs
index f922ab84..032f19f4 100644
--- a/libs/extractor/src/extractor/extract_style_from_stylex.rs
+++ b/libs/extractor/src/extractor/extract_style_from_stylex.rs
@@ -144,6 +144,7 @@ pub fn extract_stylex_namespace_styles<'a>(
selector: decomposed.selector,
style_order: None,
layer: None,
+ theme_token_resolution: Default::default(),
},
)));
}
@@ -194,6 +195,7 @@ pub fn extract_stylex_namespace_styles<'a>(
selector: None,
style_order: None,
layer: None,
+ theme_token_resolution: Default::default(),
},
)));
continue;
@@ -215,6 +217,7 @@ pub fn extract_stylex_namespace_styles<'a>(
selector: decomposed.selector,
style_order: None,
layer: None,
+ theme_token_resolution: Default::default(),
},
)));
}
@@ -236,6 +239,7 @@ pub fn extract_stylex_namespace_styles<'a>(
selector: None,
style_order: None,
layer: None,
+ theme_token_resolution: Default::default(),
},
)));
}
@@ -260,6 +264,7 @@ pub fn extract_stylex_namespace_styles<'a>(
selector: None,
style_order: None,
layer: None,
+ theme_token_resolution: Default::default(),
},
)));
continue;
@@ -284,6 +289,7 @@ pub fn extract_stylex_namespace_styles<'a>(
selector: None,
style_order: None,
layer: None,
+ theme_token_resolution: Default::default(),
},
)));
}
@@ -386,6 +392,7 @@ fn extract_stylex_dynamic_namespace<'a>(
selector: None,
style_order: None,
layer: None,
+ theme_token_resolution: Default::default(),
},
)));
continue;
@@ -403,6 +410,7 @@ fn extract_stylex_dynamic_namespace<'a>(
selector: None,
style_order: None,
layer: None,
+ theme_token_resolution: Default::default(),
},
)));
}
diff --git a/libs/extractor/src/gen_class_name.rs b/libs/extractor/src/gen_class_name.rs
index d6e06b41..3583cd80 100644
--- a/libs/extractor/src/gen_class_name.rs
+++ b/libs/extractor/src/gen_class_name.rs
@@ -37,7 +37,7 @@ fn gen_class_name<'a>(
PropertyKind::Init,
PropertyKey::StringLiteral(ast_builder.alloc_string_literal(
SPAN,
- ast_builder.atom(key),
+ ast_builder.str(key),
None,
)),
merge_expression_for_class_name(
@@ -74,7 +74,7 @@ fn gen_class_name<'a>(
st.set_style_order(style_order);
}
st.extract(filename).map(|style| {
- let v = ast_builder.atom(&match style {
+ let v = ast_builder.str(&match style {
StyleProperty::ClassName(cls) => cls,
StyleProperty::Variable { class_name, .. } => class_name,
});
@@ -134,7 +134,7 @@ fn gen_class_name<'a>(
PropertyKey::StringLiteral(
ast_builder.alloc_string_literal(
SPAN,
- ast_builder.atom(key),
+ ast_builder.str(key),
None,
),
),
@@ -184,7 +184,7 @@ pub fn merge_expression_for_class_name<'a>(
for idx in 0..unknown_expr.len() + 1 {
let tail = idx == unknown_expr.len();
let t = TemplateElementValue {
- raw: ast_builder.atom(if idx == 0 {
+ raw: ast_builder.str(if idx == 0 {
if class_name.is_empty() {
""
} else {
@@ -210,6 +210,6 @@ pub fn merge_expression_for_class_name<'a>(
} else if class_name.is_empty() {
None
} else {
- Some(ast_builder.expression_string_literal(SPAN, ast_builder.atom(&class_name), None))
+ Some(ast_builder.expression_string_literal(SPAN, ast_builder.str(&class_name), None))
}
}
diff --git a/libs/extractor/src/gen_style.rs b/libs/extractor/src/gen_style.rs
index 2bac6c6c..ed0561a3 100644
--- a/libs/extractor/src/gen_style.rs
+++ b/libs/extractor/src/gen_style.rs
@@ -44,10 +44,10 @@ fn gen_style<'a>(
PropertyKind::Init,
PropertyKey::StringLiteral(ast_builder.alloc_string_literal(
SPAN,
- ast_builder.atom(&variable_name),
+ ast_builder.str(&variable_name),
None,
)),
- ast_builder.expression_identifier(SPAN, ast_builder.atom(&identifier)),
+ ast_builder.expression_identifier(SPAN, ast_builder.str(&identifier)),
false,
false,
false,
@@ -176,7 +176,7 @@ fn gen_style<'a>(
for (key, value) in tmp_map {
let v = if value.len() == 1 {
// do not create object expression when property is single
- ast_builder.expression_identifier(SPAN, ast_builder.atom(&value[0].1))
+ ast_builder.expression_identifier(SPAN, ast_builder.str(&value[0].1))
} else {
Expression::ComputedMemberExpression(
ast_builder.alloc_computed_member_expression(
@@ -192,10 +192,10 @@ fn gen_style<'a>(
PropertyKind::Init,
ast_builder.property_key_static_identifier(
SPAN,
- ast_builder.atom(&k),
+ ast_builder.str(&k),
),
ast_builder
- .expression_identifier(SPAN, ast_builder.atom(&v)),
+ .expression_identifier(SPAN, ast_builder.str(&v)),
false,
false,
false,
@@ -215,7 +215,7 @@ fn gen_style<'a>(
PropertyKind::Init,
PropertyKey::StringLiteral(ast_builder.alloc_string_literal(
SPAN,
- ast_builder.atom(&key),
+ ast_builder.str(&key),
None,
)),
v,
diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs
index 374bb74d..745af939 100644
--- a/libs/extractor/src/lib.rs
+++ b/libs/extractor/src/lib.rs
@@ -14236,8 +14236,8 @@ export { c as Lib };"#,
let allocator = Allocator::default();
let builder = oxc_ast::AstBuilder::new(&allocator);
- let make_cond = || builder.expression_identifier(SPAN, builder.atom("cond"));
- let make_str = |s| builder.expression_string_literal(SPAN, builder.atom(s), None);
+ let make_cond = || builder.expression_identifier(SPAN, builder.str("cond"));
+ let make_str = |s| builder.expression_string_literal(SPAN, builder.str(s), None);
// (Some, Some) — both branches have classNames
let result = combine_conditional_class_name(
@@ -16544,4 +16544,184 @@ const composed = stylex.create({ combined: { ...stylex.include(base.root) } });"
.unwrap()
));
}
+
+ #[test]
+ #[serial]
+ fn test_responsive_length_token_literal_vs_array() {
+ use css::theme_tokens::set_theme_token_levels;
+
+ let mut length = BTreeMap::new();
+ length.insert("containerX".to_string(), vec![0, 2]);
+ set_theme_token_levels(length, BTreeMap::new());
+
+ // String literal: w="$containerX" → expands to multiple breakpoint classes
+ reset_class_map();
+ reset_file_map();
+ assert_debug_snapshot!(ToBTreeSet::from(
+ extract(
+ "test.tsx",
+ r#"import {Box} from '@devup-ui/react'
+
+ "#,
+ ExtractOption {
+ package: "@devup-ui/react".to_string(),
+ css_dir: "@devup-ui/react".to_string(),
+ single_css: true,
+ import_main_css: false,
+ import_aliases: HashMap::new()
+ }
+ )
+ .unwrap()
+ ));
+
+ // Expression: w={"$containerX"} → also expands (same as string literal)
+ reset_class_map();
+ reset_file_map();
+ assert_debug_snapshot!(ToBTreeSet::from(
+ extract(
+ "test.tsx",
+ r#"import {Box} from '@devup-ui/react'
+
+ "#,
+ ExtractOption {
+ package: "@devup-ui/react".to_string(),
+ css_dir: "@devup-ui/react".to_string(),
+ single_css: true,
+ import_main_css: false,
+ import_aliases: HashMap::new()
+ }
+ )
+ .unwrap()
+ ));
+
+ // Array with single element: w={["$containerX"]} → single class, base value only
+ reset_class_map();
+ reset_file_map();
+ assert_debug_snapshot!(ToBTreeSet::from(
+ extract(
+ "test.tsx",
+ r#"import {Box} from '@devup-ui/react'
+
+ "#,
+ ExtractOption {
+ package: "@devup-ui/react".to_string(),
+ css_dir: "@devup-ui/react".to_string(),
+ single_css: true,
+ import_main_css: false,
+ import_aliases: HashMap::new()
+ }
+ )
+ .unwrap()
+ ));
+
+ // Mixed array: w={["1px", null, "$containerX"]} → token inside array stays single per slot
+ reset_class_map();
+ reset_file_map();
+ assert_debug_snapshot!(ToBTreeSet::from(
+ extract(
+ "test.tsx",
+ r#"import {Box} from '@devup-ui/react'
+
+ "#,
+ ExtractOption {
+ package: "@devup-ui/react".to_string(),
+ css_dir: "@devup-ui/react".to_string(),
+ single_css: true,
+ import_main_css: false,
+ import_aliases: HashMap::new()
+ }
+ )
+ .unwrap()
+ ));
+ }
+
+ #[test]
+ #[serial]
+ fn test_responsive_shadow_token_literal_vs_array() {
+ use css::theme_tokens::set_theme_token_levels;
+
+ let mut shadow = BTreeMap::new();
+ shadow.insert("card".to_string(), vec![0, 3]);
+ set_theme_token_levels(BTreeMap::new(), shadow);
+
+ // String literal: boxShadow="$card" → expands to multiple breakpoint classes
+ reset_class_map();
+ reset_file_map();
+ assert_debug_snapshot!(ToBTreeSet::from(
+ extract(
+ "test.tsx",
+ r#"import {Box} from '@devup-ui/react'
+
+ "#,
+ ExtractOption {
+ package: "@devup-ui/react".to_string(),
+ css_dir: "@devup-ui/react".to_string(),
+ single_css: true,
+ import_main_css: false,
+ import_aliases: HashMap::new()
+ }
+ )
+ .unwrap()
+ ));
+
+ // Expression: boxShadow={"$card"} → also expands (same as string literal)
+ reset_class_map();
+ reset_file_map();
+ assert_debug_snapshot!(ToBTreeSet::from(
+ extract(
+ "test.tsx",
+ r#"import {Box} from '@devup-ui/react'
+
+ "#,
+ ExtractOption {
+ package: "@devup-ui/react".to_string(),
+ css_dir: "@devup-ui/react".to_string(),
+ single_css: true,
+ import_main_css: false,
+ import_aliases: HashMap::new()
+ }
+ )
+ .unwrap()
+ ));
+
+ // Array with single element: boxShadow={["$card"]} → single class, base value only
+ reset_class_map();
+ reset_file_map();
+ assert_debug_snapshot!(ToBTreeSet::from(
+ extract(
+ "test.tsx",
+ r#"import {Box} from '@devup-ui/react'
+
+ "#,
+ ExtractOption {
+ package: "@devup-ui/react".to_string(),
+ css_dir: "@devup-ui/react".to_string(),
+ single_css: true,
+ import_main_css: false,
+ import_aliases: HashMap::new()
+ }
+ )
+ .unwrap()
+ ));
+
+ // Mixed array: boxShadow={["none", null, null, "$card"]} → token inside array stays single per slot
+ reset_class_map();
+ reset_file_map();
+ assert_debug_snapshot!(ToBTreeSet::from(
+ extract(
+ "test.tsx",
+ r#"import {Box} from '@devup-ui/react'
+
+ "#,
+ ExtractOption {
+ package: "@devup-ui/react".to_string(),
+ css_dir: "@devup-ui/react".to_string(),
+ single_css: true,
+ import_main_css: false,
+ import_aliases: HashMap::new()
+ }
+ )
+ .unwrap()
+ ));
+ }
}
diff --git a/libs/extractor/src/prop_modify_utils.rs b/libs/extractor/src/prop_modify_utils.rs
index 605c76f0..b48e43a8 100644
--- a/libs/extractor/src/prop_modify_utils.rs
+++ b/libs/extractor/src/prop_modify_utils.rs
@@ -337,7 +337,7 @@ pub fn get_class_name_expression<'a>(
ast_builder.alloc_static_member_expression(
SPAN,
ex.clone_in(ast_builder.allocator),
- ast_builder.identifier_name(SPAN, ast_builder.atom("className")),
+ ast_builder.identifier_name(SPAN, ast_builder.str("className")),
true,
),
),
@@ -459,12 +459,12 @@ fn rebuild_template_literal_with_mapping<'a>(
let replaced = replace_classes_in_string(raw, class_mapping);
let cooked = quasi.value.cooked.as_ref().map(|c| {
let replaced_cooked = replace_classes_in_string(c.as_str(), class_mapping);
- ast_builder.atom(&replaced_cooked)
+ ast_builder.str(&replaced_cooked)
});
ast_builder.template_element(
quasi.span,
TemplateElementValue {
- raw: ast_builder.atom(&replaced),
+ raw: ast_builder.str(&replaced),
cooked,
},
quasi.tail,
@@ -507,7 +507,7 @@ fn rebuild_expression_with_mapping<'a>(
match expr {
Expression::StringLiteral(lit) => {
let replaced = replace_classes_in_string(lit.value.as_str(), class_mapping);
- ast_builder.expression_string_literal(SPAN, ast_builder.atom(&replaced), None)
+ ast_builder.expression_string_literal(SPAN, ast_builder.str(&replaced), None)
}
Expression::ConditionalExpression(cond) => {
let consequent =
@@ -623,7 +623,7 @@ pub fn get_style_expression<'a>(
Expression::StaticMemberExpression(ast_builder.alloc_static_member_expression(
SPAN,
ex.clone_in(ast_builder.allocator),
- ast_builder.identifier_name(SPAN, ast_builder.atom("style")),
+ ast_builder.identifier_name(SPAN, ast_builder.str("style")),
true,
))
})
@@ -713,7 +713,7 @@ fn merge_string_expressions<'a>(
if other_expressions.is_empty() {
return Some(ast_builder.expression_string_literal(
SPAN,
- ast_builder.atom(string_literals.join("").trim()),
+ ast_builder.str(string_literals.join("").trim()),
None,
));
}
@@ -724,7 +724,7 @@ fn merge_string_expressions<'a>(
ast_builder.template_element(
SPAN,
TemplateElementValue {
- raw: ast_builder.atom(s),
+ raw: ast_builder.str(s),
cooked: None,
},
tail,
@@ -794,7 +794,7 @@ pub fn convert_style_vars<'a>(
ast_builder.template_element(
SPAN,
TemplateElementValue {
- raw: ast_builder.atom("--"),
+ raw: ast_builder.str("--"),
cooked: None,
},
false,
@@ -803,7 +803,7 @@ pub fn convert_style_vars<'a>(
ast_builder.template_element(
SPAN,
TemplateElementValue {
- raw: ast_builder.atom(""),
+ raw: ast_builder.str(""),
cooked: None,
},
true,
@@ -830,7 +830,7 @@ pub fn convert_style_vars<'a>(
if !name.starts_with("--") {
p.key = PropertyKey::StringLiteral(ast_builder.alloc_string_literal(
SPAN,
- ast_builder.atom(&format!("--{name}")),
+ ast_builder.str(&format!("--{name}")),
None,
));
}
diff --git a/libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array-2.snap b/libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array-2.snap
new file mode 100644
index 00000000..59d0c728
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array-2.snap
@@ -0,0 +1,30 @@
+---
+source: libs/extractor/src/lib.rs
+assertion_line: 16580
+expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/react'\n \n \"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())"
+---
+ToBTreeSet {
+ styles: {
+ Static(
+ ExtractStaticStyle {
+ property: "width",
+ value: "$containerX",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "width",
+ value: "$containerX",
+ level: 2,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ },
+ code: "import \"@devup-ui/react/devup-ui.css\";\n;\n",
+}
diff --git a/libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array-3.snap b/libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array-3.snap
new file mode 100644
index 00000000..9fa5d699
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array-3.snap
@@ -0,0 +1,20 @@
+---
+source: libs/extractor/src/lib.rs
+assertion_line: 16600
+expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/react'\n \n \"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())"
+---
+ToBTreeSet {
+ styles: {
+ Static(
+ ExtractStaticStyle {
+ property: "width",
+ value: "$containerX",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ },
+ code: "import \"@devup-ui/react/devup-ui.css\";\n;\n",
+}
diff --git a/libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array-4.snap b/libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array-4.snap
new file mode 100644
index 00000000..a1d1c25f
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array-4.snap
@@ -0,0 +1,30 @@
+---
+source: libs/extractor/src/lib.rs
+assertion_line: 16620
+expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/react'\n \n \"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())"
+---
+ToBTreeSet {
+ styles: {
+ Static(
+ ExtractStaticStyle {
+ property: "width",
+ value: "$containerX",
+ level: 2,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "width",
+ value: "1px",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ },
+ code: "import \"@devup-ui/react/devup-ui.css\";\n;\n",
+}
diff --git a/libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array.snap b/libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array.snap
new file mode 100644
index 00000000..a2699333
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array.snap
@@ -0,0 +1,30 @@
+---
+source: libs/extractor/src/lib.rs
+assertion_line: 16560
+expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/react'\n \n \"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())"
+---
+ToBTreeSet {
+ styles: {
+ Static(
+ ExtractStaticStyle {
+ property: "width",
+ value: "$containerX",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "width",
+ value: "$containerX",
+ level: 2,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ },
+ code: "import \"@devup-ui/react/devup-ui.css\";\n;\n",
+}
diff --git a/libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array-2.snap b/libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array-2.snap
new file mode 100644
index 00000000..0a10d8d2
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array-2.snap
@@ -0,0 +1,30 @@
+---
+source: libs/extractor/src/lib.rs
+assertion_line: 16650
+expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/react'\n \n \"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())"
+---
+ToBTreeSet {
+ styles: {
+ Static(
+ ExtractStaticStyle {
+ property: "box-shadow",
+ value: "$card",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "box-shadow",
+ value: "$card",
+ level: 3,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ },
+ code: "import \"@devup-ui/react/devup-ui.css\";\n;\n",
+}
diff --git a/libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array-3.snap b/libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array-3.snap
new file mode 100644
index 00000000..c68ff47d
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array-3.snap
@@ -0,0 +1,20 @@
+---
+source: libs/extractor/src/lib.rs
+assertion_line: 16670
+expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/react'\n \n \"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())"
+---
+ToBTreeSet {
+ styles: {
+ Static(
+ ExtractStaticStyle {
+ property: "box-shadow",
+ value: "$card",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ },
+ code: "import \"@devup-ui/react/devup-ui.css\";\n;\n",
+}
diff --git a/libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array-4.snap b/libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array-4.snap
new file mode 100644
index 00000000..27842963
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array-4.snap
@@ -0,0 +1,30 @@
+---
+source: libs/extractor/src/lib.rs
+assertion_line: 16710
+expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/react'\n \n \"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())"
+---
+ToBTreeSet {
+ styles: {
+ Static(
+ ExtractStaticStyle {
+ property: "box-shadow",
+ value: "$card",
+ level: 3,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "box-shadow",
+ value: "none",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ },
+ code: "import \"@devup-ui/react/devup-ui.css\";\n;\n",
+}
diff --git a/libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array.snap b/libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array.snap
new file mode 100644
index 00000000..cf43cb3b
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array.snap
@@ -0,0 +1,30 @@
+---
+source: libs/extractor/src/lib.rs
+assertion_line: 16630
+expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/react'\n \n \"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())"
+---
+ToBTreeSet {
+ styles: {
+ Static(
+ ExtractStaticStyle {
+ property: "box-shadow",
+ value: "$card",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "box-shadow",
+ value: "$card",
+ level: 3,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ },
+ code: "import \"@devup-ui/react/devup-ui.css\";\n;\n",
+}
diff --git a/libs/extractor/src/utils.rs b/libs/extractor/src/utils.rs
index 2140cb0f..2b2fe455 100644
--- a/libs/extractor/src/utils.rs
+++ b/libs/extractor/src/utils.rs
@@ -238,7 +238,7 @@ pub(super) fn wrap_array_filter<'a>(
let filter_member = Expression::StaticMemberExpression(builder.alloc_static_member_expression(
SPAN,
array_expr,
- builder.identifier_name(SPAN, builder.atom("filter")),
+ builder.identifier_name(SPAN, builder.str("filter")),
false,
));
@@ -251,7 +251,7 @@ pub(super) fn wrap_array_filter<'a>(
None::>>,
oxc_allocator::Vec::from_iter_in(
vec![Argument::from(
- builder.expression_identifier(SPAN, builder.atom("Boolean")),
+ builder.expression_identifier(SPAN, builder.str("Boolean")),
)],
builder.allocator,
),
@@ -262,7 +262,7 @@ pub(super) fn wrap_array_filter<'a>(
let join_member = Expression::StaticMemberExpression(builder.alloc_static_member_expression(
SPAN,
filter_call,
- builder.identifier_name(SPAN, builder.atom("join")),
+ builder.identifier_name(SPAN, builder.str("join")),
false,
));
@@ -276,7 +276,7 @@ pub(super) fn wrap_array_filter<'a>(
oxc_allocator::Vec::from_iter_in(
vec![Argument::from(builder.expression_string_literal(
SPAN,
- builder.atom(" "),
+ builder.str(" "),
None,
))],
builder.allocator,
@@ -526,14 +526,14 @@ mod tests {
&allocator,
),
oxc_allocator::Vec::from_iter_in(
- vec![builder.expression_identifier(SPAN, builder.atom("x"))],
+ vec![builder.expression_identifier(SPAN, builder.str("x"))],
&allocator,
),
);
assert_eq!(super::get_string_by_literal_expression(&expr), None);
// Identifier 등 기타 타입 - None 반환
- let expr = builder.expression_identifier(SPAN, builder.atom("foo"));
+ let expr = builder.expression_identifier(SPAN, builder.str("foo"));
assert_eq!(super::get_string_by_literal_expression(&expr), None);
}
@@ -556,10 +556,10 @@ mod tests {
if s.starts_with('"') && s.ends_with('"') {
// String literal
let value = s.trim_matches('"');
- builder.expression_string_literal(SPAN, builder.atom(value), None)
+ builder.expression_string_literal(SPAN, builder.str(value), None)
} else {
// Identifier
- builder.expression_identifier(SPAN, builder.atom(s))
+ builder.expression_identifier(SPAN, builder.str(s))
}
})
.collect();
diff --git a/libs/extractor/src/visit.rs b/libs/extractor/src/visit.rs
index b057b88a..157b13e9 100644
--- a/libs/extractor/src/visit.rs
+++ b/libs/extractor/src/visit.rs
@@ -10,7 +10,7 @@ use crate::extractor::extract_style_from_stylex::extract_stylex_namespace_styles
use crate::extractor::{
ExtractResult, GlobalExtractResult,
extract_global_style_from_expression::extract_global_style_from_expression,
- extract_style_from_expression::extract_style_from_expression,
+ extract_style_from_expression::{LiteralHandling, extract_style_from_expression},
extract_style_from_jsx::extract_style_from_jsx,
extract_style_from_styled::extract_style_from_styled,
};
@@ -213,7 +213,7 @@ impl<'a> DevupVisitor<'a> {
{
let class_expr =
self.ast
- .expression_string_literal(SPAN, self.ast.atom(&info.class_name), None);
+ .expression_string_literal(SPAN, self.ast.str(&info.class_name), None);
let mut props = vec![];
for (param_idx, var_name) in &info.css_vars {
@@ -224,7 +224,7 @@ impl<'a> DevupVisitor<'a> {
PropertyKind::Init,
PropertyKey::StringLiteral(self.ast.alloc_string_literal(
SPAN,
- self.ast.atom(var_name),
+ self.ast.str(var_name),
None,
)),
arg_expr,
@@ -254,7 +254,7 @@ impl<'a> DevupVisitor<'a> {
{
return Some(
self.ast
- .expression_string_literal(SPAN, self.ast.atom(cn), None),
+ .expression_string_literal(SPAN, self.ast.str(cn), None),
);
}
None
@@ -352,7 +352,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> {
self.ast.alloc_import_declaration::