radix_leptos_primitives/components/
timeline.rs1use crate::utils::{merge_classes, generate_id};
2use leptos::callback::Callback;
3use leptos::children::Children;
4use leptos::prelude::*;
5
6#[component]
8pub fn Timeline(
9 #[prop(optional)] class: Option<String>,
10 #[prop(optional)] style: Option<String>,
11 #[prop(optional)] children: Option<Children>,
12 #[prop(optional)] events: Option<Vec<TimelineEvent>>,
13 #[prop(optional)] config: Option<TimelineConfig>,
14 #[prop(optional)] orientation: Option<TimelineOrientation>,
15 #[prop(optional)] show_dates: Option<bool>,
16 #[prop(optional)] show_icons: Option<bool>,
17 #[prop(optional)] on_event_click: Option<Callback<TimelineEvent>>,
18 #[prop(optional)] on_event_hover: Option<Callback<TimelineEvent>>,
19) -> impl IntoView {
20 let events = events.unwrap_or_default();
21 let config = config.unwrap_or_default();
22 let orientation = orientation.unwrap_or_default();
23 let show_dates = show_dates.unwrap_or(true);
24 let show_icons = show_icons.unwrap_or(true);
25
26 let class = merge_classes(vec![
27 "timeline",
28 &orientation.to_class(),
29 class.as_deref().unwrap_or(""),
30 ]);
31
32 view! {
33 <div
34 class=class
35 style=style
36 role="list"
37 aria-label="Timeline"
38 data-event-count=events.len()
39 data-orientation=orientation.to_string()
40 data-show-dates=show_dates
41 data-show-icons=show_icons
42 >
43 {children.map(|c| c())}
44 </div>
45 }
46}
47
48#[derive(Debug, Clone, PartialEq)]
50pub struct TimelineEvent {
51 pub id: String,
52 pub title: String,
53 pub description: Option<String>,
54 pub date: String,
55 pub icon: Option<String>,
56 pub color: Option<String>,
57 pub category: Option<String>,
58}
59
60impl Default for TimelineEvent {
61 fn default() -> Self {
62 Self {
63 id: "event".to_string(),
64 title: "Event".to_string(),
65 description: None,
66 date: "2024-01-01".to_string(),
67 icon: None,
68 color: None,
69 category: None,
70 }
71 }
72}
73
74#[derive(Debug, Clone, PartialEq)]
76pub struct TimelineConfig {
77 pub width: f64,
78 pub height: f64,
79 pub line_width: f64,
80 pub dot_size: f64,
81 pub spacing: f64,
82 pub animation: AnimationConfig,
83}
84
85impl Default for TimelineConfig {
86 fn default() -> Self {
87 Self {
88 width: 800.0,
89 height: 400.0,
90 line_width: 2.0,
91 dot_size: 12.0,
92 spacing: 60.0,
93 animation: AnimationConfig::default(),
94 }
95 }
96}
97
98#[derive(Debug, Clone, PartialEq)]
100pub struct AnimationConfig {
101 pub duration: f64,
102 pub easing: EasingType,
103 pub delay: f64,
104}
105
106impl Default for AnimationConfig {
107 fn default() -> Self {
108 Self {
109 duration: 1000.0,
110 easing: EasingType::EaseInOut,
111 delay: 0.0,
112 }
113 }
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
118pub enum EasingType {
119 #[default]
120 EaseInOut,
121 EaseIn,
122 EaseOut,
123 Linear,
124}
125
126impl EasingType {
127 pub fn to_class(&self) -> &'static str {
128 match self {
129 EasingType::EaseInOut => "ease-in-out",
130 EasingType::EaseIn => "ease-in",
131 EasingType::EaseOut => "ease-out",
132 EasingType::Linear => "linear",
133 }
134 }
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
139pub enum TimelineOrientation {
140 #[default]
141 Vertical,
142 Horizontal,
143}
144
145impl TimelineOrientation {
146 pub fn to_class(&self) -> &'static str {
147 match self {
148 TimelineOrientation::Vertical => "orientation-vertical",
149 TimelineOrientation::Horizontal => "orientation-horizontal",
150 }
151 }
152
153 pub fn to_string(&self) -> &'static str {
154 match self {
155 TimelineOrientation::Vertical => "vertical",
156 TimelineOrientation::Horizontal => "horizontal",
157 }
158 }
159}
160
161#[component]
163pub fn TimelineItem(
164 #[prop(optional)] class: Option<String>,
165 #[prop(optional)] style: Option<String>,
166 #[prop(optional)] children: Option<Children>,
167 #[prop(optional)] event: Option<TimelineEvent>,
168 #[prop(optional)] position: Option<f64>,
169 #[prop(optional)] on_click: Option<Callback<TimelineEvent>>,
170) -> impl IntoView {
171 let event = event.unwrap_or_default();
172 let position = position.unwrap_or(0.0);
173
174 let class = merge_classes(vec!["timeline-item", class.as_deref().unwrap_or("")]);
175
176 view! {
177 <div
178 class=class
179 style=style
180 role="listitem"
181 aria-label=format!("Timeline event: {}", event.title)
182 data-event-id=event.id
183 data-position=position
184 data-date=event.date
185 tabindex="0"
186 >
187 {children.map(|c| c())}
188 </div>
189 }
190}
191
192#[component]
194pub fn TimelineLine(
195 #[prop(optional)] class: Option<String>,
196 #[prop(optional)] style: Option<String>,
197 #[prop(optional)] orientation: Option<TimelineOrientation>,
198 #[prop(optional)] length: Option<f64>,
199 #[prop(optional)] thickness: Option<f64>,
200) -> impl IntoView {
201 let orientation = orientation.unwrap_or_default();
202 let length = length.unwrap_or(100.0);
203 let thickness = thickness.unwrap_or(2.0);
204
205 let class = merge_classes(vec![
206 "timeline-line",
207 &orientation.to_class(),
208 class.as_deref().unwrap_or(""),
209 ]);
210
211 view! {
212 <div
213 class=class
214 style=style
215 role="presentation"
216 aria-hidden="true"
217 data-length=length
218 data-thickness=thickness
219 data-orientation=orientation.to_string()
220 />
221 }
222}
223
224#[component]
226pub fn TimelineDot(
227 #[prop(optional)] class: Option<String>,
228 #[prop(optional)] style: Option<String>,
229 #[prop(optional)] children: Option<Children>,
230 #[prop(optional)] size: Option<f64>,
231 #[prop(optional)] color: Option<String>,
232 #[prop(optional)] filled: Option<bool>,
233) -> impl IntoView {
234 let size = size.unwrap_or(12.0);
235 let color = color.unwrap_or_default();
236 let filled = filled.unwrap_or(true);
237
238 let class = merge_classes(vec!["timeline-dot"]);
239}
240
241#[cfg(test)]
244mod tests {
245 use proptest::prelude::*;
246 use wasm_bindgen_test::*;
247
248 wasm_bindgen_test_configure!(run_in_browser);
249
250 #[test]
252 fn test_timeline_creation() {}
253 #[test]
254 fn test_timeline_with_class() {}
255 #[test]
256 fn test_timeline_with_style() {}
257 #[test]
258 fn test_timeline_with_events() {}
259 #[test]
260 fn test_timeline_with_config() {}
261 #[test]
262 fn test_timeline_orientation() {}
263 #[test]
264 fn test_timeline_show_dates() {}
265 #[test]
266 fn test_timeline_show_icons() {}
267 #[test]
268 fn test_timeline_on_event_click() {}
269 #[test]
270 fn test_timeline_on_event_hover() {}
271
272 #[test]
274 fn test_timeline_event_default() {}
275 #[test]
276 fn test_timeline_event_creation() {}
277
278 #[test]
280 fn test_timeline_config_default() {}
281 #[test]
282 fn test_timeline_config_custom() {}
283
284 #[test]
286 fn test_animation_config_default() {}
287 #[test]
288 fn test_animation_config_custom() {}
289
290 #[test]
292 fn test_easing_type_default() {}
293 #[test]
294 fn test_easing_type_ease_in_out() {}
295 #[test]
296 fn test_easing_type_ease_in() {}
297 #[test]
298 fn test_easing_type_ease_out() {}
299 #[test]
300 fn test_easing_type_linear() {}
301
302 #[test]
304 fn test_timeline_orientation_default() {}
305 #[test]
306 fn test_timeline_orientation_vertical() {}
307 #[test]
308 fn test_timeline_orientation_horizontal() {}
309
310 #[test]
312 fn test_timeline_item_creation() {}
313 #[test]
314 fn test_timeline_item_with_class() {}
315 #[test]
316 fn test_timeline_item_with_style() {}
317 #[test]
318 fn test_timeline_item_event() {}
319 #[test]
320 fn test_timeline_item_position() {}
321 #[test]
322 fn test_timeline_item_on_click() {}
323
324 #[test]
326 fn test_timeline_line_creation() {}
327 #[test]
328 fn test_timeline_line_with_class() {}
329 #[test]
330 fn test_timeline_line_with_style() {}
331 #[test]
332 fn test_timeline_line_orientation() {}
333 #[test]
334 fn test_timeline_line_length() {}
335 #[test]
336 fn test_timeline_line_thickness() {}
337
338 #[test]
340 fn test_timeline_dot_creation() {}
341 #[test]
342 fn test_timeline_dot_with_class() {}
343 #[test]
344 fn test_timeline_dot_with_style() {}
345 #[test]
346 fn test_timeline_dot_size() {}
347 #[test]
348 fn test_timeline_dot_color() {}
349 #[test]
350 fn test_timeline_dot_filled() {}
351
352 #[test]
354 fn test_merge_classes_empty() {}
355 #[test]
356 fn test_merge_classes_single() {}
357 #[test]
358 fn test_merge_classes_multiple() {}
359 #[test]
360 fn test_merge_classes_with_empty() {}
361
362 #[test]
364 fn test_timeline_property_based() {
365 proptest!(|(____class in ".*", __style in ".*")| {
366
367 });
368 }
369
370 #[test]
371 fn test_timeline_events_validation() {
372 proptest!(|(______event_count in 0..100usize)| {
373
374 });
375 }
376
377 #[test]
378 fn test_timeline_config_validation() {
379 proptest!(|(____width in 100.0..2000.0f64, __height in 100.0..2000.0f64)| {
380
381 });
382 }
383
384 #[test]
385 fn test_timeline_orientation_property_based() {
386 proptest!(|(____orientation_index in 0..2usize)| {
387
388 });
389 }
390
391 #[test]
393 fn test_timeline_user_interaction() {}
394 #[test]
395 fn test_timeline_accessibility() {}
396 #[test]
397 fn test_timeline_keyboard_navigation() {}
398 #[test]
399 fn test_timeline_event_filtering() {}
400 #[test]
401 fn test_timeline_date_sorting() {}
402
403 #[test]
405 fn test_timeline_large_event_lists() {}
406 #[test]
407 fn test_timeline_render_performance() {}
408 #[test]
409 fn test_timeline_memory_usage() {}
410 #[test]
411 fn test_timeline_animation_performance() {}
412}