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::>( SPAN, None, - self.ast.string_literal(SPAN, self.ast.atom(css_file), None), + self.ast.string_literal(SPAN, self.ast.str(css_file), None), None, None, ImportOrExportKind::Value, @@ -475,11 +475,11 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { // If include refs changed the className, use the combined string let value = if !include_refs.is_empty() && !class_name_str.is_empty() { self.ast - .expression_string_literal(SPAN, self.ast.atom(&class_name_str), None) + .expression_string_literal(SPAN, self.ast.str(&class_name_str), None) } else { class_name.unwrap_or_else(|| { self.ast - .expression_string_literal(SPAN, self.ast.atom(""), None) + .expression_string_literal(SPAN, self.ast.str(""), None) }) }; @@ -488,7 +488,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { PropertyKind::Init, PropertyKey::StringLiteral(self.ast.alloc_string_literal( SPAN, - self.ast.atom(&ns_name), + self.ast.str(&ns_name), None, )), value, @@ -517,7 +517,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { self.stylex_pending_keyframe_name = Some(name.clone()); *it = self .ast - .expression_string_literal(SPAN, self.ast.atom(&name), None); + .expression_string_literal(SPAN, self.ast.str(&name), None); } // Handle StyleX: stylex.props(...) calls @@ -580,10 +580,10 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { *it = match util_type.as_ref() { UtilType::Css | UtilType::Keyframes => { self.ast - .expression_string_literal(SPAN, self.ast.atom(""), None) + .expression_string_literal(SPAN, self.ast.str(""), None) } UtilType::GlobalCss => { - self.ast.expression_identifier(SPAN, self.ast.atom("")) + self.ast.expression_identifier(SPAN, self.ast.str("")) } }; } else { @@ -603,11 +603,12 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { }, 0, &None, + LiteralHandling::ExpandResponsiveThemeToken, ); if styles.is_empty() { self.ast - .expression_string_literal(SPAN, self.ast.atom(""), None) + .expression_string_literal(SPAN, self.ast.str(""), None) } else { // css can not reachable let class_name = gen_class_names( @@ -624,7 +625,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { cls } else { self.ast - .expression_string_literal(SPAN, self.ast.atom(""), None) + .expression_string_literal(SPAN, self.ast.str(""), None) } } } else if let UtilType::Keyframes = r { @@ -643,7 +644,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { .to_string(); self.styles.insert(ExtractStyleValue::Keyframes(keyframes)); self.ast - .expression_string_literal(SPAN, self.ast.atom(&name), None) + .expression_string_literal(SPAN, self.ast.str(&name), None) } else { // global let GlobalExtractResult { @@ -665,7 +666,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { } ex.extract() })); - self.ast.expression_identifier(SPAN, self.ast.atom("")) + self.ast.expression_identifier(SPAN, self.ast.str("")) } } } @@ -698,7 +699,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { cls } else { self.ast - .expression_string_literal(SPAN, self.ast.atom(""), None) + .expression_string_literal(SPAN, self.ast.str(""), None) } // already set style order } else if let UtilType::Keyframes = r { @@ -711,7 +712,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { self.styles.insert(ExtractStyleValue::Keyframes(keyframes)); self.ast - .expression_string_literal(SPAN, self.ast.atom(&name), None) + .expression_string_literal(SPAN, self.ast.str(&name), None) } else { let optimized_css = optimize_css_block(&css_str); if !optimized_css.is_empty() { @@ -721,7 +722,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { }); self.styles.insert(css); } - self.ast.expression_identifier(SPAN, self.ast.atom("")) + self.ast.expression_identifier(SPAN, self.ast.str("")) } } @@ -786,7 +787,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { let mut tag = self.ast - .expression_string_literal(SPAN, self.ast.atom(kind.to_tag()), None); + .expression_string_literal(SPAN, self.ast.str(kind.to_tag()), None); let mut props_styles = vec![]; let ExtractResult { styles, @@ -800,6 +801,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { it.arguments[1].to_expression_mut(), 0, &None, + LiteralHandling::ExpandResponsiveThemeToken, ); props_styles.extend(styles); @@ -1135,6 +1137,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { &mut spread.argument, 0, &None, + LiteralHandling::ExpandResponsiveThemeToken, ); if !styles.is_empty() { props_styles.extend(styles.into_iter().rev()); @@ -1237,7 +1240,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { } { let ident = self .ast - .jsx_element_name_identifier(SPAN, self.ast.atom(tag)); + .jsx_element_name_identifier(SPAN, self.ast.str(tag)); elem.opening_element.name = ident.clone_in(self.ast.allocator); if let Some(el) = &mut elem.closing_element { diff --git a/libs/sheet/Cargo.toml b/libs/sheet/Cargo.toml index 959b9dd2..a1774513 100644 --- a/libs/sheet/Cargo.toml +++ b/libs/sheet/Cargo.toml @@ -12,9 +12,10 @@ extractor = { path = "../extractor" } rustc-hash = "2" [dev-dependencies] -insta = "1.46.3" +insta = "1.47.2" criterion = { version = "0.8", features = ["html_reports"] } rstest = "0.26.1" +serial_test = "3.4.0" [[bench]] name = "my_benchmark" diff --git a/libs/sheet/src/lib.rs b/libs/sheet/src/lib.rs index f0e1a194..ae42d806 100644 --- a/libs/sheet/src/lib.rs +++ b/libs/sheet/src/lib.rs @@ -2,10 +2,12 @@ pub mod theme; use crate::theme::Theme; use css::{ - merge_selector, + merge_selector, sheet_to_classname, style_selector::{AtRuleKind, StyleSelector}, + theme_tokens::set_theme_token_levels, }; use extractor::extract_style::ExtractStyleProperty; +use extractor::extract_style::extract_static_style::ThemeTokenResolution; use extractor::extract_style::extract_style_value::ExtractStyleValue; use extractor::extract_style::style_property::StyleProperty; use regex_lite::Regex; @@ -324,6 +326,10 @@ impl StyleSheet { } pub fn set_theme(&mut self, theme: Theme) { + set_theme_token_levels( + theme.get_length_token_levels(), + theme.get_shadow_token_levels(), + ); self.theme = theme; } @@ -338,19 +344,58 @@ impl StyleSheet { for style in styles.iter() { match style { ExtractStyleValue::Static(st) => { - if let Some(StyleProperty::ClassName(cls)) = - style.extract(if !single_css { Some(filename) } else { None }) - && self.add_property_with_layer( - &cls, - st.property(), - st.level(), - st.value(), - st.selector(), - st.style_order(), - if !single_css { Some(filename) } else { None }, - st.layer(), - ) - { + let resolved_value = + if st.theme_token_resolution() == ThemeTokenResolution::FirstValue { + if let Some(token) = st.value().strip_prefix('$') { + match st.property() { + "box-shadow" => self + .theme + .get_default_shadow_value(token) + .map(str::to_string) + .unwrap_or_else(|| st.value().to_string()), + _ => self + .theme + .get_default_length_value(token) + .map(str::to_string) + .unwrap_or_else(|| st.value().to_string()), + } + } else { + st.value().to_string() + } + } else { + st.value().to_string() + }; + + let class_name = + if st.theme_token_resolution() == ThemeTokenResolution::FirstValue { + let selector = st.selector().map(ToString::to_string); + sheet_to_classname( + st.property(), + st.level(), + Some(&resolved_value), + selector.as_deref(), + st.style_order(), + if !single_css { Some(filename) } else { None }, + ) + } else { + match st.extract(if !single_css { Some(filename) } else { None }) { + StyleProperty::ClassName(cls) + | StyleProperty::Variable { + class_name: cls, .. + } => cls, + } + }; + + if self.add_property_with_layer( + &class_name, + st.property(), + st.level(), + &resolved_value, + st.selector(), + st.style_order(), + if !single_css { Some(filename) } else { None }, + st.layer(), + ) { collected = true; if st.style_order() == Some(0) { updated_base_style = true; @@ -850,10 +895,16 @@ mod tests { use crate::theme::{ColorTheme, Typography}; use super::*; + use css::{class_map::reset_class_map, file_map::reset_file_map}; + use extractor::extract_style::extract_static_style::{ + ExtractStaticStyle, ThemeTokenResolution, + }; use extractor::{ExtractOption, extract}; use insta::assert_debug_snapshot; use rstest::rstest; + use rustc_hash::FxHashSet; + use serial_test::serial; #[rstest] #[case("1px", "1px")] @@ -2069,7 +2120,10 @@ mod tests { } #[test] + #[serial] fn test_update_styles() { + reset_class_map(); + reset_file_map(); let mut sheet = StyleSheet::default(); sheet.update_styles(&FxHashSet::default(), "index.tsx", true); assert_debug_snapshot!( @@ -2275,4 +2329,88 @@ mod tests { assert!(css.contains("opacity:1;transform:scale(1)")); assert_debug_snapshot!(css.split("*/").nth(1).unwrap()); } + + #[test] + #[serial] + fn test_first_value_theme_token_resolution_uses_base_value_only() { + reset_class_map(); + reset_file_map(); + let mut sheet = StyleSheet::default(); + let theme: Theme = serde_json::from_str( + r#"{ + "length": { + "default": { + "containerX": ["1px", null, "2px"] + } + } + }"#, + ) + .unwrap(); + sheet.set_theme(theme); + + let mut styles = FxHashSet::default(); + styles.insert(ExtractStyleValue::Static( + ExtractStaticStyle::new("width", "$containerX", 0, None) + .with_theme_token_resolution(ThemeTokenResolution::FirstValue), + )); + + let (collected, _) = sheet.update_styles(&styles, "test.tsx", true); + assert!(collected); + + let css = sheet.create_css(None, false); + assert!(css.contains("width:1px")); + assert!(!css.contains("width:2px")); + } + + #[test] + #[serial] + fn test_first_value_without_dollar_prefix_uses_raw_value() { + reset_class_map(); + reset_file_map(); + let mut sheet = StyleSheet::default(); + + let mut styles = FxHashSet::default(); + // FirstValue resolution but value has no $ prefix — should use the raw value as-is + styles.insert(ExtractStyleValue::Static( + ExtractStaticStyle::new("width", "100px", 0, None) + .with_theme_token_resolution(ThemeTokenResolution::FirstValue), + )); + + let (collected, _) = sheet.update_styles(&styles, "test.tsx", true); + assert!(collected); + + let css = sheet.create_css(None, false); + assert!(css.contains("width:100px")); + } + + #[test] + #[serial] + fn test_first_value_box_shadow_resolves_shadow_token() { + reset_class_map(); + reset_file_map(); + let mut sheet = StyleSheet::default(); + let theme: Theme = serde_json::from_str( + r#"{ + "shadow": { + "default": { + "card": ["0 1px 2px #0003", null, "0 4px 8px #0003"] + } + } + }"#, + ) + .unwrap(); + sheet.set_theme(theme); + + let mut styles = FxHashSet::default(); + styles.insert(ExtractStyleValue::Static( + ExtractStaticStyle::new("box-shadow", "$card", 0, None) + .with_theme_token_resolution(ThemeTokenResolution::FirstValue), + )); + + let (collected, _) = sheet.update_styles(&styles, "test.tsx", true); + assert!(collected); + + let css = sheet.create_css(None, false); + assert!(css.contains("box-shadow:0 1px 2px #0003")); + } } diff --git a/libs/sheet/src/theme.rs b/libs/sheet/src/theme.rs index 700b49b9..f9f1a587 100644 --- a/libs/sheet/src/theme.rs +++ b/libs/sheet/src/theme.rs @@ -382,6 +382,15 @@ pub type LengthTheme = BTreeMap; /// e.g., { "sm": "0 1px 2px rgba(0,0,0,0.1)", "md": ["0 2px 4px rgba(0,0,0,0.1)", null, "0 4px 8px rgba(0,0,0,0.2)"] } pub type ShadowTheme = BTreeMap; +fn default_variant_key(themes: &BTreeMap) -> Option<&str> { + themes + .keys() + .find(|k| *k == "default") + .or_else(|| themes.keys().find(|k| *k == "light")) + .or_else(|| themes.keys().next()) + .map(String::as_str) +} + /// Convert a JSON number to a length value: `n * 4` + "px". fn number_to_length(n: &serde_json::Number) -> String { // as_f64() covers both integer and float JSON numbers @@ -451,7 +460,7 @@ pub struct Theme { pub typography: BTreeMap, #[serde(default, deserialize_with = "deserialize_length_themes")] pub length: BTreeMap, - #[serde(default)] + #[serde(default, alias = "shadow")] pub shadows: BTreeMap, } @@ -506,16 +515,63 @@ impl Theme { } pub fn get_default_theme(&self) -> Option { - self.colors - .keys() - .find(|k| *k == "default") - .or_else(|| { - self.colors - .keys() - .find(|k| *k == "light") - .or_else(|| self.colors.keys().next()) - }) - .cloned() + default_variant_key(&self.colors).map(str::to_string) + } + + pub fn get_length_token_levels(&self) -> BTreeMap> { + self.length.values().flat_map(|theme| theme.iter()).fold( + BTreeMap::>::new(), + |mut acc, (name, values)| { + let entry = acc.entry(name.clone()).or_default(); + for (idx, value) in values.0.iter().enumerate() { + if value.is_some() + && let Ok(level) = u8::try_from(idx) + && !entry.contains(&level) + { + entry.push(level); + } + } + acc + }, + ) + } + + pub fn get_shadow_token_levels(&self) -> BTreeMap> { + self.shadows.values().flat_map(|theme| theme.iter()).fold( + BTreeMap::>::new(), + |mut acc, (name, values)| { + let entry = acc.entry(name.clone()).or_default(); + for (idx, value) in values.0.iter().enumerate() { + if value.is_some() + && let Ok(level) = u8::try_from(idx) + && !entry.contains(&level) + { + entry.push(level); + } + } + acc + }, + ) + } + + pub fn get_default_length_value(&self, token: &str) -> Option<&str> { + let default_key = default_variant_key(&self.length)?; + self.length + .get(default_key)? + .get(token)? + .0 + .first()? + .as_deref() + } + + pub fn get_default_shadow_value(&self, token: &str) -> Option<&str> { + let default_key = default_variant_key(&self.shadows)?; + self.shadows + .get(default_key)? + .get(token)? + .0 + .first()? + .as_deref() } pub fn to_css(&self) -> String { @@ -2185,4 +2241,71 @@ mod tests { let css = theme.to_css(); assert_debug_snapshot!(css); } + + #[test] + fn test_shadow_alias_deserializes_to_shadows() { + let theme: Theme = serde_json::from_str( + r#"{ + "shadow": { + "light": { + "card": ["0 1px 2px #0003", null, "0 4px 8px #0003"] + } + } + }"#, + ) + .unwrap(); + + let shadow = theme.shadows.get("light").unwrap().get("card").unwrap(); + assert_eq!( + shadow.0, + vec![ + Some("0 1px 2px #0003".to_string()), + None, + Some("0 4px 8px #0003".to_string()) + ] + ); + } + + #[test] + fn test_get_shadow_token_levels() { + let mut theme = Theme::default(); + theme.add_shadow( + "default", + "sm", + vec![ + Some("0 1px 2px rgba(0,0,0,.1)".to_string()), + None, + Some("0 2px 4px rgba(0,0,0,.2)".to_string()), + ], + ); + theme.add_shadow( + "default", + "md", + vec![Some("0 4px 8px rgba(0,0,0,.1)".to_string())], + ); + + let levels = theme.get_shadow_token_levels(); + assert_eq!(levels.get("sm").unwrap(), &vec![0u8, 2]); + assert_eq!(levels.get("md").unwrap(), &vec![0u8]); + } + + #[test] + fn test_get_default_shadow_value() { + let mut theme = Theme::default(); + theme.add_shadow( + "default", + "card", + vec![Some("0 1px 2px #0003".to_string()), None], + ); + + assert_eq!( + theme.get_default_shadow_value("card"), + Some("0 1px 2px #0003") + ); + assert_eq!(theme.get_default_shadow_value("nonexistent"), None); + + // No shadows at all + let empty = Theme::default(); + assert_eq!(empty.get_default_shadow_value("card"), None); + } } diff --git a/packages/components/src/components/Button/__tests__/__snapshots__/index.browser.test.tsx.snap b/packages/components/src/components/Button/__tests__/__snapshots__/index.browser.test.tsx.snap index 88fd7f30..065451e1 100644 --- a/packages/components/src/components/Button/__tests__/__snapshots__/index.browser.test.tsx.snap +++ b/packages/components/src/components/Button/__tests__/__snapshots__/index.browser.test.tsx.snap @@ -201,7 +201,7 @@ exports[`Button should render loading spinner when loading is true 1`] = `