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
10// Default display density (1.0 = mdpi baseline)
11fn default_display_density() -> f32 { 1.0 }
12fn default_scaled_density() -> f32 { 1.0 }
13
14pub type CssProps = IndexMap<String, serde_json::Value>;
15pub type SelectorStyles = IndexMap<String, CssProps>; // selector -> props
16
17/// Convert dp to pixels using display density
18fn dp_to_px(dp: f32, density: f32) -> i32 {
19    (dp * density).round() as i32
20}
21
22/// Convert sp to pixels using scaled density  
23fn sp_to_px(sp: f32, scaled_density: f32) -> f32 {
24    sp * scaled_density
25}
26
27/// Parse a CSS value and convert to Android pixels if needed
28fn parse_and_convert_to_px(value: &serde_json::Value, density: f32) -> Option<serde_json::Value> {
29    match value {
30        serde_json::Value::Number(n) => {
31            // Bare number treated as dp
32            let dp = n.as_f64()? as f32;
33            Some(serde_json::json!(dp_to_px(dp, density)))
34        }
35        serde_json::Value::String(s) => {
36            // Parse string with units
37            let trimmed = s.trim();
38            if trimmed.ends_with("px") {
39                // Treat px as density-independent pixels (dp) for cross-platform parity
40                let px = trimmed.trim_end_matches("px").trim().parse::<f32>().ok()?;
41                Some(serde_json::json!(dp_to_px(px, density)))
42            } else if trimmed.ends_with("dp") {
43                let dp = trimmed.trim_end_matches("dp").trim().parse::<f32>().ok()?;
44                Some(serde_json::json!(dp_to_px(dp, density)))
45            } else if let Ok(num) = trimmed.parse::<f32>() {
46                // Bare number as string, treat as dp
47                Some(serde_json::json!(dp_to_px(num, density)))
48            } else {
49                // Keep as-is (e.g., "wrap_content", "match_parent")
50                None
51            }
52        }
53        _ => None
54    }
55}
56
57fn deserialize_variables<'de, D>(deserializer: D) -> Result<IndexMap<String, String>, D::Error>
58where
59    D: Deserializer<'de>,
60{
61    let value = Option::<serde_json::Value>::deserialize(deserializer)?;
62    let mut out: IndexMap<String, String> = IndexMap::new();
63    if let Some(v) = value {
64        flatten_variables(None, &v, &mut out);
65    }
66    Ok(out)
67}
68
69fn flatten_variables(prefix: Option<&str>, value: &serde_json::Value, out: &mut IndexMap<String, String>) {
70    match value {
71        serde_json::Value::Object(map) => {
72            for (k, v) in map {
73                let key = if let Some(p) = prefix {
74                    format!("{}.{}", p, k)
75                } else {
76                    k.to_string()
77                };
78                flatten_variables(Some(&key), v, out);
79            }
80        }
81        serde_json::Value::Array(arr) => {
82            for (idx, v) in arr.iter().enumerate() {
83                let key = if let Some(p) = prefix {
84                    format!("{}.{}", p, idx)
85                } else {
86                    idx.to_string()
87                };
88                flatten_variables(Some(&key), v, out);
89            }
90        }
91        serde_json::Value::Null => {}
92        serde_json::Value::Bool(b) => {
93            if let Some(p) = prefix {
94                out.insert(p.to_string(), b.to_string());
95            }
96        }
97        serde_json::Value::Number(n) => {
98            if let Some(p) = prefix {
99                out.insert(p.to_string(), n.to_string());
100            }
101        }
102        serde_json::Value::String(s) => {
103            if let Some(p) = prefix {
104                out.insert(p.to_string(), s.clone());
105            }
106        }
107    }
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize, Default)]
111pub struct ThemeEntry {
112    #[serde(default)]
113    pub name: Option<String>,
114    #[serde(default)]
115    pub inherits: Option<String>,
116    #[serde(default)]
117    pub selectors: SelectorStyles,
118    #[serde(default, deserialize_with = "deserialize_variables")]
119    pub variables: IndexMap<String, String>,
120    #[serde(default, deserialize_with = "deserialize_variables")]
121    pub breakpoints: IndexMap<String, String>,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize, Default)]
125pub struct State {
126    // New format: each theme has selectors, variables, breakpoints, and optional inherits
127    pub themes: IndexMap<String, ThemeEntry>,
128    pub default_theme: String,
129    pub current_theme: String,
130    // Platform-specific metadata for unit conversions
131    #[serde(default = "default_display_density")]
132    pub display_density: f32, // Android displayMetrics.density (1.0 for mdpi, 2.0 for xhdpi, etc.)
133    #[serde(default = "default_scaled_density")]
134    pub scaled_density: f32,  // Android displayMetrics.scaledDensity for SP conversions
135    // Legacy fields (kept for backward-compat JSON). Not used if themes[] carry variables/bps.
136    #[serde(default)]
137    pub theme_variables: IndexMap<String, IndexMap<String, String>>, // deprecated
138    #[serde(default, deserialize_with = "deserialize_variables")]
139    pub variables: IndexMap<String, String>, // deprecated global
140    #[serde(default, deserialize_with = "deserialize_variables")]
141    pub breakpoints: IndexMap<String, String>, // deprecated global
142    #[serde(default)]
143    pub used_selectors: IndexSet<String>, // deprecated: exact selector strings (kept for back-compat)
144    #[serde(default)]
145    pub used_classes: IndexSet<String>,   // observed classes on elements
146    #[serde(default)]
147    pub used_tags: IndexSet<String>,      // observed tags on elements
148    /// Observed (tag, class) pairs. Encoded as "tag|class" for JSON simplicity.
149    #[serde(default)]
150    pub used_tag_classes: IndexSet<String>,
151}
152
153#[derive(thiserror::Error, Debug)]
154pub enum Error {
155    #[error("theme not found: {0}")]
156    ThemeNotFound(String),
157}
158
159impl State {
160    pub fn new_default() -> Self {
161        // Prefer embedded Rust bundled defaults
162        return bundled_state();
163    }
164
165    /// Public helper to access the embedded default state.
166    pub fn default_state() -> Self {
167        bundled_state()
168    }
169
170    pub fn set_theme(&mut self, theme: impl Into<String>) -> Result<(), Error> {
171        let name = theme.into();
172        if !self.themes.contains_key(&name) {
173            return Err(Error::ThemeNotFound(name));
174        }
175        self.current_theme = name;
176        Ok(())
177    }
178
179    pub fn add_theme(&mut self, name: impl Into<String>, styles: SelectorStyles) {
180        let name = name.into();
181        let entry = self.themes.entry(name).or_default();
182        for (sel, props) in styles.into_iter() {
183            let e = entry.selectors.entry(sel).or_default();
184            merge_props(e, &props);
185        }
186    }
187
188    pub fn set_variables(&mut self, vars: IndexMap<String, String>) {
189        // Back-compat: set on current theme entry
190        let cur = self.current_theme.clone();
191        let entry = self.themes.entry(cur).or_default();
192        entry.variables = vars;
193    }
194
195    pub fn set_breakpoints(&mut self, map: IndexMap<String, String>) {
196        let cur = self.current_theme.clone();
197        let entry = self.themes.entry(cur).or_default();
198        entry.breakpoints = map;
199    }
200
201    pub fn process_styles(&self, mut styles: IndexMap<String, serde_json::Value>) -> IndexMap<String, serde_json::Value> {
202        let density = self.display_density;
203        
204        // Expand shorthands
205        // Order matters: Horizontal/Vertical should be expanded before general shorthands
206        // so that specific ones win if they were already present.
207        if let Some(ph) = styles.get("paddingHorizontal").cloned() {
208            styles.entry("paddingLeft".into()).or_insert(ph.clone());
209            styles.entry("paddingRight".into()).or_insert(ph.clone());
210        }
211        if let Some(pv) = styles.get("paddingVertical").cloned() {
212            styles.entry("paddingTop".into()).or_insert(pv.clone());
213            styles.entry("paddingBottom".into()).or_insert(pv.clone());
214        }
215        if let Some(p) = styles.get("padding").cloned() {
216            styles.entry("paddingTop".into()).or_insert(p.clone());
217            styles.entry("paddingBottom".into()).or_insert(p.clone());
218            styles.entry("paddingLeft".into()).or_insert(p.clone());
219            styles.entry("paddingRight".into()).or_insert(p.clone());
220        }
221        if let Some(mh) = styles.get("marginHorizontal").cloned() {
222            styles.entry("marginLeft".into()).or_insert(mh.clone());
223            styles.entry("marginRight".into()).or_insert(mh.clone());
224        }
225        if let Some(mv) = styles.get("marginVertical").cloned() {
226            styles.entry("marginTop".into()).or_insert(mv.clone());
227            styles.entry("marginBottom".into()).or_insert(mv.clone());
228        }
229        if let Some(m) = styles.get("margin").cloned() {
230            styles.entry("marginTop".into()).or_insert(m.clone());
231            styles.entry("marginBottom".into()).or_insert(m.clone());
232            styles.entry("marginLeft".into()).or_insert(m.clone());
233            styles.entry("marginRight".into()).or_insert(m.clone());
234        }
235        if let Some(r) = styles.get("borderRadius").cloned() {
236            styles.entry("borderTopLeftRadius".into()).or_insert(r.clone());
237            styles.entry("borderTopRightRadius".into()).or_insert(r.clone());
238            styles.entry("borderBottomLeftRadius".into()).or_insert(r.clone());
239            styles.entry("borderBottomRightRadius".into()).or_insert(r.clone());
240        }
241
242        // Convert only dimension properties to pixels
243        let dimension_props = [
244            "width", "height", "minWidth", "minHeight", "maxWidth", "maxHeight",
245            "padding", "paddingTop", "paddingBottom", "paddingLeft", "paddingRight",
246            "paddingHorizontal", "paddingVertical",
247            "margin", "marginTop", "marginBottom", "marginLeft", "marginRight",
248            "marginHorizontal", "marginVertical",
249            "borderRadius", "borderTopLeftRadius", "borderTopRightRadius", "borderBottomLeftRadius", "borderBottomRightRadius",
250            "borderWidth", "borderTopWidth", "borderBottomWidth", "borderLeftWidth", "borderRightWidth",
251            "gap", "rowGap", "columnGap", "elevation", "fontSize", "lineHeight", "letterSpacing"
252        ];
253        
254        for prop in &dimension_props {
255            if let Some(value) = styles.get(*prop).cloned() {
256                if let Some(converted) = parse_and_convert_to_px(&value, density) {
257                    styles.insert(prop.to_string(), converted);
258                }
259            }
260        }
261        
262        styles
263    }
264
265    pub fn set_default_theme(&mut self, name: impl Into<String>) {
266        self.default_theme = name.into();
267    }
268
269    pub fn register_selectors<I: IntoIterator<Item = String>>(&mut self, selectors: I) {
270        for s in selectors {
271            self.used_selectors.insert(s);
272        }
273    }
274
275    pub fn register_tailwind_classes<I: IntoIterator<Item = String>>(&mut self, classes: I) {
276        for c in classes {
277            self.used_classes.insert(c);
278        }
279    }
280
281    pub fn register_tags<I: IntoIterator<Item = String>>(&mut self, tags: I) {
282        for t in tags {
283            self.used_tags.insert(t);
284        }
285    }
286
287    pub fn register_tag_class(&mut self, tag: impl Into<String>, class_: impl Into<String>) {
288        let key = format!("{}|{}", tag.into(), class_.into());
289        self.used_tag_classes.insert(key);
290    }
291
292
293    pub fn clear_usage(&mut self) {
294        self.used_selectors.clear();
295        self.used_classes.clear();
296        self.used_tags.clear();
297        self.used_tag_classes.clear();
298    }
299
300    pub fn to_json(&self) -> serde_json::Value {
301        json!({
302            "themes": self.themes,
303            "default_theme": self.default_theme,
304            "current_theme": self.current_theme,
305            // legacy fields are still serialized for back-compat but may be empty
306            "theme_variables": self.theme_variables,
307            "variables": self.variables,
308            "breakpoints": self.breakpoints,
309            "used_selectors": self.used_selectors,
310            "used_classes": self.used_classes,
311            "used_tags": self.used_tags,
312            "used_tag_classes": self.used_tag_classes,
313        })
314    }
315
316    pub fn from_json(value: serde_json::Value) -> anyhow::Result<Self> {
317        let state: State = serde_json::from_value(value)?;
318        Ok(state)
319    }
320
321    pub fn css_for_web(&self) -> String {
322        // Compute CSS resolved from the effective theme (with inheritance)
323        let (eff, vars) = self.effective_theme_all();
324        let bps = self.effective_breakpoints();
325        let mut rules: Vec<(String, CssProps)> = Vec::new();
326        
327        // Build closure: if a (tag,class) pair is observed, consider both the tag and the class as used too
328        let mut used_tags: IndexSet<String> = self.used_tags.clone();
329        let mut used_classes: IndexSet<String> = self.used_classes.clone();
330        for key in &self.used_tag_classes {
331            if let Some((t, c)) = split_tag_class_key(key) {
332                used_tags.insert(t);
333                used_classes.insert(c);
334            }
335        }
336
337        // Helper to decide if a themed selector should be emitted based on observed usage.
338        // Supported selector forms:
339        //  - tag           (e.g., "h1")
340        //  - .class        (e.g., ".text-sm"), optional pseudo ":hover"
341        //  - tag.class     (e.g., "h1.text-sm"), optional pseudo ":hover"
342        for (sel, props) in eff.iter() {
343            if should_emit_selector(sel, &used_tags, &used_classes, &self.used_tag_classes) {
344                rules.push((sel.clone(), props.clone()));
345            }
346        }
347
348        // Also emit dynamic utility properties for used classes
349        for class in &used_classes {
350            let (bp_key, hover, base) = parse_prefixed_class(class);
351            let selector = if hover { format!(".{}:hover", css_escape_class(&base)) } else { format!(".{}", css_escape_class(&base)) };
352
353            // 1) Exact selector in effective theme (e.g. ".x:hover")
354            if let Some(props) = eff.get(&selector) {
355                let final_sel = wrap_with_media(&selector, bp_key.as_deref(), &bps);
356                rules.push((final_sel, props.clone()));
357                continue;
358            }
359            // 2) Dynamic generation for the base class (ignoring hover/breakpoint for props)
360            if let Some(dynamic_props) = dynamic_css_properties_for_class(&base, &vars) {
361                let sel = if hover { format!(".{}:hover", css_escape_class(&base)) } else { format!(".{}", css_escape_class(&base)) };
362                let final_sel = wrap_with_media(&sel, bp_key.as_deref(), &bps);
363                rules.push((final_sel, dynamic_props));
364                continue;
365            }
366            // 3) Fallback: class key itself in theme (rare)
367            if let Some(props) = eff.get(&base) {
368                let final_sel = wrap_with_media(&selector, bp_key.as_deref(), &bps);
369                rules.push((final_sel, props.clone()));
370            }
371        }
372
373        post_process_css(&rules, &vars)
374    }
375
376    pub fn android_base_styles(&self, selector: &str, classes: &[String]) -> IndexMap<String, serde_json::Value> {
377        let (eff, vars) = self.effective_theme_all();
378        let mut out: IndexMap<String, serde_json::Value> = IndexMap::new();
379
380        // Pre-insert androidOrientation to ensure it's early in the map for gap processing
381        out.insert("androidOrientation".to_string(), serde_json::json!("vertical"));
382
383        // 1. Apply hardcoded platform defaults (lowest priority)
384        let mut defaults = CssProps::new();
385        match selector.to_lowercase().as_str() {
386            "div" => {
387                defaults.insert("width".into(), json!("match_parent"));
388            }
389            "p" => {
390                defaults.insert("width".into(), json!("match_parent"));
391                defaults.insert("margin-vertical".into(), json!("16px"));
392            }
393            "h1" => {
394                defaults.insert("width".into(), json!("match_parent"));
395                defaults.insert("font-size".into(), json!("32px"));
396                defaults.insert("font-weight".into(), json!("bold"));
397                defaults.insert("margin-vertical".into(), json!("21.44px"));
398            }
399            "h2" => {
400                defaults.insert("width".into(), json!("match_parent"));
401                defaults.insert("font-size".into(), json!("24px"));
402                defaults.insert("font-weight".into(), json!("bold"));
403                defaults.insert("margin-vertical".into(), json!("19.92px"));
404            }
405            "h3" => {
406                defaults.insert("width".into(), json!("match_parent"));
407                defaults.insert("font-size".into(), json!("18.72px"));
408                defaults.insert("font-weight".into(), json!("bold"));
409                defaults.insert("margin-vertical".into(), json!("18.72px"));
410            }
411            "h4" => {
412                defaults.insert("width".into(), json!("match_parent"));
413                defaults.insert("font-size".into(), json!("16px"));
414                defaults.insert("font-weight".into(), json!("bold"));
415                defaults.insert("margin-vertical".into(), json!("21.28px"));
416            }
417            "h5" => {
418                defaults.insert("width".into(), json!("match_parent"));
419                defaults.insert("font-size".into(), json!("13.28px"));
420                defaults.insert("font-weight".into(), json!("bold"));
421                defaults.insert("margin-vertical".into(), json!("22.17px"));
422            }
423            "h6" => {
424                defaults.insert("width".into(), json!("match_parent"));
425                defaults.insert("font-size".into(), json!("10.72px"));
426                defaults.insert("font-weight".into(), json!("bold"));
427                defaults.insert("margin-vertical".into(), json!("24.96px"));
428            }
429            "input" => {
430                defaults.insert("padding-vertical".into(), json!("8px"));
431                defaults.insert("padding-horizontal".into(), json!("12px"));
432                defaults.insert("border-radius".into(), json!("4px"));
433                defaults.insert("border-width".into(), json!("1px"));
434                defaults.insert("border-color".into(), json!("#cccccc"));
435                defaults.insert("background-color".into(), json!("#ffffff"));
436                defaults.insert("min-height".into(), json!("40px"));
437                defaults.insert("android-gravity".into(), json!("center_vertical"));
438            }
439            "button" => {
440                defaults.insert("padding-vertical".into(), json!("8px"));
441                defaults.insert("padding-horizontal".into(), json!("16px"));
442                defaults.insert("border-radius".into(), json!("4px"));
443                defaults.insert("background-color".into(), json!("#2196F3"));
444                defaults.insert("color".into(), json!("#ffffff"));
445                defaults.insert("android-gravity".into(), json!("center"));
446            }
447            _ => {}
448        }
449        merge_android_props(&mut out, &defaults, &vars);
450
451        if selector == "button" || classes.iter().any(|c| c.contains("bg-")) {
452            log::debug!("[android_base_styles] selector={} classes={:?}", selector, classes);
453        }
454
455        // 2. Apply theme selector styles (overwrites defaults)
456        if let Some(props) = eff.get(selector) {
457            merge_android_props(&mut out, props, &vars);
458        }
459
460        // 3. Apply class styles (overwrites selector)
461        for class in classes {
462            // Normalize input: strip leading dot if present (Android may pass ".bg-primary" as selector format)
463            let normalized_class = if class.starts_with('.') {
464                class[1..].to_string()
465            } else {
466                class.clone()
467            };
468            
469            let (_bp, _hover, base) = parse_prefixed_class(&normalized_class);
470            // Prefer base selector match from theme
471            let sel = class_to_selector(&base);
472            if let Some(props) = eff.get(&sel) {
473                merge_android_props(&mut out, props, &vars);
474                continue;
475            }
476            // Dynamic mapping for base class
477            if let Some(dynamic_props) = dynamic_css_properties_for_class(&base, &vars) {
478                merge_android_props(&mut out, &dynamic_props, &vars);
479                continue;
480            }
481            if let Some(props) = eff.get(&base) {
482                merge_android_props(&mut out, props, &vars);
483            }
484        }
485        
486        // CSS semantics: display: flex defaults to flexDirection: row
487        if let Some(display) = out.get("display") {
488            if display.as_str() == Some("flex") && !out.contains_key("flexDirection") {
489                out.insert("flexDirection".to_string(), serde_json::json!("row"));
490                out.insert("androidOrientation".to_string(), serde_json::json!("horizontal"));
491            }
492        }
493
494        // Fallback for block elements to be column if not specified
495        if !out.contains_key("flexDirection") {
496            match selector.to_lowercase().as_str() {
497                "div" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" => {
498                    out.insert("flexDirection".to_string(), serde_json::json!("column"));
499                    out.insert("androidOrientation".to_string(), serde_json::json!("vertical"));
500                }
501                _ => {}
502            }
503        }
504        
505        // Ensure androidOrientation is in sync with flexDirection if it was set but orientation wasn't
506        if let Some(fd) = out.get("flexDirection").and_then(|v| v.as_str()) {
507            if !out.contains_key("androidOrientation") {
508                let orientation = if fd == "column" || fd == "column-reverse" {
509                    "vertical"
510                } else {
511                    "horizontal"
512                };
513                out.insert("androidOrientation".to_string(), serde_json::json!(orientation));
514            }
515        }
516
517        out
518    }
519
520    /// Android-specific style transformations
521    /// Converts CSS properties to Android-compatible values with platform-specific defaults
522    /// Handles unit conversions (dp/sp to px) using display density
523    pub fn android_styles_for(&self, selector: &str, classes: &[String]) -> IndexMap<String, serde_json::Value> {
524        let mut styles = self.android_base_styles(selector, classes);
525        
526        let density = self.display_density;
527        let scaled_density = self.scaled_density;
528
529        // Convert flexDirection to Android orientation EARLY so layout-dependent props (like gap) can use it
530        if let Some(flex_dir) = styles.get("flexDirection") {
531            let orientation = if flex_dir.as_str() == Some("row") { "horizontal" } else { "vertical" };
532            styles.shift_insert(0, "androidOrientation".to_string(), serde_json::json!(orientation));
533        }
534        
535        // Convert all dimension properties to pixels
536        let dimension_props = [
537            "width", "height", "minWidth", "minHeight", "maxWidth", "maxHeight",
538            "padding", "paddingTop", "paddingBottom", "paddingLeft", "paddingRight",
539            "paddingHorizontal", "paddingVertical",
540            "margin", "marginTop", "marginBottom", "marginLeft", "marginRight",
541            "marginHorizontal", "marginVertical",
542            "borderRadius", "borderWidth", "borderTopWidth", "borderBottomWidth",
543            "borderLeftWidth", "borderRightWidth",
544            "gap", "rowGap", "columnGap", "elevation", "lineHeight", "letterSpacing"
545        ];
546        
547        for prop in &dimension_props {
548            if let Some(value) = styles.get(*prop).cloned() {
549                if let Some(converted) = parse_and_convert_to_px(&value, density) {
550                    styles.insert(prop.to_string(), converted);
551                }
552            }
553        }
554        
555        // Convert font sizes (use scaled density for accessibility)
556        if let Some(font_size) = styles.get("fontSize").cloned() {
557            if let Some(serde_json::Value::Number(n)) = parse_and_convert_to_px(&font_size, density).as_ref() {
558                // For text, use scaled density for accessibility
559                let sp_value = n.as_f64().unwrap_or(14.0) as f32 / density * scaled_density;
560                styles.insert("fontSize".to_string(), serde_json::json!(sp_value));
561            }
562        }
563        
564        // Convert flexWrap to Android-friendly format
565        if let Some(flex_wrap) = styles.get("flexWrap") {
566            if flex_wrap.as_str() == Some("wrap") {
567                styles.insert("androidFlexWrap".to_string(), serde_json::json!(true));
568            }
569        }
570
571        // Map opacity to androidAlpha
572        if let Some(opacity) = styles.get("opacity").cloned() {
573            styles.insert("androidAlpha".to_string(), opacity);
574        }
575
576        let is_horizontal = styles.get("androidOrientation").and_then(|v| v.as_str()) == Some("horizontal");
577        let mut gravity_parts = Vec::new();
578
579        // Convert alignItems (cross-axis) to Android gravity equivalents
580        if let Some(align_items) = styles.get("alignItems") {
581            let part = match align_items.as_str() {
582                Some("center") => if is_horizontal { "center_vertical" } else { "center_horizontal" },
583                Some("flex-start") | Some("start") => if is_horizontal { "top" } else { "start" },
584                Some("flex-end") | Some("end") => if is_horizontal { "bottom" } else { "end" },
585                Some("stretch") => if is_horizontal { "fill_vertical" } else { "fill_horizontal" },
586                _ => ""
587            };
588            if !part.is_empty() {
589                gravity_parts.push(part);
590            }
591        }
592        
593        // Convert justifyContent (main-axis) to Android gravity equivalents
594        if let Some(justify) = styles.get("justifyContent") {
595            let part = match justify.as_str() {
596                Some("center") => if is_horizontal { "center_horizontal" } else { "center_vertical" },
597                Some("flex-start") | Some("start") => if is_horizontal { "start" } else { "top" },
598                Some("flex-end") | Some("end") => if is_horizontal { "end" } else { "bottom" },
599                _ => ""
600            };
601            if !part.is_empty() {
602                gravity_parts.push(part);
603            }
604
605            // Also keep layout gravity for compatibility or non-LinearLayout parents
606            let layout_gravity = match justify.as_str() {
607                Some("center") => "center_horizontal",
608                Some("flex-start") | Some("start") => "start",
609                Some("flex-end") | Some("end") => "end",
610                Some("space-between") | Some("between") => "space_between",
611                Some("space-around") | Some("around") => "space_around",
612                _ => ""
613            };
614            if !layout_gravity.is_empty() {
615                styles.insert("androidLayoutGravity".to_string(), serde_json::json!(layout_gravity));
616            }
617        }
618
619        if !gravity_parts.is_empty() {
620            let gravity = if gravity_parts.contains(&"center_vertical") && gravity_parts.contains(&"center_horizontal") {
621                "center".to_string()
622            } else {
623                gravity_parts.join("|")
624            };
625            styles.insert("androidGravity".to_string(), serde_json::json!(gravity));
626        }
627
628        // Handle border shorthand: "1px solid #color"
629        if let Some(serde_json::Value::String(border)) = styles.get("border").cloned() {
630            let parts: Vec<&str> = border.split_whitespace().collect();
631            for part in parts {
632                if part.ends_with("px") {
633                    if let Ok(w) = part.trim_end_matches("px").parse::<f32>() {
634                        styles.insert("borderWidth".to_string(), serde_json::json!(dp_to_px(w, density)));
635                    }
636                } else if part.starts_with('#') {
637                    styles.insert("borderColor".to_string(), serde_json::json!(part));
638                }
639            }
640        }
641
642        // Map boxShadow to elevation
643        if let Some(serde_json::Value::String(shadow)) = styles.get("boxShadow").cloned() {
644            if !shadow.is_empty() {
645                let elevation = if shadow.contains("20px") { 24 }
646                               else if shadow.contains("15px") { 16 }
647                               else if shadow.contains("10px") { 8 }
648                               else { 4 };
649                styles.insert("elevation".to_string(), serde_json::json!(dp_to_px(elevation as f32, density)));
650            }
651        }
652
653        // Convert overflow-x/overflow-y to Android scrolling hints
654        if let Some(overflow_x) = styles.get("overflowX") {
655            if overflow_x.as_str() == Some("auto") || overflow_x.as_str() == Some("scroll") {
656                styles.insert("androidScrollHorizontal".to_string(), serde_json::json!(true));
657            }
658        }
659        if let Some(overflow_y) = styles.get("overflowY") {
660            if overflow_y.as_str() == Some("auto") || overflow_y.as_str() == Some("scroll") {
661                styles.insert("androidScrollVertical".to_string(), serde_json::json!(true));
662            }
663        }
664        
665        // Convert textAlign to Android gravity
666        if let Some(text_align) = styles.get("textAlign") {
667            let gravity = match text_align.as_str() {
668                Some("center") => "center_horizontal",
669                Some("right") | Some("end") => "end",
670                Some("left") | Some("start") => "start",
671                _ => ""
672            };
673            if !gravity.is_empty() {
674                styles.insert("androidTextGravity".to_string(), serde_json::json!(gravity));
675            }
676        }
677
678        // Convert objectFit to Android scaleType
679        if let Some(object_fit) = styles.get("objectFit") {
680            let scale_type = match object_fit.as_str() {
681                Some("cover") => "center_crop",
682                Some("contain") => "fit_center",
683                Some("fill") => "fit_xy",
684                Some("none") => "center",
685                Some("scale-down") => "center_inside",
686                _ => ""
687            };
688            if !scale_type.is_empty() {
689                styles.insert("androidScaleType".to_string(), serde_json::json!(scale_type));
690            }
691        }
692
693        // Handle full width/height
694        if let Some(h) = styles.get("height").cloned() {
695            if h.as_str() == Some("100%") {
696                styles.insert("height".to_string(), serde_json::json!("match_parent"));
697            }
698        }
699        if let Some(w) = styles.get("width").cloned() {
700            if w.as_str() == Some("100%") {
701                styles.insert("width".to_string(), serde_json::json!("match_parent"));
702            }
703        }
704
705        // Handle flex/weight: if flex is present, set the dimension in the orientation direction to 0
706        // Note: We don't set it to 0 here anymore because we don't know if the parent is a LinearLayout.
707        // The NativeRenderer will handle setting it to 0 if it's inside a LinearLayout.
708        if styles.contains_key("flex") || styles.contains_key("flexGrow") {
709            // Just ensure we have some dimension if not specified
710            if !styles.contains_key("width") {
711                styles.insert("width".to_string(), serde_json::json!("wrap_content"));
712            }
713            if !styles.contains_key("height") {
714                styles.insert("height".to_string(), serde_json::json!("wrap_content"));
715            }
716        }
717        
718        // Convert fontWeight to Android typeface style
719        if let Some(font_weight) = styles.get("fontWeight") {
720            let is_bold = match font_weight {
721                serde_json::Value::String(s) => s.contains("bold") || s == "600" || s == "700" || s == "500",
722                serde_json::Value::Number(n) => {
723                    let weight = n.as_i64().unwrap_or(400);
724                    weight >= 500
725                }
726                _ => false
727            };
728            if is_bold {
729                styles.insert("androidTypefaceStyle".to_string(), serde_json::json!("bold"));
730            }
731        }
732        
733        // Convert boxShadow to elevation
734        if let Some(box_shadow) = styles.get("boxShadow") {
735            if let Some(shadow_str) = box_shadow.as_str() {
736                if !shadow_str.is_empty() {
737                    let elevation_dp = if shadow_str.contains("20px") { 24.0 }
738                    else if shadow_str.contains("15px") { 16.0 }
739                    else if shadow_str.contains("10px") { 8.0 }
740                    else if shadow_str.contains("5px") { 4.0 }
741                    else { 4.0 };
742                    styles.insert("elevation".to_string(), serde_json::json!(dp_to_px(elevation_dp, density)));
743                }
744            }
745        }
746        
747        styles
748    }
749
750    // Previously supported loading YAML at runtime; now defaults are embedded.
751
752    // Build the inheritance chain from current theme upward via `inherits` and default fallback
753    fn theme_chain(&self) -> Vec<String> {
754        let mut chain = Vec::new();
755        // Resolve base names
756        let default_name = if self.themes.contains_key(&self.default_theme) {
757            self.default_theme.clone()
758        } else if let Some((k, _)) = self.themes.first() { k.clone() } else { return chain };
759        let mut current_name = if self.themes.contains_key(&self.current_theme) {
760            self.current_theme.clone()
761        } else { default_name.clone() };
762        // push child first
763        let mut seen: IndexSet<String> = IndexSet::new();
764        while !seen.contains(&current_name) {
765            seen.insert(current_name.clone());
766            chain.push(current_name.clone());
767            // next parent via inherits, else stop
768            let inherits = self.themes.get(&current_name).and_then(|t| t.inherits.clone());
769            if let Some(p) = inherits {
770                current_name = p;
771            } else {
772                break;
773            }
774        }
775        if !chain.iter().any(|n| n == &default_name) {
776            chain.push(default_name);
777        }
778        chain
779    }
780
781    // Compute effective selectors + variables + breakpoints with inheritance.
782    // Child overrides parent/default on conflicts (expected for "inherits").
783    fn effective_theme_all(&self) -> (SelectorStyles, IndexMap<String, String>) {
784        let mut selectors: SelectorStyles = SelectorStyles::new();
785        let mut vars: IndexMap<String, String> = IndexMap::new();
786        // Start with deprecated globals as the lowest base
787        for (k, v) in self.variables.iter() { vars.insert(k.clone(), v.clone()); }
788        // Merge default -> parents -> child so child wins on conflicts
789        let chain = self.theme_chain();
790        for name in chain.into_iter().rev() {
791            if let Some(entry) = self.themes.get(&name) {
792                // merge selectors: later (child) overrides earlier (parent/default)
793                for (sel, props) in entry.selectors.iter() {
794                    // Support multiple selectors separated by commas (e.g., "h1, h2, h3")
795                    if sel.contains(',') {
796                        for s in sel.split(',') {
797                            let s = s.trim();
798                            if s.is_empty() { continue; }
799                            let e = selectors.entry(s.to_string()).or_default();
800                            merge_props(e, props);
801                        }
802                    } else {
803                        let e = selectors.entry(sel.clone()).or_default();
804                        merge_props(e, props);
805                    }
806                }
807                // merge variables
808                for (k, v) in entry.variables.iter() {
809                    vars.insert(k.clone(), v.clone());
810                }
811            }
812        }
813        (selectors, vars)
814    }
815
816    // Effective breakpoints with inheritance; child overrides parent/default.
817    pub fn effective_breakpoints(&self) -> IndexMap<String, String> {
818        let mut bps: IndexMap<String, String> = IndexMap::new();
819        // Start with deprecated globals
820        for (k, v) in self.breakpoints.iter() { bps.insert(k.clone(), v.clone()); }
821        let chain = self.theme_chain();
822        for name in chain.into_iter().rev() {
823            if let Some(entry) = self.themes.get(&name) {
824                for (k, v) in entry.breakpoints.iter() {
825                    bps.insert(k.clone(), v.clone());
826                }
827            }
828        }
829        bps
830    }
831}
832
833fn split_tag_class_key(key: &str) -> Option<(String, String)> {
834    let mut it = key.splitn(2, '|');
835    let t = it.next()?.to_string();
836    let c = it.next()?.to_string();
837    if t.is_empty() || c.is_empty() { return None; }
838    Some((t, c))
839}
840
841fn strip_hover_suffix(selector: &str) -> (&str, bool) {
842    if let Some(stripped) = selector.strip_suffix(":hover") { (stripped, true) } else { (selector, false) }
843}
844
845fn should_emit_selector(sel: &str, used_tags: &IndexSet<String>, used_classes: &IndexSet<String>, used_tag_classes: &IndexSet<String>) -> bool {
846    // Optionally handle :hover suffix
847    let (base, _hover) = strip_hover_suffix(sel);
848
849    // tag-only
850    if is_simple_tag(base) {
851        return used_tags.contains(base) || used_tag_classes.iter().any(|k| k.split('|').next() == Some(base));
852    }
853
854    // .class-only
855    if let Some(class_name) = base.strip_prefix('.') {
856        // Normalize potential escaped class names as-is
857        return used_classes.contains(class_name) || used_tag_classes.iter().any(|k| k.ends_with(&format!("|{}", class_name)));
858    }
859
860    // tag.class
861    if let Some((tag, class_name)) = split_tag_class_selector(base) {
862        let key = format!("{}|{}", tag, class_name);
863        return used_tag_classes.contains(&key) || (used_tags.contains(&tag) && used_classes.contains(&class_name));
864    }
865
866    // Other complex selectors are currently ignored
867    false
868}
869
870fn is_simple_tag(s: &str) -> bool {
871    // Match simple HTML tag-ish identifiers
872    let mut chars = s.chars();
873    match chars.next() { Some(c) if c.is_ascii_alphabetic() => {}, _ => return false }
874    chars.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
875}
876
877fn split_tag_class_selector(s: &str) -> Option<(String, String)> {
878    // "tag.class" -> (tag, class)
879    let mut parts = s.splitn(2, '.');
880    let tag = parts.next()?.to_string();
881    let class_name = parts.next()?.to_string();
882    if tag.is_empty() || class_name.is_empty() { return None; }
883    Some((tag, class_name))
884}
885
886// wasm-bindgen exports (only when compiling to wasm32)
887#[cfg(target_arch = "wasm32")]
888#[wasm_bindgen]
889pub fn render_css_for_web(state_json: &str) -> String {
890    match serde_json::from_str::<State>(state_json) {
891        Ok(s) => s.css_for_web(),
892        Err(_) => "".into(),
893    }
894}
895
896#[cfg(target_arch = "wasm32")]
897#[wasm_bindgen]
898pub fn get_android_styles(state_json: &str, selector: &str, classes_json: &str) -> String {
899    let classes: Vec<String> = serde_json::from_str(classes_json).unwrap_or_default();
900    match serde_json::from_str::<State>(state_json) {
901        Ok(s) => serde_json::to_string(&s.android_styles_for(selector, &classes)).unwrap_or_else(|_| "{}".into()),
902        Err(_) => "{}".into(),
903    }
904}
905
906#[cfg(target_arch = "wasm32")]
907#[wasm_bindgen]
908pub fn get_android_styles(state_json: &str, selector: &str, classes_json: &str) -> String {
909    get_android_styles(state_json, selector, classes_json)
910}
911
912// Expose crate version to JS via wasm-bindgen
913#[cfg(target_arch = "wasm32")]
914#[wasm_bindgen]
915pub fn get_version() -> String {
916    // CARGO_PKG_VERSION is provided at compile time
917    env!("CARGO_PKG_VERSION").to_string()
918}
919
920// Plain Rust accessor for crate version used by Android JNI glue
921pub fn version() -> &'static str {
922    env!("CARGO_PKG_VERSION")
923}
924
925/// Return the embedded default state as a JSON string.
926#[cfg(target_arch = "wasm32")]
927#[wasm_bindgen]
928pub fn get_default_state_json() -> String {
929    let st = bundled_state();
930    match serde_json::to_string(&st.to_json()) {
931        Ok(s) => s,
932        Err(_) => "{}".to_string(),
933    }
934}
935
936/// Register a theme from JSON. On duplicate, replace the theme's selectors, inheritance, and variables.
937/// Expected JSON format: `{ "name": "theme-name", "theme": { "inherits": "parent", "selectors": {...}, "variables": {...}, "breakpoints": {...} } }`
938/// Returns the updated state as JSON, or "{}" on error.
939#[cfg(target_arch = "wasm32")]
940#[wasm_bindgen]
941pub fn register_theme_json(state_json: &str, theme_json: &str) -> String {
942    match (serde_json::from_str::<State>(state_json), serde_json::from_str::<serde_json::Value>(theme_json)) {
943        (Ok(mut state), Ok(theme_obj)) => {
944            if let (Some(name), Some(theme_entry)) = (theme_obj.get("name"), theme_obj.get("theme")) {
945                if let Ok(entry) = serde_json::from_value::<ThemeEntry>(theme_entry.clone()) {
946                    let theme_name = name.as_str().unwrap_or("").to_string();
947                    if !theme_name.is_empty() {
948                        state.themes.insert(theme_name, entry);
949                    }
950                }
951            }
952            match serde_json::to_string(&state.to_json()) {
953                Ok(s) => s,
954                Err(_) => "{}".to_string(),
955            }
956        }
957        _ => "{}".to_string(),
958    }
959}
960
961/// Set the default and current theme. Returns the updated state as JSON.
962#[cfg(target_arch = "wasm32")]
963#[wasm_bindgen]
964pub fn set_theme_json(state_json: &str, theme_name: &str) -> String {
965    match serde_json::from_str::<State>(state_json) {
966        Ok(mut state) => {
967            if state.themes.contains_key(theme_name) {
968                state.default_theme = theme_name.to_string();
969                state.current_theme = theme_name.to_string();
970            }
971            match serde_json::to_string(&state.to_json()) {
972                Ok(s) => s,
973                Err(_) => "{}".to_string(),
974            }
975        }
976        _ => "{}".to_string(),
977    }
978}
979
980/// Get all theme keys and names as JSON array: [{ "key": "default", "name": "Default Theme" }, ...]
981/// Returns array of themes from the state JSON.
982#[cfg(target_arch = "wasm32")]
983#[wasm_bindgen]
984pub fn get_theme_list_json(state_json: &str) -> String {
985    match serde_json::from_str::<State>(state_json) {
986        Ok(state) => {
987            let themes: Vec<serde_json::Value> = state.themes.iter().map(|(key, entry)| {
988                json!({
989                    "key": key,
990                    "name": entry.name.as_ref().unwrap_or(key)
991                })
992            }).collect();
993            serde_json::to_string(&themes).unwrap_or_else(|_| "[]".to_string())
994        }
995        _ => "[]".to_string(),
996    }
997}
998
999fn merge_props(into: &mut CssProps, from: &CssProps) {
1000    for (k, v) in from.iter() {
1001        into.insert(k.clone(), v.clone());
1002    }
1003}
1004
1005// merge_indexmap removed — unused
1006
1007fn css_props_string(props: &CssProps, vars: &IndexMap<String, String>) -> String {
1008    let mut buf = String::new();
1009    for (k, v) in props.iter() {
1010        buf.push_str(k);
1011        buf.push(':');
1012        let val = if v.is_string() {
1013            let s = v.as_str().unwrap();
1014            resolve_vars(s, vars)
1015        } else {
1016            v.to_string()
1017        };
1018        buf.push_str(&val);
1019        if !val.ends_with(';') {
1020            buf.push(';');
1021        }
1022    }
1023    buf
1024}
1025
1026/// Parse var() references manually (replaces regex dependency)
1027/// Matches: var(--name), var(name), with optional whitespace
1028/// Supports alphanumeric, underscore, dot, and dash in variable names
1029fn parse_var_references(input: &str) -> Vec<(usize, usize, String)> {
1030    let mut results = Vec::new();
1031    let bytes = input.as_bytes();
1032    let mut i = 0;
1033    
1034    while i < bytes.len() {
1035        // Look for "var("
1036        if i + 4 <= bytes.len() && &bytes[i..i+4] == b"var(" {
1037            let start = i;
1038            i += 4;
1039            
1040            // Skip whitespace
1041            while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'\n' || bytes[i] == b'\r') {
1042                i += 1;
1043            }
1044            
1045            // Check for optional -- prefix
1046            let has_prefix = i + 2 <= bytes.len() && &bytes[i..i+2] == b"--";
1047            if has_prefix {
1048                i += 2;
1049            }
1050            
1051            // Collect variable name: [a-zA-Z0-9_.-]+
1052            let name_start = i;
1053            while i < bytes.len() {
1054                let c = bytes[i];
1055                if (c >= b'a' && c <= b'z') || (c >= b'A' && c <= b'Z') || 
1056                   (c >= b'0' && c <= b'9') || c == b'_' || c == b'.' || c == b'-' {
1057                    i += 1;
1058                } else {
1059                    break;
1060                }
1061            }
1062            
1063            let name_end = i;
1064            if name_start < name_end {
1065                // Skip trailing whitespace
1066                while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'\n' || bytes[i] == b'\r') {
1067                    i += 1;
1068                }
1069                
1070                // Check for closing )
1071                if i < bytes.len() && bytes[i] == b')' {
1072                    let end = i + 1;
1073                    let var_name = std::str::from_utf8(&bytes[name_start..name_end])
1074                        .unwrap_or("").to_string();
1075                    results.push((start, end, var_name));
1076                    i = end;
1077                    continue;
1078                }
1079            }
1080        }
1081        i += 1;
1082    }
1083    
1084    results
1085}
1086
1087// Tailwind color palette - embedded from tailwind-colors.html
1088static TAILWIND_COLORS: Lazy<IndexMap<&'static str, IndexMap<&'static str, &'static str>>> = Lazy::new(|| {
1089    let mut colors = IndexMap::new();
1090    
1091    let mut slate = IndexMap::new();
1092    slate.insert("50", "#f8fafc"); slate.insert("100", "#f1f5f9"); slate.insert("200", "#e2e8f0");
1093    slate.insert("300", "#cbd5e1"); slate.insert("400", "#94a3b8"); slate.insert("500", "#64748b");
1094    slate.insert("600", "#475569"); slate.insert("700", "#334155"); slate.insert("800", "#1e293b");
1095    slate.insert("900", "#0f172a"); slate.insert("950", "#020617");
1096    colors.insert("slate", slate);
1097    
1098    let mut gray = IndexMap::new();
1099    gray.insert("50", "#f9fafb"); gray.insert("100", "#f3f4f6"); gray.insert("200", "#e5e7eb");
1100    gray.insert("300", "#d1d5db"); gray.insert("400", "#9ca3af"); gray.insert("500", "#6b7280");
1101    gray.insert("600", "#4b5563"); gray.insert("700", "#374151"); gray.insert("800", "#1f2937");
1102    gray.insert("900", "#111827"); gray.insert("950", "#030712");
1103    colors.insert("gray", gray);
1104    
1105    let mut zinc = IndexMap::new();
1106    zinc.insert("50", "#fafafa"); zinc.insert("100", "#f4f4f5"); zinc.insert("200", "#e4e4e7");
1107    zinc.insert("300", "#d4d4d8"); zinc.insert("400", "#a1a1aa"); zinc.insert("500", "#71717a");
1108    zinc.insert("600", "#52525b"); zinc.insert("700", "#3f3f46"); zinc.insert("800", "#27272a");
1109    zinc.insert("900", "#18181b"); zinc.insert("950", "#09090b");
1110    colors.insert("zinc", zinc);
1111    
1112    let mut neutral = IndexMap::new();
1113    neutral.insert("50", "#fafafa"); neutral.insert("100", "#f5f5f5"); neutral.insert("200", "#e5e5e5");
1114    neutral.insert("300", "#d4d4d4"); neutral.insert("400", "#a3a3a3"); neutral.insert("500", "#737373");
1115    neutral.insert("600", "#525252"); neutral.insert("700", "#404040"); neutral.insert("800", "#262626");
1116    neutral.insert("900", "#171717"); neutral.insert("950", "#0a0a0a");
1117    colors.insert("neutral", neutral);
1118    
1119    let mut stone = IndexMap::new();
1120    stone.insert("50", "#fafaf9"); stone.insert("100", "#f5f5f4"); stone.insert("200", "#e7e5e4");
1121    stone.insert("300", "#d6d3d1"); stone.insert("400", "#a8a29e"); stone.insert("500", "#78716c");
1122    stone.insert("600", "#57534e"); stone.insert("700", "#44403c"); stone.insert("800", "#292524");
1123    stone.insert("900", "#1c1917"); stone.insert("950", "#0c0a09");
1124    colors.insert("stone", stone);
1125    
1126    let mut red = IndexMap::new();
1127    red.insert("50", "#fef2f2"); red.insert("100", "#fee2e2"); red.insert("200", "#fecaca");
1128    red.insert("300", "#fca5a5"); red.insert("400", "#f87171"); red.insert("500", "#ef4444");
1129    red.insert("600", "#dc2626"); red.insert("700", "#b91c1c"); red.insert("800", "#991b1b");
1130    red.insert("900", "#7f1d1d"); red.insert("950", "#450a0a");
1131    colors.insert("red", red);
1132    
1133    let mut orange = IndexMap::new();
1134    orange.insert("50", "#fff7ed"); orange.insert("100", "#ffedd5"); orange.insert("200", "#fed7aa");
1135    orange.insert("300", "#fdba74"); orange.insert("400", "#fb923c"); orange.insert("500", "#f97316");
1136    orange.insert("600", "#ea580c"); orange.insert("700", "#c2410c"); orange.insert("800", "#9a3412");
1137    orange.insert("900", "#7c2d12"); orange.insert("950", "#431407");
1138    colors.insert("orange", orange);
1139    
1140    let mut amber = IndexMap::new();
1141    amber.insert("50", "#fffbeb"); amber.insert("100", "#fef3c7"); amber.insert("200", "#fde68a");
1142    amber.insert("300", "#fcd34d"); amber.insert("400", "#fbbf24"); amber.insert("500", "#f59e0b");
1143    amber.insert("600", "#d97706"); amber.insert("700", "#b45309"); amber.insert("800", "#92400e");
1144    amber.insert("900", "#78350f"); amber.insert("950", "#451a03");
1145    colors.insert("amber", amber);
1146    
1147    let mut blue = IndexMap::new();
1148    blue.insert("50", "#eff6ff"); blue.insert("100", "#dbeafe"); blue.insert("200", "#bfdbfe");
1149    blue.insert("300", "#93c5fd"); blue.insert("400", "#60a5fa"); blue.insert("500", "#3b82f6");
1150    blue.insert("600", "#2563eb"); blue.insert("700", "#1d4ed8"); blue.insert("800", "#1e40af");
1151    blue.insert("900", "#1e3a8a"); blue.insert("950", "#0b1c52");
1152    colors.insert("blue", blue);
1153    
1154    let mut lime = IndexMap::new();
1155    lime.insert("50", "#f7fee7"); lime.insert("100", "#ecfccb"); lime.insert("200", "#d9f99d");
1156    lime.insert("300", "#bef264"); lime.insert("400", "#a3e635"); lime.insert("500", "#84cc16");
1157    lime.insert("600", "#65a30d"); lime.insert("700", "#4d7c0f"); lime.insert("800", "#3f6212");
1158    lime.insert("900", "#365314"); lime.insert("950", "#1a2e05");
1159    colors.insert("lime", lime);
1160    
1161    let mut green = IndexMap::new();
1162    green.insert("50", "#f0fdf4"); green.insert("100", "#dcfce7"); green.insert("200", "#bbf7d0");
1163    green.insert("300", "#86efac"); green.insert("400", "#4ade80"); green.insert("500", "#22c55e");
1164    green.insert("600", "#16a34a"); green.insert("700", "#15803d"); green.insert("800", "#166534");
1165    green.insert("900", "#14532d"); green.insert("950", "#052e16");
1166    colors.insert("green", green);
1167    
1168    let mut emerald = IndexMap::new();
1169    emerald.insert("50", "#ecfdf5"); emerald.insert("100", "#d1fae5"); emerald.insert("200", "#a7f3d0");
1170    emerald.insert("300", "#6ee7b7"); emerald.insert("400", "#34d399"); emerald.insert("500", "#10b981");
1171    emerald.insert("600", "#059669"); emerald.insert("700", "#047857"); emerald.insert("800", "#065f46");
1172    emerald.insert("900", "#064e3b"); emerald.insert("950", "#022c22");
1173    colors.insert("emerald", emerald);
1174    
1175    let mut teal = IndexMap::new();
1176    teal.insert("50", "#f0fdfa"); teal.insert("100", "#ccfbf1"); teal.insert("200", "#99f6e4");
1177    teal.insert("300", "#5eead4"); teal.insert("400", "#2dd4bf"); teal.insert("500", "#14b8a6");
1178    teal.insert("600", "#0d9488"); teal.insert("700", "#0f766e"); teal.insert("800", "#115e59");
1179    teal.insert("900", "#134e4a"); teal.insert("950", "#042f2e");
1180    colors.insert("teal", teal);
1181    
1182    let mut cyan = IndexMap::new();
1183    cyan.insert("50", "#ecfeff"); cyan.insert("100", "#cffafe"); cyan.insert("200", "#a5f3fc");
1184    cyan.insert("300", "#67e8f9"); cyan.insert("400", "#22d3ee"); cyan.insert("500", "#06b6d4");
1185    cyan.insert("600", "#0891b2"); cyan.insert("700", "#0e7490"); cyan.insert("800", "#155e75");
1186    cyan.insert("900", "#164e63"); cyan.insert("950", "#083344");
1187    colors.insert("cyan", cyan);
1188    
1189    let mut sky = IndexMap::new();
1190    sky.insert("50", "#f0f9ff"); sky.insert("100", "#e0f2fe"); sky.insert("200", "#bae6fd");
1191    sky.insert("300", "#7dd3fc"); sky.insert("400", "#38bdf8"); sky.insert("500", "#0ea5e9");
1192    sky.insert("600", "#0284c7"); sky.insert("700", "#0369a1"); sky.insert("800", "#075985");
1193    sky.insert("900", "#0c4a6e"); sky.insert("950", "#082f49");
1194    colors.insert("sky", sky);
1195    
1196    let mut blue = IndexMap::new();
1197    blue.insert("50", "#eff6ff"); blue.insert("100", "#dbeafe"); blue.insert("200", "#bfdbfe");
1198    blue.insert("300", "#93c5fd"); blue.insert("400", "#60a5fa"); blue.insert("500", "#3b82f6");
1199    blue.insert("600", "#2563eb"); blue.insert("700", "#1d4ed8"); blue.insert("800", "#1e40af");
1200    blue.insert("900", "#1e3a8a"); blue.insert("950", "#172554");
1201    colors.insert("blue", blue);
1202    
1203    let mut indigo = IndexMap::new();
1204    indigo.insert("50", "#eef2ff"); indigo.insert("100", "#e0e7ff"); indigo.insert("200", "#c7d2fe");
1205    indigo.insert("300", "#a5b4fc"); indigo.insert("400", "#818cf8"); indigo.insert("500", "#6366f1");
1206    indigo.insert("600", "#4f46e5"); indigo.insert("700", "#4338ca"); indigo.insert("800", "#3730a3");
1207    indigo.insert("900", "#312e81"); indigo.insert("950", "#1e1b4b");
1208    colors.insert("indigo", indigo);
1209    
1210    let mut violet = IndexMap::new();
1211    violet.insert("50", "#f5f3ff"); violet.insert("100", "#ede9fe"); violet.insert("200", "#ddd6fe");
1212    violet.insert("300", "#c4b5fd"); violet.insert("400", "#a78bfa"); violet.insert("500", "#8b5cf6");
1213    violet.insert("600", "#7c3aed"); violet.insert("700", "#6d28d9"); violet.insert("800", "#5b21b6");
1214    violet.insert("900", "#4c1d95"); violet.insert("950", "#2e1065");
1215    colors.insert("violet", violet);
1216    
1217    let mut purple = IndexMap::new();
1218    purple.insert("50", "#faf5ff"); purple.insert("100", "#f3e8ff"); purple.insert("200", "#e9d5ff");
1219    purple.insert("300", "#d8b4fe"); purple.insert("400", "#c084fc"); purple.insert("500", "#a855f7");
1220    purple.insert("600", "#9333ea"); purple.insert("700", "#7e22ce"); purple.insert("800", "#6b21a8");
1221    purple.insert("900", "#581c87"); purple.insert("950", "#3b0764");
1222    colors.insert("purple", purple);
1223    
1224    let mut fuchsia = IndexMap::new();
1225    fuchsia.insert("50", "#fdf4ff"); fuchsia.insert("100", "#fae8ff"); fuchsia.insert("200", "#f5d0fe");
1226    fuchsia.insert("300", "#f0abfc"); fuchsia.insert("400", "#e879f9"); fuchsia.insert("500", "#d946ef");
1227    fuchsia.insert("600", "#c026d3"); fuchsia.insert("700", "#a21caf"); fuchsia.insert("800", "#86198f");
1228    fuchsia.insert("900", "#701a75"); fuchsia.insert("950", "#4a044e");
1229    colors.insert("fuchsia", fuchsia);
1230    
1231    let mut pink = IndexMap::new();
1232    pink.insert("50", "#fdf2f8"); pink.insert("100", "#fce7f3"); pink.insert("200", "#fbcfe8");
1233    pink.insert("300", "#f9a8d4"); pink.insert("400", "#f472b6"); pink.insert("500", "#ec4899");
1234    pink.insert("600", "#db2777"); pink.insert("700", "#be185d"); pink.insert("800", "#9d174d");
1235    pink.insert("900", "#831843"); pink.insert("950", "#500724");
1236    colors.insert("pink", pink);
1237    
1238    let mut rose = IndexMap::new();
1239    rose.insert("50", "#fff1f2"); rose.insert("100", "#ffe4e6"); rose.insert("200", "#fecdd3");
1240    rose.insert("300", "#fda4af"); rose.insert("400", "#fb7185"); rose.insert("500", "#f43f5e");
1241    rose.insert("600", "#e11d48"); rose.insert("700", "#be123c"); rose.insert("800", "#9f1239");
1242    rose.insert("900", "#881337"); rose.insert("950", "#4c0519");
1243    colors.insert("rose", rose);
1244    
1245    colors
1246});
1247
1248fn resolve_vars(input: &str, vars: &IndexMap<String, String>) -> String {
1249    let var_refs = parse_var_references(input);
1250    
1251    if var_refs.is_empty() {
1252        // Fast path: no var() references, just check for $ prefix
1253        if input.starts_with('$') {
1254            if let Some(val) = vars.get(&input[1..]) {
1255                return val.clone();
1256            }
1257        }
1258        return input.to_string();
1259    }
1260    
1261    // Replace var() references from right to left to preserve indices
1262    let mut out = input.to_string();
1263    for (start, end, var_name) in var_refs.iter().rev() {
1264        if let Some(val) = vars.get(var_name) {
1265            out.replace_range(*start..*end, val);
1266        }
1267    }
1268    
1269    // Also handle $ prefix for direct variable references
1270    if out.starts_with('$') {
1271        if let Some(val) = vars.get(&out[1..]) {
1272            return val.clone();
1273        }
1274    }
1275    
1276    out
1277}
1278
1279fn camel_case(name: &str) -> String {
1280    let mut out = String::new();
1281    let mut upper = false;
1282    for ch in name.chars() {
1283        if ch == '-' {
1284            upper = true;
1285            continue;
1286        }
1287        if upper {
1288            out.extend(ch.to_uppercase());
1289            upper = false;
1290        } else {
1291            out.push(ch);
1292        }
1293    }
1294    out
1295}
1296
1297fn css_value_to_android(
1298    value: &serde_json::Value,
1299    vars: &IndexMap<String, String>,
1300) -> serde_json::Value {
1301    match value {
1302        serde_json::Value::String(s) => {
1303            let s2 = resolve_vars(s, vars);
1304            if let Some(n) = s2.strip_suffix("px") {
1305                if let Ok(parsed) = n.trim().parse::<f64>() {
1306                    return json!(parsed);
1307                }
1308            }
1309            json!(s2)
1310        }
1311        _ => value.clone(),
1312    }
1313}
1314
1315fn merge_android_props(
1316    into: &mut IndexMap<String, serde_json::Value>,
1317    css_props: &CssProps,
1318    vars: &IndexMap<String, String>,
1319) {
1320    for (k, v) in css_props.iter() {
1321        let val = css_value_to_android(v, vars);
1322        
1323        match k.as_str() {
1324            "padding" => {
1325                into.insert("paddingTop".to_string(), val.clone());
1326                into.insert("paddingBottom".to_string(), val.clone());
1327                into.insert("paddingLeft".to_string(), val.clone());
1328                into.insert("paddingRight".to_string(), val.clone());
1329                into.insert("paddingHorizontal".to_string(), val.clone());
1330                into.insert("paddingVertical".to_string(), val.clone());
1331                into.insert("padding".to_string(), val);
1332            }
1333            "padding-horizontal" | "paddingHorizontal" => {
1334                into.insert("paddingLeft".to_string(), val.clone());
1335                into.insert("paddingRight".to_string(), val.clone());
1336                into.insert("paddingHorizontal".to_string(), val);
1337            }
1338            "padding-vertical" | "paddingVertical" => {
1339                into.insert("paddingTop".to_string(), val.clone());
1340                into.insert("paddingBottom".to_string(), val.clone());
1341                into.insert("paddingVertical".to_string(), val);
1342            }
1343            "margin" => {
1344                into.insert("marginTop".to_string(), val.clone());
1345                into.insert("marginBottom".to_string(), val.clone());
1346                into.insert("marginLeft".to_string(), val.clone());
1347                into.insert("marginRight".to_string(), val.clone());
1348                into.insert("marginHorizontal".to_string(), val.clone());
1349                into.insert("marginVertical".to_string(), val.clone());
1350                into.insert("margin".to_string(), val);
1351            }
1352            "margin-horizontal" | "marginHorizontal" => {
1353                into.insert("marginLeft".to_string(), val.clone());
1354                into.insert("marginRight".to_string(), val.clone());
1355                into.insert("marginHorizontal".to_string(), val);
1356            }
1357            "margin-vertical" | "marginVertical" => {
1358                into.insert("marginTop".to_string(), val.clone());
1359                into.insert("marginBottom".to_string(), val.clone());
1360                into.insert("marginVertical".to_string(), val);
1361            }
1362            "border-radius" | "borderRadius" => {
1363                into.insert("borderTopLeftRadius".to_string(), val.clone());
1364                into.insert("borderTopRightRadius".to_string(), val.clone());
1365                into.insert("borderBottomLeftRadius".to_string(), val.clone());
1366                into.insert("borderBottomRightRadius".to_string(), val.clone());
1367                into.insert("borderRadius".to_string(), val);
1368            }
1369            "background-color" => { into.insert("backgroundColor".to_string(), val); }
1370            "text-align" => { into.insert("textAlign".to_string(), val); }
1371            "flex-direction" | "flexDirection" => {
1372                let orientation = if val.as_str() == Some("column") || val.as_str() == Some("column-reverse") {
1373                    "vertical"
1374                } else {
1375                    "horizontal"
1376                };
1377                into.insert("androidOrientation".to_string(), serde_json::json!(orientation));
1378                into.insert("flexDirection".to_string(), val);
1379            }
1380            _ => {
1381                into.insert(camel_case(k), val);
1382            }
1383        }
1384    }
1385}
1386
1387fn dynamic_css_properties_for_class(class: &str, vars: &IndexMap<String, String>) -> Option<CssProps> {
1388    // Display utilities
1389    match class {
1390        "block" => { let mut p = CssProps::new(); p.insert("display".into(), json!("block")); return Some(p); }
1391        "inline-block" => { let mut p = CssProps::new(); p.insert("display".into(), json!("inline-block")); return Some(p); }
1392        "inline" => { let mut p = CssProps::new(); p.insert("display".into(), json!("inline")); return Some(p); }
1393        "inline-flex" => { let mut p = CssProps::new(); p.insert("display".into(), json!("inline-flex")); return Some(p); }
1394        "grid" => { let mut p = CssProps::new(); p.insert("display".into(), json!("grid")); return Some(p); }
1395        "hidden" => { let mut p = CssProps::new(); p.insert("display".into(), json!("none")); return Some(p); }
1396        _ => {}
1397    }
1398    // Flexbox shorthands
1399    match class {
1400        "flex" => { let mut p = CssProps::new(); p.insert("display".into(), json!("flex")); return Some(p); }
1401        "flex-row" => { let mut p = CssProps::new(); p.insert("display".into(), json!("flex")); p.insert("flexDirection".into(), json!("row")); return Some(p); }
1402        "flex-col" => { let mut p = CssProps::new(); p.insert("display".into(), json!("flex")); p.insert("flexDirection".into(), json!("column")); return Some(p); }
1403        "flex-wrap" => { let mut p = CssProps::new(); p.insert("display".into(), json!("flex")); p.insert("flex-wrap".into(), json!("wrap")); return Some(p); }
1404        "flex-nowrap" => { let mut p = CssProps::new(); p.insert("display".into(), json!("flex")); p.insert("flex-wrap".into(), json!("nowrap")); return Some(p); }
1405        "flex-wrap-reverse" => { let mut p = CssProps::new(); p.insert("display".into(), json!("flex")); p.insert("flex-wrap".into(), json!("wrap-reverse")); return Some(p); }
1406        "flex-1" => { let mut p = CssProps::new(); p.insert("flex".into(), json!(1)); return Some(p); }
1407        "w-full" => { let mut p = CssProps::new(); p.insert("width".into(), json!("match_parent")); return Some(p); }
1408        "h-full" => { let mut p = CssProps::new(); p.insert("height".into(), json!("match_parent")); return Some(p); }
1409        _ => {}
1410    }
1411    if let Some(value) = class.strip_prefix("z-") {
1412        if let Ok(z) = value.parse::<i32>() {
1413            let mut p = CssProps::new();
1414            p.insert("elevation".into(), json!(z));
1415            return Some(p);
1416        }
1417    }
1418    if let Some(rest) = class.strip_prefix("items-") {
1419        let mut p = CssProps::new();
1420        let v = match rest { "start" => "flex-start", "end" => "flex-end", "center" => "center", "stretch" => "stretch", other => other };
1421        p.insert("align-items".into(), json!(v));
1422        return Some(p);
1423    }
1424    if let Some(rest) = class.strip_prefix("justify-") {
1425        let mut p = CssProps::new();
1426        let v = match rest { "start" => "flex-start", "end" => "flex-end", "center" => "center", "between" => "space-between", "around" => "space-around", "evenly" => "space-evenly", other => other };
1427        p.insert("justify-content".into(), json!(v));
1428        return Some(p);
1429    }
1430    if let Some(value) = class.strip_prefix("p-") {
1431        return parse_tailwind_spacing(value, &|px| padding_props(&["padding"], px));
1432    }
1433    if let Some(value) = class.strip_prefix("px-") {
1434        return parse_tailwind_spacing(value, &|px| padding_props(&["padding-left", "padding-right"], px));
1435    }
1436    if let Some(value) = class.strip_prefix("py-") {
1437        return parse_tailwind_spacing(value, &|px| padding_props(&["padding-top", "padding-bottom"], px));
1438    }
1439    for &(prefix, prop) in &[("pt-", "padding-top"), ("pr-", "padding-right"), ("pb-", "padding-bottom"), ("pl-", "padding-left")] {
1440        if let Some(value) = class.strip_prefix(prefix) {
1441            return parse_tailwind_spacing(value, &|px| padding_props(&[prop], px));
1442        }
1443    }
1444    // Margin utilities
1445    if let Some(value) = class.strip_prefix("m-") {
1446        if value == "auto" {
1447            let mut p = CssProps::new();
1448            p.insert("margin".into(), json!("auto"));
1449            return Some(p);
1450        }
1451        return parse_tailwind_spacing(value, &|px| margin_props(&["margin"], px));
1452    }
1453    if let Some(value) = class.strip_prefix("mx-") {
1454        if value == "auto" {
1455            let mut p = CssProps::new();
1456            p.insert("margin-left".into(), json!("auto"));
1457            p.insert("margin-right".into(), json!("auto"));
1458            return Some(p);
1459        }
1460        return parse_tailwind_spacing(value, &|px| margin_props(&["margin-left", "margin-right"], px));
1461    }
1462    if let Some(value) = class.strip_prefix("my-") {
1463        if value == "auto" {
1464            let mut p = CssProps::new();
1465            p.insert("margin-top".into(), json!("auto"));
1466            p.insert("margin-bottom".into(), json!("auto"));
1467            return Some(p);
1468        }
1469        return parse_tailwind_spacing(value, &|px| margin_props(&["margin-top", "margin-bottom"], px));
1470    }
1471    for &(prefix, prop) in &[("mt-", "margin-top"), ("mr-", "margin-right"), ("mb-", "margin-bottom"), ("ml-", "margin-left")] {
1472        if let Some(value) = class.strip_prefix(prefix) {
1473            if value == "auto" {
1474                let mut p = CssProps::new();
1475                p.insert(prop.into(), json!("auto"));
1476                return Some(p);
1477            }
1478            return parse_tailwind_spacing(value, &|px| margin_props(&[prop], px));
1479        }
1480    }
1481    // Gap utilities (works in Android with Flexbox)
1482    if let Some(value) = class.strip_prefix("gap-") {
1483        if !value.starts_with("x-") && !value.starts_with("y-") {
1484            return parse_tailwind_spacing(value, &|px| {
1485                let mut props = CssProps::new();
1486                props.insert("gap".into(), json!(format!("{}px", px)));
1487                props
1488            });
1489        }
1490    }
1491    if let Some(value) = class.strip_prefix("gap-x-") {
1492        return parse_tailwind_spacing(value, &|px| {
1493            let mut props = CssProps::new();
1494            props.insert("column-gap".into(), json!(format!("{}px", px)));
1495            props
1496        });
1497    }
1498    if let Some(value) = class.strip_prefix("gap-y-") {
1499        return parse_tailwind_spacing(value, &|px| {
1500            let mut props = CssProps::new();
1501            props.insert("row-gap".into(), json!(format!("{}px", px)));
1502            props
1503        });
1504    }
1505    // Space utilities (space-x-*, space-y-*)
1506    if let Some(value) = class.strip_prefix("space-x-") {
1507        return parse_tailwind_spacing(value, &|px| {
1508            let mut props = CssProps::new();
1509            // In CSS, this is typically done with :not(:last-child) selector
1510            // For now, we'll set it as a custom property that can be used
1511            props.insert("--space-x".into(), json!(format!("{}px", px)));
1512            props
1513        });
1514    }
1515    if let Some(value) = class.strip_prefix("space-y-") {
1516        return parse_tailwind_spacing(value, &|px| {
1517            let mut props = CssProps::new();
1518            props.insert("--space-y".into(), json!(format!("{}px", px)));
1519            props
1520        });
1521    }
1522    // Font weight utilities
1523    match class {
1524        "font-thin" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("100")); return Some(p); }
1525        "font-extralight" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("200")); return Some(p); }
1526        "font-light" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("300")); return Some(p); }
1527        "font-normal" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("400")); return Some(p); }
1528        "font-medium" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("500")); return Some(p); }
1529        "font-semibold" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("600")); return Some(p); }
1530        "font-bold" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("700")); return Some(p); }
1531        "font-extrabold" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("800")); return Some(p); }
1532        "font-black" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("900")); return Some(p); }
1533        _ => {}
1534    }
1535    // Font family utilities
1536    match class {
1537        "font-sans" => { let mut p = CssProps::new(); p.insert("font-family".into(), json!("system-ui, -apple-system, sans-serif")); return Some(p); }
1538        "font-serif" => { let mut p = CssProps::new(); p.insert("font-family".into(), json!("Georgia, serif")); return Some(p); }
1539        "font-mono" => { let mut p = CssProps::new(); p.insert("font-family".into(), json!("ui-monospace, monospace")); return Some(p); }
1540        _ => {}
1541    }
1542    // Text size utilities
1543    match class {
1544        "text-xs" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("12px")); p.insert("line-height".into(), json!("16px")); return Some(p); }
1545        "text-sm" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("14px")); p.insert("line-height".into(), json!("20px")); return Some(p); }
1546        "text-base" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("16px")); p.insert("line-height".into(), json!("24px")); return Some(p); }
1547        "text-lg" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("18px")); p.insert("line-height".into(), json!("28px")); return Some(p); }
1548        "text-xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("20px")); p.insert("line-height".into(), json!("28px")); return Some(p); }
1549        "text-2xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("24px")); p.insert("line-height".into(), json!("32px")); return Some(p); }
1550        "text-3xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("30px")); p.insert("line-height".into(), json!("36px")); return Some(p); }
1551        "text-4xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("36px")); p.insert("line-height".into(), json!("40px")); return Some(p); }
1552        "text-5xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("48px")); p.insert("line-height".into(), json!("1")); return Some(p); }
1553        "text-6xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("60px")); p.insert("line-height".into(), json!("1")); return Some(p); }
1554        _ => {}
1555    }
1556    // Text alignment
1557    match class {
1558        "text-left" => { let mut p = CssProps::new(); p.insert("text-align".into(), json!("left")); return Some(p); }
1559        "text-center" => { let mut p = CssProps::new(); p.insert("text-align".into(), json!("center")); return Some(p); }
1560        "text-right" => { let mut p = CssProps::new(); p.insert("text-align".into(), json!("right")); return Some(p); }
1561        "text-justify" => { let mut p = CssProps::new(); p.insert("text-align".into(), json!("justify")); return Some(p); }
1562        _ => {}
1563    }
1564    // Overflow utilities
1565    match class {
1566        "overflow-auto" => { let mut p = CssProps::new(); p.insert("overflow".into(), json!("auto")); return Some(p); }
1567        "overflow-hidden" => { let mut p = CssProps::new(); p.insert("overflow".into(), json!("hidden")); return Some(p); }
1568        "overflow-visible" => { let mut p = CssProps::new(); p.insert("overflow".into(), json!("visible")); return Some(p); }
1569        "overflow-scroll" => { let mut p = CssProps::new(); p.insert("overflow".into(), json!("scroll")); return Some(p); }
1570        "overflow-x-auto" => { let mut p = CssProps::new(); p.insert("overflow-x".into(), json!("auto")); return Some(p); }
1571        "overflow-x-hidden" => { let mut p = CssProps::new(); p.insert("overflow-x".into(), json!("hidden")); return Some(p); }
1572        "overflow-x-scroll" => { let mut p = CssProps::new(); p.insert("overflow-x".into(), json!("scroll")); return Some(p); }
1573        "overflow-y-auto" => { let mut p = CssProps::new(); p.insert("overflow-y".into(), json!("auto")); return Some(p); }
1574        "overflow-y-hidden" => { let mut p = CssProps::new(); p.insert("overflow-y".into(), json!("hidden")); return Some(p); }
1575        "overflow-y-scroll" => { let mut p = CssProps::new(); p.insert("overflow-y".into(), json!("scroll")); return Some(p); }
1576        _ => {}
1577    }
1578    // Opacity utilities
1579    if let Some(value) = class.strip_prefix("opacity-") {
1580        if let Ok(opacity) = value.parse::<f32>() {
1581            let mut p = CssProps::new();
1582            p.insert("opacity".into(), json!(opacity / 100.0));
1583            return Some(p);
1584        }
1585    }
1586    // Shadow utilities (basic cross-platform support)
1587    match class {
1588        "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); }
1589        "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); }
1590        "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); }
1591        "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); }
1592        "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); }
1593        "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); }
1594        "shadow-none" => { let mut p = CssProps::new(); p.insert("box-shadow".into(), json!("none")); return Some(p); }
1595        _ => {}
1596    }
1597    // Parse arbitrary values like bg-[var(--primary)], text-[#ff0000], etc.
1598    if let Some(arb_value) = parse_arbitrary_value(class) {
1599        return Some(arb_value);
1600    }
1601    // text-{color}-{shade}
1602    if let Some(rest) = class.strip_prefix("text-") {
1603        if let Some(hex) = get_tailwind_color_with_vars(rest, vars) {
1604            let mut props = CssProps::new();
1605            props.insert("color".into(), json!(hex));
1606            return Some(props);
1607        }
1608    }
1609    // bg-{color}-{shade}
1610    if let Some(rest) = class.strip_prefix("bg-") {
1611        match rest {
1612            "white" => { let mut p = CssProps::new(); p.insert("background-color".into(), json!("#ffffff")); return Some(p); }
1613            "black" => { let mut p = CssProps::new(); p.insert("background-color".into(), json!("#000000")); return Some(p); }
1614            "transparent" => { let mut p = CssProps::new(); p.insert("background-color".into(), json!("#00000000")); return Some(p); }
1615            _ => {}
1616        }
1617        if let Some(hex) = get_tailwind_color_with_vars(rest, vars) {
1618            let mut props = CssProps::new();
1619            props.insert("background-color".into(), json!(hex));
1620            return Some(props);
1621        }
1622    }
1623    // divide-{color}-{shade} (sets border-color for child dividers)
1624    if let Some(rest) = class.strip_prefix("divide-") {
1625        if let Some(hex) = get_tailwind_color_with_vars(rest, vars) {
1626            let mut props = CssProps::new();
1627            props.insert("border-color".into(), json!(hex));
1628            return Some(props);
1629        }
1630    }
1631    if class == "border" {
1632        return Some(border_props(None, 1, vars));
1633    }
1634    if let Some(rest) = class.strip_prefix("border-") {
1635        // Parse border-* classes
1636        // Possible patterns:
1637        // - border-{color}-{shade} → border-color
1638        // - border-{side}-{color}-{shade} → border-{side}-color
1639        // - border-{width} → border-width
1640        // - border-{side}-{width} → border-{side}-width
1641        
1642        let parts: Vec<&str> = rest.split('-').collect();
1643        
1644        // Check if first part is a directional side (t, b, l, r, x, y)
1645        let valid_sides = ["t", "b", "l", "r", "x", "y"];
1646        let (side, color_or_width_parts) = if parts.len() > 1 && valid_sides.contains(&parts[0]) {
1647            (Some(parts[0]), &parts[1..])
1648        } else {
1649            (None, &parts[..])
1650        };
1651        
1652        // Now check if remaining parts form a color-shade pattern
1653        if color_or_width_parts.len() == 2 {
1654            // Could be color-shade like "blue-500"
1655            let color_shade = color_or_width_parts.join("-");
1656            if let Some(hex) = get_tailwind_color_with_vars(&color_shade, vars) {
1657                let mut props = CssProps::new();
1658                let prop_name = if let Some(s) = side {
1659                    format!("border-{}-color", s)
1660                } else {
1661                    "border-color".to_string()
1662                };
1663                props.insert(prop_name, json!(hex));
1664                return Some(props);
1665            }
1666        }
1667        
1668        // Check for simple color without shade (single word color like "black", "white")
1669        if color_or_width_parts.len() == 1 {
1670            let potential_color = format!("{}-500", color_or_width_parts[0]);
1671            if let Some(hex) = get_tailwind_color_with_vars(&potential_color, vars) {
1672                let mut props = CssProps::new();
1673                let prop_name = if let Some(s) = side {
1674                    format!("border-{}-color", s)
1675                } else {
1676                    "border-color".to_string()
1677                };
1678                props.insert(prop_name, json!(hex));
1679                return Some(props);
1680            }
1681        }
1682        
1683        // Otherwise, check for width (e.g., border-2, border-t-4)
1684        if color_or_width_parts.len() == 1 {
1685            if let Ok(width) = color_or_width_parts[0].parse::<i32>() {
1686                return Some(border_props(side, width, vars));
1687            }
1688        }
1689    }
1690    // rounded* (border-radius)
1691    if class == "rounded" { return Some(rounded_props(None, Some("md"))); }
1692    if let Some(sz) = class.strip_prefix("rounded-") {
1693        return Some(rounded_props(None, Some(sz)));
1694    }
1695    for &(pref, side) in &[("rounded-t", "t"), ("rounded-b", "b"), ("rounded-l", "l"), ("rounded-r", "r")] {
1696        if class == pref { return Some(rounded_props(Some(side), Some("md"))); }
1697        if let Some(sz) = class.strip_prefix(&(pref.to_string() + "-")) {
1698            return Some(rounded_props(Some(side), Some(sz)));
1699        }
1700    }
1701    // cursor-*
1702    if let Some(cur) = class.strip_prefix("cursor-") {
1703        let mut props = CssProps::new();
1704        props.insert("cursor".into(), json!(match cur {
1705            "pointer" => "pointer",
1706            "default" => "default",
1707            "text" => "text",
1708            "move" => "move",
1709            "wait" => "wait",
1710            "not-allowed" => "not-allowed",
1711            other => other,
1712        }));
1713        return Some(props);
1714    }
1715    // transition*
1716    if class == "transition" || class == "transition-all" {
1717        let mut props = CssProps::new();
1718        props.insert("transition-property".into(), json!("all"));
1719        props.insert("transition-duration".into(), json!("150ms"));
1720        props.insert("transition-timing-function".into(), json!("ease-in-out"));
1721        return Some(props);
1722    }
1723    if class == "transition-none" {
1724        let mut props = CssProps::new();
1725        props.insert("transition-property".into(), json!("none"));
1726        props.insert("transition-duration".into(), json!("0ms"));
1727        return Some(props);
1728    }
1729    if let Some(rest) = class.strip_prefix("transition-") {
1730        // e.g., transition-colors → limit property; keep default duration/ease
1731        let mut props = CssProps::new();
1732        let property = match rest {
1733            "colors" => "color, background-color, border-color, fill, stroke",
1734            "opacity" => "opacity",
1735            "transform" => "transform",
1736            "shadow" => "box-shadow",
1737            other => other,
1738        };
1739        props.insert("transition-property".into(), json!(property));
1740        props.insert("transition-duration".into(), json!("150ms"));
1741        props.insert("transition-timing-function".into(), json!("ease-in-out"));
1742        return Some(props);
1743    }
1744    // width utilities: w-*, w-full, w-screen, w-min, w-max (treat min/max as auto), w-px
1745    if let Some(val) = class.strip_prefix("w-") {
1746        return width_like_props("width", val);
1747    }
1748    if let Some(val) = class.strip_prefix("min-w-") {
1749        return width_like_props("min-width", val);
1750    }
1751    if let Some(val) = class.strip_prefix("max-w-") {
1752        return width_like_props("max-width", val);
1753    }
1754    // Height utilities
1755    if let Some(val) = class.strip_prefix("h-") {
1756        return width_like_props("height", val);
1757    }
1758    if let Some(val) = class.strip_prefix("min-h-") {
1759        return width_like_props("min-height", val);
1760    }
1761    if let Some(val) = class.strip_prefix("max-h-") {
1762        return width_like_props("max-height", val);
1763    }
1764    None
1765}
1766
1767fn parse_tailwind_spacing<F>(value: &str, builder: &F) -> Option<CssProps>
1768where
1769    F: Fn(i32) -> CssProps,
1770{
1771    if let Ok(n) = value.parse::<i32>() {
1772        let px = n * 4;
1773        return Some(builder(px));
1774    }
1775    None
1776}
1777
1778fn padding_props(keys: &[&str], px_value: i32) -> CssProps {
1779    let mut props = CssProps::new();
1780    let val = format!("{}px", px_value);
1781    for key in keys {
1782        props.insert((*key).into(), json!(&val));
1783    }
1784    props
1785}
1786
1787fn margin_props(keys: &[&str], px_value: i32) -> CssProps {
1788    let mut props = CssProps::new();
1789    let val = format!("{}px", px_value);
1790    for key in keys {
1791        props.insert((*key).into(), json!(&val));
1792    }
1793    props
1794}
1795
1796fn border_props(side: Option<&str>, width: i32, _vars: &IndexMap<String, String>) -> CssProps {
1797    let mut props = CssProps::new();
1798    let width_str = format!("{}px", width);
1799    match side {
1800        None => {
1801            props.insert("border-width".into(), json!(&width_str));
1802        }
1803        Some("t") => {
1804            props.insert("border-top-width".into(), json!(&width_str));
1805        }
1806        Some("b") => {
1807            props.insert("border-bottom-width".into(), json!(&width_str));
1808        }
1809        Some("l") => {
1810            props.insert("border-left-width".into(), json!(&width_str));
1811        }
1812        Some("r") => {
1813            props.insert("border-right-width".into(), json!(&width_str));
1814        }
1815        Some("x") => {
1816            props.insert("border-left-width".into(), json!(&width_str));
1817            props.insert("border-right-width".into(), json!(&width_str));
1818        }
1819        Some("y") => {
1820            props.insert("border-top-width".into(), json!(&width_str));
1821            props.insert("border-bottom-width".into(), json!(&width_str));
1822        }
1823        _ => {
1824            props.insert("border-width".into(), json!(&width_str));
1825        }
1826    };
1827    props.insert("border-color".into(), json!("var(border)"));
1828    props.insert("border-style".into(), json!("solid"));
1829    props
1830}
1831
1832fn rounded_props(side: Option<&str>, size: Option<&str>) -> CssProps {
1833    let mut props = CssProps::new();
1834    let px = match size.unwrap_or("md") {
1835        "none" => 0,
1836        "sm" => 2,
1837        "md" => 4,
1838        "lg" => 8,
1839        "xl" => 12,
1840        "2xl" => 16,
1841        "3xl" => 24,
1842        "full" => 9999,
1843        s => s.parse::<i32>().unwrap_or(4),
1844    };
1845    let v = json!(format!("{}px", px));
1846    match side {
1847        None => { props.insert("border-radius".into(), v); }
1848        Some("t") => {
1849            props.insert("border-top-left-radius".into(), v.clone());
1850            props.insert("border-top-right-radius".into(), v);
1851        }
1852        Some("b") => {
1853            props.insert("border-bottom-left-radius".into(), v.clone());
1854            props.insert("border-bottom-right-radius".into(), v);
1855        }
1856        Some("l") => { props.insert("border-top-left-radius".into(), v.clone()); props.insert("border-bottom-left-radius".into(), v); }
1857        Some("r") => { props.insert("border-top-right-radius".into(), v.clone()); props.insert("border-bottom-right-radius".into(), v); }
1858        _ => { props.insert("border-radius".into(), v); }
1859    }
1860    props
1861}
1862
1863fn width_like_props(prop: &str, token: &str) -> Option<CssProps> {
1864    let mut props = CssProps::new();
1865    let value = match token {
1866        "full" => Some("100%".to_string()),
1867        "screen" => Some(if prop == "width" { "100vw" } else { "100vh" }.to_string()),
1868        "min" => Some("min-content".to_string()),
1869        "max" => Some("max-content".to_string()),
1870        "fit" => Some("fit-content".to_string()),
1871        "auto" => Some("auto".to_string()),
1872        "px" => Some("1px".to_string()),
1873        other => {
1874            // numeric scale n => n*4px, fraction e.g., 1/2 => 50%
1875            if let Some((a, b)) = other.split_once('/') {
1876                if let (Ok(na), Ok(nb)) = (a.parse::<f64>(), b.parse::<f64>()) {
1877                    let pct = (na / nb) * 100.0;
1878                    Some(format!("{}%", trim_trailing_zeros(pct)))
1879                } else { None }
1880            } else if let Ok(n) = other.parse::<i32>() {
1881                Some(format!("{}px", n * 4))
1882            } else {
1883                None
1884            }
1885        }
1886    }?;
1887    props.insert(prop.into(), json!(value));
1888    Some(props)
1889}
1890
1891fn trim_trailing_zeros(num: f64) -> String {
1892    let mut s = format!("{:.6}", num);
1893    while s.contains('.') && s.ends_with('0') { s.pop(); }
1894    if s.ends_with('.') { s.pop(); }
1895    s
1896}
1897
1898// ---------------- Tailwind subset ----------------
1899
1900// static RE_NUM: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(?P<prefix>(hover:)?(xs:|sm:|md:|lg:|xl:)*)?(?P<base>.+)$").unwrap());
1901
1902fn css_escape_class(class: &str) -> String { class.replace(':', "\\:") }
1903
1904fn class_to_selector(class: &str) -> String {
1905    let (_bp, hover, base) = parse_prefixed_class(class);
1906    if hover {
1907        format!(".{}:hover", css_escape_class(&base))
1908    } else {
1909        format!(".{}", css_escape_class(&base))
1910    }
1911}
1912
1913// ------------- helpers for CSS output of media selectors -------------
1914
1915/// Flatten CSS with potential selectors that include media prelude.
1916/// This simple post-processor merges entries that use the special selector format
1917/// "@media (min-width: X) {<sel>" where we will close the block at the end.
1918/// We group by media and inside concatenate selectors.
1919pub fn post_process_css(
1920    raw_rules: &[(String, CssProps)],
1921    vars: &IndexMap<String, String>,
1922) -> String {
1923    // Group into normal rules and media rules
1924    let mut normal = vec![];
1925    let mut media_map: IndexMap<String, Vec<(String, CssProps)>> = IndexMap::new();
1926    for (sel, props) in raw_rules.iter() {
1927        if let Some((media, inner)) = sel.split_once('{') {
1928            if media.trim_start().starts_with("@media ") && inner.ends_with('}') {
1929                let inner_sel = inner.trim_end_matches('}').to_string();
1930                media_map
1931                    .entry(media.trim().to_string())
1932                    .or_default()
1933                    .push((inner_sel, props.clone()));
1934                continue;
1935            }
1936        }
1937        normal.push((sel.clone(), props.clone()));
1938    }
1939    let mut out = String::new();
1940    for (sel, props) in normal {
1941        out.push_str(&sel);
1942        out.push('{');
1943        out.push_str(&css_props_string(&props, vars));
1944        out.push_str("}\n");
1945    }
1946    for (media, entries) in media_map {
1947        out.push_str(&media);
1948        out.push('{');
1949        for (sel, props) in entries {
1950            out.push_str(&sel);
1951            out.push('{');
1952            out.push_str(&css_props_string(&props, vars));
1953            out.push_str("}");
1954        }
1955        out.push_str("}\n");
1956    }
1957    out
1958}
1959
1960// -------- Prefix parsing (hover:, breakpoint:) --------
1961
1962fn parse_prefixed_class(class: &str) -> (Option<String>, bool, String) {
1963    // Split by ':' to find prefixes like md:hover:block
1964    let parts: Vec<&str> = class.split(':').collect();
1965    if parts.len() == 1 {
1966        return (None, false, class.to_string());
1967    }
1968    let mut bp: Option<String> = None;
1969    let mut hover = false;
1970    for &p in &parts[..parts.len() - 1] {
1971        match p {
1972            "hover" => hover = true,
1973            "xs" | "sm" | "md" | "lg" | "xl" => bp = Some(p.to_string()),
1974            _ => {}
1975        }
1976    }
1977    let base = parts.last().unwrap().to_string();
1978    (bp, hover, base)
1979}
1980
1981fn wrap_with_media(selector: &str, bp_key: Option<&str>, bps: &IndexMap<String, String>) -> String {
1982    if let Some(k) = bp_key {
1983        if let Some(val) = bps.get(k) {
1984            return format!("@media (min-width: {}) {{{}}}", val, selector);
1985        }
1986    }
1987    selector.to_string()
1988}
1989
1990/// Get a Tailwind color hex value from a string like "slate-200" or "blue-500"
1991fn get_tailwind_color(color_shade: &str) -> Option<String> {
1992    let parts: Vec<&str> = color_shade.split('-').collect();
1993    if parts.len() != 2 {
1994        return None;
1995    }
1996    let color_name = parts[0];
1997    let shade = parts[1];
1998    
1999    // First try standard Tailwind colors
2000    if let Some(hex) = TAILWIND_COLORS
2001        .get(color_name)
2002        .and_then(|shades| shades.get(shade))
2003    {
2004        return Some(hex.to_string());
2005    }
2006    
2007    None
2008}
2009
2010fn get_tailwind_color_with_vars(color_shade: &str, vars: &IndexMap<String, String>) -> Option<String> {
2011    // First try standard Tailwind colors
2012    if let Some(hex) = get_tailwind_color(color_shade) {
2013        return Some(hex);
2014    }
2015    
2016    // If not found, check if color_shade matches a variable
2017    // Theme variables are flattened with "." separators, e.g., "colors.primary"
2018    // So we need to check:
2019    // 1. Direct match: "primary" → look for "primary" in vars
2020    // 2. Color namespace: "primary" → look for "colors.primary" in vars (plural)
2021    // 3. Color namespace: "primary" → look for "color.primary" in vars (singular)
2022    // 4. With shade: "primary-500" → look for "colors.primary" or "colors.primary-500" in vars
2023    
2024    if let Some(val) = vars.get(color_shade) {
2025        return Some(val.clone());
2026    }
2027    
2028    // Try with "colors." namespace prefix (plural - HookRenderer uses this)
2029    if let Some(val) = vars.get(&format!("colors.{}", color_shade)) {
2030        return Some(val.clone());
2031    }
2032    
2033    // Try with "color." namespace prefix (singular - fallback)
2034    if let Some(val) = vars.get(&format!("color.{}", color_shade)) {
2035        return Some(val.clone());
2036    }
2037    
2038    // Handle cases where the color name doesn't have a shade but we need to look for a variable
2039    // e.g., "primary" (from bg-primary) → look for "color.primary"
2040    let parts: Vec<&str> = color_shade.split('-').collect();
2041    if parts.len() >= 1 {
2042        let color_name = parts[0];
2043        
2044        // Try direct variable
2045        if let Some(val) = vars.get(color_name) {
2046            return Some(val.clone());
2047        }
2048        
2049        // Try with "color." namespace
2050        if let Some(val) = vars.get(&format!("color.{}", color_name)) {
2051            return Some(val.clone());
2052        }
2053    }
2054    
2055    None
2056}
2057
2058/// Parse arbitrary values like bg-[var(--primary)], text-[#ff0000], border-[hsl(200,50%,50%)]
2059fn parse_arbitrary_value(class: &str) -> Option<CssProps> {
2060    // Match pattern: prefix-[value]
2061    if let Some(bracket_start) = class.find('[') {
2062        if !class.ends_with(']') {
2063            return None;
2064        }
2065        let prefix = &class[..bracket_start];
2066        let value = &class[bracket_start + 1..class.len() - 1];
2067        
2068        let mut props = CssProps::new();
2069        match prefix {
2070            "bg" => {
2071                props.insert("background-color".into(), json!(value));
2072                return Some(props);
2073            }
2074            "text" => {
2075                props.insert("color".into(), json!(value));
2076                return Some(props);
2077            }
2078            "border" => {
2079                props.insert("border-color".into(), json!(value));
2080                return Some(props);
2081            }
2082            "divide" => {
2083                props.insert("border-color".into(), json!(value));
2084                return Some(props);
2085            }
2086            _ => return None,
2087        }
2088    }
2089    None
2090}
2091
2092// re-export minimal API for CLI
2093pub mod api {
2094    pub use super::{SelectorStyles, State};
2095}
2096
2097#[cfg(test)]
2098mod tests {
2099    use super::*;
2100
2101    #[test]
2102    fn default_theme_has_p2() {
2103        let mut st = State::new_default();
2104        st.register_tailwind_classes(["p-2".to_string()]);
2105        let css = st.css_for_web();
2106        assert!(css.contains(".p-2{"));
2107        assert!(css.contains("padding:8px"));
2108    }
2109
2110    #[test]
2111    fn android_conversion() {
2112        let mut st = State::new_default();
2113        // Add a theme with button styles
2114        let mut styles = IndexMap::new();
2115        let mut button_props = IndexMap::new();
2116        button_props.insert("backgroundColor".to_string(), json!("#007bff"));
2117        styles.insert("button".to_string(), button_props);
2118        st.add_theme("default", styles);
2119        st.set_theme("default").ok();
2120        
2121        let out = st.android_styles_for("button", &[]);
2122        assert!(out.get("backgroundColor").is_some());
2123    }
2124
2125    #[test]
2126    fn embedded_defaults_and_version() {
2127        // Test that we can create a state and add a theme with variables
2128        let mut st = State::default_state();
2129        st.add_theme("default", IndexMap::new());
2130        st.set_theme("default").ok();
2131        
2132        let mut vars = IndexMap::new();
2133        vars.insert("primary".to_string(), "#007bff".to_string());
2134        st.set_variables(vars);
2135        
2136        assert!(st.themes.contains_key("default"));
2137        let def = st.themes.get("default").unwrap();
2138        assert!(def.variables.contains_key("primary"));
2139
2140        // Version should compile and be non-empty (env! evaluated at compile-time)
2141        // Note: get_version() is only available for wasm32 target
2142        #[cfg(target_arch = "wasm32")]
2143        {
2144            let v = get_version();
2145            assert!(!v.is_empty());
2146        }
2147    }
2148
2149    #[test]
2150    fn border_color_with_direction() {
2151        let mut st = State::new_default();
2152        
2153        // Test border-b-blue-500 (border-bottom with blue color shade 500)
2154        st.register_tailwind_classes(["border-b-blue-500".to_string()]);
2155        let css = st.css_for_web();
2156        assert!(css.contains(".border-b-blue-500{"));
2157        assert!(css.contains("border-bottom-color:#3b82f6") || css.contains("border-b-color:#3b82f6"));
2158        
2159        // Test border-t-red-500
2160        st.register_tailwind_classes(["border-t-red-500".to_string()]);
2161        let css = st.css_for_web();
2162        assert!(css.contains(".border-t-red-500{"));
2163        
2164        // Test border-blue-500 (all borders)
2165        st.register_tailwind_classes(["border-blue-500".to_string()]);
2166        let css = st.css_for_web();
2167        assert!(css.contains(".border-blue-500{"));
2168        assert!(css.contains("border-color:#3b82f6"));
2169    }
2170
2171    #[test]
2172    fn multiple_selectors_support() {
2173        let mut st = State::new_default();
2174        let mut selectors = SelectorStyles::new();
2175        let mut props = CssProps::new();
2176        props.insert("color".to_string(), serde_json::json!("#ff0000"));
2177        selectors.insert("h1, h2, h3".to_string(), props);
2178        
2179        st.add_theme("test", selectors);
2180        st.set_theme("test").ok();
2181        
2182        // Test h1
2183        let android = st.android_styles_for("h1", &[]);
2184        assert_eq!(android.get("color").and_then(|v| v.as_str()), Some("#ff0000"), "h1 should have red color");
2185        
2186        // Test h2
2187        let android = st.android_styles_for("h2", &[]);
2188        assert_eq!(android.get("color").and_then(|v| v.as_str()), Some("#ff0000"), "h2 should have red color");
2189        
2190        // Test h3
2191        let android = st.android_styles_for("h3", &[]);
2192        assert_eq!(android.get("color").and_then(|v| v.as_str()), Some("#ff0000"), "h3 should have red color");
2193    }
2194
2195    #[test]
2196    fn multiple_selectors_classes() {
2197        let mut st = State::new_default();
2198        let mut selectors = SelectorStyles::new();
2199        let mut props = CssProps::new();
2200        props.insert("padding".to_string(), serde_json::json!("10px"));
2201        selectors.insert(".btn, .link".to_string(), props);
2202        
2203        st.add_theme("test", selectors);
2204        st.set_theme("test").ok();
2205        
2206        // Test .btn
2207        let android = st.android_styles_for("div", &["btn".to_string()]);
2208        assert_eq!(android.get("padding").and_then(|v| v.as_f64()), Some(10.0), ".btn should have 10px padding");
2209        
2210        // Test .link
2211        let android = st.android_styles_for("div", &["link".to_string()]);
2212        assert_eq!(android.get("padding").and_then(|v| v.as_f64()), Some(10.0), ".link should have 10px padding");
2213    }
2214
2215    #[test]
2216    fn border_width_with_direction() {
2217        let mut st = State::new_default();
2218        
2219        // Test border-b-2 (border-bottom width 2px)
2220        st.register_tailwind_classes(["border-b-2".to_string()]);
2221        let css = st.css_for_web();
2222        assert!(css.contains(".border-b-2{"));
2223        assert!(css.contains("border-bottom-width:2px"));
2224        
2225        // Test border-2 (all borders width 2px)
2226        st.register_tailwind_classes(["border-2".to_string()]);
2227        let css = st.css_for_web();
2228        assert!(css.contains(".border-2{"));
2229        assert!(css.contains("border-width:2px"));
2230    }
2231
2232    #[test]
2233    fn display_flex_hover_breakpoint() {
2234        let mut st = State::new_default();
2235        
2236        // Set up theme with breakpoints
2237        st.add_theme("default", IndexMap::new());
2238        st.set_theme("default").ok();
2239        
2240        let mut breakpoints = IndexMap::new();
2241        breakpoints.insert("md".to_string(), "768px".to_string());
2242        st.set_breakpoints(breakpoints);
2243        
2244        st.register_tailwind_classes([
2245            "block".into(),
2246            "inline-flex".into(),
2247            "hidden".into(),
2248            "md:flex".into(),
2249            "md:hover:block".into(),
2250        ]);
2251        let css = st.css_for_web();
2252        assert!(css.contains(".block{"));
2253        assert!(css.contains("display:block"));
2254        assert!(css.contains(".inline-flex{"));
2255        assert!(css.contains("display:inline-flex"));
2256        assert!(css.contains(".hidden{"));
2257        assert!(css.contains("display:none"));
2258        // breakpoint rule
2259        assert!(css.contains("@media (min-width: 768px)"));
2260        assert!(css.contains(".flex{display:flex"));
2261        // hover inside media (substring check)
2262        assert!(css.contains(":hover{display:block"));
2263
2264        // Android resolves base class styles ignoring prefixes
2265        let android = st.android_styles_for("div", &["md:flex".into()]);
2266        assert_eq!(android.get("display").and_then(|v| v.as_str()), Some("flex"));
2267    }
2268
2269    #[test]
2270    fn parse_var_references_basic() {
2271        // Test basic var() parsing
2272        let refs = parse_var_references("var(color)");
2273        assert_eq!(refs.len(), 1);
2274        assert_eq!(refs[0].2, "color");
2275        assert_eq!(refs[0].0, 0); // start
2276        assert_eq!(refs[0].1, 10); // end (exclusive, so "var(color)" is 0..10)
2277
2278        // Test var() with -- prefix
2279        let refs = parse_var_references("var(--primary)");
2280        assert_eq!(refs.len(), 1);
2281        assert_eq!(refs[0].2, "primary");
2282
2283        // Test multiple var() references
2284        let refs = parse_var_references("var(--color) and var(size)");
2285        assert_eq!(refs.len(), 2);
2286        assert_eq!(refs[0].2, "color");
2287        assert_eq!(refs[1].2, "size");
2288
2289        // Test with whitespace
2290        let refs = parse_var_references("var( --spacing )");
2291        assert_eq!(refs.len(), 1);
2292        assert_eq!(refs[0].2, "spacing");
2293
2294        // Test with dots and dashes
2295        let refs = parse_var_references("var(color.primary-500)");
2296        assert_eq!(refs.len(), 1);
2297        assert_eq!(refs[0].2, "color.primary-500");
2298
2299        // Test no matches
2300        let refs = parse_var_references("no variables here");
2301        assert_eq!(refs.len(), 0);
2302
2303        // Test incomplete var(
2304        let refs = parse_var_references("var(");
2305        assert_eq!(refs.len(), 0);
2306
2307        // Test var without closing
2308        let refs = parse_var_references("var(color");
2309        assert_eq!(refs.len(), 0);
2310    }
2311
2312    #[test]
2313    fn resolve_vars_basic() {
2314        let mut vars = IndexMap::new();
2315        vars.insert("primary".to_string(), "#ff0000".to_string());
2316        vars.insert("spacing".to_string(), "8px".to_string());
2317        vars.insert("color.blue".to_string(), "#0000ff".to_string());
2318
2319        // Test basic resolution
2320        assert_eq!(resolve_vars("var(--primary)", &vars), "#ff0000");
2321        assert_eq!(resolve_vars("var(primary)", &vars), "#ff0000");
2322        assert_eq!(resolve_vars("var( --primary )", &vars), "#ff0000");
2323
2324        // Test multiple vars
2325        assert_eq!(
2326            resolve_vars("var(--primary) var(--spacing)", &vars),
2327            "#ff0000 8px"
2328        );
2329
2330        // Test dotted variable names
2331        assert_eq!(resolve_vars("var(--color.blue)", &vars), "#0000ff");
2332
2333        // Test undefined variable (should not replace)
2334        assert_eq!(resolve_vars("var(--undefined)", &vars), "var(--undefined)");
2335
2336        // Test $ prefix syntax
2337        assert_eq!(resolve_vars("$primary", &vars), "#ff0000");
2338
2339        // Test no variables
2340        assert_eq!(resolve_vars("plain text", &vars), "plain text");
2341    }
2342
2343    #[test]
2344    fn resolve_vars_edge_cases() {
2345        let mut vars = IndexMap::new();
2346        vars.insert("a".to_string(), "1".to_string());
2347        vars.insert("b".to_string(), "2".to_string());
2348
2349        // Test adjacent vars
2350        assert_eq!(resolve_vars("var(a)var(b)", &vars), "12");
2351
2352        // Test var in middle of text
2353        assert_eq!(resolve_vars("prefix var(a) suffix", &vars), "prefix 1 suffix");
2354
2355        // Test empty input
2356        assert_eq!(resolve_vars("", &vars), "");
2357
2358        // Test var with numbers
2359        vars.insert("var123".to_string(), "value".to_string());
2360        assert_eq!(resolve_vars("var(var123)", &vars), "value");
2361
2362        // Test var with underscores
2363        vars.insert("my_var".to_string(), "test".to_string());
2364        assert_eq!(resolve_vars("var(my_var)", &vars), "test");
2365    }
2366
2367    #[test]
2368    fn test_android_scrolling_mapping() {
2369        let mut state = State::default();
2370        state.display_density = 2.0;
2371        state.scaled_density = 2.0;
2372        state.current_theme = "default".to_string();
2373        
2374        let mut themes = IndexMap::new();
2375        let mut default_theme = crate::ThemeEntry::default();
2376        default_theme.name = Some("Default".to_string());
2377        
2378        let mut overflow_styles = IndexMap::new();
2379        overflow_styles.insert("overflowX".to_string(), serde_json::json!("auto"));
2380        overflow_styles.insert("overflowY".to_string(), serde_json::json!("scroll"));
2381        
2382        default_theme.selectors.insert(".scroller".to_string(), overflow_styles);
2383        themes.insert("default".to_string(), default_theme);
2384        state.themes = themes;
2385        
2386        let styles = state.android_styles_for("div", &vec![".scroller".to_string()]);
2387        
2388        assert_eq!(styles.get("androidScrollHorizontal"), Some(&serde_json::json!(true)));
2389        assert_eq!(styles.get("androidScrollVertical"), Some(&serde_json::json!(true)));
2390    }
2391
2392    #[test]
2393    fn android_flex_row_default() {
2394        let st = State::new_default();
2395        // div with flex class should be horizontal (row) on Android
2396        let styles = st.android_styles_for("div", &["flex".to_string()]);
2397        assert_eq!(styles.get("androidOrientation").and_then(|v| v.as_str()), Some("horizontal"));
2398        assert_eq!(styles.get("flexDirection").and_then(|v| v.as_str()), Some("row"));
2399        
2400        // div without flex class should be vertical (column) on Android
2401        let styles = st.android_styles_for("div", &[]);
2402        assert_eq!(styles.get("androidOrientation").and_then(|v| v.as_str()), Some("vertical"));
2403        assert_eq!(styles.get("flexDirection").and_then(|v| v.as_str()), Some("column"));
2404    }
2405
2406    #[test]
2407    fn android_gap_orientation_order() {
2408        let st = State::new_default();
2409        let styles = st.android_styles_for("div", &["flex".to_string(), "gap-4".to_string()]);
2410        
2411        // Check that androidOrientation comes BEFORE gap in the map
2412        let keys: Vec<&String> = styles.keys().collect();
2413        let orientation_idx = keys.iter().position(|&k| k == "androidOrientation").unwrap();
2414        let gap_idx = keys.iter().position(|&k| k == "gap").unwrap();
2415        
2416        assert!(orientation_idx < gap_idx, "androidOrientation should come before gap for correct layout processing");
2417    }
2418
2419    #[test]
2420    fn margin_auto_support() {
2421        let mut st = State::new_default();
2422        st.register_tailwind_classes(["ml-auto".to_string(), "mr-auto".to_string(), "mx-auto".to_string()]);
2423        
2424        // Web CSS check
2425        let css = st.css_for_web();
2426        assert!(css.contains("margin-left:auto"));
2427        assert!(css.contains("margin-right:auto"));
2428        
2429        // Android check
2430        let styles = st.android_styles_for("div", &["ml-auto".to_string()]);
2431        assert_eq!(styles.get("marginLeft").and_then(|v| v.as_str()), Some("auto"));
2432        
2433        let styles = st.android_styles_for("div", &["mx-auto".to_string()]);
2434        assert_eq!(styles.get("marginLeft").and_then(|v| v.as_str()), Some("auto"));
2435        assert_eq!(styles.get("marginRight").and_then(|v| v.as_str()), Some("auto"));
2436    }
2437
2438    #[test]
2439    fn alignment_mapping() {
2440        let st = State::new_default();
2441        
2442        // Test Row (default)
2443        let row_styles = st.android_styles_for("div", &["flex".to_string(), "justify-center".to_string(), "items-center".to_string()]);
2444        assert_eq!(row_styles.get("androidOrientation").and_then(|v| v.as_str()), Some("horizontal"));
2445        // Row: justify-center (horizontal) + items-center (vertical) -> center
2446        assert_eq!(row_styles.get("androidGravity").and_then(|v| v.as_str()), Some("center"));
2447        
2448        // Test Column
2449        let col_styles = st.android_styles_for("div", &["flex".to_string(), "flex-col".to_string(), "justify-center".to_string(), "items-center".to_string()]);
2450        assert_eq!(col_styles.get("androidOrientation").and_then(|v| v.as_str()), Some("vertical"));
2451        // Column: justify-center (vertical) + items-center (horizontal) -> center
2452        assert_eq!(col_styles.get("androidGravity").and_then(|v| v.as_str()), Some("center"));
2453
2454        // Test Row Start/End
2455        let row_start_styles = st.android_styles_for("div", &["flex".to_string(), "justify-start".to_string(), "items-end".to_string()]);
2456        assert_eq!(row_start_styles.get("androidGravity").and_then(|v| v.as_str()), Some("bottom|start"));
2457    }
2458
2459    #[test]
2460    fn test_button_bg_override() {
2461        let mut themes = IndexMap::new();
2462        
2463        let mut variables = IndexMap::new();
2464        variables.insert("color.bg".to_string(), "#ffffff".to_string());
2465        
2466        let mut selectors = IndexMap::new();
2467        let mut button_props = IndexMap::new();
2468        button_props.insert("background-color".to_string(), json!("#2563eb"));
2469        selectors.insert("button".to_string(), button_props);
2470        
2471        let default_theme = ThemeEntry {
2472            name: Some("default".to_string()),
2473            inherits: None,
2474            selectors,
2475            variables,
2476            breakpoints: IndexMap::new(),
2477        };
2478        
2479        themes.insert("default".to_string(), default_theme);
2480        
2481        let mut state = State::new_default();
2482        state.themes = themes;
2483        state.current_theme = "default".to_string();
2484        
2485        // Test button with bg-bg class and p-4
2486        let classes = vec!["bg-bg".to_string(), "p-4".to_string()];
2487        let styles = state.android_styles_for("button", &classes);
2488        
2489        println!("[test_button_bg_override] styles: {:?}", styles);
2490        
2491        // Should have white background from bg-bg, not blue from button selector
2492        assert_eq!(styles.get("backgroundColor").and_then(|v: &serde_json::Value| v.as_str()), Some("#ffffff"));
2493        
2494        // Should have 16px padding from p-4, not 8px from button selector
2495        // p-4 = 1rem = 16px (default)
2496        assert_eq!(styles.get("paddingTop"), Some(&serde_json::json!(16)));
2497        assert_eq!(styles.get("paddingVertical"), Some(&serde_json::json!(16)));
2498    }
2499}
2500
2501#[cfg(all(target_os = "android", feature = "android"))]
2502#[cfg(feature = "android")]
2503mod android_jni;
2504
2505mod bridge_common;
2506mod ffi;
2507
2508pub use ffi::*;
2509
2510#[cfg(target_vendor = "apple")]
2511mod ios_ffi;