Skip to main content

ui_storybook/components/editor/
timeline_skeleton.rs

1//! `TimelineSkeleton` + transport + track rows (M-UI.19 / AUT-139).
2//!
3//! Layout-only timeline scaffold — transport row + per-track labels +
4//! placeholder content. No editing semantics, no real keyframes
5//! (those live in `DopeSheet`). The parent supplies timecode labels
6//! and selection state.
7
8use leptos::prelude::*;
9
10/// One row in the skeleton.
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct TimelineTrackView {
13    /// Stable id ("video", "auto-zoom", "audio").
14    pub id: &'static str,
15    /// Display label.
16    pub label: &'static str,
17    /// Placeholder shown when the track is empty
18    /// (`"drop a clip to fill"`).
19    pub placeholder: Option<&'static str>,
20    /// `true` to render disabled.
21    pub disabled: bool,
22    /// `true` for the currently-selected track.
23    pub selected: bool,
24}
25
26/// Timeline view-model.
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct TimelineView {
29    /// Pre-formatted playhead label ("00:03").
30    pub playhead_label: &'static str,
31    /// Pre-formatted duration label ("01:24").
32    pub duration_label: &'static str,
33    /// `true` for the playing glyph; `false` for the paused glyph.
34    pub is_playing: bool,
35    /// Tracks in display order.
36    pub tracks: Vec<TimelineTrackView>,
37}
38
39#[component]
40pub fn TimelineTransport(
41    /// Pre-formatted playhead label.
42    #[prop(into)]
43    playhead_label: String,
44    /// Pre-formatted duration label.
45    #[prop(into)]
46    duration_label: String,
47    /// `true` for the playing glyph.
48    is_playing: bool,
49) -> impl IntoView {
50    let glyph = if is_playing { "❚❚" } else { "▶" };
51    let label = if is_playing { "Pause" } else { "Play" };
52    view! {
53        <div class="timeline-transport" role="group" aria-label="Transport">
54            <button class="timeline-transport-toggle" aria-label=label>{glyph}</button>
55            <span class="timeline-timecode">{playhead_label}</span>
56            <span class="timeline-timecode-sep">"/"</span>
57            <span class="timeline-timecode-total">{duration_label}</span>
58        </div>
59    }
60}
61
62#[component]
63pub fn TimelineTrackRow(track: TimelineTrackView) -> impl IntoView {
64    let mut class = String::from("timeline-track-row");
65    if track.selected {
66        class.push_str(" timeline-track-row-selected");
67    }
68    if track.disabled {
69        class.push_str(" timeline-track-row-disabled");
70    }
71    let placeholder = track.placeholder.unwrap_or("");
72    view! {
73        <li class=class data-id=track.id>
74            <span class="timeline-track-label">{track.label}</span>
75            <span class="timeline-track-body">
76                <span class="timeline-track-placeholder">{placeholder}</span>
77            </span>
78        </li>
79    }
80}
81
82#[component]
83pub fn TimelineSkeleton(view: TimelineView) -> impl IntoView {
84    let TimelineView {
85        playhead_label,
86        duration_label,
87        is_playing,
88        tracks,
89    } = view;
90    let track_rows: Vec<_> = tracks
91        .into_iter()
92        .map(|t| view! { <TimelineTrackRow track=t /> })
93        .collect();
94    view! {
95        <section class="timeline-skeleton" aria-label="Timeline">
96            <TimelineTransport
97                playhead_label=playhead_label
98                duration_label=duration_label
99                is_playing=is_playing
100            />
101            <ul class="timeline-tracks" role="list">{track_rows}</ul>
102        </section>
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn track_view_carries_state() {
112        let t = TimelineTrackView {
113            id: "video",
114            label: "Video",
115            placeholder: Some("drop a clip to fill"),
116            disabled: false,
117            selected: true,
118        };
119        assert!(t.selected);
120        assert!(t.placeholder.is_some());
121    }
122}