themed_styler/
lib.rs

1use indexmap::{IndexMap, IndexSet};
2use once_cell::sync::Lazy;
3use serde::{de::Deserializer, Deserialize, Serialize};
4use serde_json::json;
5#[cfg(target_arch = "wasm32")]
6use wasm_bindgen::prelude::*;
7mod default_state;
8use default_state::bundled_state;
9
10pub type CssProps = IndexMap<String, serde_json::Value>;
11pub type SelectorStyles = IndexMap<String, CssProps>; // selector -> props
12
13fn deserialize_variables<'de, D>(deserializer: D) -> Result<IndexMap<String, String>, D::Error>
14where
15    D: Deserializer<'de>,
16{
17    let value = Option::<serde_json::Value>::deserialize(deserializer)?;
18    let mut out: IndexMap<String, String> = IndexMap::new();
19    if let Some(v) = value {
20        flatten_variables(None, &v, &mut out);
21    }
22    Ok(out)
23}
24
25fn flatten_variables(prefix: Option<&str>, value: &serde_json::Value, out: &mut IndexMap<String, String>) {
26    match value {
27        serde_json::Value::Object(map) => {
28            for (k, v) in map {
29                let key = if let Some(p) = prefix {
30                    format!("{}.{}", p, k)
31                } else {
32                    k.to_string()
33                };
34                flatten_variables(Some(&key), v, out);
35            }
36        }
37        serde_json::Value::Array(arr) => {
38            for (idx, v) in arr.iter().enumerate() {
39                let key = if let Some(p) = prefix {
40                    format!("{}.{}", p, idx)
41                } else {
42                    idx.to_string()
43                };
44                flatten_variables(Some(&key), v, out);
45            }
46        }
47        serde_json::Value::Null => {}
48        serde_json::Value::Bool(b) => {
49            if let Some(p) = prefix {
50                out.insert(p.to_string(), b.to_string());
51            }
52        }
53        serde_json::Value::Number(n) => {
54            if let Some(p) = prefix {
55                out.insert(p.to_string(), n.to_string());
56            }
57        }
58        serde_json::Value::String(s) => {
59            if let Some(p) = prefix {
60                out.insert(p.to_string(), s.clone());
61            }
62        }
63    }
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize, Default)]
67pub struct ThemeEntry {
68    #[serde(default)]
69    pub name: Option<String>,
70    #[serde(default)]
71    pub inherits: Option<String>,
72    #[serde(default)]
73    pub selectors: SelectorStyles,
74    #[serde(default, deserialize_with = "deserialize_variables")]
75    pub variables: IndexMap<String, String>,
76    #[serde(default)]
77    pub breakpoints: IndexMap<String, String>,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize, Default)]
81pub struct State {
82    // New format: each theme has selectors, variables, breakpoints, and optional inherits
83    pub themes: IndexMap<String, ThemeEntry>,
84    pub default_theme: String,
85    pub current_theme: String,
86    // Legacy fields (kept for backward-compat JSON). Not used if themes[] carry variables/bps.
87    #[serde(default)]
88    pub theme_variables: IndexMap<String, IndexMap<String, String>>, // deprecated
89    #[serde(default)]
90    pub variables: IndexMap<String, String>, // deprecated global
91    #[serde(default)]
92    pub breakpoints: IndexMap<String, String>, // deprecated global
93    #[serde(default)]
94    pub used_selectors: IndexSet<String>, // deprecated: exact selector strings (kept for back-compat)
95    #[serde(default)]
96    pub used_classes: IndexSet<String>,   // observed classes on elements
97    #[serde(default)]
98    pub used_tags: IndexSet<String>,      // observed tags on elements
99    /// Observed (tag, class) pairs. Encoded as "tag|class" for JSON simplicity.
100    #[serde(default)]
101    pub used_tag_classes: IndexSet<String>,
102}
103
104#[derive(thiserror::Error, Debug)]
105pub enum Error {
106    #[error("theme not found: {0}")]
107    ThemeNotFound(String),
108}
109
110impl State {
111    pub fn new_default() -> Self {
112        // Prefer embedded Rust bundled defaults
113        return bundled_state();
114    }
115
116    /// Public helper to access the embedded default state.
117    pub fn default_state() -> Self {
118        bundled_state()
119    }
120
121    pub fn set_theme(&mut self, theme: impl Into<String>) -> Result<(), Error> {
122        let name = theme.into();
123        if !self.themes.contains_key(&name) {
124            return Err(Error::ThemeNotFound(name));
125        }
126        self.current_theme = name;
127        Ok(())
128    }
129
130    pub fn add_theme(&mut self, name: impl Into<String>, styles: SelectorStyles) {
131        let name = name.into();
132        let entry = self.themes.entry(name).or_default();
133        for (sel, props) in styles.into_iter() {
134            let e = entry.selectors.entry(sel).or_default();
135            merge_props(e, &props);
136        }
137    }
138
139    pub fn set_variables(&mut self, vars: IndexMap<String, String>) {
140        // Back-compat: set on current theme entry
141        let cur = self.current_theme.clone();
142        let entry = self.themes.entry(cur).or_default();
143        entry.variables = vars;
144    }
145
146    pub fn set_breakpoints(&mut self, map: IndexMap<String, String>) {
147        let cur = self.current_theme.clone();
148        let entry = self.themes.entry(cur).or_default();
149        entry.breakpoints = map;
150    }
151
152    pub fn set_default_theme(&mut self, name: impl Into<String>) {
153        self.default_theme = name.into();
154    }
155
156    pub fn register_selectors<I: IntoIterator<Item = String>>(&mut self, selectors: I) {
157        for s in selectors {
158            self.used_selectors.insert(s);
159        }
160    }
161
162    pub fn register_tailwind_classes<I: IntoIterator<Item = String>>(&mut self, classes: I) {
163        for c in classes {
164            self.used_classes.insert(c);
165        }
166    }
167
168    pub fn register_tags<I: IntoIterator<Item = String>>(&mut self, tags: I) {
169        for t in tags {
170            self.used_tags.insert(t);
171        }
172    }
173
174    pub fn register_tag_class(&mut self, tag: impl Into<String>, class_: impl Into<String>) {
175        let key = format!("{}|{}", tag.into(), class_.into());
176        self.used_tag_classes.insert(key);
177    }
178
179
180    pub fn clear_usage(&mut self) {
181        self.used_selectors.clear();
182        self.used_classes.clear();
183        self.used_tags.clear();
184        self.used_tag_classes.clear();
185    }
186
187    pub fn to_json(&self) -> serde_json::Value {
188        json!({
189            "themes": self.themes,
190            "default_theme": self.default_theme,
191            "current_theme": self.current_theme,
192            // legacy fields are still serialized for back-compat but may be empty
193            "theme_variables": self.theme_variables,
194            "variables": self.variables,
195            "breakpoints": self.breakpoints,
196            "used_selectors": self.used_selectors,
197            "used_classes": self.used_classes,
198            "used_tags": self.used_tags,
199            "used_tag_classes": self.used_tag_classes,
200        })
201    }
202
203    pub fn from_json(value: serde_json::Value) -> anyhow::Result<Self> {
204        let state: State = serde_json::from_value(value)?;
205        Ok(state)
206    }
207
208    pub fn css_for_web(&self) -> String {
209        // Compute CSS resolved from the effective theme (with inheritance)
210        let (eff, vars) = self.effective_theme_all();
211        let bps = self.effective_breakpoints();
212        let mut rules: Vec<(String, CssProps)> = Vec::new();
213        
214        // Build closure: if a (tag,class) pair is observed, consider both the tag and the class as used too
215        let mut used_tags: IndexSet<String> = self.used_tags.clone();
216        let mut used_classes: IndexSet<String> = self.used_classes.clone();
217        for key in &self.used_tag_classes {
218            if let Some((t, c)) = split_tag_class_key(key) {
219                used_tags.insert(t);
220                used_classes.insert(c);
221            }
222        }
223
224        // Helper to decide if a themed selector should be emitted based on observed usage.
225        // Supported selector forms:
226        //  - tag           (e.g., "h1")
227        //  - .class        (e.g., ".text-sm"), optional pseudo ":hover"
228        //  - tag.class     (e.g., "h1.text-sm"), optional pseudo ":hover"
229        for (sel, props) in eff.iter() {
230            if should_emit_selector(sel, &used_tags, &used_classes, &self.used_tag_classes) {
231                rules.push((sel.clone(), props.clone()));
232            }
233        }
234
235        // Also emit dynamic utility properties for used classes
236        for class in &used_classes {
237            let (bp_key, hover, base) = parse_prefixed_class(class);
238            let selector = if hover { format!(".{}:hover", css_escape_class(&base)) } else { format!(".{}", css_escape_class(&base)) };
239
240            // 1) Exact selector in effective theme (e.g. ".x:hover")
241            if let Some(props) = eff.get(&selector) {
242                let final_sel = wrap_with_media(&selector, bp_key.as_deref(), &bps);
243                rules.push((final_sel, props.clone()));
244                continue;
245            }
246            // 2) Dynamic generation for the base class (ignoring hover/breakpoint for props)
247            if let Some(dynamic_props) = dynamic_css_properties_for_class(&base, &vars) {
248                let sel = if hover { format!(".{}:hover", css_escape_class(&base)) } else { format!(".{}", css_escape_class(&base)) };
249                let final_sel = wrap_with_media(&sel, bp_key.as_deref(), &bps);
250                rules.push((final_sel, dynamic_props));
251                continue;
252            }
253            // 3) Fallback: class key itself in theme (rare)
254            if let Some(props) = eff.get(&base) {
255                let final_sel = wrap_with_media(&selector, bp_key.as_deref(), &bps);
256                rules.push((final_sel, props.clone()));
257            }
258        }
259
260        post_process_css(&rules, &vars)
261    }
262
263    pub fn rn_styles_for(&self, selector: &str, classes: &[String]) -> IndexMap<String, serde_json::Value> {
264        let (eff, vars) = self.effective_theme_all();
265        let mut out: IndexMap<String, serde_json::Value> = IndexMap::new();
266        if let Some(props) = eff.get(selector) {
267            merge_rn_props(&mut out, props, &vars);
268        }
269        for class in classes {
270            let (_bp, _hover, base) = parse_prefixed_class(class);
271            // Prefer base selector match from theme
272            let sel = class_to_selector(&base);
273            if let Some(props) = eff.get(&sel) {
274                merge_rn_props(&mut out, props, &vars);
275                continue;
276            }
277            // Dynamic mapping for base class
278            if let Some(dynamic_props) = dynamic_css_properties_for_class(&base, &vars) {
279                merge_rn_props(&mut out, &dynamic_props, &vars);
280                continue;
281            }
282            if let Some(props) = eff.get(&base) {
283                merge_rn_props(&mut out, props, &vars);
284            }
285        }
286        out
287    }
288
289    // Previously supported loading YAML at runtime; now defaults are embedded.
290
291    // Build the inheritance chain from current theme upward via `inherits` and default fallback
292    fn theme_chain(&self) -> Vec<String> {
293        let mut chain = Vec::new();
294        // Resolve base names
295        let default_name = if self.themes.contains_key(&self.default_theme) {
296            self.default_theme.clone()
297        } else if let Some((k, _)) = self.themes.first() { k.clone() } else { return chain };
298        let mut current_name = if self.themes.contains_key(&self.current_theme) {
299            self.current_theme.clone()
300        } else { default_name.clone() };
301        // push child first
302        let mut seen: IndexSet<String> = IndexSet::new();
303        while !seen.contains(&current_name) {
304            seen.insert(current_name.clone());
305            chain.push(current_name.clone());
306            // next parent via inherits, else stop
307            let inherits = self.themes.get(&current_name).and_then(|t| t.inherits.clone());
308            if let Some(p) = inherits {
309                current_name = p;
310            } else {
311                break;
312            }
313        }
314        if !chain.iter().any(|n| n == &default_name) {
315            chain.push(default_name);
316        }
317        chain
318    }
319
320    // Compute effective selectors + variables + breakpoints with inheritance.
321    // Child overrides parent/default on conflicts (expected for "inherits").
322    fn effective_theme_all(&self) -> (SelectorStyles, IndexMap<String, String>) {
323        let mut selectors: SelectorStyles = SelectorStyles::new();
324        let mut vars: IndexMap<String, String> = IndexMap::new();
325        // Start with deprecated globals as the lowest base
326        for (k, v) in self.variables.iter() { vars.insert(k.clone(), v.clone()); }
327        // Merge default -> parents -> child so child wins on conflicts
328        let chain = self.theme_chain();
329        for name in chain.into_iter().rev() {
330            if let Some(entry) = self.themes.get(&name) {
331                // merge selectors: later (child) overrides earlier (parent/default)
332                for (sel, props) in entry.selectors.iter() {
333                    let e = selectors.entry(sel.clone()).or_default();
334                    merge_props(e, props);
335                }
336                // merge variables
337                for (k, v) in entry.variables.iter() {
338                    vars.insert(k.clone(), v.clone());
339                }
340            }
341        }
342        (selectors, vars)
343    }
344
345    // Effective breakpoints with inheritance; child overrides parent/default.
346    pub fn effective_breakpoints(&self) -> IndexMap<String, String> {
347        let mut bps: IndexMap<String, String> = IndexMap::new();
348        // Start with deprecated globals
349        for (k, v) in self.breakpoints.iter() { bps.insert(k.clone(), v.clone()); }
350        let chain = self.theme_chain();
351        for name in chain.into_iter().rev() {
352            if let Some(entry) = self.themes.get(&name) {
353                for (k, v) in entry.breakpoints.iter() {
354                    bps.insert(k.clone(), v.clone());
355                }
356            }
357        }
358        bps
359    }
360}
361
362fn split_tag_class_key(key: &str) -> Option<(String, String)> {
363    let mut it = key.splitn(2, '|');
364    let t = it.next()?.to_string();
365    let c = it.next()?.to_string();
366    if t.is_empty() || c.is_empty() { return None; }
367    Some((t, c))
368}
369
370fn strip_hover_suffix(selector: &str) -> (&str, bool) {
371    if let Some(stripped) = selector.strip_suffix(":hover") { (stripped, true) } else { (selector, false) }
372}
373
374fn should_emit_selector(sel: &str, used_tags: &IndexSet<String>, used_classes: &IndexSet<String>, used_tag_classes: &IndexSet<String>) -> bool {
375    // Optionally handle :hover suffix
376    let (base, _hover) = strip_hover_suffix(sel);
377
378    // tag-only
379    if is_simple_tag(base) {
380        return used_tags.contains(base) || used_tag_classes.iter().any(|k| k.split('|').next() == Some(base));
381    }
382
383    // .class-only
384    if let Some(class_name) = base.strip_prefix('.') {
385        // Normalize potential escaped class names as-is
386        return used_classes.contains(class_name) || used_tag_classes.iter().any(|k| k.ends_with(&format!("|{}", class_name)));
387    }
388
389    // tag.class
390    if let Some((tag, class_name)) = split_tag_class_selector(base) {
391        let key = format!("{}|{}", tag, class_name);
392        return used_tag_classes.contains(&key) || (used_tags.contains(&tag) && used_classes.contains(&class_name));
393    }
394
395    // Other complex selectors are currently ignored
396    false
397}
398
399fn is_simple_tag(s: &str) -> bool {
400    // Match simple HTML tag-ish identifiers
401    let mut chars = s.chars();
402    match chars.next() { Some(c) if c.is_ascii_alphabetic() => {}, _ => return false }
403    chars.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
404}
405
406fn split_tag_class_selector(s: &str) -> Option<(String, String)> {
407    // "tag.class" -> (tag, class)
408    let mut parts = s.splitn(2, '.');
409    let tag = parts.next()?.to_string();
410    let class_name = parts.next()?.to_string();
411    if tag.is_empty() || class_name.is_empty() { return None; }
412    Some((tag, class_name))
413}
414
415// wasm-bindgen exports (only when compiling to wasm32)
416#[cfg(target_arch = "wasm32")]
417#[wasm_bindgen]
418pub fn render_css_for_web(state_json: &str) -> String {
419    match serde_json::from_str::<State>(state_json) {
420        Ok(s) => s.css_for_web(),
421        Err(_) => "".into(),
422    }
423}
424
425#[cfg(target_arch = "wasm32")]
426#[wasm_bindgen]
427pub fn get_rn_styles(state_json: &str, selector: &str, classes_json: &str) -> String {
428    let classes: Vec<String> = serde_json::from_str(classes_json).unwrap_or_default();
429    match serde_json::from_str::<State>(state_json) {
430        Ok(s) => serde_json::to_string(&s.rn_styles_for(selector, &classes)).unwrap_or_else(|_| "{}".into()),
431        Err(_) => "{}".into(),
432    }
433}
434
435/// Android-specific accessor for styles; currently mirrors RN mapping.
436/// Kept distinct to allow future Android-only adjustments without changing RN/web.
437#[cfg(target_arch = "wasm32")]
438#[wasm_bindgen]
439pub fn get_android_styles(state_json: &str, selector: &str, classes_json: &str) -> String {
440    get_rn_styles(state_json, selector, classes_json)
441}
442
443// Expose crate version to JS via wasm-bindgen
444#[cfg(target_arch = "wasm32")]
445#[wasm_bindgen]
446pub fn get_version() -> String {
447    // CARGO_PKG_VERSION is provided at compile time
448    env!("CARGO_PKG_VERSION").to_string()
449}
450
451// Plain Rust accessor for crate version used by Android JNI glue
452pub fn version() -> &'static str {
453    env!("CARGO_PKG_VERSION")
454}
455
456/// Return the embedded default state as a JSON string.
457#[cfg(target_arch = "wasm32")]
458#[wasm_bindgen]
459pub fn get_default_state_json() -> String {
460    let st = bundled_state();
461    match serde_json::to_string(&st.to_json()) {
462        Ok(s) => s,
463        Err(_) => "{}".to_string(),
464    }
465}
466
467/// Register a theme from JSON. On duplicate, replace the theme's selectors, inheritance, and variables.
468/// Expected JSON format: `{ "name": "theme-name", "theme": { "inherits": "parent", "selectors": {...}, "variables": {...}, "breakpoints": {...} } }`
469/// Returns the updated state as JSON, or "{}" on error.
470#[cfg(target_arch = "wasm32")]
471#[wasm_bindgen]
472pub fn register_theme_json(state_json: &str, theme_json: &str) -> String {
473    match (serde_json::from_str::<State>(state_json), serde_json::from_str::<serde_json::Value>(theme_json)) {
474        (Ok(mut state), Ok(theme_obj)) => {
475            if let (Some(name), Some(theme_entry)) = (theme_obj.get("name"), theme_obj.get("theme")) {
476                if let Ok(entry) = serde_json::from_value::<ThemeEntry>(theme_entry.clone()) {
477                    let theme_name = name.as_str().unwrap_or("").to_string();
478                    if !theme_name.is_empty() {
479                        state.themes.insert(theme_name, entry);
480                    }
481                }
482            }
483            match serde_json::to_string(&state.to_json()) {
484                Ok(s) => s,
485                Err(_) => "{}".to_string(),
486            }
487        }
488        _ => "{}".to_string(),
489    }
490}
491
492/// Set the default and current theme. Returns the updated state as JSON.
493#[cfg(target_arch = "wasm32")]
494#[wasm_bindgen]
495pub fn set_theme_json(state_json: &str, theme_name: &str) -> String {
496    match serde_json::from_str::<State>(state_json) {
497        Ok(mut state) => {
498            if state.themes.contains_key(theme_name) {
499                state.default_theme = theme_name.to_string();
500                state.current_theme = theme_name.to_string();
501            }
502            match serde_json::to_string(&state.to_json()) {
503                Ok(s) => s,
504                Err(_) => "{}".to_string(),
505            }
506        }
507        _ => "{}".to_string(),
508    }
509}
510
511/// Get all theme keys and names as JSON array: [{ "key": "default", "name": "Default Theme" }, ...]
512/// Returns array of themes from the state JSON.
513#[cfg(target_arch = "wasm32")]
514#[wasm_bindgen]
515pub fn get_theme_list_json(state_json: &str) -> String {
516    match serde_json::from_str::<State>(state_json) {
517        Ok(state) => {
518            let themes: Vec<serde_json::Value> = state.themes.iter().map(|(key, entry)| {
519                json!({
520                    "key": key,
521                    "name": entry.name.as_ref().unwrap_or(key)
522                })
523            }).collect();
524            serde_json::to_string(&themes).unwrap_or_else(|_| "[]".to_string())
525        }
526        _ => "[]".to_string(),
527    }
528}
529
530fn merge_props(into: &mut CssProps, from: &CssProps) {
531    for (k, v) in from.iter() {
532        into.insert(k.clone(), v.clone());
533    }
534}
535
536// merge_indexmap removed — unused
537
538fn css_props_string(props: &CssProps, vars: &IndexMap<String, String>) -> String {
539    let mut buf = String::new();
540    for (k, v) in props.iter() {
541        buf.push_str(k);
542        buf.push(':');
543        let val = if v.is_string() {
544            let s = v.as_str().unwrap();
545            resolve_vars(s, vars)
546        } else {
547            v.to_string()
548        };
549        buf.push_str(&val);
550        if !val.ends_with(';') {
551            buf.push(';');
552        }
553    }
554    buf
555}
556
557/// Parse var() references manually (replaces regex dependency)
558/// Matches: var(--name), var(name), with optional whitespace
559/// Supports alphanumeric, underscore, dot, and dash in variable names
560fn parse_var_references(input: &str) -> Vec<(usize, usize, String)> {
561    let mut results = Vec::new();
562    let bytes = input.as_bytes();
563    let mut i = 0;
564    
565    while i < bytes.len() {
566        // Look for "var("
567        if i + 4 <= bytes.len() && &bytes[i..i+4] == b"var(" {
568            let start = i;
569            i += 4;
570            
571            // Skip whitespace
572            while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'\n' || bytes[i] == b'\r') {
573                i += 1;
574            }
575            
576            // Check for optional -- prefix
577            let has_prefix = i + 2 <= bytes.len() && &bytes[i..i+2] == b"--";
578            if has_prefix {
579                i += 2;
580            }
581            
582            // Collect variable name: [a-zA-Z0-9_.-]+
583            let name_start = i;
584            while i < bytes.len() {
585                let c = bytes[i];
586                if (c >= b'a' && c <= b'z') || (c >= b'A' && c <= b'Z') || 
587                   (c >= b'0' && c <= b'9') || c == b'_' || c == b'.' || c == b'-' {
588                    i += 1;
589                } else {
590                    break;
591                }
592            }
593            
594            let name_end = i;
595            if name_start < name_end {
596                // Skip trailing whitespace
597                while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'\n' || bytes[i] == b'\r') {
598                    i += 1;
599                }
600                
601                // Check for closing )
602                if i < bytes.len() && bytes[i] == b')' {
603                    let end = i + 1;
604                    let var_name = std::str::from_utf8(&bytes[name_start..name_end])
605                        .unwrap_or("").to_string();
606                    results.push((start, end, var_name));
607                    i = end;
608                    continue;
609                }
610            }
611        }
612        i += 1;
613    }
614    
615    results
616}
617
618// Tailwind color palette - embedded from tailwind-colors.html
619static TAILWIND_COLORS: Lazy<IndexMap<&'static str, IndexMap<&'static str, &'static str>>> = Lazy::new(|| {
620    let mut colors = IndexMap::new();
621    
622    let mut slate = IndexMap::new();
623    slate.insert("50", "#f8fafc"); slate.insert("100", "#f1f5f9"); slate.insert("200", "#e2e8f0");
624    slate.insert("300", "#cbd5e1"); slate.insert("400", "#94a3b8"); slate.insert("500", "#64748b");
625    slate.insert("600", "#475569"); slate.insert("700", "#334155"); slate.insert("800", "#1e293b");
626    slate.insert("900", "#0f172a"); slate.insert("950", "#020617");
627    colors.insert("slate", slate);
628    
629    let mut gray = IndexMap::new();
630    gray.insert("50", "#f9fafb"); gray.insert("100", "#f3f4f6"); gray.insert("200", "#e5e7eb");
631    gray.insert("300", "#d1d5db"); gray.insert("400", "#9ca3af"); gray.insert("500", "#6b7280");
632    gray.insert("600", "#4b5563"); gray.insert("700", "#374151"); gray.insert("800", "#1f2937");
633    gray.insert("900", "#111827"); gray.insert("950", "#030712");
634    colors.insert("gray", gray);
635    
636    let mut zinc = IndexMap::new();
637    zinc.insert("50", "#fafafa"); zinc.insert("100", "#f4f4f5"); zinc.insert("200", "#e4e4e7");
638    zinc.insert("300", "#d4d4d8"); zinc.insert("400", "#a1a1aa"); zinc.insert("500", "#71717a");
639    zinc.insert("600", "#52525b"); zinc.insert("700", "#3f3f46"); zinc.insert("800", "#27272a");
640    zinc.insert("900", "#18181b"); zinc.insert("950", "#09090b");
641    colors.insert("zinc", zinc);
642    
643    let mut neutral = IndexMap::new();
644    neutral.insert("50", "#fafafa"); neutral.insert("100", "#f5f5f5"); neutral.insert("200", "#e5e5e5");
645    neutral.insert("300", "#d4d4d4"); neutral.insert("400", "#a3a3a3"); neutral.insert("500", "#737373");
646    neutral.insert("600", "#525252"); neutral.insert("700", "#404040"); neutral.insert("800", "#262626");
647    neutral.insert("900", "#171717"); neutral.insert("950", "#0a0a0a");
648    colors.insert("neutral", neutral);
649    
650    let mut stone = IndexMap::new();
651    stone.insert("50", "#fafaf9"); stone.insert("100", "#f5f5f4"); stone.insert("200", "#e7e5e4");
652    stone.insert("300", "#d6d3d1"); stone.insert("400", "#a8a29e"); stone.insert("500", "#78716c");
653    stone.insert("600", "#57534e"); stone.insert("700", "#44403c"); stone.insert("800", "#292524");
654    stone.insert("900", "#1c1917"); stone.insert("950", "#0c0a09");
655    colors.insert("stone", stone);
656    
657    let mut red = IndexMap::new();
658    red.insert("50", "#fef2f2"); red.insert("100", "#fee2e2"); red.insert("200", "#fecaca");
659    red.insert("300", "#fca5a5"); red.insert("400", "#f87171"); red.insert("500", "#ef4444");
660    red.insert("600", "#dc2626"); red.insert("700", "#b91c1c"); red.insert("800", "#991b1b");
661    red.insert("900", "#7f1d1d"); red.insert("950", "#450a0a");
662    colors.insert("red", red);
663    
664    let mut orange = IndexMap::new();
665    orange.insert("50", "#fff7ed"); orange.insert("100", "#ffedd5"); orange.insert("200", "#fed7aa");
666    orange.insert("300", "#fdba74"); orange.insert("400", "#fb923c"); orange.insert("500", "#f97316");
667    orange.insert("600", "#ea580c"); orange.insert("700", "#c2410c"); orange.insert("800", "#9a3412");
668    orange.insert("900", "#7c2d12"); orange.insert("950", "#431407");
669    colors.insert("orange", orange);
670    
671    let mut amber = IndexMap::new();
672    amber.insert("50", "#fffbeb"); amber.insert("100", "#fef3c7"); amber.insert("200", "#fde68a");
673    amber.insert("300", "#fcd34d"); amber.insert("400", "#fbbf24"); amber.insert("500", "#f59e0b");
674    amber.insert("600", "#d97706"); amber.insert("700", "#b45309"); amber.insert("800", "#92400e");
675    amber.insert("900", "#78350f"); amber.insert("950", "#451a03");
676    colors.insert("amber", amber);
677    
678    let mut blue = IndexMap::new();
679    blue.insert("50", "#eff6ff"); blue.insert("100", "#dbeafe"); blue.insert("200", "#bfdbfe");
680    blue.insert("300", "#93c5fd"); blue.insert("400", "#60a5fa"); blue.insert("500", "#3b82f6");
681    blue.insert("600", "#2563eb"); blue.insert("700", "#1d4ed8"); blue.insert("800", "#1e40af");
682    blue.insert("900", "#1e3a8a"); blue.insert("950", "#0b1c52");
683    colors.insert("blue", blue);
684    
685    let mut lime = IndexMap::new();
686    lime.insert("50", "#f7fee7"); lime.insert("100", "#ecfccb"); lime.insert("200", "#d9f99d");
687    lime.insert("300", "#bef264"); lime.insert("400", "#a3e635"); lime.insert("500", "#84cc16");
688    lime.insert("600", "#65a30d"); lime.insert("700", "#4d7c0f"); lime.insert("800", "#3f6212");
689    lime.insert("900", "#365314"); lime.insert("950", "#1a2e05");
690    colors.insert("lime", lime);
691    
692    let mut green = IndexMap::new();
693    green.insert("50", "#f0fdf4"); green.insert("100", "#dcfce7"); green.insert("200", "#bbf7d0");
694    green.insert("300", "#86efac"); green.insert("400", "#4ade80"); green.insert("500", "#22c55e");
695    green.insert("600", "#16a34a"); green.insert("700", "#15803d"); green.insert("800", "#166534");
696    green.insert("900", "#14532d"); green.insert("950", "#052e16");
697    colors.insert("green", green);
698    
699    let mut emerald = IndexMap::new();
700    emerald.insert("50", "#ecfdf5"); emerald.insert("100", "#d1fae5"); emerald.insert("200", "#a7f3d0");
701    emerald.insert("300", "#6ee7b7"); emerald.insert("400", "#34d399"); emerald.insert("500", "#10b981");
702    emerald.insert("600", "#059669"); emerald.insert("700", "#047857"); emerald.insert("800", "#065f46");
703    emerald.insert("900", "#064e3b"); emerald.insert("950", "#022c22");
704    colors.insert("emerald", emerald);
705    
706    let mut teal = IndexMap::new();
707    teal.insert("50", "#f0fdfa"); teal.insert("100", "#ccfbf1"); teal.insert("200", "#99f6e4");
708    teal.insert("300", "#5eead4"); teal.insert("400", "#2dd4bf"); teal.insert("500", "#14b8a6");
709    teal.insert("600", "#0d9488"); teal.insert("700", "#0f766e"); teal.insert("800", "#115e59");
710    teal.insert("900", "#134e4a"); teal.insert("950", "#042f2e");
711    colors.insert("teal", teal);
712    
713    let mut cyan = IndexMap::new();
714    cyan.insert("50", "#ecfeff"); cyan.insert("100", "#cffafe"); cyan.insert("200", "#a5f3fc");
715    cyan.insert("300", "#67e8f9"); cyan.insert("400", "#22d3ee"); cyan.insert("500", "#06b6d4");
716    cyan.insert("600", "#0891b2"); cyan.insert("700", "#0e7490"); cyan.insert("800", "#155e75");
717    cyan.insert("900", "#164e63"); cyan.insert("950", "#083344");
718    colors.insert("cyan", cyan);
719    
720    let mut sky = IndexMap::new();
721    sky.insert("50", "#f0f9ff"); sky.insert("100", "#e0f2fe"); sky.insert("200", "#bae6fd");
722    sky.insert("300", "#7dd3fc"); sky.insert("400", "#38bdf8"); sky.insert("500", "#0ea5e9");
723    sky.insert("600", "#0284c7"); sky.insert("700", "#0369a1"); sky.insert("800", "#075985");
724    sky.insert("900", "#0c4a6e"); sky.insert("950", "#082f49");
725    colors.insert("sky", sky);
726    
727    let mut blue = IndexMap::new();
728    blue.insert("50", "#eff6ff"); blue.insert("100", "#dbeafe"); blue.insert("200", "#bfdbfe");
729    blue.insert("300", "#93c5fd"); blue.insert("400", "#60a5fa"); blue.insert("500", "#3b82f6");
730    blue.insert("600", "#2563eb"); blue.insert("700", "#1d4ed8"); blue.insert("800", "#1e40af");
731    blue.insert("900", "#1e3a8a"); blue.insert("950", "#172554");
732    colors.insert("blue", blue);
733    
734    let mut indigo = IndexMap::new();
735    indigo.insert("50", "#eef2ff"); indigo.insert("100", "#e0e7ff"); indigo.insert("200", "#c7d2fe");
736    indigo.insert("300", "#a5b4fc"); indigo.insert("400", "#818cf8"); indigo.insert("500", "#6366f1");
737    indigo.insert("600", "#4f46e5"); indigo.insert("700", "#4338ca"); indigo.insert("800", "#3730a3");
738    indigo.insert("900", "#312e81"); indigo.insert("950", "#1e1b4b");
739    colors.insert("indigo", indigo);
740    
741    let mut violet = IndexMap::new();
742    violet.insert("50", "#f5f3ff"); violet.insert("100", "#ede9fe"); violet.insert("200", "#ddd6fe");
743    violet.insert("300", "#c4b5fd"); violet.insert("400", "#a78bfa"); violet.insert("500", "#8b5cf6");
744    violet.insert("600", "#7c3aed"); violet.insert("700", "#6d28d9"); violet.insert("800", "#5b21b6");
745    violet.insert("900", "#4c1d95"); violet.insert("950", "#2e1065");
746    colors.insert("violet", violet);
747    
748    let mut purple = IndexMap::new();
749    purple.insert("50", "#faf5ff"); purple.insert("100", "#f3e8ff"); purple.insert("200", "#e9d5ff");
750    purple.insert("300", "#d8b4fe"); purple.insert("400", "#c084fc"); purple.insert("500", "#a855f7");
751    purple.insert("600", "#9333ea"); purple.insert("700", "#7e22ce"); purple.insert("800", "#6b21a8");
752    purple.insert("900", "#581c87"); purple.insert("950", "#3b0764");
753    colors.insert("purple", purple);
754    
755    let mut fuchsia = IndexMap::new();
756    fuchsia.insert("50", "#fdf4ff"); fuchsia.insert("100", "#fae8ff"); fuchsia.insert("200", "#f5d0fe");
757    fuchsia.insert("300", "#f0abfc"); fuchsia.insert("400", "#e879f9"); fuchsia.insert("500", "#d946ef");
758    fuchsia.insert("600", "#c026d3"); fuchsia.insert("700", "#a21caf"); fuchsia.insert("800", "#86198f");
759    fuchsia.insert("900", "#701a75"); fuchsia.insert("950", "#4a044e");
760    colors.insert("fuchsia", fuchsia);
761    
762    let mut pink = IndexMap::new();
763    pink.insert("50", "#fdf2f8"); pink.insert("100", "#fce7f3"); pink.insert("200", "#fbcfe8");
764    pink.insert("300", "#f9a8d4"); pink.insert("400", "#f472b6"); pink.insert("500", "#ec4899");
765    pink.insert("600", "#db2777"); pink.insert("700", "#be185d"); pink.insert("800", "#9d174d");
766    pink.insert("900", "#831843"); pink.insert("950", "#500724");
767    colors.insert("pink", pink);
768    
769    let mut rose = IndexMap::new();
770    rose.insert("50", "#fff1f2"); rose.insert("100", "#ffe4e6"); rose.insert("200", "#fecdd3");
771    rose.insert("300", "#fda4af"); rose.insert("400", "#fb7185"); rose.insert("500", "#f43f5e");
772    rose.insert("600", "#e11d48"); rose.insert("700", "#be123c"); rose.insert("800", "#9f1239");
773    rose.insert("900", "#881337"); rose.insert("950", "#4c0519");
774    colors.insert("rose", rose);
775    
776    colors
777});
778
779fn resolve_vars(input: &str, vars: &IndexMap<String, String>) -> String {
780    let var_refs = parse_var_references(input);
781    
782    if var_refs.is_empty() {
783        // Fast path: no var() references, just check for $ prefix
784        if input.starts_with('$') {
785            if let Some(val) = vars.get(&input[1..]) {
786                return val.clone();
787            }
788        }
789        return input.to_string();
790    }
791    
792    // Replace var() references from right to left to preserve indices
793    let mut out = input.to_string();
794    for (start, end, var_name) in var_refs.iter().rev() {
795        if let Some(val) = vars.get(var_name) {
796            out.replace_range(*start..*end, val);
797        }
798    }
799    
800    // Also handle $ prefix for direct variable references
801    if out.starts_with('$') {
802        if let Some(val) = vars.get(&out[1..]) {
803            return val.clone();
804        }
805    }
806    
807    out
808}
809
810fn camel_case(name: &str) -> String {
811    let mut out = String::new();
812    let mut upper = false;
813    for ch in name.chars() {
814        if ch == '-' {
815            upper = true;
816            continue;
817        }
818        if upper {
819            out.extend(ch.to_uppercase());
820            upper = false;
821        } else {
822            out.push(ch);
823        }
824    }
825    out
826}
827
828fn css_value_to_rn(
829    value: &serde_json::Value,
830    vars: &IndexMap<String, String>,
831) -> serde_json::Value {
832    match value {
833        serde_json::Value::String(s) => {
834            let s2 = resolve_vars(s, vars);
835            if let Some(n) = s2.strip_suffix("px") {
836                if let Ok(parsed) = n.trim().parse::<f64>() {
837                    return json!(parsed);
838                }
839            }
840            json!(s2)
841        }
842        _ => value.clone(),
843    }
844}
845
846fn merge_rn_props(
847    into: &mut IndexMap<String, serde_json::Value>,
848    css_props: &CssProps,
849    vars: &IndexMap<String, String>,
850) {
851    for (k, v) in css_props.iter() {
852        let rn_key = match k.as_str() {
853            // Minimal explicit mappings
854            "background-color" => "backgroundColor".to_string(),
855            "text-align" => "textAlign".to_string(),
856            _ => camel_case(k),
857        };
858        let rn_val = css_value_to_rn(v, vars);
859        into.insert(rn_key, rn_val);
860    }
861}
862
863fn dynamic_css_properties_for_class(class: &str, vars: &IndexMap<String, String>) -> Option<CssProps> {
864    // Display utilities
865    match class {
866        "block" => { let mut p = CssProps::new(); p.insert("display".into(), json!("block")); return Some(p); }
867        "inline-block" => { let mut p = CssProps::new(); p.insert("display".into(), json!("inline-block")); return Some(p); }
868        "inline" => { let mut p = CssProps::new(); p.insert("display".into(), json!("inline")); return Some(p); }
869        "inline-flex" => { let mut p = CssProps::new(); p.insert("display".into(), json!("inline-flex")); return Some(p); }
870        "grid" => { let mut p = CssProps::new(); p.insert("display".into(), json!("grid")); return Some(p); }
871        "hidden" => { let mut p = CssProps::new(); p.insert("display".into(), json!("none")); return Some(p); }
872        _ => {}
873    }
874    // Flexbox shorthands
875    match class {
876        "flex" => { let mut p = CssProps::new(); p.insert("display".into(), json!("flex")); return Some(p); }
877        "flex-row" => { let mut p = CssProps::new(); p.insert("display".into(), json!("flex")); p.insert("flex-direction".into(), json!("row")); return Some(p); }
878        "flex-col" => { let mut p = CssProps::new(); p.insert("display".into(), json!("flex")); p.insert("flex-direction".into(), json!("column")); return Some(p); }
879        "flex-1" => { let mut p = CssProps::new(); p.insert("flex".into(), json!(1)); return Some(p); }
880        _ => {}
881    }
882    if let Some(rest) = class.strip_prefix("items-") {
883        let mut p = CssProps::new();
884        let v = match rest { "start" => "flex-start", "end" => "flex-end", "center" => "center", "stretch" => "stretch", other => other };
885        p.insert("align-items".into(), json!(v));
886        return Some(p);
887    }
888    if let Some(rest) = class.strip_prefix("justify-") {
889        let mut p = CssProps::new();
890        let v = match rest { "start" => "flex-start", "end" => "flex-end", "center" => "center", "between" => "space-between", "around" => "space-around", "evenly" => "space-evenly", other => other };
891        p.insert("justify-content".into(), json!(v));
892        return Some(p);
893    }
894    if let Some(value) = class.strip_prefix("p-") {
895        return parse_tailwind_spacing(value, &|px| padding_props(&["padding"], px));
896    }
897    if let Some(value) = class.strip_prefix("px-") {
898        return parse_tailwind_spacing(value, &|px| padding_props(&["padding-left", "padding-right"], px));
899    }
900    if let Some(value) = class.strip_prefix("py-") {
901        return parse_tailwind_spacing(value, &|px| padding_props(&["padding-top", "padding-bottom"], px));
902    }
903    for &(prefix, prop) in &[("pt-", "padding-top"), ("pr-", "padding-right"), ("pb-", "padding-bottom"), ("pl-", "padding-left")] {
904        if let Some(value) = class.strip_prefix(prefix) {
905            return parse_tailwind_spacing(value, &|px| padding_props(&[prop], px));
906        }
907    }
908    // Margin utilities
909    if let Some(value) = class.strip_prefix("m-") {
910        return parse_tailwind_spacing(value, &|px| margin_props(&["margin"], px));
911    }
912    if let Some(value) = class.strip_prefix("mx-") {
913        return parse_tailwind_spacing(value, &|px| margin_props(&["margin-left", "margin-right"], px));
914    }
915    if let Some(value) = class.strip_prefix("my-") {
916        return parse_tailwind_spacing(value, &|px| margin_props(&["margin-top", "margin-bottom"], px));
917    }
918    for &(prefix, prop) in &[("mt-", "margin-top"), ("mr-", "margin-right"), ("mb-", "margin-bottom"), ("ml-", "margin-left")] {
919        if let Some(value) = class.strip_prefix(prefix) {
920            return parse_tailwind_spacing(value, &|px| margin_props(&[prop], px));
921        }
922    }
923    // Gap utilities (works in RN 0.71+ with Flexbox)
924    if let Some(value) = class.strip_prefix("gap-") {
925        if !value.starts_with("x-") && !value.starts_with("y-") {
926            return parse_tailwind_spacing(value, &|px| {
927                let mut props = CssProps::new();
928                props.insert("gap".into(), json!(format!("{}px", px)));
929                props
930            });
931        }
932    }
933    if let Some(value) = class.strip_prefix("gap-x-") {
934        return parse_tailwind_spacing(value, &|px| {
935            let mut props = CssProps::new();
936            props.insert("column-gap".into(), json!(format!("{}px", px)));
937            props
938        });
939    }
940    if let Some(value) = class.strip_prefix("gap-y-") {
941        return parse_tailwind_spacing(value, &|px| {
942            let mut props = CssProps::new();
943            props.insert("row-gap".into(), json!(format!("{}px", px)));
944            props
945        });
946    }
947    // Space utilities (space-x-*, space-y-*)
948    if let Some(value) = class.strip_prefix("space-x-") {
949        return parse_tailwind_spacing(value, &|px| {
950            let mut props = CssProps::new();
951            // In CSS, this is typically done with :not(:last-child) selector
952            // For now, we'll set it as a custom property that can be used
953            props.insert("--space-x".into(), json!(format!("{}px", px)));
954            props
955        });
956    }
957    if let Some(value) = class.strip_prefix("space-y-") {
958        return parse_tailwind_spacing(value, &|px| {
959            let mut props = CssProps::new();
960            props.insert("--space-y".into(), json!(format!("{}px", px)));
961            props
962        });
963    }
964    // Font weight utilities
965    match class {
966        "font-thin" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("100")); return Some(p); }
967        "font-extralight" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("200")); return Some(p); }
968        "font-light" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("300")); return Some(p); }
969        "font-normal" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("400")); return Some(p); }
970        "font-medium" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("500")); return Some(p); }
971        "font-semibold" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("600")); return Some(p); }
972        "font-bold" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("700")); return Some(p); }
973        "font-extrabold" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("800")); return Some(p); }
974        "font-black" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("900")); return Some(p); }
975        _ => {}
976    }
977    // Font family utilities
978    match class {
979        "font-sans" => { let mut p = CssProps::new(); p.insert("font-family".into(), json!("system-ui, -apple-system, sans-serif")); return Some(p); }
980        "font-serif" => { let mut p = CssProps::new(); p.insert("font-family".into(), json!("Georgia, serif")); return Some(p); }
981        "font-mono" => { let mut p = CssProps::new(); p.insert("font-family".into(), json!("ui-monospace, monospace")); return Some(p); }
982        _ => {}
983    }
984    // Text size utilities
985    match class {
986        "text-xs" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("12px")); p.insert("line-height".into(), json!("16px")); return Some(p); }
987        "text-sm" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("14px")); p.insert("line-height".into(), json!("20px")); return Some(p); }
988        "text-base" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("16px")); p.insert("line-height".into(), json!("24px")); return Some(p); }
989        "text-lg" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("18px")); p.insert("line-height".into(), json!("28px")); return Some(p); }
990        "text-xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("20px")); p.insert("line-height".into(), json!("28px")); return Some(p); }
991        "text-2xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("24px")); p.insert("line-height".into(), json!("32px")); return Some(p); }
992        "text-3xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("30px")); p.insert("line-height".into(), json!("36px")); return Some(p); }
993        "text-4xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("36px")); p.insert("line-height".into(), json!("40px")); return Some(p); }
994        "text-5xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("48px")); p.insert("line-height".into(), json!("1")); return Some(p); }
995        "text-6xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("60px")); p.insert("line-height".into(), json!("1")); return Some(p); }
996        _ => {}
997    }
998    // Text alignment
999    match class {
1000        "text-left" => { let mut p = CssProps::new(); p.insert("text-align".into(), json!("left")); return Some(p); }
1001        "text-center" => { let mut p = CssProps::new(); p.insert("text-align".into(), json!("center")); return Some(p); }
1002        "text-right" => { let mut p = CssProps::new(); p.insert("text-align".into(), json!("right")); return Some(p); }
1003        "text-justify" => { let mut p = CssProps::new(); p.insert("text-align".into(), json!("justify")); return Some(p); }
1004        _ => {}
1005    }
1006    // Overflow utilities
1007    match class {
1008        "overflow-auto" => { let mut p = CssProps::new(); p.insert("overflow".into(), json!("auto")); return Some(p); }
1009        "overflow-hidden" => { let mut p = CssProps::new(); p.insert("overflow".into(), json!("hidden")); return Some(p); }
1010        "overflow-visible" => { let mut p = CssProps::new(); p.insert("overflow".into(), json!("visible")); return Some(p); }
1011        "overflow-scroll" => { let mut p = CssProps::new(); p.insert("overflow".into(), json!("scroll")); return Some(p); }
1012        "overflow-x-auto" => { let mut p = CssProps::new(); p.insert("overflow-x".into(), json!("auto")); return Some(p); }
1013        "overflow-x-hidden" => { let mut p = CssProps::new(); p.insert("overflow-x".into(), json!("hidden")); return Some(p); }
1014        "overflow-x-scroll" => { let mut p = CssProps::new(); p.insert("overflow-x".into(), json!("scroll")); return Some(p); }
1015        "overflow-y-auto" => { let mut p = CssProps::new(); p.insert("overflow-y".into(), json!("auto")); return Some(p); }
1016        "overflow-y-hidden" => { let mut p = CssProps::new(); p.insert("overflow-y".into(), json!("hidden")); return Some(p); }
1017        "overflow-y-scroll" => { let mut p = CssProps::new(); p.insert("overflow-y".into(), json!("scroll")); return Some(p); }
1018        _ => {}
1019    }
1020    // Shadow utilities (basic cross-platform support)
1021    match class {
1022        "shadow-sm" => { let mut p = CssProps::new(); p.insert("box-shadow".into(), json!("0 1px 2px 0 rgba(0, 0, 0, 0.05)")); return Some(p); }
1023        "shadow" => { let mut p = CssProps::new(); p.insert("box-shadow".into(), json!("0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)")); return Some(p); }
1024        "shadow-md" => { let mut p = CssProps::new(); p.insert("box-shadow".into(), json!("0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)")); return Some(p); }
1025        "shadow-lg" => { let mut p = CssProps::new(); p.insert("box-shadow".into(), json!("0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)")); return Some(p); }
1026        "shadow-xl" => { let mut p = CssProps::new(); p.insert("box-shadow".into(), json!("0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)")); return Some(p); }
1027        "shadow-2xl" => { let mut p = CssProps::new(); p.insert("box-shadow".into(), json!("0 25px 50px -12px rgba(0, 0, 0, 0.25)")); return Some(p); }
1028        "shadow-none" => { let mut p = CssProps::new(); p.insert("box-shadow".into(), json!("none")); return Some(p); }
1029        _ => {}
1030    }
1031    // Parse arbitrary values like bg-[var(--primary)], text-[#ff0000], etc.
1032    if let Some(arb_value) = parse_arbitrary_value(class) {
1033        return Some(arb_value);
1034    }
1035    // text-{color}-{shade}
1036    if let Some(rest) = class.strip_prefix("text-") {
1037        if let Some(hex) = get_tailwind_color(rest) {
1038            let mut props = CssProps::new();
1039            props.insert("color".into(), json!(hex));
1040            return Some(props);
1041        }
1042    }
1043    // bg-{color}-{shade}
1044    if let Some(rest) = class.strip_prefix("bg-") {
1045        if let Some(hex) = get_tailwind_color(rest) {
1046            let mut props = CssProps::new();
1047            props.insert("background-color".into(), json!(hex));
1048            return Some(props);
1049        }
1050    }
1051    // divide-{color}-{shade} (sets border-color for child dividers)
1052    if let Some(rest) = class.strip_prefix("divide-") {
1053        if let Some(hex) = get_tailwind_color(rest) {
1054            let mut props = CssProps::new();
1055            props.insert("border-color".into(), json!(hex));
1056            return Some(props);
1057        }
1058    }
1059    if class == "border" {
1060        return Some(border_props(None, 1, vars));
1061    }
1062    if let Some(rest) = class.strip_prefix("border-") {
1063        // Parse border-* classes
1064        // Possible patterns:
1065        // - border-{color}-{shade} → border-color
1066        // - border-{side}-{color}-{shade} → border-{side}-color
1067        // - border-{width} → border-width
1068        // - border-{side}-{width} → border-{side}-width
1069        
1070        let parts: Vec<&str> = rest.split('-').collect();
1071        
1072        // Check if first part is a directional side (t, b, l, r, x, y)
1073        let valid_sides = ["t", "b", "l", "r", "x", "y"];
1074        let (side, color_or_width_parts) = if parts.len() > 1 && valid_sides.contains(&parts[0]) {
1075            (Some(parts[0]), &parts[1..])
1076        } else {
1077            (None, &parts[..])
1078        };
1079        
1080        // Now check if remaining parts form a color-shade pattern
1081        if color_or_width_parts.len() == 2 {
1082            // Could be color-shade like "blue-500"
1083            let color_shade = color_or_width_parts.join("-");
1084            if let Some(hex) = get_tailwind_color(&color_shade) {
1085                let mut props = CssProps::new();
1086                let prop_name = if let Some(s) = side {
1087                    format!("border-{}-color", s)
1088                } else {
1089                    "border-color".to_string()
1090                };
1091                props.insert(prop_name, json!(hex));
1092                return Some(props);
1093            }
1094        }
1095        
1096        // Check for simple color without shade (single word color like "black", "white")
1097        if color_or_width_parts.len() == 1 {
1098            let potential_color = format!("{}-500", color_or_width_parts[0]);
1099            if let Some(hex) = get_tailwind_color(&potential_color) {
1100                let mut props = CssProps::new();
1101                let prop_name = if let Some(s) = side {
1102                    format!("border-{}-color", s)
1103                } else {
1104                    "border-color".to_string()
1105                };
1106                props.insert(prop_name, json!(hex));
1107                return Some(props);
1108            }
1109        }
1110        
1111        // Otherwise, check for width (e.g., border-2, border-t-4)
1112        if color_or_width_parts.len() == 1 {
1113            if let Ok(width) = color_or_width_parts[0].parse::<i32>() {
1114                return Some(border_props(side, width, vars));
1115            }
1116        }
1117    }
1118    // rounded* (border-radius)
1119    if class == "rounded" { return Some(rounded_props(None, Some("md"))); }
1120    if let Some(sz) = class.strip_prefix("rounded-") {
1121        return Some(rounded_props(None, Some(sz)));
1122    }
1123    for &(pref, side) in &[("rounded-t", "t"), ("rounded-b", "b"), ("rounded-l", "l"), ("rounded-r", "r")] {
1124        if class == pref { return Some(rounded_props(Some(side), Some("md"))); }
1125        if let Some(sz) = class.strip_prefix(&(pref.to_string() + "-")) {
1126            return Some(rounded_props(Some(side), Some(sz)));
1127        }
1128    }
1129    // cursor-*
1130    if let Some(cur) = class.strip_prefix("cursor-") {
1131        let mut props = CssProps::new();
1132        props.insert("cursor".into(), json!(match cur {
1133            "pointer" => "pointer",
1134            "default" => "default",
1135            "text" => "text",
1136            "move" => "move",
1137            "wait" => "wait",
1138            "not-allowed" => "not-allowed",
1139            other => other,
1140        }));
1141        return Some(props);
1142    }
1143    // transition*
1144    if class == "transition" || class == "transition-all" {
1145        let mut props = CssProps::new();
1146        props.insert("transition-property".into(), json!("all"));
1147        props.insert("transition-duration".into(), json!("150ms"));
1148        props.insert("transition-timing-function".into(), json!("ease-in-out"));
1149        return Some(props);
1150    }
1151    if class == "transition-none" {
1152        let mut props = CssProps::new();
1153        props.insert("transition-property".into(), json!("none"));
1154        props.insert("transition-duration".into(), json!("0ms"));
1155        return Some(props);
1156    }
1157    if let Some(rest) = class.strip_prefix("transition-") {
1158        // e.g., transition-colors → limit property; keep default duration/ease
1159        let mut props = CssProps::new();
1160        let property = match rest {
1161            "colors" => "color, background-color, border-color, fill, stroke",
1162            "opacity" => "opacity",
1163            "transform" => "transform",
1164            "shadow" => "box-shadow",
1165            other => other,
1166        };
1167        props.insert("transition-property".into(), json!(property));
1168        props.insert("transition-duration".into(), json!("150ms"));
1169        props.insert("transition-timing-function".into(), json!("ease-in-out"));
1170        return Some(props);
1171    }
1172    // width utilities: w-*, w-full, w-screen, w-min, w-max (treat min/max as auto), w-px
1173    if let Some(val) = class.strip_prefix("w-") {
1174        return width_like_props("width", val);
1175    }
1176    if let Some(val) = class.strip_prefix("min-w-") {
1177        return width_like_props("min-width", val);
1178    }
1179    if let Some(val) = class.strip_prefix("max-w-") {
1180        return width_like_props("max-width", val);
1181    }
1182    // Height utilities
1183    if let Some(val) = class.strip_prefix("h-") {
1184        return width_like_props("height", val);
1185    }
1186    if let Some(val) = class.strip_prefix("min-h-") {
1187        return width_like_props("min-height", val);
1188    }
1189    if let Some(val) = class.strip_prefix("max-h-") {
1190        return width_like_props("max-height", val);
1191    }
1192    None
1193}
1194
1195fn parse_tailwind_spacing<F>(value: &str, builder: &F) -> Option<CssProps>
1196where
1197    F: Fn(i32) -> CssProps,
1198{
1199    if let Ok(n) = value.parse::<i32>() {
1200        let px = n * 4;
1201        return Some(builder(px));
1202    }
1203    None
1204}
1205
1206fn padding_props(keys: &[&str], px_value: i32) -> CssProps {
1207    let mut props = CssProps::new();
1208    let val = format!("{}px", px_value);
1209    for key in keys {
1210        props.insert((*key).into(), json!(&val));
1211    }
1212    props
1213}
1214
1215fn margin_props(keys: &[&str], px_value: i32) -> CssProps {
1216    let mut props = CssProps::new();
1217    let val = format!("{}px", px_value);
1218    for key in keys {
1219        props.insert((*key).into(), json!(&val));
1220    }
1221    props
1222}
1223
1224fn border_props(side: Option<&str>, width: i32, _vars: &IndexMap<String, String>) -> CssProps {
1225    let mut props = CssProps::new();
1226    let width_str = format!("{}px", width);
1227    match side {
1228        None => {
1229            props.insert("border-width".into(), json!(&width_str));
1230        }
1231        Some("t") => {
1232            props.insert("border-top-width".into(), json!(&width_str));
1233        }
1234        Some("b") => {
1235            props.insert("border-bottom-width".into(), json!(&width_str));
1236        }
1237        Some("l") => {
1238            props.insert("border-left-width".into(), json!(&width_str));
1239        }
1240        Some("r") => {
1241            props.insert("border-right-width".into(), json!(&width_str));
1242        }
1243        Some("x") => {
1244            props.insert("border-left-width".into(), json!(&width_str));
1245            props.insert("border-right-width".into(), json!(&width_str));
1246        }
1247        Some("y") => {
1248            props.insert("border-top-width".into(), json!(&width_str));
1249            props.insert("border-bottom-width".into(), json!(&width_str));
1250        }
1251        _ => {
1252            props.insert("border-width".into(), json!(&width_str));
1253        }
1254    };
1255    props.insert("border-color".into(), json!("var(border)"));
1256    props.insert("border-style".into(), json!("solid"));
1257    props
1258}
1259
1260fn rounded_props(side: Option<&str>, size: Option<&str>) -> CssProps {
1261    let mut props = CssProps::new();
1262    let px = match size.unwrap_or("md") {
1263        "none" => 0,
1264        "sm" => 2,
1265        "md" => 4,
1266        "lg" => 8,
1267        "xl" => 12,
1268        "2xl" => 16,
1269        "3xl" => 24,
1270        "full" => 9999,
1271        s => s.parse::<i32>().unwrap_or(4),
1272    };
1273    let v = json!(format!("{}px", px));
1274    match side {
1275        None => { props.insert("border-radius".into(), v); }
1276        Some("t") => {
1277            props.insert("border-top-left-radius".into(), v.clone());
1278            props.insert("border-top-right-radius".into(), v);
1279        }
1280        Some("b") => {
1281            props.insert("border-bottom-left-radius".into(), v.clone());
1282            props.insert("border-bottom-right-radius".into(), v);
1283        }
1284        Some("l") => { props.insert("border-top-left-radius".into(), v.clone()); props.insert("border-bottom-left-radius".into(), v); }
1285        Some("r") => { props.insert("border-top-right-radius".into(), v.clone()); props.insert("border-bottom-right-radius".into(), v); }
1286        _ => { props.insert("border-radius".into(), v); }
1287    }
1288    props
1289}
1290
1291fn width_like_props(prop: &str, token: &str) -> Option<CssProps> {
1292    let mut props = CssProps::new();
1293    let value = match token {
1294        "full" => Some("100%".to_string()),
1295        "screen" => Some(if prop == "width" { "100vw" } else { "100vh" }.to_string()),
1296        "min" => Some("min-content".to_string()),
1297        "max" => Some("max-content".to_string()),
1298        "fit" => Some("fit-content".to_string()),
1299        "auto" => Some("auto".to_string()),
1300        "px" => Some("1px".to_string()),
1301        other => {
1302            // numeric scale n => n*4px, fraction e.g., 1/2 => 50%
1303            if let Some((a, b)) = other.split_once('/') {
1304                if let (Ok(na), Ok(nb)) = (a.parse::<f64>(), b.parse::<f64>()) {
1305                    let pct = (na / nb) * 100.0;
1306                    Some(format!("{}%", trim_trailing_zeros(pct)))
1307                } else { None }
1308            } else if let Ok(n) = other.parse::<i32>() {
1309                Some(format!("{}px", n * 4))
1310            } else {
1311                None
1312            }
1313        }
1314    }?;
1315    props.insert(prop.into(), json!(value));
1316    Some(props)
1317}
1318
1319fn trim_trailing_zeros(num: f64) -> String {
1320    let mut s = format!("{:.6}", num);
1321    while s.contains('.') && s.ends_with('0') { s.pop(); }
1322    if s.ends_with('.') { s.pop(); }
1323    s
1324}
1325
1326// ---------------- Tailwind subset ----------------
1327
1328// static RE_NUM: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(?P<prefix>(hover:)?(xs:|sm:|md:|lg:|xl:)*)?(?P<base>.+)$").unwrap());
1329
1330fn css_escape_class(class: &str) -> String { class.replace(':', "\\:") }
1331
1332fn class_to_selector(class: &str) -> String {
1333    let (_bp, hover, base) = parse_prefixed_class(class);
1334    if hover {
1335        format!(".{}:hover", css_escape_class(&base))
1336    } else {
1337        format!(".{}", css_escape_class(&base))
1338    }
1339}
1340
1341// ------------- helpers for CSS output of media selectors -------------
1342
1343/// Flatten CSS with potential selectors that include media prelude.
1344/// This simple post-processor merges entries that use the special selector format
1345/// "@media (min-width: X) {<sel>" where we will close the block at the end.
1346/// We group by media and inside concatenate selectors.
1347pub fn post_process_css(
1348    raw_rules: &[(String, CssProps)],
1349    vars: &IndexMap<String, String>,
1350) -> String {
1351    // Group into normal rules and media rules
1352    let mut normal = vec![];
1353    let mut media_map: IndexMap<String, Vec<(String, CssProps)>> = IndexMap::new();
1354    for (sel, props) in raw_rules.iter() {
1355        if let Some((media, inner)) = sel.split_once('{') {
1356            if media.trim_start().starts_with("@media ") && inner.ends_with('}') {
1357                let inner_sel = inner.trim_end_matches('}').to_string();
1358                media_map
1359                    .entry(media.trim().to_string())
1360                    .or_default()
1361                    .push((inner_sel, props.clone()));
1362                continue;
1363            }
1364        }
1365        normal.push((sel.clone(), props.clone()));
1366    }
1367    let mut out = String::new();
1368    for (sel, props) in normal {
1369        out.push_str(&sel);
1370        out.push('{');
1371        out.push_str(&css_props_string(&props, vars));
1372        out.push_str("}\n");
1373    }
1374    for (media, entries) in media_map {
1375        out.push_str(&media);
1376        out.push('{');
1377        for (sel, props) in entries {
1378            out.push_str(&sel);
1379            out.push('{');
1380            out.push_str(&css_props_string(&props, vars));
1381            out.push_str("}");
1382        }
1383        out.push_str("}\n");
1384    }
1385    out
1386}
1387
1388// -------- Prefix parsing (hover:, breakpoint:) --------
1389
1390fn parse_prefixed_class(class: &str) -> (Option<String>, bool, String) {
1391    // Split by ':' to find prefixes like md:hover:block
1392    let parts: Vec<&str> = class.split(':').collect();
1393    if parts.len() == 1 {
1394        return (None, false, class.to_string());
1395    }
1396    let mut bp: Option<String> = None;
1397    let mut hover = false;
1398    for &p in &parts[..parts.len() - 1] {
1399        match p {
1400            "hover" => hover = true,
1401            "xs" | "sm" | "md" | "lg" | "xl" => bp = Some(p.to_string()),
1402            _ => {}
1403        }
1404    }
1405    let base = parts.last().unwrap().to_string();
1406    (bp, hover, base)
1407}
1408
1409fn wrap_with_media(selector: &str, bp_key: Option<&str>, bps: &IndexMap<String, String>) -> String {
1410    if let Some(k) = bp_key {
1411        if let Some(val) = bps.get(k) {
1412            return format!("@media (min-width: {}) {{{}}}", val, selector);
1413        }
1414    }
1415    selector.to_string()
1416}
1417
1418/// Get a Tailwind color hex value from a string like "slate-200" or "blue-500"
1419fn get_tailwind_color(color_shade: &str) -> Option<String> {
1420    let parts: Vec<&str> = color_shade.split('-').collect();
1421    if parts.len() != 2 {
1422        return None;
1423    }
1424    let color_name = parts[0];
1425    let shade = parts[1];
1426    
1427    TAILWIND_COLORS
1428        .get(color_name)
1429        .and_then(|shades| shades.get(shade))
1430        .map(|&hex| hex.to_string())
1431}
1432
1433/// Parse arbitrary values like bg-[var(--primary)], text-[#ff0000], border-[hsl(200,50%,50%)]
1434fn parse_arbitrary_value(class: &str) -> Option<CssProps> {
1435    // Match pattern: prefix-[value]
1436    if let Some(bracket_start) = class.find('[') {
1437        if !class.ends_with(']') {
1438            return None;
1439        }
1440        let prefix = &class[..bracket_start];
1441        let value = &class[bracket_start + 1..class.len() - 1];
1442        
1443        let mut props = CssProps::new();
1444        match prefix {
1445            "bg" => {
1446                props.insert("background-color".into(), json!(value));
1447                return Some(props);
1448            }
1449            "text" => {
1450                props.insert("color".into(), json!(value));
1451                return Some(props);
1452            }
1453            "border" => {
1454                props.insert("border-color".into(), json!(value));
1455                return Some(props);
1456            }
1457            "divide" => {
1458                props.insert("border-color".into(), json!(value));
1459                return Some(props);
1460            }
1461            _ => return None,
1462        }
1463    }
1464    None
1465}
1466
1467// re-export minimal API for CLI
1468pub mod api {
1469    pub use super::{SelectorStyles, State};
1470}
1471
1472#[cfg(test)]
1473mod tests {
1474    use super::*;
1475
1476    #[test]
1477    fn default_theme_has_p2() {
1478        let mut st = State::new_default();
1479        st.register_tailwind_classes(["p-2".to_string()]);
1480        let css = st.css_for_web();
1481        assert!(css.contains(".p-2{"));
1482        assert!(css.contains("padding:8px"));
1483    }
1484
1485    #[test]
1486    fn rn_conversion() {
1487        let mut st = State::new_default();
1488        // Add a theme with button styles
1489        let mut styles = IndexMap::new();
1490        let mut button_props = IndexMap::new();
1491        button_props.insert("backgroundColor".to_string(), json!("#007bff"));
1492        styles.insert("button".to_string(), button_props);
1493        st.add_theme("default", styles);
1494        st.set_theme("default").ok();
1495        
1496        let out = st.rn_styles_for("button", &[]);
1497        assert!(out.get("backgroundColor").is_some());
1498    }
1499
1500    #[test]
1501    fn embedded_defaults_and_version() {
1502        // Test that we can create a state and add a theme with variables
1503        let mut st = State::default_state();
1504        st.add_theme("default", IndexMap::new());
1505        st.set_theme("default").ok();
1506        
1507        let mut vars = IndexMap::new();
1508        vars.insert("primary".to_string(), "#007bff".to_string());
1509        st.set_variables(vars);
1510        
1511        assert!(st.themes.contains_key("default"));
1512        let def = st.themes.get("default").unwrap();
1513        assert!(def.variables.contains_key("primary"));
1514
1515        // Version should compile and be non-empty (env! evaluated at compile-time)
1516        // Note: get_version() is only available for wasm32 target
1517        #[cfg(target_arch = "wasm32")]
1518        {
1519            let v = get_version();
1520            assert!(!v.is_empty());
1521        }
1522    }
1523
1524    #[test]
1525    fn border_color_with_direction() {
1526        let mut st = State::new_default();
1527        
1528        // Test border-b-blue-500 (border-bottom with blue color shade 500)
1529        st.register_tailwind_classes(["border-b-blue-500".to_string()]);
1530        let css = st.css_for_web();
1531        assert!(css.contains(".border-b-blue-500{"));
1532        assert!(css.contains("border-bottom-color:#3b82f6") || css.contains("border-b-color:#3b82f6"));
1533        
1534        // Test border-t-red-500
1535        st.register_tailwind_classes(["border-t-red-500".to_string()]);
1536        let css = st.css_for_web();
1537        assert!(css.contains(".border-t-red-500{"));
1538        
1539        // Test border-blue-500 (all borders)
1540        st.register_tailwind_classes(["border-blue-500".to_string()]);
1541        let css = st.css_for_web();
1542        assert!(css.contains(".border-blue-500{"));
1543        assert!(css.contains("border-color:#3b82f6"));
1544    }
1545
1546    #[test]
1547    fn border_width_with_direction() {
1548        let mut st = State::new_default();
1549        
1550        // Test border-b-2 (border-bottom width 2px)
1551        st.register_tailwind_classes(["border-b-2".to_string()]);
1552        let css = st.css_for_web();
1553        assert!(css.contains(".border-b-2{"));
1554        assert!(css.contains("border-bottom-width:2px"));
1555        
1556        // Test border-2 (all borders width 2px)
1557        st.register_tailwind_classes(["border-2".to_string()]);
1558        let css = st.css_for_web();
1559        assert!(css.contains(".border-2{"));
1560        assert!(css.contains("border-width:2px"));
1561    }
1562
1563    #[test]
1564    fn display_flex_hover_breakpoint() {
1565        let mut st = State::new_default();
1566        
1567        // Set up theme with breakpoints
1568        st.add_theme("default", IndexMap::new());
1569        st.set_theme("default").ok();
1570        
1571        let mut breakpoints = IndexMap::new();
1572        breakpoints.insert("md".to_string(), "768px".to_string());
1573        st.set_breakpoints(breakpoints);
1574        
1575        st.register_tailwind_classes([
1576            "block".into(),
1577            "inline-flex".into(),
1578            "hidden".into(),
1579            "md:flex".into(),
1580            "md:hover:block".into(),
1581        ]);
1582        let css = st.css_for_web();
1583        assert!(css.contains(".block{"));
1584        assert!(css.contains("display:block"));
1585        assert!(css.contains(".inline-flex{"));
1586        assert!(css.contains("display:inline-flex"));
1587        assert!(css.contains(".hidden{"));
1588        assert!(css.contains("display:none"));
1589        // breakpoint rule
1590        assert!(css.contains("@media (min-width: 768px)"));
1591        assert!(css.contains(".flex{display:flex"));
1592        // hover inside media (substring check)
1593        assert!(css.contains(":hover{display:block"));
1594
1595        // RN resolves base class styles ignoring prefixes
1596        let rn = st.rn_styles_for("div", &["md:flex".into()]);
1597        assert_eq!(rn.get("display").and_then(|v| v.as_str()), Some("flex"));
1598    }
1599
1600    #[test]
1601    fn parse_var_references_basic() {
1602        // Test basic var() parsing
1603        let refs = parse_var_references("var(color)");
1604        assert_eq!(refs.len(), 1);
1605        assert_eq!(refs[0].2, "color");
1606        assert_eq!(refs[0].0, 0); // start
1607        assert_eq!(refs[0].1, 10); // end (exclusive, so "var(color)" is 0..10)
1608
1609        // Test var() with -- prefix
1610        let refs = parse_var_references("var(--primary)");
1611        assert_eq!(refs.len(), 1);
1612        assert_eq!(refs[0].2, "primary");
1613
1614        // Test multiple var() references
1615        let refs = parse_var_references("var(--color) and var(size)");
1616        assert_eq!(refs.len(), 2);
1617        assert_eq!(refs[0].2, "color");
1618        assert_eq!(refs[1].2, "size");
1619
1620        // Test with whitespace
1621        let refs = parse_var_references("var( --spacing )");
1622        assert_eq!(refs.len(), 1);
1623        assert_eq!(refs[0].2, "spacing");
1624
1625        // Test with dots and dashes
1626        let refs = parse_var_references("var(color.primary-500)");
1627        assert_eq!(refs.len(), 1);
1628        assert_eq!(refs[0].2, "color.primary-500");
1629
1630        // Test no matches
1631        let refs = parse_var_references("no variables here");
1632        assert_eq!(refs.len(), 0);
1633
1634        // Test incomplete var(
1635        let refs = parse_var_references("var(");
1636        assert_eq!(refs.len(), 0);
1637
1638        // Test var without closing
1639        let refs = parse_var_references("var(color");
1640        assert_eq!(refs.len(), 0);
1641    }
1642
1643    #[test]
1644    fn resolve_vars_basic() {
1645        let mut vars = IndexMap::new();
1646        vars.insert("primary".to_string(), "#ff0000".to_string());
1647        vars.insert("spacing".to_string(), "8px".to_string());
1648        vars.insert("color.blue".to_string(), "#0000ff".to_string());
1649
1650        // Test basic resolution
1651        assert_eq!(resolve_vars("var(--primary)", &vars), "#ff0000");
1652        assert_eq!(resolve_vars("var(primary)", &vars), "#ff0000");
1653        assert_eq!(resolve_vars("var( --primary )", &vars), "#ff0000");
1654
1655        // Test multiple vars
1656        assert_eq!(
1657            resolve_vars("var(--primary) var(--spacing)", &vars),
1658            "#ff0000 8px"
1659        );
1660
1661        // Test dotted variable names
1662        assert_eq!(resolve_vars("var(--color.blue)", &vars), "#0000ff");
1663
1664        // Test undefined variable (should not replace)
1665        assert_eq!(resolve_vars("var(--undefined)", &vars), "var(--undefined)");
1666
1667        // Test $ prefix syntax
1668        assert_eq!(resolve_vars("$primary", &vars), "#ff0000");
1669
1670        // Test no variables
1671        assert_eq!(resolve_vars("plain text", &vars), "plain text");
1672    }
1673
1674    #[test]
1675    fn resolve_vars_edge_cases() {
1676        let mut vars = IndexMap::new();
1677        vars.insert("a".to_string(), "1".to_string());
1678        vars.insert("b".to_string(), "2".to_string());
1679
1680        // Test adjacent vars
1681        assert_eq!(resolve_vars("var(a)var(b)", &vars), "12");
1682
1683        // Test var in middle of text
1684        assert_eq!(resolve_vars("prefix var(a) suffix", &vars), "prefix 1 suffix");
1685
1686        // Test empty input
1687        assert_eq!(resolve_vars("", &vars), "");
1688
1689        // Test var with numbers
1690        vars.insert("var123".to_string(), "value".to_string());
1691        assert_eq!(resolve_vars("var(var123)", &vars), "value");
1692
1693        // Test var with underscores
1694        vars.insert("my_var".to_string(), "test".to_string());
1695        assert_eq!(resolve_vars("var(my_var)", &vars), "test");
1696    }
1697}
1698
1699#[cfg(all(target_os = "android", feature = "android"))]
1700#[cfg(feature = "android")]
1701mod android_jni;
1702
1703mod bridge_common;
1704mod ffi;
1705
1706pub use ffi::*;
1707
1708#[cfg(target_vendor = "apple")]
1709mod ios_ffi;