Skip to main content

ui_storybook/stories/
editor.rs

1//! Editor stories — `DopeSheet`, `PlayerControls`, editor compositions.
2//! UI-16..19 expand this with `EditorShell` / `WispCanvasHost` /
3//! `InspectorPanel` / `TimelineSkeleton`.
4
5use leptos::prelude::*;
6
7use crate::components::editor::{
8    CanvasBackendView, DopeSheet, EditorDropZoneCanvas, EditorShell, InspectorPanel, PlayState,
9    PlayerControls, PropertyControlView, PropertyRowView, PropertySection, PropertySectionView,
10    TimelineSkeleton, WispCanvasHost,
11};
12use crate::components::primitives::{Card, CardBody, CardHeader};
13use crate::fixtures::editor::{
14    sample_dope_sheet_dense, sample_dope_sheet_tracks, sample_editor_drop_zone,
15    sample_editor_drop_zone_no_recent, sample_editor_shell, sample_editor_shell_export_disabled,
16    sample_inspector_cursor_tab, sample_inspector_disabled_section, sample_inspector_style_tab,
17    sample_timeline_skeleton, sample_timeline_skeleton_empty, sample_timeline_skeleton_playing,
18};
19
20use super::{Story, StoryViewport, render};
21
22fn fixed(width: u16, height: u16) -> StoryViewport {
23    StoryViewport::Fixed { width, height }
24}
25
26fn s(
27    id: &'static str,
28    category: &'static str,
29    title: &'static str,
30    viewport: StoryViewport,
31    render: fn() -> String,
32) -> Story {
33    Story {
34        id,
35        category,
36        title,
37        viewport,
38        render,
39    }
40}
41
42fn legacy_editor_stories() -> Vec<Story> {
43    let auto = StoryViewport::Auto;
44    vec![
45        s(
46            "dope-sheet-basic",
47            "Editor",
48            "Dope sheet — multi-track",
49            auto,
50            render_dope_sheet_basic,
51        ),
52        s(
53            "dope-sheet-dense",
54            "Editor",
55            "Dope sheet — dense keyframes",
56            auto,
57            render_dope_sheet_dense_story,
58        ),
59        s(
60            "card-with-dope-sheet",
61            "Compositions",
62            "Editor panel — card wrapping dope sheet",
63            auto,
64            render_editor_panel,
65        ),
66        s(
67            "player-controls-paused",
68            "Player",
69            "Player controls — paused at start",
70            auto,
71            render_player_paused,
72        ),
73        s(
74            "player-controls-playing",
75            "Player",
76            "Player controls — playing mid-clip",
77            auto,
78            render_player_playing,
79        ),
80        s(
81            "player-controls-near-end",
82            "Player",
83            "Player controls — near end of clip",
84            auto,
85            render_player_near_end,
86        ),
87        s(
88            "editor-mock",
89            "Compositions",
90            "Editor mock — drop zone result + player + dope sheet",
91            auto,
92            render_editor_mock,
93        ),
94    ]
95}
96
97fn editor_shell_stories() -> Vec<Story> {
98    let shell = fixed(960, 600);
99    vec![
100        s(
101            "editor-shell-empty",
102            "Editor",
103            "Editor shell — empty (no clip)",
104            shell,
105            render_editor_shell_empty,
106        ),
107        s(
108            "editor-shell-clip-loaded",
109            "Editor",
110            "Editor shell — clip loaded",
111            shell,
112            render_editor_shell_loaded,
113        ),
114        s(
115            "editor-toolbar-states",
116            "Editor",
117            "Editor toolbar — selected + disabled mix",
118            fixed(720, 80),
119            render_editor_toolbar_states,
120        ),
121        s(
122            "editor-shell-export-disabled",
123            "Editor",
124            "Editor shell — export disabled",
125            shell,
126            render_editor_shell_export_disabled,
127        ),
128    ]
129}
130
131fn timeline_stories() -> Vec<Story> {
132    let strip = fixed(960, 200);
133    vec![
134        s(
135            "timeline-empty",
136            "Editor",
137            "Timeline — empty (no placeholders)",
138            strip,
139            render_timeline_empty,
140        ),
141        s(
142            "timeline-with-placeholders",
143            "Editor",
144            "Timeline — with placeholders",
145            strip,
146            render_timeline_with_placeholders,
147        ),
148        s(
149            "timeline-playing",
150            "Editor",
151            "Timeline — playing",
152            strip,
153            render_timeline_playing,
154        ),
155        s(
156            "timeline-selected-track",
157            "Editor",
158            "Timeline — selected video track",
159            strip,
160            render_timeline_selected,
161        ),
162    ]
163}
164
165fn inspector_stories() -> Vec<Story> {
166    let panel = fixed(320, 480);
167    vec![
168        s(
169            "inspector-style-tab",
170            "Inspector",
171            "Inspector — Style tab",
172            panel,
173            render_inspector_style,
174        ),
175        s(
176            "inspector-cursor-tab",
177            "Inspector",
178            "Inspector — Cursor tab",
179            panel,
180            render_inspector_cursor,
181        ),
182        s(
183            "property-row-slider",
184            "Inspector",
185            "Property row — slider",
186            fixed(320, 80),
187            render_row_slider,
188        ),
189        s(
190            "property-row-toggle",
191            "Inspector",
192            "Property row — toggle",
193            fixed(320, 80),
194            render_row_toggle,
195        ),
196        s(
197            "property-row-color-swatches",
198            "Inspector",
199            "Property row — color swatches",
200            fixed(320, 80),
201            render_row_swatches,
202        ),
203        s(
204            "inspector-disabled-section",
205            "Inspector",
206            "Inspector — disabled rows",
207            panel,
208            render_inspector_disabled,
209        ),
210    ]
211}
212
213fn editor_canvas_stories() -> Vec<Story> {
214    let drop_zone = fixed(760, 520);
215    let canvas = fixed(600, 360);
216    vec![
217        s(
218            "editor-drop-zone-empty",
219            "Editor",
220            "Editor drop zone — empty",
221            drop_zone,
222            render_drop_zone_empty,
223        ),
224        s(
225            "editor-drop-zone-drag-active",
226            "Editor",
227            "Editor drop zone — drag active",
228            drop_zone,
229            render_drop_zone_drag,
230        ),
231        s(
232            "editor-drop-zone-with-recent",
233            "Editor",
234            "Editor drop zone — with recent clips",
235            drop_zone,
236            render_drop_zone_with_recent,
237        ),
238        s(
239            "wisp-canvas-host-fallback",
240            "Editor",
241            "Wisp canvas host — CSS fallback",
242            canvas,
243            render_canvas_host_fallback,
244        ),
245        s(
246            "wisp-canvas-host-asset",
247            "Editor",
248            "Wisp canvas host — runtime unavailable",
249            canvas,
250            render_canvas_host_unavailable,
251        ),
252    ]
253}
254
255/// All editor-surface stories, in display order.
256#[must_use]
257pub fn stories() -> Vec<Story> {
258    let mut out = legacy_editor_stories();
259    out.extend(editor_shell_stories());
260    out.extend(editor_canvas_stories());
261    out.extend(inspector_stories());
262    out.extend(timeline_stories());
263    out
264}
265
266fn render_dope_sheet_basic() -> String {
267    render(view! {
268        <DopeSheet
269            tracks=sample_dope_sheet_tracks()
270            duration_seconds=8.0
271            playhead_seconds=3.4
272        />
273    })
274}
275
276fn render_dope_sheet_dense_story() -> String {
277    render(view! {
278        <DopeSheet
279            tracks=sample_dope_sheet_dense()
280            duration_seconds=8.0
281            playhead_seconds=5.1
282        />
283    })
284}
285
286fn render_editor_panel() -> String {
287    render(view! {
288        <Card>
289            <CardHeader title="Timeline" subtitle="4 tracks · 8.0s" />
290            <CardBody>
291                <DopeSheet
292                    tracks=sample_dope_sheet_tracks()
293                    duration_seconds=8.0
294                    playhead_seconds=2.0
295                />
296            </CardBody>
297        </Card>
298    })
299}
300
301fn render_player_paused() -> String {
302    render(view! {
303        <PlayerControls state=PlayState::Paused position=0.0 duration_seconds=84.0 />
304    })
305}
306
307fn render_player_playing() -> String {
308    render(view! {
309        <PlayerControls state=PlayState::Playing position=0.32 duration_seconds=84.0 />
310    })
311}
312
313fn render_player_near_end() -> String {
314    render(view! {
315        <PlayerControls state=PlayState::Playing position=0.94 duration_seconds=84.0 />
316    })
317}
318
319fn render_editor_shell_empty() -> String {
320    render(view! { <EditorShell view=sample_editor_shell(false) /> })
321}
322
323fn render_editor_shell_loaded() -> String {
324    render(view! { <EditorShell view=sample_editor_shell(true) /> })
325}
326
327fn render_editor_shell_export_disabled() -> String {
328    render(view! { <EditorShell view=sample_editor_shell_export_disabled() /> })
329}
330
331fn render_editor_toolbar_states() -> String {
332    use crate::components::editor::EditorToolbar;
333    let v = sample_editor_shell(true);
334    render(view! {
335        <EditorToolbar actions=v.toolbar_actions export_enabled=true share_enabled=false />
336    })
337}
338
339fn render_drop_zone_empty() -> String {
340    render(view! { <EditorDropZoneCanvas view=sample_editor_drop_zone_no_recent() /> })
341}
342
343fn render_drop_zone_drag() -> String {
344    render(view! { <EditorDropZoneCanvas view=sample_editor_drop_zone(true) /> })
345}
346
347fn render_drop_zone_with_recent() -> String {
348    render(view! { <EditorDropZoneCanvas view=sample_editor_drop_zone(false) /> })
349}
350
351fn render_canvas_host_fallback() -> String {
352    render(view! { <WispCanvasHost backend=CanvasBackendView::CssFallback /> })
353}
354
355fn render_canvas_host_unavailable() -> String {
356    render(view! { <WispCanvasHost backend=CanvasBackendView::WispRuntimeUnavailable /> })
357}
358
359fn render_inspector_style() -> String {
360    render(view! { <InspectorPanel view=sample_inspector_style_tab() /> })
361}
362
363fn render_inspector_cursor() -> String {
364    render(view! { <InspectorPanel view=sample_inspector_cursor_tab() /> })
365}
366
367fn render_inspector_disabled() -> String {
368    render(view! { <InspectorPanel view=sample_inspector_disabled_section() /> })
369}
370
371fn render_row_slider() -> String {
372    let section = PropertySectionView {
373        title: "EXAMPLE",
374        rows: vec![PropertyRowView {
375            label: "Auto-zoom",
376            value: Some("2.0×"),
377            disabled: false,
378            control: PropertyControlView::SliderPercent { percent: 55 },
379        }],
380    };
381    render(view! { <PropertySection section=section /> })
382}
383
384fn render_row_toggle() -> String {
385    let section = PropertySectionView {
386        title: "EXAMPLE",
387        rows: vec![PropertyRowView {
388            label: "Drop shadow",
389            value: None,
390            disabled: false,
391            control: PropertyControlView::Toggle { on: true },
392        }],
393    };
394    render(view! { <PropertySection section=section /> })
395}
396
397fn render_timeline_empty() -> String {
398    render(view! { <TimelineSkeleton view=sample_timeline_skeleton_empty() /> })
399}
400
401fn render_timeline_with_placeholders() -> String {
402    render(view! { <TimelineSkeleton view=sample_timeline_skeleton() /> })
403}
404
405fn render_timeline_playing() -> String {
406    render(view! { <TimelineSkeleton view=sample_timeline_skeleton_playing() /> })
407}
408
409fn render_timeline_selected() -> String {
410    let mut v = sample_timeline_skeleton();
411    if let Some(t) = v.tracks.first_mut() {
412        t.selected = true;
413    }
414    render(view! { <TimelineSkeleton view=v /> })
415}
416
417fn render_row_swatches() -> String {
418    let section = PropertySectionView {
419        title: "EXAMPLE",
420        rows: vec![PropertyRowView {
421            label: "Color",
422            value: None,
423            disabled: false,
424            control: PropertyControlView::ColorSwatches {
425                swatches: vec!["#fafafa", "#facc15", "#38bdf8", "#a78bfa", "#f97316"],
426                selected: 2,
427            },
428        }],
429    };
430    render(view! { <PropertySection section=section /> })
431}
432
433fn render_editor_mock() -> String {
434    render(view! {
435        <div class="editor-mock">
436            <Card>
437                <CardHeader title="Recording 02" subtitle="Captured 2026-05-09 · 1m 24s" />
438                <CardBody>
439                    <div class="editor-mock-preview">
440                        <div class="editor-mock-screen">
441                            <div class="editor-mock-screen-label">"Player surface"</div>
442                        </div>
443                    </div>
444                    <PlayerControls
445                        state=PlayState::Playing
446                        position=0.42
447                        duration_seconds=84.0
448                    />
449                </CardBody>
450            </Card>
451            <Card>
452                <CardHeader title="Timeline" subtitle="4 tracks · 8.0s window" />
453                <CardBody>
454                    <DopeSheet
455                        tracks=sample_dope_sheet_tracks()
456                        duration_seconds=8.0
457                        playhead_seconds=3.4
458                    />
459                </CardBody>
460            </Card>
461        </div>
462    })
463}