From 13c246bea963254dcdd77ff59d40915e94eeb8e9 Mon Sep 17 00:00:00 2001 From: Lann Martin Date: Mon, 7 Sep 2026 12:00:24 -0400 Subject: [PATCH] visor: move to the top of the page, drawer above the strip Users know trusted UI at the top from browsers, and visor interactions are rarer than app interactions, so the strip leaves thumb range on a handheld. The drawer sits above the strip and pushes it down when open: the strip is the line between trusted pixels and the app zone, and nothing the visor opens goes on the app's side of it. --- visor/src/style.rs | 4 +-- visor/src/ui.rs | 72 ++++++++++++++++++++++++---------------------- web/index.html | 18 ++++++------ 3 files changed, 50 insertions(+), 44 deletions(-) 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; - } -
+