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),
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 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 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 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 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 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
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}