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)}
-
-
-
-
-
+ // 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 = $(`
+
+
+
+
+
+
+
${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;
}
}