From 25b0e377e882bb4a6dfac9975078ea434ce0279b Mon Sep 17 00:00:00 2001 From: jelveh Date: Wed, 22 Jul 2026 21:23:06 -0700 Subject: [PATCH] Dashboard drawer: morph the tongue into the tray instead of sliding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The drawer is now ONE glass surface that changes shape, rather than a tray that slides in from off-screen with a handle below it. At rest it's the tongue; opened, the same surface swells into the tray while the grabber bar rides the morph and settles into a slim strip along the tray's bottom edge as the dismiss handle — same handle, both states. Content is revealed from the center outward through an inner clipping layer (the surface itself can't be overflow:hidden without clipping the toggle's touch halo) and condenses in/out of focus with an opacity + blur + scale crossfade. Opening gets a soft overshoot and settles; closing is quicker and lands without bounce. The open width is DERIVED in CSS from the same part variables that lay out the controls row, so the surface always fits its content exactly and breakpoints (touch sizes, hidden title under 500px) only override parts. --- src/gui/src/UI/UIWindow.js | 75 ++++----- src/gui/src/css/dashboard.css | 287 ++++++++++++++++++++-------------- 2 files changed, 213 insertions(+), 149 deletions(-) diff --git a/src/gui/src/UI/UIWindow.js b/src/gui/src/UI/UIWindow.js index 6b03763be..50b9aae5e 100644 --- a/src/gui/src/UI/UIWindow.js +++ b/src/gui/src/UI/UIWindow.js @@ -4257,16 +4257,19 @@ window.addEventListener('popstate', () => { }); /** - * The control drawer for headless dashboard app windows: a slim glass tray + * The control drawer for headless dashboard app windows: one glass surface * flush with the top edge of the app (parent DOM, above the app's iframe) - * carrying the head's surviving controls — app identity, minimize, and - * close — with a tongue-shaped handle hanging from its bottom center like a - * drawer pull. It opens expanded so first-time and deep-link users see the - * controls, then the tray retracts off the top edge leaving only the tongue - * peeking in; hovering the tongue (mouse), tapping it, or keyboard-focusing - * the drawer pulls it back down. Timers rather than hover alone drive the - * collapse because mousemove inside the app's iframe is invisible to the - * parent — the drawer itself is the only hover surface there is. + * that MORPHS between two shapes. At rest it's a small tongue — a grabber + * bar peeking in from the edge; opened, the same surface swells into a + * tray carrying the head's surviving controls — app identity, minimize, + * and close — while the grabber settles into a slim strip along the + * tray's bottom edge as the dismiss handle. It opens expanded so + * first-time and deep-link users see the controls, then shrinks back into + * the tongue; hovering the tongue (mouse), tapping it, or keyboard- + * focusing the drawer opens it again. Timers rather than hover alone + * drive the collapse because mousemove inside the app's iframe is + * invisible to the parent — the drawer itself is the only hover surface + * there is. * * The drawer is a CHILD of the window element, so it shows/hides/scales with * the window for free (minimize morphs, display:none, fullscreen requests @@ -4277,28 +4280,30 @@ function attach_dashboard_app_drawer (el_window, options) { const icon = options.icon || window.icons['app.svg']; const title = options.title || app_name; - // The handle comes FIRST in the DOM so Tab reaches the toggle before - // the tray's buttons; flex column-reverse (see dashboard.css) puts it - // back below the tray visually. + // The toggle comes FIRST in the DOM so Tab reaches it before the + // controls' buttons; both layers are absolutely positioned (see + // dashboard.css), so DOM order doesn't affect the visuals. const $drawer = $(`