1use 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 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 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 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 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 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 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 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 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 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
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}