diff --git a/visor/src/style.rs b/visor/src/style.rs index 820aeffc..76ae771d 100644 --- a/visor/src/style.rs +++ b/visor/src/style.rs @@ -16,7 +16,7 @@ pub(crate) const CSS: &str = r#" display: flex; align-items: center; gap: 12px; padding: 0 12px; background: #18181b; - border-top: 1px solid #3f3f46; + border-bottom: 1px solid #3f3f46; } /* Content-sized, capped, and scrolling past the cap: growth is bounded so @@ -26,7 +26,7 @@ pub(crate) const CSS: &str = r#" max-height: 60vh; overflow-y: auto; padding: 12px; background: #27272a; - border-top: 1px solid #3f3f46; + border-bottom: 1px solid #3f3f46; } #visor-identity { display: flex; align-items: center; gap: 8px; } diff --git a/visor/src/ui.rs b/visor/src/ui.rs index 5f197c41..8754cfa2 100644 --- a/visor/src/ui.rs +++ b/visor/src/ui.rs @@ -469,40 +469,10 @@ pub(crate) fn Visor() -> Element { // visor ships its own stylesheet as part of its own tree. style { "{CSS}" } - div { id: "visor-strip", class: "{strip_class}", - Identity { ident } - div { id: "visor-context", - match (&live, &*notice.read()) { - (Some((_, app)), _) => rsx! { - span { class: "{Voice::Framework.class()}", "showing " } - AppVoice { text: app.title.clone() } - }, - (None, Some(Notice::Ended { app, reason })) => rsx! { - AppVoice { text: app.clone() } - span { class: "{Voice::Framework.class()}", " ended: {reason}" } - }, - (None, Some(Notice::Plain(message))) => rsx! { - span { class: "{Voice::Framework.class()}", "{message}" } - }, - (None, None) => rsx! { - span { class: "{Voice::Framework.class()}", "nothing is running" } - }, - } - } - div { id: "visor-actions", - // Both tenants need a kernel that answers, so neither is - // offered before the seal opens; the ceremony the boot - // raised is what the user has to act on instead. - TenantButton { label: "Apps", tenant: Tenant::Apps, open: tenant == Some(Tenant::Apps), disabled: !claimed, - onpress: toggle_apps } - TenantButton { label: "Settings", tenant: Tenant::Settings, open: tenant == Some(Tenant::Settings), disabled: !claimed, - onpress: show_settings } - if let Some((id, _)) = live { - button { onclick: move |_| async move { close_session(id).await }, "Close" } - } - } - } - + // Drawer above strip: the strip is the line between trusted pixels + // and the app zone, so whatever the visor opens goes on its own side + // of that line and pushes the strip down rather than sitting between + // it and the app. if let Some(tenant) = tenant { div { id: "visor-drawer", match tenant { @@ -635,6 +605,40 @@ pub(crate) fn Visor() -> Element { } } } + + div { id: "visor-strip", class: "{strip_class}", + Identity { ident } + div { id: "visor-context", + match (&live, &*notice.read()) { + (Some((_, app)), _) => rsx! { + span { class: "{Voice::Framework.class()}", "showing " } + AppVoice { text: app.title.clone() } + }, + (None, Some(Notice::Ended { app, reason })) => rsx! { + AppVoice { text: app.clone() } + span { class: "{Voice::Framework.class()}", " ended: {reason}" } + }, + (None, Some(Notice::Plain(message))) => rsx! { + span { class: "{Voice::Framework.class()}", "{message}" } + }, + (None, None) => rsx! { + span { class: "{Voice::Framework.class()}", "nothing is running" } + }, + } + } + div { id: "visor-actions", + // Both tenants need a kernel that answers, so neither is + // offered before the seal opens; the ceremony the boot + // raised is what the user has to act on instead. + TenantButton { label: "Apps", tenant: Tenant::Apps, open: tenant == Some(Tenant::Apps), disabled: !claimed, + onpress: toggle_apps } + TenantButton { label: "Settings", tenant: Tenant::Settings, open: tenant == Some(Tenant::Settings), disabled: !claimed, + onpress: show_settings } + if let Some((id, _)) = live { + button { onclick: move |_| async move { close_session(id).await }, "Close" } + } + } + } } } diff --git a/web/index.html b/web/index.html index d1efff98..2bed5eb7 100644 --- a/web/index.html +++ b/web/index.html @@ -21,7 +21,15 @@ flex-direction: column; font: 16px/1.4 system-ui, sans-serif; } - /* The app zone fills everything above the visor. Frames are the only + /* The visor sits at the top, where browsers put their own trusted UI, + and out of thumb range on a handheld — app interactions are the + common case. Its geometry must not move when an app mounts + (docs/design.md "M1": "strip geometry immobile with the app + mounted"). */ + #visor { + flex: 0 0 auto; + } + /* The app zone fills everything below the visor. Frames are the only thing that ever goes in it. */ #app-zone { flex: 1 1 auto; @@ -34,17 +42,11 @@ height: 100%; display: block; } - /* The visor is fixed to the bottom: its geometry must not move when an - app mounts (docs/design.md "M1": "strip geometry immobile with the - app mounted"). */ - #visor { - flex: 0 0 auto; - } -
+