mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-28 08:56:58 +00:00
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:
+2
-2
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user