Dashboard: redesign app control pill as a top-edge drawer

The floating control capsule over headless dashboard apps consumed
noticeable space, especially on mobile. Replace it with a drawer: a
slim glass tray flush with the top edge (icon + title + minimize +
close) whose tongue-shaped handle hangs from its bottom center. At
rest only the tongue peeks in from the edge; hovering it (mouse),
tapping it, or keyboard-focusing the drawer slides the tray down.
The collapse is a single GPU transform instead of the pill's
max-width squeezes, and the open-then-retract intro now visibly
retracts into the tongue, teaching where the controls live.

Hover-expansion is gated on pointerType === 'mouse': touch taps
synthesize a pointerenter right before click, which made the old
pill's toggle see an already-open state and shut it again.

Also fixes two window-stacking bugs surfaced while testing:

- showWindow demoted stay_on_top windows (every fullpage/dashboard
  app window, created in the 99999999+ z band) to a plain counter
  z-index on restore. focusWindow deliberately never raises
  stay_on_top windows, so after minimize/restore a single dashboard
  focus buried the app permanently. Restore now re-raises into the
  stay-on-top band.

- Pressing the drawer now focuses its window, as the titlebar it
  replaces did. The document-level activation handler works off
  mouseover_window, which only mousemove refreshes — a tap with no
  intervening mousemove (touch, restored windows) would re-raise
  the dashboard over the app.
This commit is contained in:
jelveh
2026-07-22 21:00:40 -07:00
parent 928d5fec16
commit 33963ef8bd
3 changed files with 242 additions and 167 deletions
+2 -2
View File
@@ -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
+97 -60
View File
@@ -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,9 @@ $.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
// Re-introduce the control drawer on restore (headless
// dashboard windows only; no-op otherwise).
el_window._dashboard_pill_flash?.();
el_window._dashboard_drawer_flash?.();
// 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 +3980,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 +4256,110 @@ 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: a slim glass tray
* 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.
*
* 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 = $(`
<div class="dashboard-app-pill">
<button type="button" class="dashboard-app-pill-toggle" aria-expanded="true" title="App controls" aria-label="App controls">
<img class="dashboard-app-pill-icon" src="${html_encode(icon)}" alt="" draggable="false">
</button>
<span class="dashboard-app-pill-title">${html_encode(title)}</span>
<button type="button" class="dashboard-app-pill-btn dashboard-app-pill-minimize" title="Minimize to Dashboard" aria-label="Minimize to Dashboard">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><line x1="5" y1="12" x2="19" y2="12"/></svg>
</button>
<button type="button" class="dashboard-app-pill-btn dashboard-app-pill-close" title="Close App" aria-label="Close App">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/></svg>
// 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.
const $drawer = $(`
<div class="dashboard-app-drawer collapsed">
<button type="button" class="dashboard-app-drawer-handle" aria-expanded="false" title="App controls" aria-label="App controls">
<span class="dashboard-app-drawer-grabber" aria-hidden="true"></span>
</button>
<div class="dashboard-app-drawer-tray">
<img class="dashboard-app-drawer-icon" src="${html_encode(icon)}" alt="" draggable="false">
<span class="dashboard-app-drawer-title">${html_encode(title)}</span>
<button type="button" class="dashboard-app-drawer-btn dashboard-app-drawer-minimize" title="Minimize to Dashboard" aria-label="Minimize to Dashboard">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><line x1="5" y1="12" x2="19" y2="12"/></svg>
</button>
<button type="button" class="dashboard-app-drawer-btn dashboard-app-drawer-close" title="Close App" aria-label="Close App">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/></svg>
</button>
</div>
</div>
`);
const pill = $pill.get(0);
const toggle = $pill.find('.dashboard-app-pill-toggle').get(0);
const drawer = $drawer.get(0);
const handle = $drawer.find('.dashboard-app-drawer-handle').get(0);
let collapse_timer = null;
let opened_at = 0;
const expand = () => {
clearTimeout(collapse_timer);
pill.classList.remove('collapsed');
toggle.setAttribute('aria-expanded', 'true');
if ( drawer.classList.contains('collapsed') ) opened_at = Date.now();
drawer.classList.remove('collapsed');
handle.setAttribute('aria-expanded', 'true');
};
const collapse = () => {
clearTimeout(collapse_timer);
pill.classList.add('collapsed');
toggle.setAttribute('aria-expanded', 'false');
drawer.classList.add('collapsed');
handle.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.
// 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 handle's 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 tongue is the drawer's one toggle: pulls it open when shut,
// pushes it shut 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.
$(handle).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 +4368,24 @@ 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;
el_window._dashboard_drawer_flash = flash;
$(el_window).append($pill);
flash();
$(el_window).append($drawer);
// Two frames so the collapsed state paints first and the intro slides
// in from the top edge instead of popping in fully open.
requestAnimationFrame(() => requestAnimationFrame(flash));
}
/**
+143 -105
View File
@@ -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,143 @@ 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. A slim
glass tray flush with the top edge (icon + title + minimize + close) with
a tongue-shaped handle hanging from its bottom center, like a drawer
pull. Collapsed, the tray slides up off-screen and only the tongue peeks
in from the top edge; hover (mouse), tap, or keyboard focus pulls it
back down (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.
========================================================================== */
.dashboard-app-pill {
.dashboard-app-drawer {
/* --tray-h is both the tray's height and the collapse travel the
retracted transform below must hide exactly the tray. --safe-top
keeps the tray's contents below a notch in standalone/fullscreen
display modes while the drawer stays flush with the physical edge. */
--tray-h: 40px;
--safe-top: env(safe-area-inset-top, 0px);
position: absolute;
top: calc(env(safe-area-inset-top, 0px) + 10px);
top: 0;
left: 50%;
transform: translateX(-50%);
z-index: 500;
display: flex;
/* Handle first in the DOM (Tab reaches the toggle before the tray's
buttons) but drawn below the tray. */
flex-direction: column-reverse;
align-items: center;
transform: translateX(-50%);
transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
color: #fff;
-webkit-user-select: none;
user-select: none;
/* The wrapper is layout-only it must never eat clicks meant for the
app. The tray and handle opt back in themselves. */
pointer-events: none;
}
.dashboard-app-drawer.collapsed {
transform: translateX(-50%)
translateY(calc(-1 * (var(--tray-h) + var(--safe-top))));
}
.dashboard-app-drawer-tray {
display: flex;
align-items: center;
gap: 2px;
padding: 4px 6px;
border-radius: 999px;
box-sizing: border-box;
height: calc(var(--tray-h) + var(--safe-top));
padding: var(--safe-top) 8px 0 10px;
border-radius: 0 0 14px 14px;
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;
pointer-events: auto;
}
/* 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;
/* Retracted, the tray sits above the viewport visibility (flipped AFTER
the slide finishes, hence the transition-delay; removing the class
restores it instantly) additionally drops its buttons out of the Tab
order and the accessibility tree. */
.dashboard-app-drawer.collapsed .dashboard-app-drawer-tray {
visibility: hidden;
transition: visibility 0s linear 0.3s;
pointer-events: none;
}
/* Belt and braces for the same trap: the interactive children stack
explicitly above the halo. */
.dashboard-app-pill-toggle,
.dashboard-app-pill-btn {
/* The tongue. No top border and a -1px overlap fuse it with the tray's
bottom edge into one drawer-pull silhouette; collapsed, the same missing
top border makes it read as emerging from the screen edge itself. */
.dashboard-app-drawer-handle {
position: relative;
z-index: 1;
}
.dashboard-app-pill.collapsed {
/* 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;
}
.dashboard-app-pill.collapsed:hover {
opacity: 0.9;
}
.dashboard-app-pill-toggle {
display: flex;
align-items: center;
justify-content: center;
flex: none;
background: none;
border: none;
padding: 2px;
margin: 0;
width: 56px;
height: 17px;
margin: -1px 0 0;
padding: 0;
border: 1px solid rgba(255, 255, 255, 0.22);
border-top: none;
border-radius: 0 0 12px 12px;
background: rgba(24, 24, 28, 0.62);
-webkit-backdrop-filter: blur(14px) saturate(140%);
backdrop-filter: blur(14px) saturate(140%);
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.22);
cursor: pointer;
border-radius: 50%;
pointer-events: auto;
transition: opacity 0.18s ease;
}
.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. */
/* 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-handle::after {
content: '';
position: absolute;
inset: 0 -14px -14px;
}
/* Subtle but findable over both light and dark app content the white
hairline border carries the dark-on-dark case. */
.dashboard-app-drawer.collapsed .dashboard-app-drawer-handle {
opacity: 0.7;
}
@media (hover: hover) {
.dashboard-app-drawer.collapsed .dashboard-app-drawer-handle:hover {
opacity: 1;
}
}
.dashboard-app-drawer-grabber {
width: 20px;
height: 3px;
border-radius: 2px;
background: rgba(255, 255, 255, 0.78);
}
.dashboard-app-drawer-icon {
/* 20px balances the 26px buttons and the 40px tray; radius stays 25%
of the icon for the app-icon look. Square icons only cover guards
against a non-square one. */
width: 20px;
height: 20px;
border-radius: 5px;
object-fit: cover;
display: block;
transition: width 0.18s ease, height 0.18s ease;
flex: none;
}
.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 +4370,22 @@ 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. */
the box like a classic titlebar. */
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;
}
.dashboard-app-pill-btn {
.dashboard-app-drawer-btn {
display: flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
max-width: 26px;
flex: none;
background: none;
border: none;
@@ -4360,73 +4394,77 @@ 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;
transition: background-color 0.12s ease;
}
.dashboard-app-pill-btn svg {
.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-handle: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 {
--tray-h: 48px;
}
.dashboard-app-pill.collapsed {
padding: 5px 14px;
.dashboard-app-drawer-tray {
padding: var(--safe-top) 10px 0 12px;
}
.dashboard-app-pill-btn {
width: 32px;
height: 32px;
max-width: 32px;
.dashboard-app-drawer-btn {
width: 36px;
height: 36px;
}
.dashboard-app-pill::after {
inset: -12px;
.dashboard-app-drawer-handle {
width: 68px;
height: 21px;
}
.dashboard-app-drawer-grabber {
width: 26px;
height: 4px;
}
.dashboard-app-drawer-handle::after {
inset: 0 -16px -20px;
}
}
/* Tight screens: identity shrinks to the icon; the controls keep their
size they're the part that must stay usable. */
@media (max-width: 500px) {
.dashboard-app-pill-title {
.dashboard-app-drawer-title {
display: none;
}
.dashboard-app-drawer-icon {
margin-right: 6px;
}
}
@media (prefers-reduced-motion: reduce) {
.dashboard-app-pill,
.dashboard-app-pill * {
.dashboard-app-drawer,
.dashboard-app-drawer * {
transition: none !important;
}
}