From ee425e84f8ea198f40d3708de50cb925113729a3 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Wed, 11 Mar 2026 11:24:41 +0900 Subject: [PATCH 1/4] Fix fragment issue --- .../changepack_log_NED5kOvrD9wOrdbVuRzvb.json | 1 + .../changepack_log_Q6z3QSwmevuk9Df9FeUME.json | 1 + .../changepack_log_pv1OXKqHvbUyYJcMD1pz2.json | 1 + Cargo.lock | 158 ++++---- SKILL.md | 364 ++++++++++++++---- bindings/devup-ui-wasm/Cargo.toml | 6 +- libs/extractor/Cargo.toml | 18 +- .../extract_style_from_member_expression.rs | 17 + libs/extractor/src/lib.rs | 28 ++ ...ractor__tests__apply_var_typography-5.snap | 21 + libs/extractor/src/visit.rs | 37 +- 11 files changed, 474 insertions(+), 178 deletions(-) create mode 100644 .changepacks/changepack_log_NED5kOvrD9wOrdbVuRzvb.json create mode 100644 .changepacks/changepack_log_Q6z3QSwmevuk9Df9FeUME.json create mode 100644 .changepacks/changepack_log_pv1OXKqHvbUyYJcMD1pz2.json create mode 100644 libs/extractor/src/snapshots/extractor__tests__apply_var_typography-5.snap diff --git a/.changepacks/changepack_log_NED5kOvrD9wOrdbVuRzvb.json b/.changepacks/changepack_log_NED5kOvrD9wOrdbVuRzvb.json new file mode 100644 index 00000000..b61c6730 --- /dev/null +++ b/.changepacks/changepack_log_NED5kOvrD9wOrdbVuRzvb.json @@ -0,0 +1 @@ +{"changes":{"bindings/devup-ui-wasm/package.json":"Patch"},"note":"Fix TSAsExpression issue","date":"2026-03-11T02:24:16.075735500Z"} \ No newline at end of file diff --git a/.changepacks/changepack_log_Q6z3QSwmevuk9Df9FeUME.json b/.changepacks/changepack_log_Q6z3QSwmevuk9Df9FeUME.json new file mode 100644 index 00000000..cb12033d --- /dev/null +++ b/.changepacks/changepack_log_Q6z3QSwmevuk9Df9FeUME.json @@ -0,0 +1 @@ +{"changes":{"bindings/devup-ui-wasm/package.json":"Patch"},"note":"Update lib","date":"2026-03-10T13:48:02.962570800Z"} \ No newline at end of file diff --git a/.changepacks/changepack_log_pv1OXKqHvbUyYJcMD1pz2.json b/.changepacks/changepack_log_pv1OXKqHvbUyYJcMD1pz2.json new file mode 100644 index 00000000..3b156c82 --- /dev/null +++ b/.changepacks/changepack_log_pv1OXKqHvbUyYJcMD1pz2.json @@ -0,0 +1 @@ +{"changes":{"bindings/devup-ui-wasm/package.json":"Patch"},"note":"Fix fragment issue","date":"2026-03-11T02:24:33.353004900Z"} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index e55a2bb7..548c7c7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -259,9 +259,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytemuck" @@ -300,9 +300,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.55" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "shlex", @@ -398,7 +398,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -672,7 +672,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1056,9 +1056,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.90" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14dc6f6450b3f6d4ed5b16327f38fed626d375a886159ca555bd7822c0c3a5a6" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -1072,9 +1072,9 @@ checksum = "a3c2a6c0b4b5637c41719973ef40c6a1cf564f9db6958350de6193fbee9c23f5" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libm" @@ -1156,7 +1156,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1252,15 +1252,14 @@ checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" [[package]] name = "oxc-browserslist" -version = "2.3.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b1853bc34cadaa90aa09f95713d8b77ec0c0d3e2d90ccf7a74216f40d20850" +checksum = "bc15cd06df6b0464b763ec97a511527047350a6bfd93daf8ac82fedf21050083" dependencies = [ "flate2", "postcard", "rustc-hash", "serde", - "serde_json", "thiserror", ] @@ -1292,9 +1291,9 @@ dependencies = [ [[package]] name = "oxc_allocator" -version = "0.115.0" +version = "0.117.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d4295cf7888893b80ae70ff65c078ae3f9f52d5381cfc7eeffab089e07305" +checksum = "97b44277218c002c09167474648a478d3d29a29095ef8950ec9f1fac016c62d7" dependencies = [ "allocator-api2", "hashbrown 0.16.1", @@ -1304,9 +1303,9 @@ dependencies = [ [[package]] name = "oxc_ast" -version = "0.115.0" +version = "0.117.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be755331a7de00100c60e03151663f26037a0dd720be238de57c036be03b4033" +checksum = "e4222e4e7a1ab01b2a20420a5a65798377a748ea37ee7ece4d7a6b733f86eb61" dependencies = [ "bitflags", "oxc_allocator", @@ -1321,9 +1320,9 @@ dependencies = [ [[package]] name = "oxc_ast_macros" -version = "0.115.0" +version = "0.117.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13a58adcfaadd4710b4f7d80ad422599ed5bb4956f4747d07e821c5897b16ef" +checksum = "8e65a38ae589e284dd45a85008024f04aa680e9ddf1321c163cf7f187c805e91" dependencies = [ "phf", "proc-macro2", @@ -1333,9 +1332,9 @@ dependencies = [ [[package]] name = "oxc_ast_visit" -version = "0.115.0" +version = "0.117.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e33ffb874949ea07fce9b686c2dba7e221c849e047232c04a84b13bae4496ef" +checksum = "7fddbcd453c55d11995a55f5b1b0dec2768f7e578eb0a7fdcf17d7724c2b45e2" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1345,9 +1344,9 @@ dependencies = [ [[package]] name = "oxc_codegen" -version = "0.115.0" +version = "0.117.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f81db7038dc0288704c5ad72453c96933a46e2d5139376c87b1f5730b3d9cd03" +checksum = "62ac61963e2af6c1d1b2fd8716bef2657d9470a1a9a6527c0a417cffedf796cd" dependencies = [ "bitflags", "cow-utils", @@ -1366,9 +1365,9 @@ dependencies = [ [[package]] name = "oxc_compat" -version = "0.115.0" +version = "0.117.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96a136e3422c1b14babd3fe1103e4bc93036c10e72fe4f8634c881ec5285c2d" +checksum = "5e2fc6f1baab710dd63a9a1e973c65e4d4e42ecba8f8ce00a6e3f9ace5ff6d15" dependencies = [ "cow-utils", "oxc-browserslist", @@ -1379,18 +1378,18 @@ dependencies = [ [[package]] name = "oxc_data_structures" -version = "0.115.0" +version = "0.117.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6c22a48542899e5f74162d55710ea2f95735c5d3a809196308b2dbf557f434" +checksum = "f53bed71cad192596aee8f87f6d6bc2a38a4f898255a69b1d41da1968b9b2c6f" dependencies = [ "ropey", ] [[package]] name = "oxc_diagnostics" -version = "0.115.0" +version = "0.117.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe5961a78ce2a24d288f5e7090f19ce49d062486e0d65e6140d01582198c94fd" +checksum = "1a2d2491c0a1ea29a83abe645424f85c64b5c825f60e5304a453e4314a8b6d88" dependencies = [ "cow-utils", "oxc-miette", @@ -1399,9 +1398,9 @@ dependencies = [ [[package]] name = "oxc_ecmascript" -version = "0.115.0" +version = "0.117.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1fb3d121c372df31514f95d87c92693001739d2c9e56be37909499b5396faf1" +checksum = "71b23b64fa8c4a84b1406de383c4666366c9f54ffb9cb11a63b8d7433950460a" dependencies = [ "cow-utils", "num-bigint", @@ -1415,9 +1414,9 @@ dependencies = [ [[package]] name = "oxc_estree" -version = "0.115.0" +version = "0.117.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38fc12975751e104dc53c369cba1598ff15aa8ca30aaac49e63937256316969" +checksum = "a47515ead44bc8beec1ae1514f10ecca63cde043da167c0395dc914f098ea5d2" [[package]] name = "oxc_index" @@ -1431,9 +1430,9 @@ dependencies = [ [[package]] name = "oxc_parser" -version = "0.115.0" +version = "0.117.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "341602ba5eb6629f7f90e49c1fce06bb8eed989412a4178acd8e7f48cf2c7f9d" +checksum = "3278d4f34d01cdaf85a2391d7b12daba1d95c20c1ff2ac9316d3c28f36353e4e" dependencies = [ "bitflags", "cow-utils", @@ -1454,9 +1453,9 @@ dependencies = [ [[package]] name = "oxc_regular_expression" -version = "0.115.0" +version = "0.117.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e810182cbde172aeada70acc45dae74f6773384e0d295cc27e6e377b1fc277c" +checksum = "f3d680252672b22c24abbaf6e401eace0be9f53072a03411936204625ff349d0" dependencies = [ "bitflags", "oxc_allocator", @@ -1470,9 +1469,9 @@ dependencies = [ [[package]] name = "oxc_semantic" -version = "0.115.0" +version = "0.117.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb04bd9f59bb6d8340bb186b0003bb6e8f1988e17048c61a5473ea216e9ed71" +checksum = "208725f572872b1d53d3d734f959eada9f3b93ca9f64381a625d4e55ec6dea19" dependencies = [ "itertools 0.14.0", "memchr", @@ -1505,9 +1504,9 @@ dependencies = [ [[package]] name = "oxc_span" -version = "0.115.0" +version = "0.117.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9999ef787b0b989b8c2b31669069d3bdca20d017ff34a7284ff9e983cf7b1d8" +checksum = "b6eb1bd62de89fb0c646bfb053b72370750fab43a84ebe09ad97cfa020712314" dependencies = [ "compact_str", "oxc-miette", @@ -1519,9 +1518,9 @@ dependencies = [ [[package]] name = "oxc_str" -version = "0.115.0" +version = "0.117.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6fde66bc256ea0d09895c2a56a24f79e76abffd977f6c171516e42f1efdea51" +checksum = "2e65cbfb06ecbae07e0da931815b6b03ade886d016302c400bda7dc0a2f600d3" dependencies = [ "compact_str", "hashbrown 0.16.1", @@ -1531,9 +1530,9 @@ dependencies = [ [[package]] name = "oxc_syntax" -version = "0.115.0" +version = "0.117.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e77ea5bd4ea42ce05b2f51bcef8e46a4598cea5038ab25877a2d27601a90da83" +checksum = "e0f1617f0aa890517fb61ffa1d2d73a8497aca52e84ef6f027fad1e93250eccc" dependencies = [ "bitflags", "cow-utils", @@ -1550,9 +1549,9 @@ dependencies = [ [[package]] name = "oxc_transformer" -version = "0.115.0" +version = "0.117.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58dd1805067e1770a648cd53fcf6c48da4312fedda734ef556880936f975320f" +checksum = "a57e160e5a2df719b7cfcd23f2d291bfdbbc887ec3bd0d7147214e7aa3a7b0a6" dependencies = [ "base64", "compact_str", @@ -1579,9 +1578,9 @@ dependencies = [ [[package]] name = "oxc_traverse" -version = "0.115.0" +version = "0.117.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aea73a8421e6a433a187fca1c5fe48237ee65eaf40e5dae158d2853f0b2d8949" +checksum = "74dabd9c79380a5b11b020eed0affc488f9ceeb6557fd81610653998d30af583" dependencies = [ "itoa", "oxc_allocator", @@ -1712,9 +1711,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "plotters" @@ -1806,9 +1805,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -2001,7 +2000,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -2325,7 +2324,7 @@ dependencies = [ "getrandom", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -2525,9 +2524,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.113" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60722a937f594b7fde9adb894d7c092fc1bb6612897c46368d18e7a20208eff2" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -2538,9 +2537,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.63" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a89f4650b770e4521aa6573724e2aed4704372151bd0de9d16a3bbabb87441a" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", "futures-util", @@ -2552,9 +2551,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.113" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac8c6395094b6b91c4af293f4c79371c163f9a6f56184d2c9a85f5a95f3950" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2562,9 +2561,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.113" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3fabce6159dc20728033842636887e4877688ae94382766e00b180abac9d60" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -2575,18 +2574,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.113" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0e091bdb824da87dc01d967388880d017a0a9bc4f3bdc0d86ee9f9336e3bb5" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-bindgen-test" -version = "0.3.63" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e6fc7a6f61926fa909ee570d4ca194e264545ebbbb4ffd63ac07ba921bff447" +checksum = "6311c867385cc7d5602463b31825d454d0837a3aba7cdb5e56d5201792a3f7fe" dependencies = [ "async-trait", "cast", @@ -2606,9 +2605,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.63" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f745a117245c232859f203d6c8d52c72d4cfc42de7e668c147ca6b3e45f1157e" +checksum = "67008cdde4769831958536b0f11b3bdd0380bde882be17fff9c2f34bb4549abd" dependencies = [ "proc-macro2", "quote", @@ -2617,15 +2616,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-shared" -version = "0.2.113" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f88e7ae201cc7c291da857532eb1c8712e89494e76ec3967b9805221388e938" +checksum = "cfe29135b180b72b04c74aa97b2b4a2ef275161eff9a6c7955ea9eaedc7e1d4e" [[package]] name = "web-sys" -version = "0.3.90" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "705eceb4ce901230f8625bd1d665128056ccbe4b7408faa625eec1ba80f59a97" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -2653,7 +2652,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -2677,6 +2676,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/SKILL.md b/SKILL.md index 8ddcb656..f1036d81 100644 --- a/SKILL.md +++ b/SKILL.md @@ -5,11 +5,13 @@ description: | TRIGGER WHEN: - Writing/modifying Devup UI components (Box, Flex, Grid, Text, Button, etc.) - - Using styling APIs: css(), styled(), globalCss(), keyframes() - - Configuring devup.json theme (colors, typography) + - Using styling APIs: css(), globalCss(), keyframes() + - Configuring devup.json theme (colors, typography, extends) - Setting up build plugins (Vite, Next.js, Webpack, Rsbuild, Bun) - Debugging "Cannot run on the runtime" errors - - Working with responsive arrays or pseudo-selectors (_hover, _dark, etc.) + - 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.) --- # Devup UI @@ -18,7 +20,7 @@ Build-time CSS extraction. No runtime JS for styling. ## Critical: Components Are Compile-Time Only -All `@devup-ui/react` components (`Box`, `Flex`, `Text`, etc.) throw `Error('Cannot run on the runtime')`. They are **placeholders** that build plugins transform to `
`. +All `@devup-ui/react` components throw `Error('Cannot run on the runtime')`. They are **placeholders** that build plugins transform to native HTML elements with classNames. ```tsx // BEFORE BUILD (what you write): @@ -28,23 +30,120 @@ All `@devup-ui/react` components (`Box`, `Flex`, `Text`, etc.) throw `Error('Can
// + CSS: .a{background:red} .b{padding:16px} .c:hover{background:blue} ``` -## Style Prop Syntax +## Components + +### @devup-ui/react (Layout Primitives) + +All are polymorphic (accept `as` prop). Default element is `
` unless noted. + +| Component | Default Element | Purpose | +|-----------|----------------|---------| +| `Box` | `div` | Base layout primitive, accepts all style props | +| `Flex` | `div` | Flexbox container (shorthand for `display: flex`) | +| `Grid` | `div` | CSS Grid container | +| `VStack` | `div` | Vertical stack (flex column) | +| `Center` | `div` | Centered content | +| `Text` | `p` | Text/typography | +| `Image` | `img` | Image element | +| `Input` | `input` | Input element | +| `Button` | `button` | Button element | +| `ThemeScript` | -- | SSR theme hydration (add to ``) | + +### @devup-ui/components (Pre-built UI) + +Higher-level components with built-in behavior. These are **runtime components** (not compile-time only). -### Shorthands (ALWAYS use these) +| Component | Key Props | +|-----------|-----------| +| `Button` | `variant` (`primary`/`default`), `size` (`sm`/`md`/`lg`), `loading`, `danger`, `icon`, `colors` | +| `Checkbox` | `children` (label), `onChange(checked)`, `colors` | +| `Input` | `error`, `errorMessage`, `allowClear`, `icon`, `typography`, `colors` | +| `Textarea` | `error`, `errorMessage`, `typography`, `colors` | +| `Radio` | `variant` (`default`/`button`), `colors` | +| `RadioGroup` | `options[]`, `direction` (`row`/`column`), `variant`, `value`, `onChange` | +| `Toggle` | `variant` (`default`/`switch`), `value`, `onChange(boolean)`, `colors` | +| `Select` | `type` (`default`/`radio`/`checkbox`), `options[]`, `value`, `onChange`, `colors` | +| `Stepper` | `min`, `max`, `type` (`input`/`text`), `value`, `onValueChange` | -| Short | Full | Short | Full | -|-------|------|-------|------| -| `bg` | background | `m`, `mt`, `mr`, `mb`, `ml`, `mx`, `my` | margin-* | -| `p`, `pt`, `pr`, `pb`, `pl`, `px`, `py` | padding-* | `w`, `h` | width, height | -| `minW`, `maxW`, `minH`, `maxH` | min/max width/height | `boxSize` | width + height (same value) | -| `gap` | gap | | | +**Select compound:** `SelectTrigger`, `SelectContainer`, `SelectOption`, `SelectDivider` +**Stepper compound:** `StepperContainer`, `StepperDecreaseButton`, `StepperIncreaseButton`, `StepperInput` +**Hooks:** `useSelect()`, `useStepper()` -### Spacing Scale (× 4 = px) +All components accept a `colors` prop object for runtime color customization via CSS variables. + +## Style Prop Syntax + +### Shorthand Props (ALWAYS prefer these) + +**Spacing (unitless number x 4 = px)** + +| Shorthand | CSS Property | +|-----------|-------------| +| `m`, `mt`, `mr`, `mb`, `ml`, `mx`, `my` | margin-* | +| `p`, `pt`, `pr`, `pb`, `pl`, `px`, `py` | padding-* | + +**Sizing** + +| Shorthand | CSS Property | +|-----------|-------------| +| `w` | width | +| `h` | height | +| `minW`, `maxW` | min-width, max-width | +| `minH`, `maxH` | min-height, max-height | +| `boxSize` | width + height (same value) | + +**Background** + +| Shorthand | CSS Property | +|-----------|-------------| +| `bg` | background | +| `bgColor` | background-color | +| `bgImage`, `bgImg`, `backgroundImg` | background-image | +| `bgSize` | background-size | +| `bgPosition`, `bgPos` | background-position | +| `bgPositionX`, `bgPosX` | background-position-x | +| `bgPositionY`, `bgPosY` | background-position-y | +| `bgRepeat` | background-repeat | +| `bgAttachment` | background-attachment | +| `bgClip` | background-clip | +| `bgOrigin` | background-origin | +| `bgBlendMode` | background-blend-mode | + +**Border** + +| Shorthand | CSS Property | +|-----------|-------------| +| `borderTopRadius` | border-top-left-radius + border-top-right-radius | +| `borderBottomRadius` | border-bottom-left-radius + border-bottom-right-radius | +| `borderLeftRadius` | border-top-left-radius + border-bottom-left-radius | +| `borderRightRadius` | border-top-right-radius + border-bottom-right-radius | + +**Layout & Position** + +| Shorthand | CSS Property | +|-----------|-------------| +| `flexDir` | flex-direction | +| `pos` | position | +| `positioning` | Helper: `"top"`, `"bottom-right"`, etc. (sets edges to 0) | +| `objectPos` | object-position | +| `offsetPos` | offset-position | +| `maskPos` | mask-position | +| `maskImg` | mask-image | + +**Typography** + +| Shorthand | Effect | +|-----------|--------| +| `typography` | Applies theme typography token (fontFamily, fontSize, fontWeight, lineHeight, letterSpacing) | + +All standard CSS properties from `csstype` are also accepted directly (e.g., `display`, `gap`, `opacity`, `transform`, `animation`, etc.). + +### Spacing Scale (unitless number x 4 = px) ```tsx // padding: 4px // padding: 16px - // padding: 16px (unitless string also × 4) + // padding: 16px (unitless string also x 4) // padding: 20px (with unit = exact value) ``` @@ -65,13 +164,60 @@ All `@devup-ui/react` components (`Box`, `Flex`, `Text`, etc.) throw `Error('Can ``` +All CSS pseudo-classes and pseudo-elements from `csstype` are supported with `_camelCase` naming. + +### Group Selectors + +Style children based on parent state: + +```tsx + + + +``` + +### Theme Selectors + +```tsx + + +``` + +### At-Rules (Media, Container, Supports) + +```tsx +// Underscore prefix syntax + + + + + + +// @ prefix syntax (equivalent) + +``` + +### Custom Selectors + +```tsx +"' }, + "&:nth-child(2n)": { bg: "gray" }, +}} /> +``` + ### Dynamic Values = CSS Variables ```tsx @@ -85,20 +231,49 @@ All `@devup-ui/react` components (`Box`, `Flex`, `Text`, etc.) throw `Error('Can // className={isActive ? "a" : "b"} ``` -### Dynamic Values with Custom Components +### Responsive + Pseudo Combined + +```tsx + +// Alternative syntax: + +``` -`css()` only accepts **static values** (extracted at build time). For dynamic values on custom components, use ``: +## Special Props + +### `as` (Polymorphic Element) + +Changes the rendered HTML element or renders a custom component: ```tsx -// WRONG - css() cannot handle dynamic values -const MyComponent = ({ width }) => ( - // ERROR: width is dynamic! -); + // renders
+ // renders + // renders with extracted styles + // conditional element type +``` + +### `props` (Pass-Through to `as` Component) -// CORRECT - use Box with `as` prop for dynamic values -const MyComponent = ({ width }) => ( - // Works: generates CSS variable -); +When `as` is a custom component, use `props` to pass component-specific props: + +```tsx + +``` + +### `styleVars` (Manual CSS Variable Injection) + +```tsx + +``` + +### `styleOrder` (CSS Cascade Priority) + +Controls specificity when combining `className` with direct props. **Required** when mixing `css()` classNames with inline style props. + +```tsx + +// Conditional styleOrder + ``` ## Styling APIs @@ -106,95 +281,138 @@ const MyComponent = ({ width }) => ( ### css() Returns className String (NOT object) ```tsx -import { css, styled, globalCss, keyframes } from "@devup-ui/react"; +import { css, globalCss, keyframes } from "@devup-ui/react"; import clsx from "clsx"; -// css() returns a className STRING - use with className prop +// css() returns a className STRING const cardStyle = css({ bg: "white", p: 4, borderRadius: "8px" }); - // CORRECT - -// WRONG - css() is NOT an object to spread -// // ERROR! +
-// Combine multiple styles with clsx +// Combine with clsx const baseStyle = css({ p: 4, borderRadius: "8px" }); const activeStyle = css({ bg: "$primary", color: "white" }); - -// styleOrder={1} REQUIRED when mixing className with direct props - -``` - -### styled() API - -```tsx -// Styled component (familiar styled-components/Emotion API) -const Card = styled("div", { bg: "white", p: 4, _hover: { shadow: "lg" } }); ``` ### globalCss() and keyframes() ```tsx -// Global styles globalCss({ body: { margin: 0 }, "*": { boxSizing: "border-box" } }); -// Keyframes const spin = keyframes({ from: { transform: "rotate(0)" }, to: { transform: "rotate(360deg)" } }); ``` +### Dynamic Values with Custom Components + +`css()` only accepts **static values**. For dynamic values on custom components, use ``: + +```tsx +// WRONG - css() cannot handle dynamic values + + +// CORRECT - Box with as prop handles dynamic values via CSS variables + +``` + ## Theme (devup.json) ```json { + "extends": ["./base-theme.json"], "theme": { "colors": { - "default": { "primary": "#0070f3", "text": "#000" }, - "dark": { "primary": "#3291ff", "text": "#fff" } + "default": { "primary": "#0070f3", "text": "#000", "bg": "#fff" }, + "dark": { "primary": "#3291ff", "text": "#fff", "bg": "#111" } }, "typography": { - "heading": { "fontFamily": "Pretendard", "fontSize": "24px", "fontWeight": 700 } + "heading": { + "fontFamily": "Pretendard", + "fontSize": "24px", + "fontWeight": 700, + "lineHeight": 1.3, + "letterSpacing": "-0.02em" + }, + "body": [ + { "fontSize": "14px", "lineHeight": 1.5 }, + null, + { "fontSize": "16px", "lineHeight": 1.6 } + ] } } } ``` -Use colors with `$` prefix: `` -Use typography without prefix: `` +- **Colors**: Use with `$` prefix in JSX props: `` +- **Typography**: Use with `$` prefix: `` +- **extends**: Inherit from base config files (deep merge, last wins) +- **Responsive typography**: Use arrays with `null` for unchanged breakpoints + +Theme types are auto-generated via module augmentation of `DevupTheme` and `DevupThemeTypography`. + +### Theme API -Theme API: ```tsx import { useTheme, setTheme, getTheme, initTheme, ThemeScript } from "@devup-ui/react"; -setTheme("dark"); // switch theme -const theme = useTheme(); // hook for current theme - // SSR hydration (add to ) + +setTheme("dark"); // Switch theme (sets data-theme + localStorage) +const theme = getTheme(); // Get current theme name +const theme = useTheme(); // React hook (reactive) +initTheme(); // Initialize on startup (auto-detect system preference) + // SSR hydration script (add to , prevents FOUC) ``` ## Build Plugin Setup +### Vite + ```ts -// vite.config.ts import DevupUI from "@devup-ui/vite-plugin"; export default defineConfig({ plugins: [react(), DevupUI()] }); +``` + +### Next.js -// next.config.ts +```ts import { DevupUI } from "@devup-ui/next-plugin"; -export default DevupUI({ - // Next.js config here -}); +export default DevupUI({ /* Next.js config */ }); +``` -// rsbuild.config.ts +### Rsbuild + +```ts import DevupUI from "@devup-ui/rsbuild-plugin"; export default defineConfig({ plugins: [DevupUI()] }); ``` -Options: -- `singleCss: true` - single CSS file (recommended for Turbopack) -- `include: ["@devup/hello"]` - process external libraries that use @devup-ui internally +### Webpack + +```ts +import { DevupUIWebpackPlugin } from "@devup-ui/webpack-plugin"; +// Add to plugins array +``` + +### Bun + +```ts +import { plugin } from "@devup-ui/bun-plugin"; +// Auto-registers, always uses singleCss: true +``` + +### Plugin Options ```ts -// When using external library that uses @devup-ui (e.g. @devup/hello) -DevupUI({ include: ["@devup/hello"] }) // required to extract and merge their styles +DevupUI({ + singleCss: true, // Single CSS file (recommended for Turbopack) + include: ["@devup/hello"], // Process external libs using @devup-ui + prefix: "du", // Class name prefix (e.g., "du-a" instead of "a") + debug: true, // Enable debug logging + importAliases: { // Redirect imports from other CSS-in-JS libs + "@emotion/styled": "styled", // default: enabled + "styled-components": "styled", // default: enabled + "@vanilla-extract/css": true, // default: enabled + }, +}) ``` ## $color Token Scope @@ -204,15 +422,15 @@ DevupUI({ include: ["@devup/hello"] }) // required to extract and merge their s ```tsx // CORRECT - $color in JSX prop - // inline object OK + // WRONG - $color in external object (won't be transformed) -const colors = { active: '$primary' } // '$primary' stays as string literal +const colors = { active: '$primary' } // broken! // CORRECT - var(--color) in external object const colors = { active: 'var(--primary)' } - // works + ``` ## Inline Variant Pattern (Preferred) @@ -220,15 +438,15 @@ const colors = { active: 'var(--primary)' } Use inline object indexing instead of external config objects: ```tsx -// PREFERRED - inline object indexing +// PREFERRED - inline object indexing (build-time extractable) -// AVOID - external config object +// AVOID - external config object (becomes dynamic, uses CSS variables) const sizeStyles = { lg: { h: '48px' }, md: { h: '40px' } } - // unnecessary indirection + ``` ## Anti-Patterns (NEVER do) @@ -241,3 +459,9 @@ const sizeStyles = { lg: { h: '48px' }, md: { h: '40px' } } | `$color` in external object | `var(--color)` in external object | $color only transformed in JSX props | | No build plugin configured | Configure plugin first | Components throw at runtime without transformation | | `as any` on style props | Fix types properly | Type errors indicate real issues | +| `@ts-ignore` / `@ts-expect-error` | Fix the type issue | Suppression hides real problems | +| `background="red"` | `bg="red"` | Always use shorthands | +| `padding={4}` | `p={4}` | Always use shorthands | +| `width="100%"` | `w="100%"` | Always use shorthands | +| `styled("div", {...})` | `` | Use Box component with props, not styled() | +| `stylex.create({...})` | `` | Use Box component with props, not stylex | diff --git a/bindings/devup-ui-wasm/Cargo.toml b/bindings/devup-ui-wasm/Cargo.toml index 8cd360c0..e33d9a27 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.113" +wasm-bindgen = "0.2.114" extractor = { path = "../../libs/extractor" } sheet = { path = "../../libs/sheet" } css = { path = "../../libs/css" } @@ -27,13 +27,13 @@ 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.90" +js-sys = "0.3.91" 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.63" +wasm-bindgen-test = "0.3.64" serial_test = "3.4.0" insta = "1.46.3" rstest = "0.26.1" diff --git a/libs/extractor/Cargo.toml b/libs/extractor/Cargo.toml index 48da23e0..fd777eb5 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.115.0" -oxc_syntax = "0.115.0" -oxc_span = "0.115.0" -oxc_allocator = "0.115.0" -oxc_ast = "0.115.0" -oxc_ast_visit = "0.115.0" -oxc_codegen = "0.115.0" -oxc_transformer = "0.115.0" -oxc_semantic = "0.115.0" +oxc_parser = "0.117.0" +oxc_syntax = "0.117.0" +oxc_span = "0.117.0" +oxc_allocator = "0.117.0" +oxc_ast = "0.117.0" +oxc_ast_visit = "0.117.0" +oxc_codegen = "0.117.0" +oxc_transformer = "0.117.0" +oxc_semantic = "0.117.0" css = { path = "../css" } phf = "0.13" strum = "0.28.0" 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 091e4f27..4cd27bea 100644 --- a/libs/extractor/src/extractor/extract_style_from_member_expression.rs +++ b/libs/extractor/src/extractor/extract_style_from_member_expression.rs @@ -28,6 +28,23 @@ pub(super) fn extract_style_from_member_expression<'a>( let mem_expression = &mem.expression.clone_in(ast_builder.allocator); let mut ret: Vec = vec![]; + // Unwrap type assertions and parenthesized expressions (e.g., `({...} as const)[key]`) + loop { + let inner = match &mem.object { + Expression::TSAsExpression(ts_as) => { + Some(ts_as.expression.clone_in(ast_builder.allocator)) + } + Expression::ParenthesizedExpression(p) => { + Some(p.expression.clone_in(ast_builder.allocator)) + } + _ => None, + }; + match inner { + Some(unwrapped) => mem.object = unwrapped, + None => break, + } + } + if let Expression::ArrayExpression(array) = &mut mem.object && !array.elements.is_empty() { diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs index 66a8a8c2..c35d22dc 100644 --- a/libs/extractor/src/lib.rs +++ b/libs/extractor/src/lib.rs @@ -4160,6 +4160,34 @@ import clsx from 'clsx' ) .unwrap() )); + + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {Text} from '@devup-ui/core' + + {children} + + "#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: true, + import_main_css: false, + import_aliases: HashMap::new() + } + ) + .unwrap() + )); } #[test] diff --git a/libs/extractor/src/snapshots/extractor__tests__apply_var_typography-5.snap b/libs/extractor/src/snapshots/extractor__tests__apply_var_typography-5.snap new file mode 100644 index 00000000..35b02b3a --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__apply_var_typography-5.snap @@ -0,0 +1,21 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Text} from '@devup-ui/core'\n \n {children}\n \n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Typography( + "button", + ), + Typography( + "buttonLg", + ), + Typography( + "buttonSm", + ), + Typography( + "tag", + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n\n {children}\n ;\n", +} diff --git a/libs/extractor/src/visit.rs b/libs/extractor/src/visit.rs index d6e0dab3..b057b88a 100644 --- a/libs/extractor/src/visit.rs +++ b/libs/extractor/src/visit.rs @@ -74,6 +74,10 @@ pub struct DevupVisitor<'a> { /// Maps variable names to their keyframe animation names. /// e.g., "fadeIn" → "a-a" stylex_keyframe_names: FxHashMap, + /// Pending JSXFragment children from dynamic `as` prop resolution. + /// Set in `visit_jsx_element`, consumed in `visit_expression` to replace + /// `Expression::JSXElement` with `Expression::JSXFragment`. + pending_fragment_children: Option>>, } impl<'a> DevupVisitor<'a> { @@ -103,6 +107,7 @@ impl<'a> DevupVisitor<'a> { stylex_namespaces: FxHashMap::default(), stylex_pending_keyframe_name: None, stylex_keyframe_names: FxHashMap::default(), + pending_fragment_children: None, } } } @@ -719,6 +724,16 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { self.ast.expression_identifier(SPAN, self.ast.atom("")) } } + + // Replace JSXElement with JSXFragment when dynamic `as` prop produced an empty name + if let Some(children) = self.pending_fragment_children.take() { + *it = self.ast.expression_jsx_fragment( + SPAN, + self.ast.jsx_opening_fragment(SPAN), + children, + self.ast.jsx_closing_fragment(SPAN), + ); + } } fn visit_call_expression(&mut self, it: &mut CallExpression<'a>) { let jsx = if let Expression::Identifier(ident) = &it.callee { @@ -1217,27 +1232,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { el.expression.clone_in(self.ast.allocator).into(), ), )); - *elem = self.ast.jsx_element( - SPAN, - self.ast.alloc_jsx_opening_element( - SPAN, - self.ast - .jsx_element_name_identifier(SPAN, self.ast.atom("")), - Some(self.ast.alloc_ts_type_parameter_instantiation( - SPAN, - oxc_allocator::Vec::new_in(self.ast.allocator), - )), - oxc_allocator::Vec::new_in(self.ast.allocator), - ), - children, - Some( - self.ast.alloc_jsx_closing_element( - SPAN, - self.ast - .jsx_element_name_identifier(SPAN, self.ast.atom("")), - ), - ), - ); + self.pending_fragment_children = Some(children); None } { let ident = self From 6c557535304532f1c09c3b93b585282d2e233727 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Wed, 11 Mar 2026 11:48:26 +0900 Subject: [PATCH 2/4] Add testcase --- libs/extractor/src/lib.rs | 29 +++++++++++++++++++ ...ractor__tests__apply_var_typography-6.snap | 21 ++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 libs/extractor/src/snapshots/extractor__tests__apply_var_typography-6.snap diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs index c35d22dc..bf6410a8 100644 --- a/libs/extractor/src/lib.rs +++ b/libs/extractor/src/lib.rs @@ -4188,6 +4188,35 @@ import clsx from 'clsx' ) .unwrap() )); + + reset_class_map(); + reset_file_map(); + // ParenthesizedExpression wrapping object in computed member + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {Text} from '@devup-ui/core' + + {children} + + "#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: true, + import_main_css: false, + import_aliases: HashMap::new() + } + ) + .unwrap() + )); } #[test] diff --git a/libs/extractor/src/snapshots/extractor__tests__apply_var_typography-6.snap b/libs/extractor/src/snapshots/extractor__tests__apply_var_typography-6.snap new file mode 100644 index 00000000..ba70c53a --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__apply_var_typography-6.snap @@ -0,0 +1,21 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Text} from '@devup-ui/core'\n \n {children}\n \n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Typography( + "button", + ), + Typography( + "buttonLg", + ), + Typography( + "buttonSm", + ), + Typography( + "tag", + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n\n {children}\n ;\n", +} From 344a4d4d9d3d406993ae4dd88e34088583cbb4d6 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Wed, 11 Mar 2026 12:21:54 +0900 Subject: [PATCH 3/4] Add testcase --- libs/extractor/src/lib.rs | 80 +++++++++++++++++++ ...ts__props_wrong_direct_array_select-6.snap | 31 +++++++ ...ts__props_wrong_direct_array_select-7.snap | 30 +++++++ ...ts__props_wrong_direct_array_select-8.snap | 40 ++++++++++ ...s__props_wrong_direct_object_select-5.snap | 50 ++++++++++++ 5 files changed, 231 insertions(+) create mode 100644 libs/extractor/src/snapshots/extractor__tests__props_wrong_direct_array_select-6.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__props_wrong_direct_array_select-7.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__props_wrong_direct_array_select-8.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__props_wrong_direct_object_select-5.snap diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs index bf6410a8..1600f77e 100644 --- a/libs/extractor/src/lib.rs +++ b/libs/extractor/src/lib.rs @@ -4825,6 +4825,66 @@ export default function Card({ "test.tsx", r#"import { Box } from "@devup-ui/core"; ; +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: true, + import_main_css: false, + import_aliases: HashMap::new() + } + ) + .unwrap() + )); + + // Array with spread + numeric index (spread captured, element found after spread) + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import { Flex } from "@devup-ui/core"; +; +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: true, + import_main_css: false, + import_aliases: HashMap::new() + } + ) + .unwrap() + )); + + // Array with spread + numeric index out of range (etc Some fallback) + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import { Flex } from "@devup-ui/core"; +; +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: true, + import_main_css: false, + import_aliases: HashMap::new() + } + ) + .unwrap() + )); + + // Array with spread + dynamic index + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import { Flex } from "@devup-ui/core"; +; "#, ExtractOption { package: "@devup-ui/core".to_string(), @@ -5052,6 +5112,26 @@ export default function Card({ ) .unwrap() )); + + // Object with spread + string literal key not matching (etc Some fallback) + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {Flex} from '@devup-ui/core' + + "#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: true, + import_main_css: false, + import_aliases: HashMap::new() + } + ) + .unwrap() + )); } #[test] diff --git a/libs/extractor/src/snapshots/extractor__tests__props_wrong_direct_array_select-6.snap b/libs/extractor/src/snapshots/extractor__tests__props_wrong_direct_array_select-6.snap new file mode 100644 index 00000000..413ea638 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__props_wrong_direct_array_select-6.snap @@ -0,0 +1,31 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { Flex } from \"@devup-ui/core\";\n;\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "display", + value: "flex", + level: 0, + selector: None, + style_order: Some( + 0, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "opacity", + value: "1", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__props_wrong_direct_array_select-7.snap b/libs/extractor/src/snapshots/extractor__tests__props_wrong_direct_array_select-7.snap new file mode 100644 index 00000000..0f878e5d --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__props_wrong_direct_array_select-7.snap @@ -0,0 +1,30 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { Flex } from \"@devup-ui/core\";\n;\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "display", + value: "flex", + level: 0, + selector: None, + style_order: Some( + 0, + ), + layer: None, + }, + ), + Dynamic( + ExtractDynamicStyle { + property: "opacity", + level: 0, + identifier: "arr[5]", + selector: None, + style_order: None, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__props_wrong_direct_array_select-8.snap b/libs/extractor/src/snapshots/extractor__tests__props_wrong_direct_array_select-8.snap new file mode 100644 index 00000000..ad1358a1 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__props_wrong_direct_array_select-8.snap @@ -0,0 +1,40 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { Flex } from \"@devup-ui/core\";\n;\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "display", + value: "flex", + level: 0, + selector: None, + style_order: Some( + 0, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "opacity", + value: "1", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + Dynamic( + ExtractDynamicStyle { + property: "opacity", + level: 0, + identifier: "arr[idx]", + selector: None, + style_order: None, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__props_wrong_direct_object_select-5.snap b/libs/extractor/src/snapshots/extractor__tests__props_wrong_direct_object_select-5.snap new file mode 100644 index 00000000..6c54f4a3 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__props_wrong_direct_object_select-5.snap @@ -0,0 +1,50 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Flex} from '@devup-ui/core'\n \n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "display", + value: "flex", + level: 0, + selector: None, + style_order: Some( + 0, + ), + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "opacity", + value: ".5", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "opacity", + value: "1", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + Dynamic( + ExtractDynamicStyle { + property: "opacity", + level: 0, + identifier: "rest[`nonexistent`]", + selector: None, + style_order: None, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", +} From bcdb9db95d0dfd02b16e28329b7068e5df5b25c1 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Wed, 11 Mar 2026 13:36:43 +0900 Subject: [PATCH 4/4] Refactor --- .../extract_style_from_member_expression.rs | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) 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 4cd27bea..066d4970 100644 --- a/libs/extractor/src/extractor/extract_style_from_member_expression.rs +++ b/libs/extractor/src/extractor/extract_style_from_member_expression.rs @@ -29,20 +29,14 @@ pub(super) fn extract_style_from_member_expression<'a>( let mut ret: Vec = vec![]; // Unwrap type assertions and parenthesized expressions (e.g., `({...} as const)[key]`) - loop { - let inner = match &mem.object { - Expression::TSAsExpression(ts_as) => { - Some(ts_as.expression.clone_in(ast_builder.allocator)) - } - Expression::ParenthesizedExpression(p) => { - Some(p.expression.clone_in(ast_builder.allocator)) - } - _ => None, - }; - match inner { - Some(unwrapped) => mem.object = unwrapped, - None => break, + while let Some(inner) = match &mem.object { + Expression::TSAsExpression(ts_as) => Some(ts_as.expression.clone_in(ast_builder.allocator)), + Expression::ParenthesizedExpression(p) => { + Some(p.expression.clone_in(ast_builder.allocator)) } + _ => None, + } { + mem.object = inner; } if let Expression::ArrayExpression(array) = &mut mem.object