app_ui/
app_shell_mount.rs1use leptos::prelude::*;
16use ui_storybook::components::shell::{
17 AppSection, AppShell, NavigationRail, StatusBar, StatusKind,
18};
19use ui_storybook::fixtures::shell::{sample_nav_items, sample_user_avatar, sample_workspace_badge};
20
21#[component]
22pub fn AppShellRoot(initial: AppSection) -> impl IntoView {
23 let active = RwSignal::new(initial);
24
25 let editor_project = RwSignal::new(None::<edit::EditProject>);
29 provide_context(editor_project);
30 crate::editor_ipc::install_editor_project_listener(editor_project);
31 Effect::new(move |_| {
32 if editor_project.get().is_some() {
33 active.set(AppSection::Editor);
34 }
35 });
36
37 crate::editor_ipc::install_file_drop_to_editor_listener();
42 let editor_drag_active = RwSignal::new(false);
43 provide_context(editor_drag_active);
44 crate::editor_ipc::install_drag_active_listeners(editor_drag_active);
45
46 let editor_status = RwSignal::new(crate::editor_ipc::EditorStatus::default());
51 provide_context(editor_status);
52 crate::editor_ipc::install_editor_status_listener(editor_status);
53 gloo_timers::callback::Interval::new(33, move || {
54 if editor_status.get_untracked().playing {
55 crate::editor_ipc::editor_transport(&crate::editor_ipc::TransportAction::Tick {
56 dt_ms: 33,
57 });
58 }
59 })
60 .forget();
61
62 let editor_selection = RwSignal::new(None::<usize>);
65 provide_context(editor_selection);
66
67 let editor_peaks = RwSignal::new(Vec::<crate::waveform::WaveBucket>::new());
70 provide_context(editor_peaks);
71
72 let editor_history = StoredValue::new(None::<edit::History>);
75 provide_context(editor_history);
76
77 let editor_zoom_selection = RwSignal::new(None::<edit::zoom::ZoomId>);
79 provide_context(editor_zoom_selection);
80
81 let editor_export = RwSignal::new(crate::editor_ipc::ExportUiState::Idle);
83 provide_context(editor_export);
84 crate::editor_ipc::install_editor_export_listeners(editor_export);
85
86 let editor_saved = RwSignal::new(None::<String>);
88 provide_context(editor_saved);
89 crate::editor_ipc::install_editor_saved_listener(editor_saved);
90
91 let editor_recordings = RwSignal::new(Vec::<crate::editor_ipc::RecordingEntry>::new());
93 provide_context(editor_recordings);
94 crate::editor_ipc::install_recordings_listener(editor_recordings);
95
96 let on_select = Callback::new(move |section: AppSection| {
100 active.set(section);
101 push_surface_query(section);
102 });
103
104 let nav_items = sample_nav_items(false);
105 let workspace = sample_workspace_badge();
106 let user = sample_user_avatar();
107
108 view! {
109 <AppShell
110 rail=ToChildren::to_children(move || view! {
111 <NavigationRail
112 items=nav_items.clone()
113 active=active.get()
114 workspace=workspace.clone()
115 user=user.clone()
116 on_select=on_select
117 />
118 })
119 main=ToChildren::to_children(move || view! {
120 <SurfacePane active=active />
121 })
122 footer=ToChildren::to_children(move || view! {
123 <StatusBar
124 fps=60.0_f32
125 encoder="—"
126 file_bytes=0_u64
127 kind=StatusKind::Ready
128 />
129 })
130 />
131 }
132}
133
134#[component]
140fn SurfacePane(active: RwSignal<AppSection>) -> impl IntoView {
141 view! {
142 <Show
143 when=move || matches!(active.get(), AppSection::Record)
144 fallback=move || view! { <NonRecorderSurfaces active=active /> }
145 >
146 <section class="app-surface app-surface--recorder">
147 <crate::recorder_page::RecorderPage />
148 </section>
149 </Show>
150 }
151}
152
153#[component]
154fn NonRecorderSurfaces(active: RwSignal<AppSection>) -> impl IntoView {
155 view! {
156 <Show
157 when=move || matches!(active.get(), AppSection::Library)
158 fallback=move || view! { <EditorOrLater active=active /> }
159 >
160 <section class="app-surface app-surface--library">
161 <crate::recordings_library::RecordingsLibrary active=active />
162 </section>
163 </Show>
164 }
165}
166
167#[component]
168fn EditorOrLater(active: RwSignal<AppSection>) -> impl IntoView {
169 view! {
170 <Show
171 when=move || matches!(active.get(), AppSection::Editor)
172 fallback=move || view! { <CursorOrPrefs active=active /> }
173 >
174 <crate::editor_surface::EditorSurface />
175 </Show>
176 }
177}
178
179#[component]
180fn CursorOrPrefs(active: RwSignal<AppSection>) -> impl IntoView {
181 view! {
182 <Show
183 when=move || matches!(active.get(), AppSection::Cursor)
184 fallback=move || view! {
185 <section class="app-surface app-surface--prefs">
186 <h1>"Preferences"</h1>
187 <p>"App preferences. Future ticket."</p>
188 </section>
189 }
190 >
191 <section class="app-surface app-surface--cursor">
192 <h1>"Cursor Studio"</h1>
193 <p>"Cursor style picker. Future ticket: AUT-140."</p>
194 </section>
195 </Show>
196 }
197}
198
199fn push_surface_query(section: AppSection) {
203 let Some(window) = web_sys::window() else {
204 return;
205 };
206 let Ok(history) = window.history() else {
207 return;
208 };
209 let slug = crate::surface_to_query(section);
210 let new_url = format!("?surface={slug}");
211 let _ = history.replace_state_with_url(&wasm_bindgen::JsValue::NULL, "", Some(&new_url));
213}
214
215#[cfg(test)]
216mod tests {
217 }