Skip to main content

ui_storybook/fixtures/
library.rs

1//! Library fixtures — recordings list, storage meter values. Filled in
2//! across UI-14 / UI-15.
3
4/// One recording shown in the library grid.
5#[derive(Debug, Clone, PartialEq)]
6pub struct RecordingFixture {
7    /// Stable id (file uuid in production).
8    pub id: &'static str,
9    /// Display title.
10    pub title: &'static str,
11    /// Captured-at label ("Captured 2026-05-09").
12    pub captured_at: &'static str,
13    /// Duration label ("1m 24s").
14    pub duration: &'static str,
15    /// Size label ("328 MB").
16    pub size: &'static str,
17    /// `true` when this card is currently selected.
18    pub selected: bool,
19}
20
21/// Sample recordings for the library grid.
22#[must_use]
23pub fn sample_recordings() -> Vec<RecordingFixture> {
24    vec![
25        RecordingFixture {
26            id: "rec-01",
27            title: "Demo · auth login",
28            captured_at: "Captured 2026-05-09",
29            duration: "1m 24s",
30            size: "328 MB",
31            selected: true,
32        },
33        RecordingFixture {
34            id: "rec-02",
35            title: "Standup recap",
36            captured_at: "Captured 2026-05-08",
37            duration: "4m 02s",
38            size: "1.1 GB",
39            selected: false,
40        },
41        RecordingFixture {
42            id: "rec-03",
43            title: "Bug repro · payments",
44            captured_at: "Captured 2026-05-07",
45            duration: "0m 38s",
46            size: "112 MB",
47            selected: false,
48        },
49        RecordingFixture {
50            id: "rec-04",
51            title: "Onboarding tour",
52            captured_at: "Captured 2026-05-06",
53            duration: "2m 11s",
54            size: "604 MB",
55            selected: false,
56        },
57    ]
58}
59
60/// Storage meter fixture for the sidebar.
61#[derive(Debug, Clone, Copy, PartialEq)]
62pub struct StorageMeter {
63    /// Bytes used.
64    pub used_bytes: u64,
65    /// Bytes available (the meter's max).
66    pub total_bytes: u64,
67}
68
69impl StorageMeter {
70    /// Fraction `used / total`, clamped to `[0, 1]`.
71    #[must_use]
72    pub fn fraction(self) -> f32 {
73        if self.total_bytes == 0 {
74            0.0
75        } else {
76            #[allow(
77                clippy::cast_precision_loss,
78                reason = "ratio fits f32 without meaningful loss"
79            )]
80            let used = self.used_bytes as f32;
81            #[allow(
82                clippy::cast_precision_loss,
83                reason = "ratio fits f32 without meaningful loss"
84            )]
85            let total = self.total_bytes as f32;
86            (used / total).clamp(0.0, 1.0)
87        }
88    }
89}
90
91/// Sample storage-meter values (~62% full of 50 GB).
92#[must_use]
93pub const fn sample_storage_meter() -> StorageMeter {
94    StorageMeter {
95        used_bytes: 33_285_996_544,
96        total_bytes: 53_687_091_200,
97    }
98}
99
100/// Sample card view-models for UI-15. Hand-picked gradients so the
101/// thumbnails read as a contact sheet.
102#[must_use]
103pub fn sample_recording_cards() -> Vec<crate::components::library::RecordingCardView> {
104    use crate::components::library::{
105        RecordingCardState, RecordingCardView, RecordingMetricsView, ThumbnailView,
106    };
107    vec![
108        RecordingCardView {
109            id: "rec-01".into(),
110            title: "Demo · auth login".into(),
111            subtitle: "Captured 2026-05-09 · 1080p".into(),
112            duration_label: "1m 24s".into(),
113            category: Some("Tutorial".into()),
114            metrics: RecordingMetricsView {
115                views: 142,
116                comments: 6,
117                reactions: 12,
118            },
119            thumbnail: ThumbnailView {
120                css_background: "linear-gradient(135deg,#1e3a8a,#7c3aed)".into(),
121            },
122            state: RecordingCardState::Ready,
123            selected: true,
124        },
125        RecordingCardView {
126            id: "rec-02".into(),
127            title: "Standup recap".into(),
128            subtitle: "Captured 2026-05-08 · 4K".into(),
129            duration_label: "4m 02s".into(),
130            category: Some("Demo".into()),
131            metrics: RecordingMetricsView {
132                views: 38,
133                comments: 1,
134                reactions: 4,
135            },
136            thumbnail: ThumbnailView {
137                css_background: "linear-gradient(135deg,#0f766e,#facc15)".into(),
138            },
139            state: RecordingCardState::Processing { percent: 64 },
140            selected: false,
141        },
142        RecordingCardView {
143            id: "rec-03".into(),
144            title: "Bug repro · payments".into(),
145            subtitle: "Captured 2026-05-07".into(),
146            duration_label: "0m 38s".into(),
147            category: Some("Bug".into()),
148            metrics: RecordingMetricsView {
149                views: 7,
150                comments: 3,
151                reactions: 0,
152            },
153            thumbnail: ThumbnailView {
154                css_background: "linear-gradient(135deg,#7f1d1d,#f97316)".into(),
155            },
156            state: RecordingCardState::Ready,
157            selected: false,
158        },
159        RecordingCardView {
160            id: "rec-04".into(),
161            title: "Onboarding tour".into(),
162            subtitle: "Captured 2026-05-06 · 1080p".into(),
163            duration_label: "2m 11s".into(),
164            category: None,
165            metrics: RecordingMetricsView {
166                views: 88,
167                comments: 0,
168                reactions: 9,
169            },
170            thumbnail: ThumbnailView {
171                css_background: String::new(),
172            },
173            state: RecordingCardState::Ready,
174            selected: false,
175        },
176    ]
177}
178
179/// Default `LibraryGridView` — toolbar + four cards.
180#[must_use]
181pub fn sample_library_grid() -> crate::components::library::LibraryGridView {
182    use crate::components::library::{
183        LibraryGridView, LibraryLayoutMode, LibraryToolbarView, RecordingFilterView,
184    };
185    LibraryGridView {
186        toolbar: LibraryToolbarView {
187            filters: vec![
188                RecordingFilterView {
189                    id: "all",
190                    label: "All",
191                    active: true,
192                },
193                RecordingFilterView {
194                    id: "tutorial",
195                    label: "Tutorials",
196                    active: false,
197                },
198                RecordingFilterView {
199                    id: "demo",
200                    label: "Demos",
201                    active: false,
202                },
203                RecordingFilterView {
204                    id: "bug",
205                    label: "Bug repros",
206                    active: false,
207                },
208            ],
209            sort_label: "Most recent",
210            layout: LibraryLayoutMode::Grid,
211        },
212        recordings: sample_recording_cards(),
213        empty_message: "No recordings yet — press ⌘⇧2 to start one.",
214    }
215}
216
217/// Variant — empty grid (drives the empty-state story).
218#[must_use]
219pub fn sample_library_grid_empty() -> crate::components::library::LibraryGridView {
220    let mut v = sample_library_grid();
221    v.recordings.clear();
222    v
223}
224
225/// Variant — list layout.
226#[must_use]
227pub fn sample_library_grid_list_mode() -> crate::components::library::LibraryGridView {
228    use crate::components::library::LibraryLayoutMode;
229    let mut v = sample_library_grid();
230    v.toolbar.layout = LibraryLayoutMode::List;
231    v
232}
233
234fn library_primary_nav(inbox_unread: u32) -> Vec<crate::components::library::LibraryNavItemView> {
235    use crate::components::library::LibraryNavItemView;
236    vec![
237        LibraryNavItemView {
238            id: "new",
239            label: "New recording",
240            icon: "+",
241            count: None,
242            badge: None,
243            selected: false,
244            disabled: false,
245        },
246        LibraryNavItemView {
247            id: "all",
248            label: "All recordings",
249            icon: "▦",
250            count: Some(42),
251            badge: None,
252            selected: true,
253            disabled: false,
254        },
255        LibraryNavItemView {
256            id: "starred",
257            label: "Starred",
258            icon: "★",
259            count: Some(7),
260            badge: None,
261            selected: false,
262            disabled: false,
263        },
264        LibraryNavItemView {
265            id: "shared",
266            label: "Shared with me",
267            icon: "↗",
268            count: Some(3),
269            badge: None,
270            selected: false,
271            disabled: false,
272        },
273        LibraryNavItemView {
274            id: "inbox",
275            label: "Inbox",
276            icon: "✉",
277            count: None,
278            badge: (inbox_unread > 0).then_some(inbox_unread),
279            selected: false,
280            disabled: false,
281        },
282    ]
283}
284
285fn library_spaces_and_tags() -> Vec<crate::components::library::LibrarySectionView> {
286    use crate::components::library::{LibraryNavItemView, LibrarySectionView};
287    let spaces = LibrarySectionView {
288        heading: "SPACES",
289        items: vec![
290            LibraryNavItemView {
291                id: "space-team",
292                label: "Northwind Studio",
293                icon: "🟢",
294                count: Some(24),
295                badge: None,
296                selected: false,
297                disabled: false,
298            },
299            LibraryNavItemView {
300                id: "space-product",
301                label: "Product reviews",
302                icon: "🟣",
303                count: Some(8),
304                badge: None,
305                selected: false,
306                disabled: false,
307            },
308        ],
309    };
310    let tags = LibrarySectionView {
311        heading: "TAGS",
312        items: vec![
313            LibraryNavItemView {
314                id: "tag-tutorial",
315                label: "Tutorials",
316                icon: "🟦",
317                count: Some(12),
318                badge: None,
319                selected: false,
320                disabled: false,
321            },
322            LibraryNavItemView {
323                id: "tag-bug",
324                label: "Bug repros",
325                icon: "🟥",
326                count: Some(6),
327                badge: None,
328                selected: false,
329                disabled: false,
330            },
331            LibraryNavItemView {
332                id: "tag-demo",
333                label: "Customer demos",
334                icon: "🟨",
335                count: Some(3),
336                badge: None,
337                selected: false,
338                disabled: false,
339            },
340        ],
341    };
342    vec![spaces, tags]
343}
344
345/// Sample sidebar view for UI-14. `inbox_unread` lets stories
346/// sweep the badge count.
347#[must_use]
348pub fn sample_library_sidebar(inbox_unread: u32) -> crate::components::library::LibrarySidebarView {
349    use crate::components::library::{LibrarySidebarView, StorageMeterView};
350    LibrarySidebarView {
351        primary: library_primary_nav(inbox_unread),
352        sections: library_spaces_and_tags(),
353        storage: StorageMeterView {
354            used_bytes_label: "12.4 GB".to_owned(),
355            quota_label: "50 GB".to_owned(),
356            percent_used: sample_storage_meter().fraction(),
357            plan_label: Some("Free"),
358        },
359    }
360}
361
362/// Variant — inbox selected (not "all"). Drives the `inbox-active`
363/// story.
364#[must_use]
365pub fn sample_library_sidebar_inbox_active() -> crate::components::library::LibrarySidebarView {
366    let mut v = sample_library_sidebar(3);
367    for item in &mut v.primary {
368        item.selected = item.id == "inbox";
369    }
370    v
371}
372
373/// Variant — near-full storage drives the warning style.
374#[must_use]
375pub fn sample_library_sidebar_high_storage() -> crate::components::library::LibrarySidebarView {
376    let mut v = sample_library_sidebar(0);
377    "47.6 GB".clone_into(&mut v.storage.used_bytes_label);
378    v.storage.percent_used = 0.95;
379    v
380}
381
382#[cfg(test)]
383mod tests {
384    use super::*;
385
386    #[test]
387    fn library_sidebar_has_primary_and_sections() {
388        let v = sample_library_sidebar(3);
389        assert!(v.primary.iter().any(|i| i.id == "inbox"));
390        assert_eq!(v.sections.len(), 2);
391        assert_eq!(v.sections[0].heading, "SPACES");
392        assert_eq!(v.sections[1].heading, "TAGS");
393    }
394
395    #[test]
396    fn high_storage_variant_passes_warn_threshold() {
397        let v = sample_library_sidebar_high_storage();
398        assert!(v.storage.percent_used >= 0.85);
399    }
400
401    #[test]
402    fn recordings_non_empty_with_one_selected() {
403        let r = sample_recordings();
404        assert!(!r.is_empty());
405        assert_eq!(r.iter().filter(|x| x.selected).count(), 1);
406    }
407
408    #[test]
409    fn storage_fraction_in_unit_interval() {
410        let f = sample_storage_meter().fraction();
411        assert!((0.0..=1.0).contains(&f));
412    }
413}