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),
48        ("sm", s.sm),
49        ("base", s.base),
50        ("md", s.md),
51        ("lg", s.lg),
52        ("xl", s.xl),
53        ("2xl", s.x2),
54        ("3xl", s.x3),
55        ("4xl", s.x4),
56    ] {
57        out.push_str(&format!("        \"{k}\": \"{v}rem\",\n"));
58    }
59    out.push_str("      },\n");
60
61    // spacing
62    out.push_str("      spacing: {\n");
63    for (k, v) in t.spacing.pairs() {
64        out.push_str(&format!("        \"{k}\": \"{v}px\",\n"));
65    }
66    out.push_str("      },\n");
67
68    // borderRadius
69    out.push_str("      borderRadius: {\n");
70    for (k, v) in t.radius.pairs() {
71        out.push_str(&format!("        \"{k}\": \"{v}px\",\n"));
72    }
73    out.push_str("      },\n");
74
75    // boxShadow
76    out.push_str("      boxShadow: {\n");
77    let tone = t.color.shadow_tone;
78    for (k, spec) in t.shadow.pairs() {
79        out.push_str(&format!(
80            "        \"{k}\": \"{}\",\n",
81            tailwind_shadow(spec, tone)
82        ));
83    }
84    out.push_str("      },\n");
85
86    // transitionTimingFunction
87    out.push_str("      transitionTimingFunction: {\n");
88    let e = &t.motion.easing;
89    for (k, c) in [
90        ("standard", e.standard),
91        ("decelerate", e.decelerate),
92        ("accelerate", e.accelerate),
93        ("sonic-boom", e.sonic_boom),
94        ("saber", e.saber),
95    ] {
96        out.push_str(&format!(
97            "        \"{k}\": \"cubic-bezier({}, {}, {}, {})\",\n",
98            c.0, c.1, c.2, c.3
99        ));
100    }
101    out.push_str("      },\n");
102
103    // transitionDuration
104    out.push_str("      transitionDuration: {\n");
105    let d = &t.motion.duration;
106    for (k, v) in [
107        ("instant", d.instant_ms),
108        ("fast", d.fast_ms),
109        ("base", d.base_ms),
110        ("slow", d.slow_ms),
111        ("hero", d.hero_ms),
112    ] {
113        out.push_str(&format!("        \"{k}\": \"{v}ms\",\n"));
114    }
115    out.push_str("      },\n");
116
117    out.push_str("    },\n  },\n  plugins: [],\n};\n");
118    out
119}
120
121fn js_stack(stack: &str) -> String {
122    // stack is "'Foo', 'Bar', sans-serif" → `"Foo", "Bar", "sans-serif"`
123    stack
124        .split(',')
125        .map(|p| {
126            let p = p.trim().trim_matches('\'').trim_matches('"');
127            format!("\"{p}\"")
128        })
129        .collect::<Vec<_>>()
130        .join(", ")
131}
132
133fn tailwind_shadow(spec: &ShadowSpec, tone: ishou_tokens::Rgb) -> String {
134    if spec.alpha_pct == 0 {
135        return "none".into();
136    }
137    let alpha = f32::from(spec.alpha_pct) / 100.0;
138    format!(
139        "{}px {}px {}px {}px rgba({}, {}, {}, {:.2})",
140        spec.offset_x, spec.offset_y, spec.blur, spec.spread, tone.r, tone.g, tone.b, alpha
141    )
142}