1use crate::components::icon::{Icon, IconKind};
2use crate::theme::use_theme;
3use dioxus::prelude::*;
4
5#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
7pub enum TimelineMode {
8 #[default]
10 Left,
11 Right,
13 Alternate,
15}
16
17impl TimelineMode {
18 fn as_class(&self) -> &'static str {
19 match self {
20 TimelineMode::Left => "adui-timeline-left",
21 TimelineMode::Right => "adui-timeline-right",
22 TimelineMode::Alternate => "adui-timeline-alternate",
23 }
24 }
25}
26
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
29pub enum TimelineOrientation {
30 #[default]
31 Vertical,
32 Horizontal,
33}
34
35impl TimelineOrientation {
36 fn as_class(&self) -> &'static str {
37 match self {
38 TimelineOrientation::Vertical => "adui-timeline-vertical",
39 TimelineOrientation::Horizontal => "adui-timeline-horizontal",
40 }
41 }
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub enum TimelineColor {
47 Blue,
48 Green,
49 Red,
50 Gray,
51}
52
53impl TimelineColor {
54 fn as_class(&self) -> &'static str {
55 match self {
56 TimelineColor::Blue => "adui-timeline-item-blue",
57 TimelineColor::Green => "adui-timeline-item-green",
58 TimelineColor::Red => "adui-timeline-item-red",
59 TimelineColor::Gray => "adui-timeline-item-gray",
60 }
61 }
62}
63
64#[derive(Clone, PartialEq)]
66pub struct TimelineItem {
67 pub key: String,
68 pub title: Option<Element>,
69 pub content: Option<Element>,
70 pub icon: Option<Element>,
71 pub color: Option<TimelineColor>,
72 pub pending: bool,
74}
75
76impl TimelineItem {
77 pub fn new(key: impl Into<String>) -> Self {
78 Self {
79 key: key.into(),
80 title: None,
81 content: None,
82 icon: None,
83 color: None,
84 pending: false,
85 }
86 }
87
88 pub fn title(mut self, title: Element) -> Self {
89 self.title = Some(title);
90 self
91 }
92
93 pub fn content(mut self, content: Element) -> Self {
94 self.content = Some(content);
95 self
96 }
97
98 pub fn icon(mut self, icon: Element) -> Self {
99 self.icon = Some(icon);
100 self
101 }
102
103 pub fn color(mut self, color: TimelineColor) -> Self {
104 self.color = Some(color);
105 self
106 }
107
108 pub fn pending(mut self, pending: bool) -> Self {
109 self.pending = pending;
110 self
111 }
112}
113
114#[derive(Props, Clone, PartialEq)]
116pub struct TimelineProps {
117 pub items: Vec<TimelineItem>,
119 #[props(default)]
121 pub mode: TimelineMode,
122 #[props(default)]
124 pub orientation: TimelineOrientation,
125 #[props(default)]
127 pub reverse: bool,
128 #[props(optional)]
130 pub class: Option<String>,
131 #[props(optional)]
133 pub style: Option<String>,
134}
135
136#[component]
138pub fn Timeline(props: TimelineProps) -> Element {
139 let TimelineProps {
140 items,
141 mode,
142 orientation,
143 reverse,
144 class,
145 style,
146 } = props;
147
148 let theme = use_theme();
149 let tokens = theme.tokens();
150
151 let mut class_list = vec!["adui-timeline".to_string()];
153 class_list.push(mode.as_class().to_string());
154 class_list.push(orientation.as_class().to_string());
155 if let Some(extra) = class {
156 class_list.push(extra);
157 }
158 let class_attr = class_list.join(" ");
159
160 let style_attr = style.unwrap_or_default();
161
162 let display_items: Vec<&TimelineItem> = if reverse {
164 items.iter().rev().collect()
165 } else {
166 items.iter().collect()
167 };
168
169 rsx! {
170 div {
171 class: "{class_attr}",
172 style: "{style_attr}",
173 role: "list",
174 {display_items.iter().enumerate().map(|(idx, item)| {
175 let key = item.key.clone();
176 let title = item.title.clone();
177 let content = item.content.clone();
178 let icon = item.icon.clone();
179 let color = item.color;
180 let pending = item.pending;
181
182 let is_left = match mode {
184 TimelineMode::Left => true,
185 TimelineMode::Right => false,
186 TimelineMode::Alternate => idx % 2 == 0,
187 };
188
189 let mut item_class = vec!["adui-timeline-item".to_string()];
190 if pending {
191 item_class.push("adui-timeline-item-pending".into());
192 }
193 if is_left {
194 item_class.push("adui-timeline-item-left".into());
195 } else {
196 item_class.push("adui-timeline-item-right".into());
197 }
198 if let Some(c) = color {
199 item_class.push(c.as_class().into());
200 }
201 let item_class_attr = item_class.join(" ");
202
203 rsx! {
204 div {
205 key: "{key}",
206 class: "{item_class_attr}",
207 role: "listitem",
208 div { class: "adui-timeline-item-tail",
209 style: "border-left-color: {tokens.color_border};"
210 },
211 div { class: "adui-timeline-item-head",
212 style: "border-color: {tokens.color_border};",
213 {if let Some(icon_elem) = icon {
214 Some(rsx! {
215 span { class: "adui-timeline-item-icon",
216 {icon_elem}
217 }
218 })
219 } else if pending {
220 Some(rsx! {
221 span { class: "adui-timeline-item-icon",
222 Icon {
223 kind: IconKind::Loading,
224 spin: true,
225 }
226 }
227 })
228 } else {
229 None
230 }}
231 },
232 div { class: "adui-timeline-item-content",
233 {title.map(|t| rsx! {
234 div { class: "adui-timeline-item-title",
235 {t}
236 }
237 })},
238 {content.map(|c| rsx! {
239 div { class: "adui-timeline-item-description",
240 {c}
241 }
242 })},
243 }
244 }
245 }
246 })}
247 }
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn timeline_mode_class_mapping_is_stable() {
257 assert_eq!(TimelineMode::Left.as_class(), "adui-timeline-left");
258 assert_eq!(TimelineMode::Right.as_class(), "adui-timeline-right");
259 assert_eq!(
260 TimelineMode::Alternate.as_class(),
261 "adui-timeline-alternate"
262 );
263 }
264
265 #[test]
266 fn timeline_orientation_class_mapping_is_stable() {
267 assert_eq!(
268 TimelineOrientation::Vertical.as_class(),
269 "adui-timeline-vertical"
270 );
271 assert_eq!(
272 TimelineOrientation::Horizontal.as_class(),
273 "adui-timeline-horizontal"
274 );
275 }
276
277 #[test]
278 fn timeline_color_class_mapping_is_stable() {
279 assert_eq!(TimelineColor::Blue.as_class(), "adui-timeline-item-blue");
280 assert_eq!(TimelineColor::Green.as_class(), "adui-timeline-item-green");
281 assert_eq!(TimelineColor::Red.as_class(), "adui-timeline-item-red");
282 assert_eq!(TimelineColor::Gray.as_class(), "adui-timeline-item-gray");
283 }
284}