1use crate::theme::use_theme;
6use dioxus::prelude::*;
7
8#[derive(Default, Clone, PartialEq, Debug)]
10pub enum HeadingLevel {
11 #[default]
12 H1,
13 H2,
14 H3,
15 H4,
16 H5,
17 H6,
18}
19
20impl HeadingLevel {
21 #[allow(dead_code)]
23 fn as_tag(&self) -> &'static str {
24 match self {
25 HeadingLevel::H1 => "h1",
26 HeadingLevel::H2 => "h2",
27 HeadingLevel::H3 => "h3",
28 HeadingLevel::H4 => "h4",
29 HeadingLevel::H5 => "h5",
30 HeadingLevel::H6 => "h6",
31 }
32 }
33
34 fn default_size(&self) -> u16 {
36 match self {
37 HeadingLevel::H1 => 36,
38 HeadingLevel::H2 => 30,
39 HeadingLevel::H3 => 24,
40 HeadingLevel::H4 => 20,
41 HeadingLevel::H5 => 18,
42 HeadingLevel::H6 => 16,
43 }
44 }
45}
46
47#[derive(Props, Clone, PartialEq)]
49pub struct HeadingProps {
50 #[props(default = HeadingLevel::H1)]
52 pub level: HeadingLevel,
53 pub size: Option<u16>,
55 #[props(default = 600)]
57 pub weight: u16,
58 pub color: Option<String>,
60 #[props(default)]
62 pub class: Option<String>,
63 #[props(default)]
65 pub style: Option<String>,
66 #[props(default)]
68 pub align: Option<String>,
69 pub children: Element,
71}
72
73#[component]
75pub fn Heading(props: HeadingProps) -> Element {
76 let theme = use_theme();
77
78 let font_size = props.size.unwrap_or_else(|| props.level.default_size());
79 let color = props.color.unwrap_or_else(|| {
80 theme
82 .tokens
83 .try_read()
84 .map(|t| t.colors.foreground.to_rgba())
85 .unwrap_or("#111827".to_string())
86 });
87
88 let align_css = props
89 .align
90 .as_ref()
91 .map(|a| format!("text-align: {};", a))
92 .unwrap_or_default();
93
94 let class_css = props
95 .class
96 .as_ref()
97 .map(|c| format!(" {}", c))
98 .unwrap_or_default();
99
100 let style_css = props
101 .style
102 .as_ref()
103 .map(|s| format!(" {}", s))
104 .unwrap_or_default();
105
106 let weight = props.weight;
107 let style = format!(
108 "font-size: {}px; font-weight: {}; color: {}; margin: 0; line-height: 1.2;{}{}",
109 font_size, weight, color, align_css, style_css
110 );
111
112 match props.level {
113 HeadingLevel::H1 => rsx! {
114 h1 { class: "heading heading-h1{class_css}", style: "{style}", {props.children} }
115 },
116 HeadingLevel::H2 => rsx! {
117 h2 { class: "heading heading-h2{class_css}", style: "{style}", {props.children} }
118 },
119 HeadingLevel::H3 => rsx! {
120 h3 { class: "heading heading-h3{class_css}", style: "{style}", {props.children} }
121 },
122 HeadingLevel::H4 => rsx! {
123 h4 { class: "heading heading-h4{class_css}", style: "{style}", {props.children} }
124 },
125 HeadingLevel::H5 => rsx! {
126 h5 { class: "heading heading-h5{class_css}", style: "{style}", {props.children} }
127 },
128 HeadingLevel::H6 => rsx! {
129 h6 { class: "heading heading-h6{class_css}", style: "{style}", {props.children} }
130 },
131 }
132}
133
134#[derive(Props, Clone, PartialEq)]
136pub struct ParagraphProps {
137 #[props(default = 16)]
139 pub size: u16,
140 #[props(default = 1.6)]
142 pub line_height: f32,
143 pub color: Option<String>,
145 #[props(default)]
147 pub align: Option<String>,
148 #[props(default = 75)]
150 pub max_chars: u16,
151 #[props(default)]
153 pub class: Option<String>,
154 #[props(default)]
156 pub style: Option<String>,
157 pub children: Element,
159}
160
161#[component]
163pub fn Paragraph(props: ParagraphProps) -> Element {
164 let theme = use_theme();
165
166 let color = props.color.unwrap_or_else(|| {
167 theme
169 .tokens
170 .try_read()
171 .map(|t| t.colors.foreground.to_rgba())
172 .unwrap_or("#111827".to_string())
173 });
174
175 let align_css = props
176 .align
177 .as_ref()
178 .map(|a| format!("text-align: {};", a))
179 .unwrap_or_default();
180
181 let class_css = props
182 .class
183 .as_ref()
184 .map(|c| format!(" {}", c))
185 .unwrap_or_default();
186
187 let style_css = props
188 .style
189 .as_ref()
190 .map(|s| format!(" {}", s))
191 .unwrap_or_default();
192
193 let max_width = format!("max-width: {}ch;", props.max_chars);
194
195 let line_height = props.line_height;
196
197 rsx! {
198 p {
199 class: "paragraph{class_css}",
200 style: "font-size: {props.size}px; line-height: {line_height}; color: {color}; margin: 0; {max_width} {align_css}{style_css}",
201 {props.children}
202 }
203 }
204}
205
206#[derive(Props, Clone, PartialEq)]
208pub struct CaptionProps {
209 #[props(default = 12)]
211 pub size: u8,
212 #[props(default = CaptionColor::Muted)]
214 pub color: CaptionColor,
215 #[props(default)]
217 pub class: Option<String>,
218 pub children: Element,
220}
221
222#[derive(Default, Clone, PartialEq, Debug)]
224pub enum CaptionColor {
225 #[default]
226 Muted,
227 Secondary,
228 Error,
229 Success,
230 Custom(String),
231}
232
233#[component]
235pub fn Caption(props: CaptionProps) -> Element {
236 let theme = use_theme();
237
238 let color = match props.color {
239 CaptionColor::Muted => theme
240 .tokens
241 .try_read()
242 .map(|t| t.colors.muted.to_rgba())
243 .unwrap_or("#6b7280".to_string()),
244 CaptionColor::Secondary => theme
245 .tokens
246 .try_read()
247 .map(|t| t.colors.secondary.to_rgba())
248 .unwrap_or("#4b5563".to_string()),
249 CaptionColor::Error => theme
250 .tokens
251 .try_read()
252 .map(|t| t.colors.destructive.to_rgba())
253 .unwrap_or("#dc2626".to_string()),
254 CaptionColor::Success => "#16a34a".to_string(),
255 CaptionColor::Custom(c) => c,
256 };
257
258 let class_css = props
259 .class
260 .as_ref()
261 .map(|c| format!(" {}", c))
262 .unwrap_or_default();
263
264 rsx! {
265 span {
266 class: "caption{class_css}",
267 style: "font-size: {props.size}px; color: {color}; line-height: 1.4;",
268 {props.children}
269 }
270 }
271}
272
273#[derive(Props, Clone, PartialEq)]
275pub struct BlockquoteProps {
276 pub border_color: Option<String>,
278 pub background: Option<String>,
280 #[props(default)]
282 pub class: Option<String>,
283 #[props(default)]
285 pub cite: Option<String>,
286 pub children: Element,
288}
289
290#[component]
292pub fn Blockquote(props: BlockquoteProps) -> Element {
293 let theme = use_theme();
294
295 let border_color = props.border_color.unwrap_or_else(|| {
296 theme
297 .tokens
298 .try_read()
299 .map(|t| t.colors.primary.to_rgba())
300 .unwrap_or("#3b82f6".to_string())
301 });
302
303 let background = props.background.unwrap_or_else(|| {
304 theme
305 .tokens
306 .try_read()
307 .map(|t| t.colors.muted.to_rgba())
308 .unwrap_or("#f3f4f6".to_string())
309 });
310
311 let class_css = props
312 .class
313 .as_ref()
314 .map(|c| format!(" {}", c))
315 .unwrap_or_default();
316
317 rsx! {
318 blockquote {
319 class: "blockquote{class_css}",
320 style: "margin: 0; padding: 16px 20px; border-left: 4px solid {border_color}; background: {background}; border-radius: 0 8px 8px 0;",
321 {props.children}
322
323 if let Some(cite) = props.cite {
324 footer {
325 style: "margin-top: 12px; font-size: 14px; color: {theme.tokens.read().colors.muted.to_rgba()}; font-style: normal;",
326 "— {cite}"
327 }
328 }
329 }
330 }
331}