diff --git a/src/gui/src/IPC.js b/src/gui/src/IPC.js index 4a7dce974..fd0fff1aa 100644 --- a/src/gui/src/IPC.js +++ b/src/gui/src/IPC.js @@ -615,9 +615,9 @@ const ipc_listener = async (event, handled) => { } // set window title (headless dashboard windows show it in the - // floating control pill instead of a head) + // control drawer's tray instead of a head) $(el_window).find('.window-head-title').html(html_encode(event.data.new_title)); - $(el_window).find('.dashboard-app-pill-title').text(event.data.new_title); + $(el_window).find('.dashboard-app-drawer-title').text(event.data.new_title); // In dashboard mode the app's title is also the browser-tab title, // and data-name is what a restore re-applies (see showWindow's // push) — keep both fresh. The tab title only changes while this diff --git a/src/gui/src/UI/UIWindow.js b/src/gui/src/UI/UIWindow.js index 6573b7df5..e655e7ebc 100644 --- a/src/gui/src/UI/UIWindow.js +++ b/src/gui/src/UI/UIWindow.js @@ -238,8 +238,8 @@ async function UIWindow (options) { // -------------------------------------------------------- // In dashboard mode a maximized app window renders HEADLESS — the app // covers the full tab, and the titlebar's controls live elsewhere: Back - // minimizes (URL ownership), the floating pill overlay carries - // minimize/close (attach_dashboard_app_pill), and the app's tile shows + // minimizes (URL ownership), the top-edge control drawer carries + // minimize/close (attach_dashboard_app_drawer), and the app's tile shows // a running dot with a Quit item. Everything else keeps its head: // dialogs and the dashboard window itself (update_window_url is false // for both), explorer, and non-maximized windows (apps launching child @@ -689,10 +689,10 @@ async function UIWindow (options) { push_dashboard_app_url(options.app, options.title); } - // Headless dashboard app windows get the floating control pill in + // Headless dashboard app windows get the top-edge control drawer in // place of the head (see the Dashboard app chrome section above). if ( is_dashboard_app_chrome ) { - attach_dashboard_app_pill(el_window, options); + attach_dashboard_app_drawer(el_window, options); } // Let the Apps tab refresh its tiles' running dots (see TabApps). @@ -3904,6 +3904,16 @@ $.fn.showWindow = async function (options) { // show window const el_window = this; + // Stay-on-top windows (every fullpage/dashboard app window) are + // CREATED in the 99999999+ z band; restore must re-raise them + // into that same band. A plain counter z would demote the window + // below whatever gets focused next — and focusWindow deliberately + // never raises stay_on_top windows, so the demotion would stick + // (a dashboard-mode app would sit buried under the dashboard). + const raised_zindex = () => ($(el_window).attr('data-stay_on_top') === 'true' + ? 99999999 + (++window.last_window_zindex) + : ++window.last_window_zindex); + // A window minimized with no taskbar to animate toward (dashboard // mode) was hidden in place by hideWindow — its geometry was // never disturbed. If the app's tile is visible on the Apps tab's @@ -3917,7 +3927,7 @@ $.fn.showWindow = async function (options) { 'data-is_minimized': false, 'data-minimized_in_place': '0', }); - $(el_window).css('z-index', ++window.last_window_zindex); + $(el_window).css('z-index', raised_zindex()); const reduce_motion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; // Skip the morph while another morph still owns the window's @@ -3946,9 +3956,10 @@ $.fn.showWindow = async function (options) { if ( ! morphed && was_hidden ) $(el_window).hide(); } if ( ! morphed ) $(el_window).fadeIn(150); - // Re-introduce the control pill on restore (headless - // dashboard windows only; no-op otherwise). - el_window._dashboard_pill_flash?.(); + // Restores come back with the control drawer collapsed — + // whatever state it was minimized in (headless dashboard + // windows only; no-op otherwise). + el_window._dashboard_drawer_collapse?.(); // A restore re-claims the URL for this app — except when // the restore was DRIVEN by a history traversal (popstate // passes no_history: the entry is already current). @@ -3970,7 +3981,7 @@ $.fn.showWindow = async function (options) { width: `${$(el_window).attr('data-orig-width') }px`, height: `${$(el_window).attr('data-orig-height') }px`, }); - $(el_window).css('z-index', ++window.last_window_zindex); + $(el_window).css('z-index', raised_zindex()); $(el_window).attr({ 'data-is_minimized': false, @@ -4246,85 +4257,116 @@ window.addEventListener('popstate', () => { }); /** - * The floating control pill for headless dashboard app windows: a small - * translucent capsule top-center of the app (parent DOM, above the app's - * iframe) carrying the head's surviving controls — app identity, minimize, - * and close. It opens expanded so first-time and deep-link users see it, - * then collapses to a subtle handle; hovering, tapping, or keyboard-focusing - * it expands it again. Timers rather than hover alone drive the collapse - * because mousemove inside the app's iframe is invisible to the parent — - * the pill itself is the only hover surface there is. + * 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) + * 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 bar fades away, leaving a bare strip + * along the tray's bottom edge as the dismiss hit area (the bar + * reappears as the drawer shuts). 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 pill is a CHILD of the window element, so it shows/hides/scales with + * 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 * from the iframe hide it via the browser's own fullscreen stacking). */ -function attach_dashboard_app_pill (el_window, options) { +function attach_dashboard_app_drawer (el_window, options) { const app_name = options.app; const icon = options.icon || window.icons['app.svg']; const title = options.title || app_name; - const $pill = $(` -
- - ${html_encode(title)} - - +
+
+ + ${html_encode(title)} + + +
+
`); - const pill = $pill.get(0); - const toggle = $pill.find('.dashboard-app-pill-toggle').get(0); + const drawer = $drawer.get(0); + const toggle = $drawer.find('.dashboard-app-drawer-toggle').get(0); let collapse_timer = null; + let opened_at = 0; const expand = () => { clearTimeout(collapse_timer); - pill.classList.remove('collapsed'); + if ( drawer.classList.contains('collapsed') ) opened_at = Date.now(); + drawer.classList.remove('collapsed'); toggle.setAttribute('aria-expanded', 'true'); }; const collapse = () => { clearTimeout(collapse_timer); - pill.classList.add('collapsed'); + drawer.classList.add('collapsed'); toggle.setAttribute('aria-expanded', 'false'); }; const schedule_collapse = (ms) => { clearTimeout(collapse_timer); collapse_timer = setTimeout(collapse, ms); }; - // Expand + auto-collapse: played on open and again on restore, so the - // controls introduce themselves without permanently costing pixels. + // Expand + auto-collapse: played once on open, so the controls + // introduce themselves — and retract INTO the tongue, teaching where + // they live — without permanently costing pixels. const flash = () => { expand(); schedule_collapse(2600); }; - // Hover keeps it open (pointer devices); leaving lets it settle. Touch - // devices never fire mouseleave, so every expansion also self-schedules - // a collapse — the mouseenter clear undoes it for real hovers. - $pill.on('mouseenter', () => expand()); - $pill.on('mouseleave', () => schedule_collapse(1100)); - $pill.on('focusin', () => expand()); - $pill.on('focusout', () => schedule_collapse(1100)); + // Hover pulls the drawer open and leaving lets it settle — mouse only: + // a touch tap synthesizes a pointerenter right before its click, which + // would make the toggle below see an already-open drawer and shut it + // again. Touch devices open by tap instead, and since they never fire + // pointerleave, that path self-schedules its collapse. + $drawer.on('pointerenter', (e) => { + if ( e.pointerType === 'mouse' ) expand(); + }); + $drawer.on('pointerleave', (e) => { + if ( e.pointerType === 'mouse' ) schedule_collapse(900); + }); + $drawer.on('focusin', () => expand()); + $drawer.on('focusout', () => schedule_collapse(1100)); - // A click anywhere on the collapsed pill (incl. its invisible touch - // halo) expands it; the toggle also collapses an expanded pill for an - // explicit dismiss. Buttons are pointer-events:none while collapsed, - // so a collapsed tap can't accidentally minimize or close. - $pill.on('click', function (e) { - if ( pill.classList.contains('collapsed') ) { - e.stopPropagation(); + // Pressing the drawer activates its window, as pressing a titlebar + // would — the drawer took over the head's job. Deferred a tick because + // the document-level activation handler (initgui's mousedown → + // mouseover_window) runs after this one on hover bookkeeping that only + // mousemove refreshes; a tap with no mousemove since a dashboard click + // (touch, restored windows) would re-raise the dashboard OVER the app. + $drawer.on('mousedown', () => { + setTimeout(() => $(el_window).focusWindow(), 0); + }); + + // The grabber is the drawer's one toggle: opens it when shut, shuts + // it when open. A click landing within a beat of the open is the + // SAME gesture that opened it (hover-then-click mice, tap) — hold + // the drawer open instead of instantly re-shutting it. + $(toggle).on('click', function (e) { + e.stopPropagation(); + if ( drawer.classList.contains('collapsed') ) { expand(); schedule_collapse(3500); - } - }); - $(toggle).on('click', function (e) { - if ( ! pill.classList.contains('collapsed') ) { - e.stopPropagation(); + } else if ( Date.now() - opened_at < 500 ) { + schedule_collapse(3500); + } else { collapse(); } }); @@ -4333,22 +4375,27 @@ function attach_dashboard_app_pill (el_window, options) { // used: minimize consumes the app's URL entry when it owns it (the // popstate handler then plays the minimize morph — one path with the // browser's Back button), and close tears the window down normally. - $pill.find('.dashboard-app-pill-minimize').on('click', function (e) { + $drawer.find('.dashboard-app-drawer-minimize').on('click', function (e) { e.stopPropagation(); collapse(); if ( pop_dashboard_app_url(app_name) ) return; $(el_window).hideWindow(); }); - $pill.find('.dashboard-app-pill-close').on('click', function (e) { + $drawer.find('.dashboard-app-drawer-close').on('click', function (e) { e.stopPropagation(); $(el_window).close(); }); - // showWindow re-plays the intro when the window is restored. - el_window._dashboard_pill_flash = flash; + // showWindow forces the drawer shut when the window is restored — the + // intro already ran on open, and a restore should bring back the app, + // not the chrome. (Minimize collapses too, but the Back-button path + // hides the window without touching the drawer.) + el_window._dashboard_drawer_collapse = collapse; - $(el_window).append($pill); - flash(); + $(el_window).append($drawer); + // Two frames so the collapsed state paints first and the intro + // morphs out of the tongue instead of popping in fully open. + requestAnimationFrame(() => requestAnimationFrame(flash)); } /** diff --git a/src/gui/src/css/dashboard.css b/src/gui/src/css/dashboard.css index 29cbedc42..172b44b00 100644 --- a/src/gui/src/css/dashboard.css +++ b/src/gui/src/css/dashboard.css @@ -4213,7 +4213,7 @@ body.dashboard-mode .window-minimize-btn { no titlebar — but dashboard-mode app windows WITH a head still lose 29px to it, so compensate for those. Headless dashboard app windows (.window-dashboard-headless — the maximized in-page apps, whose controls - live in the floating pill) get the full height. */ + live in the top-edge control drawer) get the full height. */ body.dashboard-mode .window:not(.window-dashboard-headless) .window-body-app { height: calc(100% - 29px) !important; } @@ -4223,105 +4223,217 @@ body.dashboard-mode .window-dashboard-headless .window-body-app { } /* ========================================================================== - Dashboard app pill — floating controls for headless in-page app windows. - A child of the window element (so it shows/hides/scales with it), painted - above the app's iframe. Expanded it shows icon + title + minimize + close; - collapsed it shrinks to a subtle icon capsule (see - attach_dashboard_app_pill in UIWindow.js for the expand/collapse logic). - Dark translucent glass on every theme: it overlays arbitrary app content, - not the dashboard's surfaces. + Dashboard app drawer — controls for headless in-page app windows. One + glass surface flush with the top edge that MORPHS between two shapes: + at rest a small tongue (a grabber bar peeking in from the edge); + opened, the same surface swells into a tray carrying icon + title + + minimize + close while the grabber bar dissolves — the open tray is + clean glass, and a bare strip along its bottom edge stays behind as + the dismiss hit area; shutting condenses the bar back into the + settling tongue. Hover (mouse), tap, or keyboard focus opens it (see + attach_dashboard_app_drawer in UIWindow.js). A child of the window element (so it shows/hides/scales + with it), painted above the app's iframe. Dark translucent glass on + every theme: it overlays arbitrary app content, not the dashboard's + surfaces. + + The base rules below describe the OPEN shape; .collapsed overrides to + the tongue. Each state carries its own transition, so opening gets a + soft overshoot (the tongue swells past the tray and settles) while + closing is quicker and lands without bounce. ========================================================================== */ -.dashboard-app-pill { +.dashboard-app-drawer { + /* The morph's two shapes. --open-w is DERIVED from the part sizes — + the controls row is laid out with the same variables, so the open + surface always fits its content exactly and breakpoints only + override parts. --safe-top keeps content below a notch in + standalone/fullscreen display modes while the surface stays flush + with the physical edge. */ + --safe-top: env(safe-area-inset-top, 0px); + --icon: 20px; + --btn: 26px; + --title-w: 150px; + --i2t: 8px; /* icon → title gap */ + --t2b: 6px; /* title → buttons gap */ + --controls-h: 40px; + /* With no visible handle when open, the tray hugs the controls row: + --open-h leaves just enough glass below the buttons to balance the + padding above them (plus ~2px the bottom corner radius eats + optically). The bare dismiss strip fills exactly that leftover — + it must never rise past the buttons' bottom edge at + (controls-h + btn)/2, because the toggle stacks OVER the controls + layer and would steal their clicks. */ + --strip-h: 9px; + --open-w: calc(12px + var(--icon) + var(--i2t) + var(--title-w) + var(--t2b) + var(--btn) + 2px + var(--btn) + 10px); + --open-h: 42px; + --shut-w: 56px; + --shut-h: 16px; + position: absolute; - top: calc(env(safe-area-inset-top, 0px) + 10px); + top: 0; left: 50%; transform: translateX(-50%); z-index: 500; - display: flex; - align-items: center; - gap: 2px; - padding: 4px 6px; - border-radius: 999px; + width: var(--open-w); + height: calc(var(--open-h) + var(--safe-top)); + border-radius: 0 0 16px 16px; background: rgba(24, 24, 28, 0.62); -webkit-backdrop-filter: blur(14px) saturate(140%); backdrop-filter: blur(14px) saturate(140%); border: 1px solid rgba(255, 255, 255, 0.22); + border-top: none; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.28), 0 1px 3px rgba(0, 0, 0, 0.2); color: #fff; -webkit-user-select: none; user-select: none; - transition: opacity 0.18s ease, padding 0.18s ease; + transition: + width 0.45s cubic-bezier(0.34, 1.2, 0.4, 1), + height 0.45s cubic-bezier(0.34, 1.2, 0.4, 1), + border-radius 0.45s ease, + opacity 0.2s ease; } -/* Invisible halo: keeps the collapsed capsule comfortably hittable and - hoverable without costing more visual space. (Clicks it absorbs would - have hit the app within ~10px of the pill — acceptable dead zone.) - z-index -1 keeps it BELOW the pill's own content: absolutely-positioned - pseudo-elements otherwise paint above static siblings, and the halo - would swallow every real click on the buttons (element.click() works, - hit-testing doesn't). The pill is its own stacking context, so even at - -1 the halo still sits above the app's iframe for the ring outside - the capsule. */ -.dashboard-app-pill::after { - content: ''; - position: absolute; - inset: -10px; - border-radius: inherit; - z-index: -1; -} - -/* Belt and braces for the same trap: the interactive children stack - explicitly above the halo. */ -.dashboard-app-pill-toggle, -.dashboard-app-pill-btn { - position: relative; - z-index: 1; -} - -.dashboard-app-pill.collapsed { +.dashboard-app-drawer.collapsed { + width: var(--shut-w); + height: calc(var(--shut-h) + var(--safe-top)); + border-radius: 0 0 12px 12px; /* Subtle but findable over both light and dark app content — the white hairline border carries the dark-on-dark case. */ - opacity: 0.65; - padding: 3px 12px; - gap: 0; + opacity: 0.7; + transition: + width 0.3s cubic-bezier(0.5, 0.05, 0.25, 1), + height 0.3s cubic-bezier(0.5, 0.05, 0.25, 1), + border-radius 0.3s ease, + opacity 0.2s ease 0.08s; } -.dashboard-app-pill.collapsed:hover { - opacity: 0.9; +/* Clipping window for the controls. The surface itself can't be + overflow:hidden — that would clip the toggle's invisible touch halo — + so an inner layer inherits the rounded shape and does the clipping: + mid-morph, the growing surface reveals the controls from the center + outward instead of letting them spill past the glass. */ +.dashboard-app-drawer-clip { + position: absolute; + inset: 0; + overflow: hidden; + border-radius: inherit; + pointer-events: none; } -.dashboard-app-pill-toggle { +/* The controls row is a FIXED-size layer centered on the surface: it + never reflows during the morph, the glass just grows around it. */ +.dashboard-app-drawer-controls { + position: absolute; + top: var(--safe-top); + left: 50%; + transform: translateX(-50%); + box-sizing: border-box; + width: var(--open-w); + height: var(--controls-h); + display: flex; + align-items: center; + padding: 0 10px 0 12px; + transition: + opacity 0.25s ease 0.1s, + filter 0.25s ease 0.1s, + transform 0.45s cubic-bezier(0.34, 1.2, 0.4, 1); +} + +/* Shut, the controls condense out of focus — fade + blur + slight + shrink — and visibility (flipped AFTER the morph, hence the delay; + removing the class restores it instantly) drops the buttons out of + the Tab order and the accessibility tree. */ +.dashboard-app-drawer.collapsed .dashboard-app-drawer-controls { + opacity: 0; + filter: blur(5px); + transform: translateX(-50%) scale(0.82); + visibility: hidden; + transition: + opacity 0.12s ease, + filter 0.12s ease, + transform 0.3s cubic-bezier(0.5, 0.05, 0.25, 1), + visibility 0s linear 0.3s; +} + +/* The grabber toggle: fills the tongue when shut; open, it settles into + a slim strip along the tray's bottom edge — same dismiss hit area, + but the bar inside it hides (below), so the open tray shows no + handle. Absolutely positioned, so its DOM position (first, for Tab + order) doesn't affect the visuals. */ +.dashboard-app-drawer-toggle { + position: absolute; + inset: calc(var(--safe-top) + var(--open-h) - var(--strip-h)) 0 0 0; + z-index: 1; display: flex; align-items: center; justify-content: center; - flex: none; background: none; border: none; - padding: 2px; margin: 0; + padding: 0; cursor: pointer; - border-radius: 50%; + transition: inset 0.45s cubic-bezier(0.34, 1.2, 0.4, 1); } -.dashboard-app-pill-icon { - /* 20px balances the 26px buttons and the 36px capsule (16px read as - undersized); radius stays 25% of the icon for the app-icon look. - Square icons only — cover guards against a non-square one. */ +.dashboard-app-drawer.collapsed .dashboard-app-drawer-toggle { + inset: var(--safe-top) 0 0 0; + transition: inset 0.3s cubic-bezier(0.5, 0.05, 0.25, 1); +} + +/* Invisible halo: keeps the tongue comfortably hittable and hoverable + without costing more visual space. A pseudo-element OF the button, so + hits on it are hits on the button — no stacking games needed. (Clicks + it absorbs would have hit the app within ~14px of the tongue — + acceptable dead zone.) */ +.dashboard-app-drawer-toggle::after { + content: ''; + position: absolute; + inset: 0 -14px -14px; +} + +/* Open, the bar is GONE (base = open shape): it dissolves in the first + beat of the swell — stretching a touch as it goes, absorbed into the + widening glass — before the controls bloom at 0.1s, one handing off + to the other. On collapse it condenses back in: the delay below skips + the controls' 0.12s fade-out so the bar never materializes mid-tray, + then it fades up while riding the shrinking toggle home, landing with + the tongue (0.14s + 0.18s ≈ the 0.3s shut morph). */ +.dashboard-app-drawer-grabber { + width: 24px; + height: 3px; + border-radius: 2px; + background: rgba(255, 255, 255, 0.6); + opacity: 0; + transition: width 0.3s ease, background-color 0.15s ease, opacity 0.12s ease; +} + +.dashboard-app-drawer.collapsed .dashboard-app-drawer-grabber { width: 20px; - height: 20px; + background: rgba(255, 255, 255, 0.78); + opacity: 1; + transition: width 0.3s ease, background-color 0.15s ease, opacity 0.18s ease 0.14s; +} + +@media (hover: hover) { + .dashboard-app-drawer-toggle:hover .dashboard-app-drawer-grabber { + background: rgba(255, 255, 255, 0.95); + } +} + +.dashboard-app-drawer-icon { + /* Sized to balance the buttons and the tray; radius stays 25% of the + icon for the app-icon look. Square icons only — cover guards + against a non-square one. */ + width: var(--icon); + height: var(--icon); border-radius: 5px; object-fit: cover; display: block; - transition: width 0.18s ease, height 0.18s ease; + flex: none; + margin-right: var(--i2t); } -.dashboard-app-pill.collapsed .dashboard-app-pill-icon { - width: 12px; - height: 12px; -} - -.dashboard-app-pill-title { +.dashboard-app-drawer-title { font-size: 12px; font-weight: 500; /* Roomy enough for descenders: overflow:hidden (needed for the @@ -4332,26 +4444,25 @@ body.dashboard-mode .window-dashboard-headless .window-body-app { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - /* FIXED title box (not a cap): every app's pill is the same width, so + /* FIXED title box (not a cap): every app's tray is the same width, so the minimize/close buttons sit at the same screen position in every - app — they're operated by muscle memory. Short titles center in - the box like a classic titlebar. The collapse animation still - drives max-width to 0. */ + app — they're operated by muscle memory. Left-aligned against the + icon: the tray is asymmetric (one icon left, two buttons right), so + a centered title lands off the tray's true center and reads as + misaligned against the centered tongue below. Icon + title cluster + as the identity on the left, controls cluster on the right. */ flex: none; - width: 150px; - max-width: 150px; - text-align: center; - margin: 0 6px 0 4px; - transition: max-width 0.22s ease, opacity 0.15s ease, margin 0.22s ease; + width: var(--title-w); + text-align: left; + margin-right: var(--t2b); } -.dashboard-app-pill-btn { +.dashboard-app-drawer-btn { display: flex; align-items: center; justify-content: center; - width: 26px; - height: 26px; - max-width: 26px; + width: var(--btn); + height: var(--btn); flex: none; background: none; border: none; @@ -4360,73 +4471,81 @@ body.dashboard-mode .window-dashboard-headless .window-body-app { margin: 0; color: rgba(255, 255, 255, 0.9); cursor: pointer; - overflow: hidden; - transition: max-width 0.22s ease, opacity 0.15s ease, background-color 0.12s ease; + /* The clip layer is pointer-events:none so stray clicks fall through + to the surface; the buttons opt back in. */ + pointer-events: auto; + transition: background-color 0.12s ease; } -.dashboard-app-pill-btn svg { +.dashboard-app-drawer-minimize { + margin-right: 2px; +} + +.dashboard-app-drawer-btn svg { width: 14px; height: 14px; flex: none; } @media (hover: hover) { - .dashboard-app-pill-btn:hover { + .dashboard-app-drawer-btn:hover { background-color: rgba(255, 255, 255, 0.16); } } -.dashboard-app-pill-btn:active { +.dashboard-app-drawer-btn:active { background-color: rgba(255, 255, 255, 0.26); } -.dashboard-app-pill.collapsed .dashboard-app-pill-title, -.dashboard-app-pill.collapsed .dashboard-app-pill-btn { - max-width: 0; - opacity: 0; - margin: 0; - pointer-events: none; -} - -.dashboard-app-pill-toggle:focus-visible, -.dashboard-app-pill-btn:focus-visible { +.dashboard-app-drawer-toggle:focus-visible, +.dashboard-app-drawer-btn:focus-visible { outline: 2px solid rgba(255, 255, 255, 0.9); outline-offset: 1px; } -/* Touch: bigger targets (≥32px buttons + halo ≈ 44px effective), and the - collapsed capsule keeps a generous tap zone via the halo. */ +/* Touch: a taller tray with 36px buttons, and a bigger tongue whose halo + brings the effective target to ~44px. */ @media (pointer: coarse) { - .dashboard-app-pill { - padding: 6px 8px; + .dashboard-app-drawer { + --btn: 36px; + --controls-h: 48px; + --strip-h: 8px; + --open-h: 50px; + --shut-w: 68px; + --shut-h: 20px; } - .dashboard-app-pill.collapsed { - padding: 5px 14px; + .dashboard-app-drawer-grabber { + width: 28px; + height: 4px; } - .dashboard-app-pill-btn { - width: 32px; - height: 32px; - max-width: 32px; + .dashboard-app-drawer.collapsed .dashboard-app-drawer-grabber { + width: 24px; } - .dashboard-app-pill::after { - inset: -12px; + .dashboard-app-drawer-toggle::after { + inset: 0 -16px -18px; } } /* Tight screens: identity shrinks to the icon; the controls keep their - size — they're the part that must stay usable. */ + size — they're the part that must stay usable. Zeroing the title + variables shrinks --open-w to match. */ @media (max-width: 500px) { - .dashboard-app-pill-title { + .dashboard-app-drawer { + --title-w: 0px; + --t2b: 0px; + } + + .dashboard-app-drawer-title { display: none; } } @media (prefers-reduced-motion: reduce) { - .dashboard-app-pill, - .dashboard-app-pill * { + .dashboard-app-drawer, + .dashboard-app-drawer * { transition: none !important; } }