themed_styler/
lib.rs

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