diff --git a/static/css/components/itemTooltip.css b/static/css/components/itemTooltip.css index eaf3b5d8..1248d773 100644 --- a/static/css/components/itemTooltip.css +++ b/static/css/components/itemTooltip.css @@ -1,29 +1,19 @@ /* ── Item tooltip — "technical sheet" ─────────────────────────────────────── */ /* */ -/* Overlays the search bar area in the top-bar while hovering a file item. */ -/* Spans from the sidebar right-edge to just before .user-controls. */ +/* Absolutely overlays `.search-slot` in the top-bar. The search bar stays in */ +/* place underneath; only the tooltip's opacity is animated, so there is no */ +/* second element to keep in sync and no display:none flicker between rapid */ +/* hover transitions. RTL just works (inset:0). */ /* */ -/* 3-column CSS grid: [icon] [label] [value] */ -/* All values start at the same x position regardless of label width. */ +/* 3-column CSS grid: [icon] [label] [value] */ +/* All values start at the same x position regardless of label width. */ .path-tooltip { - position: fixed; - - /* Fill most of the top-bar (topbar height 70px, 8px margin top/bottom) */ - top: 8px; - - /* Desktop: starts at sidebar right edge + top-bar padding */ - left: calc(var(--sidebar-width) + 30px); - - /* - * Right edge stops just before .user-controls: - * 30px (top-bar right padding) + ~90px (notif bell + gap + avatar btn) + 4px gap - * Using left + right instead of width so the browser computes it dynamically. - */ - right: 124px; - - /* High enough to sit above the top-bar content */ - z-index: 200; + /* Cover the search slot, sit above it (later in DOM order). */ + position: absolute; + inset: 0; + /* Opaque background so the search bar underneath is fully masked. */ + background-color: var(--color-bg-surface); display: grid; grid-template-columns: 1em max-content 1fr; @@ -31,20 +21,24 @@ row-gap: 6px; align-items: center; - /* Fill the top-bar height minus an 8px margin on each side */ - height: 54px; - padding: 0 16px; - border-radius: 10px; - border: 2px solid var(--color-border-medium); - - background-color: var(--color-bg-input); color: var(--color-text-muted); font-size: 0.75rem; pointer-events: none; - /* avoid blinking when pointer moves between items */ - transition: display 0.2s allow-discrete; + /* Show instantly. The hide direction (.hidden) overrides this with a + 1s delay so the tooltip lingers after the mouse leaves an item; + rapid item-to-item moves cancel the pending fade before it fires. */ + opacity: 1; + transition: opacity 0.2s ease 0s; +} + +/* Hidden state — override the global `.hidden { display:none !important }` + so we can fade with opacity instead of snapping to display:none. */ +.path-tooltip.hidden { + display: grid !important; + opacity: 0; + transition-delay: 1s; } /* Icon column */ @@ -85,23 +79,6 @@ font-style: italic; } -/* ── Narrow screens (sidebar hidden) ────────────────────────────────────── */ -/* On mobile the sidebar slides off-screen; align after the #sidebar-toggle */ -/* button (≈44px) that takes its place in the top-bar. */ - -@media (max-width: 768px) { - .path-tooltip { - /* 16px topbar padding + ~44px sidebar-toggle + 8px gap */ - left: 68px; - - /* - * Right edge stops before search-toggle-btn + user-controls: - * ~40px (search-toggle) + 12px (gap) + ~90px (user-controls) + 16px (padding) - */ - right: 158px; - } -} - /* ── Vignette inside tooltip ─────────────────────────────────────────────── */ /* The vignette name hard-codes 14px; scale it down to match the tooltip. */ diff --git a/static/css/layout/topbar.css b/static/css/layout/topbar.css index d0a392ea..d0f27eda 100644 --- a/static/css/layout/topbar.css +++ b/static/css/layout/topbar.css @@ -86,11 +86,14 @@ color: var(--color-text-heading); } -.search-container { +/* Slot wrapping .search-container + #path-tooltip — it owns the flex slot + so the tooltip can absolutely overlay the search bar via inset:0 instead + of viewport-fixed positioning. */ +.search-slot { flex-grow: 1; max-width: 600px; - position: relative; margin-right: 20px; + position: relative; display: flex; align-items: center; @@ -100,6 +103,13 @@ } } +.search-container { + flex-grow: 1; + position: relative; + display: flex; + align-items: center; +} + .search-container input { width: 100%; padding: 12px 50px 12px 44px; @@ -204,8 +214,8 @@ justify-content: center; } - /* Hide full search bar on mobile by default */ - .search-container { + /* Hide full search slot on mobile by default */ + .search-slot { display: none; } @@ -224,7 +234,7 @@ justify-content: center; } - .top-bar--search-active .search-container { + .top-bar--search-active .search-slot { display: flex; flex-grow: 1; max-width: none; diff --git a/static/index.html b/static/index.html index e6434fcd..08a3a772 100644 --- a/static/index.html +++ b/static/index.html @@ -136,12 +136,17 @@ -
- - - + +
+
+ + + +
+
diff --git a/static/js/features/itemTooltip.js b/static/js/features/itemTooltip.js index 3848b55a..78318fa1 100644 --- a/static/js/features/itemTooltip.js +++ b/static/js/features/itemTooltip.js @@ -27,21 +27,19 @@ import { i18n } from '../core/i18n.js'; import { systemUsers } from '../model/systemUsers.js'; // ── Tooltip DOM ─────────────────────────────────────────────────────────────── +// +// The tooltip element is a static node inside `.search-slot` (see index.html) +// and is absolutely positioned over `.search-container`. Show/hide is a +// simple `.hidden` toggle on the tooltip itself — the search bar underneath +// is never touched, so there's no two-element swap to keep in sync. -/** @returns {HTMLElement} */ -function _getOrCreateTooltip() { - let el = document.getElementById('path-tooltip'); - if (!el) { - el = document.createElement('div'); - el.id = 'path-tooltip'; - el.className = 'path-tooltip hidden'; - document.body.appendChild(el); - } - return el; +/** @returns {HTMLElement | null} */ +function _getTooltip() { + return document.getElementById('path-tooltip'); } function _hide() { - const el = document.getElementById('path-tooltip'); + const el = _getTooltip(); if (el) el.classList.add('hidden'); } @@ -98,7 +96,8 @@ function _onEnter(e) { // Nothing to show — don't display an all-? tooltip. if (!ownerId && !path) return; - const tooltip = _getOrCreateTooltip(); + const tooltip = _getTooltip(); + if (!tooltip) return; // Clear previous content. while (tooltip.firstChild) tooltip.removeChild(tooltip.firstChild); @@ -140,14 +139,19 @@ const _registry = new WeakMap(); // ── Public API ──────────────────────────────────────────────────────────────── +/** Containers known to expose `data-path`/`data-owner-id` for tooltip use. + * Add new opt-in row classes here when other views want the tooltip — each + * one must stamp the dataset attributes itself. */ +const _TOOLTIP_SELECTOR = '.file-item, .ms-resource-row'; + /** - * Attach tooltip listeners to every `.file-item` inside `container`. - * Items with neither `data-owner-id` nor `data-path` will not trigger the + * Attach tooltip listeners to every tooltip-capable row inside `container`. + * Rows with neither `data-owner-id` nor `data-path` will not trigger the * tooltip. Safe to call repeatedly — already-wired elements are skipped. * @param {HTMLElement} container */ function init(container) { - for (const item of container.querySelectorAll('.file-item')) { + for (const item of container.querySelectorAll(_TOOLTIP_SELECTOR)) { const el = /** @type {HTMLElement} */ (item); if (_registry.has(el)) continue; // already wired @@ -161,12 +165,12 @@ function init(container) { } /** - * Remove tooltip listeners from all `.file-item` elements inside `container` + * Remove tooltip listeners from all tooltip-capable rows inside `container` * and hide any visible tooltip. * @param {HTMLElement} container */ function destroy(container) { - for (const item of container.querySelectorAll('.file-item')) { + for (const item of container.querySelectorAll(_TOOLTIP_SELECTOR)) { const el = /** @type {HTMLElement} */ (item); const h = _registry.get(el); if (h) {