radix_leptos_primitives/components/
timeline.rs

1use crate::utils::{merge_classes, generate_id};
2use leptos::callback::Callback;
3use leptos::children::Children;
4use leptos::prelude::*;
5
6/// Timeline component - Event visualization
7#[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/// Timeline Event structure
49#[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/// Timeline Configuration
75#[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/// Animation Configuration
99#[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/// Easing Type
117#[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/// Timeline Orientation
138#[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/// Timeline Item component
162#[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/// Timeline Line component
193#[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/// Timeline Dot component
225#[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/// Helper function to merge CSS classes
242
243#[cfg(test)]
244mod tests {
245    use proptest::prelude::*;
246    use wasm_bindgen_test::*;
247
248    wasm_bindgen_test_configure!(run_in_browser);
249
250    // Unit Tests
251    #[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    // Timeline Event tests
273    #[test]
274    fn test_timeline_event_default() {}
275    #[test]
276    fn test_timeline_event_creation() {}
277
278    // Timeline Config tests
279    #[test]
280    fn test_timeline_config_default() {}
281    #[test]
282    fn test_timeline_config_custom() {}
283
284    // Animation Config tests
285    #[test]
286    fn test_animation_config_default() {}
287    #[test]
288    fn test_animation_config_custom() {}
289
290    // Easing Type tests
291    #[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    // Timeline Orientation tests
303    #[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    // Timeline Item tests
311    #[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    // Timeline Line tests
325    #[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    // Timeline Dot tests
339    #[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    // Helper function tests
353    #[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    // Property-based Tests
363    #[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    // Integration Tests
392    #[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    // Performance Tests
404    #[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}