dioxus_ui_system/molecules/
media_object.rs1use crate::theme::use_theme;
6use dioxus::prelude::*;
7
8#[derive(Default, Clone, PartialEq, Debug)]
10pub enum MediaAlign {
11 #[default]
12 Start,
13 Center,
14 End,
15}
16
17#[derive(Props, Clone, PartialEq)]
19pub struct MediaObjectProps {
20 pub media: Element,
22 pub children: Element,
24 #[props(default = MediaAlign::Start)]
26 pub align: MediaAlign,
27 #[props(default = 16)]
29 pub gap: u16,
30 #[props(default = false)]
32 pub reverse: bool,
33 #[props(default = true)]
35 pub responsive: bool,
36 #[props(default)]
38 pub media_width: Option<String>,
39 #[props(default)]
41 pub class: Option<String>,
42}
43
44#[component]
46pub fn MediaObject(props: MediaObjectProps) -> Element {
47 let class_css = props
48 .class
49 .as_ref()
50 .map(|c| format!(" {}", c))
51 .unwrap_or_default();
52
53 let flex_direction = if props.reverse { "row-reverse" } else { "row" };
54 let align_items = match props.align {
55 MediaAlign::Start => "flex-start",
56 MediaAlign::Center => "center",
57 MediaAlign::End => "flex-end",
58 };
59
60 let responsive_css = if props.responsive {
61 format!("@media (max-width: 640px) {{ .media-object{class_css} {{ flex-direction: column; }} }}")
62 } else {
63 String::new()
64 };
65
66 let media_style = props
67 .media_width
68 .as_ref()
69 .map(|w| format!("flex-shrink: 0; width: {};", w))
70 .unwrap_or_else(|| "flex-shrink: 0;".to_string());
71
72 let gap = props.gap;
73
74 rsx! {
75 div {
76 class: "media-object{class_css}",
77 style: "display: flex; flex-direction: {flex_direction}; align-items: {align_items}; gap: {gap}px;",
78
79 div {
80 class: "media-object-media",
81 style: "{media_style}",
82 {props.media}
83 }
84
85 div {
86 class: "media-object-content",
87 style: "flex: 1; min-width: 0;",
88 {props.children}
89 }
90 }
91
92 if !responsive_css.is_empty() {
93 style { "{{ {responsive_css} }}" }
94 }
95 }
96}
97
98#[derive(Props, Clone, PartialEq)]
100pub struct MediaContentProps {
101 #[props(default)]
103 pub title: Option<String>,
104 #[props(default = 4)]
106 pub title_level: u8,
107 #[props(default)]
109 pub description: Option<String>,
110 pub children: Option<Element>,
112 #[props(default)]
114 pub meta: Option<String>,
115 #[props(default)]
117 pub actions: Option<Element>,
118}
119
120#[component]
122pub fn MediaContent(props: MediaContentProps) -> Element {
123 let theme = use_theme();
124
125 let _title_tag = format!("h{}", props.title_level.clamp(1, 6));
126 let font_size = match props.title_level {
127 1 => 24,
128 2 => 20,
129 3 => 18,
130 _ => 16,
131 };
132 let title_color = theme.tokens.read().colors.foreground.to_rgba();
133 let title_style = format!(
134 "margin: 0; font-size: {}px; font-weight: 600; color: {}; line-height: 1.3;",
135 font_size, title_color
136 );
137
138 rsx! {
139 div {
140 class: "media-content",
141 style: "display: flex; flex-direction: column; gap: 8px;",
142
143 if let Some(title) = props.title.clone() {
144 match props.title_level {
145 1 => rsx! { h1 { class: "media-content-title", style: "{title_style}", "{title}" } },
146 2 => rsx! { h2 { class: "media-content-title", style: "{title_style}", "{title}" } },
147 3 => rsx! { h3 { class: "media-content-title", style: "{title_style}", "{title}" } },
148 4 => rsx! { h4 { class: "media-content-title", style: "{title_style}", "{title}" } },
149 5 => rsx! { h5 { class: "media-content-title", style: "{title_style}", "{title}" } },
150 _ => rsx! { h6 { class: "media-content-title", style: "{title_style}", "{title}" } },
151 }
152 }
153
154 if let Some(meta) = props.meta {
155 span {
156 class: "media-content-meta",
157 style: "font-size: 12px; color: {theme.tokens.read().colors.muted.to_rgba()};",
158 "{meta}"
159 }
160 }
161
162 if let Some(description) = props.description {
163 p {
164 class: "media-content-description",
165 style: "margin: 0; font-size: 14px; color: {theme.tokens.read().colors.foreground.to_rgba()}; line-height: 1.6;",
166 "{description}"
167 }
168 }
169
170 if let Some(children) = props.children {
171 div {
172 class: "media-content-body",
173 {children}
174 }
175 }
176
177 if let Some(actions) = props.actions {
178 div {
179 class: "media-content-actions",
180 style: "display: flex; gap: 8px; margin-top: 4px;",
181 {actions}
182 }
183 }
184 }
185 }
186}
187
188#[derive(Props, Clone, PartialEq)]
190pub struct CommentProps {
191 pub author: String,
193 #[props(default)]
195 pub avatar: Option<Element>,
196 pub content: String,
198 pub timestamp: String,
200 #[props(default)]
202 pub on_reply: Option<EventHandler<()>>,
203 #[props(default)]
205 pub on_like: Option<EventHandler<()>>,
206 #[props(default = false)]
208 pub liked: bool,
209 #[props(default = 0)]
211 pub like_count: u32,
212 #[props(default)]
214 pub replies: Option<Element>,
215 #[props(default)]
217 pub class: Option<String>,
218}
219
220#[component]
222pub fn Comment(props: CommentProps) -> Element {
223 let theme = use_theme();
224
225 let class_css = props
226 .class
227 .as_ref()
228 .map(|c| format!(" {}", c))
229 .unwrap_or_default();
230
231 let like_color = if props.liked {
232 "#ef4444".to_string()
233 } else {
234 theme.tokens.read().colors.muted.to_rgba()
235 };
236
237 let avatar_element = props.avatar.unwrap_or_else(|| {
238 rsx! {
239 div {
240 style: "width: 40px; height: 40px; border-radius: 50%; background: {theme.tokens.read().colors.muted.to_rgba()}; display: flex; align-items: center; justify-content: center; font-size: 16px; font-weight: 600; color: {theme.tokens.read().colors.foreground.to_rgba()};",
241 "{props.author.chars().next().unwrap_or('?').to_uppercase()}"
242 }
243 }
244 });
245
246 rsx! {
247 div {
248 class: "comment{class_css}",
249 style: "display: flex; gap: 12px;",
250
251 div {
252 class: "comment-avatar",
253 style: "flex-shrink: 0;",
254 {avatar_element}
255 }
256
257 div {
258 class: "comment-body",
259 style: "flex: 1;",
260
261 div {
262 class: "comment-header",
263 style: "display: flex; align-items: center; gap: 8px; margin-bottom: 4px;",
264
265 span {
266 class: "comment-author",
267 style: "font-weight: 600; font-size: 14px; color: {theme.tokens.read().colors.foreground.to_rgba()};",
268 "{props.author}"
269 }
270
271 span {
272 class: "comment-timestamp",
273 style: "font-size: 12px; color: {theme.tokens.read().colors.muted.to_rgba()};",
274 "{props.timestamp}"
275 }
276 }
277
278 p {
279 class: "comment-content",
280 style: "margin: 0 0 8px 0; font-size: 14px; color: {theme.tokens.read().colors.foreground.to_rgba()}; line-height: 1.5;",
281 "{props.content}"
282 }
283
284 div {
285 class: "comment-actions",
286 style: "display: flex; gap: 16px;",
287
288 if let Some(on_reply) = props.on_reply {
289 button {
290 type: "button",
291 class: "comment-reply",
292 style: "font-size: 13px; color: {theme.tokens.read().colors.muted.to_rgba()}; background: none; border: none; cursor: pointer;",
293 onclick: move |_| on_reply.call(()),
294 "Reply"
295 }
296 }
297
298 if let Some(on_like) = props.on_like {
299 button {
300 type: "button",
301 class: "comment-like",
302 style: "font-size: 13px; color: {like_color}; background: none; border: none; cursor: pointer; display: flex; align-items: center; gap: 4px;",
303 onclick: move |_| on_like.call(()),
304
305 if props.liked {
306 "❤️"
307 } else {
308 "🤍"
309 }
310
311 if props.like_count > 0 {
312 "{props.like_count}"
313 }
314 }
315 }
316 }
317
318 if let Some(replies) = props.replies {
319 div {
320 class: "comment-replies",
321 style: "margin-top: 16px; padding-left: 20px; border-left: 2px solid {theme.tokens.read().colors.border.to_rgba()};",
322 {replies}
323 }
324 }
325 }
326 }
327 }
328}