diff --git a/.changeset/fn-8196-mobile-footer-stat-tooltips.md b/.changeset/fn-8196-mobile-footer-stat-tooltips.md new file mode 100644 index 0000000000..e5dda558a3 --- /dev/null +++ b/.changeset/fn-8196-mobile-footer-stat-tooltips.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Add tap-to-reveal names for mobile executor footer stats. +category: feature +dev: Portals mobile stat tooltips beyond footer clipping while preserving desktop labels. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index f617ecd29d..a43deb6f81 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -1370,6 +1370,8 @@ The global AI engine stop/start control and triage pause/resume control live in Brief, single-poll executor stats fetch blips keep showing the last good footer stats instead of flashing **Connecting…**. Routine executor stats heartbeats also keep the populated footer mounted after initial load, so an open engine/concurrency popover stays open while counts refresh. The footer only switches to **Connecting…** for sustained suspension-like stats failures, or to an explicit error state for non-transient failures. +On mobile, the compact footer hides stat names to preserve space. Tap a colored stat dot and count to show its name; tap it again, tap elsewhere, press Escape, or scroll to dismiss the tooltip. Desktop and tablet continue to show stat names inline. + ### Engine status banner When a project dashboard is open but no project engine is connected, Fusion shows a sticky **Engine disconnected** banner above the project content. This covers paused projects, failed or still-starting project engines, delayed reconciliation, and dashboard-only/dev launches where the UI is available before an engine manager is attached. diff --git a/packages/dashboard/app/components/ExecutorStatusBar.css b/packages/dashboard/app/components/ExecutorStatusBar.css index d30c55fc6b..5b3e06ac44 100644 --- a/packages/dashboard/app/components/ExecutorStatusBar.css +++ b/packages/dashboard/app/components/ExecutorStatusBar.css @@ -186,6 +186,49 @@ FN-6887 makes the footer status bar the canonical desktop/tablet terminal launch line-height: 1; } +/* Mobile mode is component-owned so short landscape phones hide labels and + expose tap targets under the same viewport contract. */ +.executor-status-bar--mobile .executor-status-bar__label { + display: none; +} + +.executor-status-bar__stat-tooltip { + position: fixed; + z-index: var(--z-popover, 60); + transform: translate(-50%, calc(-100% - var(--space-sm))); + padding: var(--space-xs) var(--space-sm); + border-radius: var(--radius-sm); + background: var(--surface); + box-shadow: var(--shadow-lg); + color: var(--text); + font-size: inherit; + line-height: 1; + pointer-events: none; + white-space: nowrap; +} + +.executor-status-bar__segment--stat { + border: none; + background: transparent; + color: inherit; + font: inherit; + padding: 0; + text-align: inherit; +} + +.executor-status-bar--mobile .executor-status-bar__segment--stat { + min-width: var(--executor-status-touch-size, calc(var(--space-lg) * 2 + var(--space-xs))); + min-height: var(--executor-status-touch-size, calc(var(--space-lg) * 2 + var(--space-xs))); + justify-content: center; + cursor: pointer; +} + +.executor-status-bar__segment--stat:focus-visible { + outline: none; + box-shadow: var(--focus-ring-strong); + border-radius: var(--radius-sm); +} + /* Max concurrent display */ .executor-status-bar__max { font-family: var(--font-mono); @@ -416,10 +459,6 @@ FN-6887 makes the footer status bar the canonical desktop/tablet terminal launch overflow: hidden; } - .executor-status-bar__label { - display: none; - } - .executor-status-bar__project-path { max-width: min(26ch, 30vw); } diff --git a/packages/dashboard/app/components/ExecutorStatusBar.tsx b/packages/dashboard/app/components/ExecutorStatusBar.tsx index f1e55b7701..5da86b8d3a 100644 --- a/packages/dashboard/app/components/ExecutorStatusBar.tsx +++ b/packages/dashboard/app/components/ExecutorStatusBar.tsx @@ -1,5 +1,6 @@ import "./ExecutorStatusBar.css"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState, type ReactNode } from "react"; +import { createPortal } from "react-dom"; import type { TFunction } from "i18next"; import { useTranslation } from "react-i18next"; import { @@ -18,6 +19,44 @@ import { EngineControlMenu, type EngineControlMenuHandle } from "./EngineControl import { TerminalLauncher } from "./TerminalLauncher"; import { useViewportMode } from "../hooks/useViewportMode"; +type FooterStatId = "queued" | "running" | "stuck" | "blocked" | "review" | "fanout"; + +interface OpenStatTooltip { + id: FooterStatId; + label: string; + rect: DOMRect; +} + +interface MobileStatSegmentProps { + className?: string; + id: FooterStatId; + isMobile: boolean; + isOpen: boolean; + label: string; + onToggle: (id: FooterStatId, label: string, rect: DOMRect) => void; + children: ReactNode; +} + +function MobileStatSegment({ className = "", id, isMobile, isOpen, label, onToggle, children }: MobileStatSegmentProps) { + const segmentClassName = `executor-status-bar__segment executor-status-bar__segment--stat ${className}`.trim(); + if (!isMobile) return