Skip to main content

ui_storybook/fixtures/
editor.rs

1//! Editor fixtures — timeline tracks, inspector property rows, dope-sheet
2//! keyframes. Filled in across UI-16..19. The dope-sheet fixture lifts the
3//! previously-inline `sample_tracks()` so multiple editor stories can share
4//! it.
5
6use crate::components::editor::{DopeSheetKeyframe, DopeSheetTrack, KeyframeKind, TrackKind};
7
8/// Multi-track sample used by `dope-sheet-basic` and the editor compositions.
9#[must_use]
10pub fn sample_dope_sheet_tracks() -> Vec<DopeSheetTrack> {
11    vec![
12        DopeSheetTrack {
13            label: "Video".into(),
14            kind: TrackKind::Video,
15            keyframes: vec![
16                DopeSheetKeyframe {
17                    time: 0.0,
18                    kind: KeyframeKind::Hold,
19                },
20                DopeSheetKeyframe {
21                    time: 2.5,
22                    kind: KeyframeKind::Ease,
23                },
24                DopeSheetKeyframe {
25                    time: 6.0,
26                    kind: KeyframeKind::Linear,
27                },
28            ],
29        },
30        DopeSheetTrack {
31            label: "Cursor".into(),
32            kind: TrackKind::Cursor,
33            keyframes: vec![
34                DopeSheetKeyframe {
35                    time: 1.2,
36                    kind: KeyframeKind::Linear,
37                },
38                DopeSheetKeyframe {
39                    time: 4.5,
40                    kind: KeyframeKind::Linear,
41                },
42            ],
43        },
44        DopeSheetTrack {
45            label: "Audio".into(),
46            kind: TrackKind::Audio,
47            keyframes: vec![
48                DopeSheetKeyframe {
49                    time: 0.0,
50                    kind: KeyframeKind::Hold,
51                },
52                DopeSheetKeyframe {
53                    time: 4.0,
54                    kind: KeyframeKind::Hold,
55                },
56                DopeSheetKeyframe {
57                    time: 7.5,
58                    kind: KeyframeKind::Hold,
59                },
60            ],
61        },
62        DopeSheetTrack {
63            label: "Caption".into(),
64            kind: TrackKind::Caption,
65            keyframes: vec![DopeSheetKeyframe {
66                time: 3.0,
67                kind: KeyframeKind::Marker,
68            }],
69        },
70    ]
71}
72
73/// Dense variant — adds a noisy effect track with twelve keyframes for the
74/// `dope-sheet-dense` story.
75#[must_use]
76pub fn sample_dope_sheet_dense() -> Vec<DopeSheetTrack> {
77    let mut tracks = sample_dope_sheet_tracks();
78    tracks.push(DopeSheetTrack {
79        label: "Zoom".into(),
80        kind: TrackKind::Effect,
81        keyframes: (0..12)
82            .map(|i| DopeSheetKeyframe {
83                #[allow(
84                    clippy::cast_precision_loss,
85                    reason = "i ≤ 12 fits in f32 without loss"
86                )]
87                time: i as f32 * 0.65,
88                kind: if i % 3 == 0 {
89                    KeyframeKind::Ease
90                } else {
91                    KeyframeKind::Linear
92                },
93            })
94            .collect(),
95    });
96    tracks
97}
98
99/// Sample editor-shell view (UI-16).
100#[must_use]
101pub fn sample_editor_shell(has_clip_loaded: bool) -> crate::components::editor::EditorShellView {
102    use crate::components::editor::{EditorShellView, ToolbarActionView};
103    let actions = vec![
104        ToolbarActionView {
105            id: "16-9",
106            label: "16:9",
107            icon: "▭",
108            selected: true,
109            disabled: false,
110        },
111        ToolbarActionView {
112            id: "crop",
113            label: "Crop",
114            icon: "✂",
115            selected: false,
116            disabled: false,
117        },
118        ToolbarActionView {
119            id: "annotate",
120            label: "Annotate",
121            icon: "✎",
122            selected: false,
123            disabled: !has_clip_loaded,
124        },
125        ToolbarActionView {
126            id: "trim",
127            label: "Trim",
128            icon: "⎯",
129            selected: false,
130            disabled: !has_clip_loaded,
131        },
132    ];
133    EditorShellView {
134        document_title: "Demo · auth login".into(),
135        document_subtitle: Some("Captured 2026-05-09 · 1m 24s".into()),
136        has_clip_loaded,
137        toolbar_actions: actions,
138        export_enabled: has_clip_loaded,
139        share_enabled: has_clip_loaded,
140    }
141}
142
143/// Variant — export disabled.
144#[must_use]
145pub fn sample_editor_shell_export_disabled() -> crate::components::editor::EditorShellView {
146    let mut v = sample_editor_shell(true);
147    v.export_enabled = false;
148    v
149}
150
151/// Sample drop-zone view (UI-17). The backend defaults to `CssFallback`
152/// so SSR snapshots stay stable.
153#[must_use]
154pub fn sample_editor_drop_zone(drag_active: bool) -> crate::components::editor::EditorDropZoneView {
155    use crate::components::editor::{CanvasBackendView, DropZoneActionView, EditorDropZoneView};
156    EditorDropZoneView {
157        backend: CanvasBackendView::CssFallback,
158        headline: "Drop a clip to start editing",
159        subtext: "or press ⌘⇧2 to record one now",
160        actions: vec![
161            DropZoneActionView {
162                id: "record",
163                label: "Record screen",
164                description: "Open the tray recorder",
165                icon: "●",
166            },
167            DropZoneActionView {
168                id: "library",
169                label: "Open library",
170                description: "Pick a recent recording",
171                icon: "▦",
172            },
173            DropZoneActionView {
174                id: "file",
175                label: "Import file",
176                description: "Choose an MP4 / MOV",
177                icon: "📁",
178            },
179        ],
180        recent_clips: sample_recent_clips(),
181        drag_active,
182    }
183}
184
185fn sample_recent_clips() -> Vec<crate::components::editor::RecentClipView> {
186    use crate::components::editor::RecentClipView;
187    vec![
188        RecentClipView {
189            id: "rec-01",
190            title: "Demo · auth login",
191            duration_label: "1m 24s",
192            thumbnail_css: "linear-gradient(135deg,#1e3a8a,#7c3aed)",
193        },
194        RecentClipView {
195            id: "rec-02",
196            title: "Standup recap",
197            duration_label: "4m 02s",
198            thumbnail_css: "linear-gradient(135deg,#0f766e,#facc15)",
199        },
200        RecentClipView {
201            id: "rec-03",
202            title: "Bug repro · payments",
203            duration_label: "0m 38s",
204            thumbnail_css: "linear-gradient(135deg,#7f1d1d,#f97316)",
205        },
206    ]
207}
208
209/// Variant — empty recent strip.
210#[must_use]
211pub fn sample_editor_drop_zone_no_recent() -> crate::components::editor::EditorDropZoneView {
212    let mut v = sample_editor_drop_zone(false);
213    v.recent_clips.clear();
214    v
215}
216
217/// Sample inspector view (UI-18) — Style tab with appearance + motion sections.
218#[must_use]
219pub fn sample_inspector_style_tab() -> crate::components::editor::InspectorPanelView {
220    use crate::components::editor::{
221        InspectorPanelView, InspectorTab, PropertyControlView, PropertyRowView, PropertySectionView,
222    };
223    let appearance = PropertySectionView {
224        title: "APPEARANCE",
225        rows: vec![
226            PropertyRowView {
227                label: "Background",
228                value: Some("Studio dark"),
229                disabled: false,
230                control: PropertyControlView::SelectPill {
231                    current_label: "Studio dark",
232                },
233            },
234            PropertyRowView {
235                label: "Corner radius",
236                value: Some("12 px"),
237                disabled: false,
238                control: PropertyControlView::SliderPercent { percent: 35 },
239            },
240            PropertyRowView {
241                label: "Drop shadow",
242                value: None,
243                disabled: false,
244                control: PropertyControlView::Toggle { on: true },
245            },
246        ],
247    };
248    let motion = PropertySectionView {
249        title: "MOTION",
250        rows: vec![
251            PropertyRowView {
252                label: "Auto-zoom",
253                value: Some("2.0×"),
254                disabled: false,
255                control: PropertyControlView::SliderPercent { percent: 50 },
256            },
257            PropertyRowView {
258                label: "Smooth pan",
259                value: None,
260                disabled: false,
261                control: PropertyControlView::Toggle { on: true },
262            },
263        ],
264    };
265    InspectorPanelView {
266        tabs: vec![
267            InspectorTab::Style,
268            InspectorTab::Cursor,
269            InspectorTab::Audio,
270            InspectorTab::Captions,
271            InspectorTab::Ai,
272        ],
273        active: InspectorTab::Style,
274        sections: vec![appearance, motion],
275    }
276}
277
278/// Variant — cursor tab active with appearance swatches.
279#[must_use]
280pub fn sample_inspector_cursor_tab() -> crate::components::editor::InspectorPanelView {
281    use crate::components::editor::{
282        InspectorTab, PropertyControlView, PropertyRowView, PropertySectionView,
283    };
284    let mut v = sample_inspector_style_tab();
285    v.active = InspectorTab::Cursor;
286    v.sections = vec![
287        PropertySectionView {
288            title: "APPEARANCE",
289            rows: vec![
290                PropertyRowView {
291                    label: "Size",
292                    value: Some("120 %"),
293                    disabled: false,
294                    control: PropertyControlView::SliderPercent { percent: 60 },
295                },
296                PropertyRowView {
297                    label: "Color",
298                    value: None,
299                    disabled: false,
300                    control: PropertyControlView::ColorSwatches {
301                        swatches: vec!["#fafafa", "#facc15", "#38bdf8", "#a78bfa", "#f97316"],
302                        selected: 2,
303                    },
304                },
305                PropertyRowView {
306                    label: "Halo",
307                    value: None,
308                    disabled: false,
309                    control: PropertyControlView::Toggle { on: true },
310                },
311            ],
312        },
313        PropertySectionView {
314            title: "CLICK EFFECT",
315            rows: vec![PropertyRowView {
316                label: "Effect",
317                value: Some("Ring"),
318                disabled: false,
319                control: PropertyControlView::SelectPill {
320                    current_label: "Ring",
321                },
322            }],
323        },
324    ];
325    v
326}
327
328/// Variant — disabled rows (read-only).
329#[must_use]
330pub fn sample_inspector_disabled_section() -> crate::components::editor::InspectorPanelView {
331    let mut v = sample_inspector_style_tab();
332    for s in &mut v.sections {
333        for r in &mut s.rows {
334            r.disabled = true;
335        }
336    }
337    v
338}
339
340/// Sample timeline skeleton view (UI-19).
341#[must_use]
342pub fn sample_timeline_skeleton() -> crate::components::editor::TimelineView {
343    use crate::components::editor::{TimelineTrackView, TimelineView};
344    TimelineView {
345        playhead_label: "00:03",
346        duration_label: "01:24",
347        is_playing: false,
348        tracks: vec![
349            TimelineTrackView {
350                id: "video",
351                label: "Video",
352                placeholder: Some("drop a clip to fill"),
353                disabled: false,
354                selected: false,
355            },
356            TimelineTrackView {
357                id: "auto-zoom",
358                label: "Auto-zoom",
359                placeholder: Some("detected from cursor"),
360                disabled: false,
361                selected: false,
362            },
363            TimelineTrackView {
364                id: "audio",
365                label: "Audio",
366                placeholder: Some("mic + system"),
367                disabled: false,
368                selected: false,
369            },
370        ],
371    }
372}
373
374/// Variant — selected video track + playing.
375#[must_use]
376pub fn sample_timeline_skeleton_playing() -> crate::components::editor::TimelineView {
377    let mut v = sample_timeline_skeleton();
378    v.is_playing = true;
379    if let Some(t) = v.tracks.first_mut() {
380        t.selected = true;
381    }
382    v
383}
384
385/// Variant — placeholders gone (all tracks empty without hints).
386#[must_use]
387pub fn sample_timeline_skeleton_empty() -> crate::components::editor::TimelineView {
388    let mut v = sample_timeline_skeleton();
389    for t in &mut v.tracks {
390        t.placeholder = None;
391    }
392    v
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398
399    #[test]
400    fn dope_sheet_tracks_have_four_rows() {
401        assert_eq!(sample_dope_sheet_tracks().len(), 4);
402    }
403
404    #[test]
405    fn dope_sheet_dense_adds_a_zoom_track() {
406        let tracks = sample_dope_sheet_dense();
407        assert_eq!(tracks.len(), 5);
408        assert_eq!(tracks.last().unwrap().keyframes.len(), 12);
409    }
410}