Skip to main content

ishou_render/
tailwind.rs

1//! Tailwind renderer — emits a complete `tailwind.config.js` with colors,
2//! fonts, spacing, radius, and shadow extensions keyed off ishou tokens.
3
4use ishou_tokens::{ShadowSpec, TokenSet};
5
6pub fn render(t: &TokenSet) -> String {
7    let mut out = String::from(
8        "/** @type {import('tailwindcss').Config} */\n\
9         /* ishou — pleme-io Tailwind config (generated; do not edit) */\n\
10         module.exports = {\n  \
11         content: [\"./src/**/*.{rs,html,js,ts,tsx,jsx}\", \"./index.html\"],\n  \
12         darkMode: \"class\",\n  \
13         theme: {\n    extend: {\n",
14    );
15
16    // colors
17    out.push_str("      colors: {\n");
18    out.push_str("        ishou: {\n");
19    for (k, v) in t.color.entries() {
20        out.push_str(&format!(
21            "          \"{}\": \"{}\",\n",
22            k.replace('_', "-"),
23            v.hex()
24        ));
25    }
26    out.push_str("        },\n");
27    // role aliases
28    for (role, palette_key) in t.roles.pairs() {
29        let rgb = t.color.get(palette_key).expect("role resolves");
30        out.push_str(&format!("        \"{role}\": \"{}\",\n", rgb.hex()));
31    }
32    out.push_str("      },\n");
33
34    // fontFamily
35    out.push_str("      fontFamily: {\n");
36    let f = &t.typography.families;
37    out.push_str(&format!("        serif: [{}],\n", js_stack(f.serif)));
38    out.push_str(&format!("        sans: [{}],\n", js_stack(f.sans)));
39    out.push_str(&format!("        mono: [{}],\n", js_stack(f.mono)));
40    out.push_str(&format!("        display: [{}],\n", js_stack(f.display)));
41    out.push_str("      },\n");
42
43    // fontSize
44    out.push_str("      fontSize: {\n");
45    let s = &t.typography.scale;
46    for (k, v) in [
47        ("xs", s.xs), ("sm", s.sm), ("base", s.base), ("md", s.md), ("lg", s.lg),
48        ("xl", s.xl), ("2xl", s.x2), ("3xl", s.x3), ("4xl", s.x4),
49    ] {
50        out.push_str(&format!("        \"{k}\": \"{v}rem\",\n"));
51    }
52    out.push_str("      },\n");
53
54    // spacing
55    out.push_str("      spacing: {\n");
56    for (k, v) in t.spacing.pairs() {
57        out.push_str(&format!("        \"{k}\": \"{v}px\",\n"));
58    }
59    out.push_str("      },\n");
60
61    // borderRadius
62    out.push_str("      borderRadius: {\n");
63    for (k, v) in t.radius.pairs() {
64        out.push_str(&format!("        \"{k}\": \"{v}px\",\n"));
65    }
66    out.push_str("      },\n");
67
68    // boxShadow
69    out.push_str("      boxShadow: {\n");
70    let tone = t.color.shadow_tone;
71    for (k, spec) in t.shadow.pairs() {
72        out.push_str(&format!(
73            "        \"{k}\": \"{}\",\n",
74            tailwind_shadow(spec, tone)
75        ));
76    }
77    out.push_str("      },\n");
78
79    // transitionTimingFunction
80    out.push_str("      transitionTimingFunction: {\n");
81    let e = &t.motion.easing;
82    for (k, c) in [
83        ("standard", e.standard), ("decelerate", e.decelerate), ("accelerate", e.accelerate),
84        ("sonic-boom", e.sonic_boom), ("saber", e.saber),
85    ] {
86        out.push_str(&format!(
87            "        \"{k}\": \"cubic-bezier({}, {}, {}, {})\",\n",
88            c.0, c.1, c.2, c.3
89        ));
90    }
91    out.push_str("      },\n");
92
93    // transitionDuration
94    out.push_str("      transitionDuration: {\n");
95    let d = &t.motion.duration;
96    for (k, v) in [
97        ("instant", d.instant_ms), ("fast", d.fast_ms), ("base", d.base_ms),
98        ("slow", d.slow_ms), ("hero", d.hero_ms),
99    ] {
100        out.push_str(&format!("        \"{k}\": \"{v}ms\",\n"));
101    }
102    out.push_str("      },\n");
103
104    out.push_str("    },\n  },\n  plugins: [],\n};\n");
105    out
106}
107
108fn js_stack(stack: &str) -> String {
109    // stack is "'Foo', 'Bar', sans-serif" → `"Foo", "Bar", "sans-serif"`
110    stack
111        .split(',')
112        .map(|p| {
113            let p = p.trim().trim_matches('\'').trim_matches('"');
114            format!("\"{p}\"")
115        })
116        .collect::<Vec<_>>()
117        .join(", ")
118}
119
120fn tailwind_shadow(spec: &ShadowSpec, tone: ishou_tokens::Rgb) -> String {
121    if spec.alpha_pct == 0 {
122        return "none".into();
123    }
124    let alpha = f32::from(spec.alpha_pct) / 100.0;
125    format!(
126        "{}px {}px {}px {}px rgba({}, {}, {}, {:.2})",
127        spec.offset_x, spec.offset_y, spec.blur, spec.spread, tone.r, tone.g, tone.b, alpha
128    )
129}