feat(dashboard): polish modal widths, popover usage, onboarding contrast, TUI logo
- Resize the dashboard TUI splash logo to scale with the terminal: a Colossal-font variant kicks in on terminals ≥70 cols × 16 rows; gradient palette is now strictly white → blueBright → blue (no cyan/magenta) - Lower-clip TUI Kanban cards to a single-line title and constrain each column with overflow hidden so cards no longer bleed past the header row - Center the TerminalModal vertically by overriding overlay alignment via :has() and trimming the modal height - Match GitHub Import modal width to the file browser modal (90vw / 1600px max) - Render the Usage indicator as a top-right popover on desktop instead of a centered modal (mobile keeps the full-screen sheet) - Widen SetupWizard / Model Onboarding modals (540→880px, 560→880px) and replace the washed-out done step indicator background with a solid success swatch - Drop the duplicate mailbox icon button from the desktop header (view switch covers it; overflow menu still surfaces it on compact) - Loosen agent health detection: bump the heartbeat grace multiplier 2× → 4× and the staleness floor 60s → 5min so transient ticks don't flag agents Unresponsive Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,7 +41,7 @@ import type {
|
||||
} from "./state.js";
|
||||
import { SECTION_ORDER } from "./state.js";
|
||||
import type { LogEntry } from "./log-ring-buffer.js";
|
||||
import { FUSION_LOGO_LINES, FUSION_TAGLINE } from "./logo.js";
|
||||
import { FUSION_LOGO_LINES, FUSION_LOGO_LARGE_LINES, FUSION_TAGLINE } from "./logo.js";
|
||||
import { useProjects, useTasks } from "./hooks/use-projects.js";
|
||||
|
||||
// ── Format helpers ────────────────────────────────────────────────────────────
|
||||
@@ -75,19 +75,26 @@ function formatRelativeTime(iso: string): string {
|
||||
return `${h}h ago`;
|
||||
}
|
||||
|
||||
// All-blue vertical gradient — top: brightest, bottom: shadow.
|
||||
const LOGO_COLORS = ["whiteBright", "cyanBright", "cyan", "blueBright", "blue", "blue"] as const;
|
||||
// All-blue vertical gradient — top: brightest white, bottom: deep blue.
|
||||
// Strictly white + blue shades, no cyan/purple.
|
||||
const LOGO_COLORS = ["whiteBright", "white", "blueBright", "blueBright", "blue", "blue", "blue", "blue"] as const;
|
||||
type InkColor = typeof LOGO_COLORS[number];
|
||||
|
||||
function logoColor(index: number): InkColor {
|
||||
return LOGO_COLORS[Math.min(index, LOGO_COLORS.length - 1)];
|
||||
function logoColor(index: number, total: number): InkColor {
|
||||
// Map this row's index proportionally across LOGO_COLORS so gradient
|
||||
// looks consistent regardless of how tall the chosen logo variant is.
|
||||
const slot = Math.min(
|
||||
LOGO_COLORS.length - 1,
|
||||
Math.floor((index / Math.max(1, total - 1)) * (LOGO_COLORS.length - 1)),
|
||||
);
|
||||
return LOGO_COLORS[slot];
|
||||
}
|
||||
|
||||
function AnimatedFusionLogo() {
|
||||
function AnimatedFusionLogo({ lines }: { lines: readonly string[] }) {
|
||||
return (
|
||||
<Box flexDirection="column" alignItems="center">
|
||||
{FUSION_LOGO_LINES.map((line, i) => (
|
||||
<Text key={i} color={logoColor(i)} bold>{line}</Text>
|
||||
{lines.map((line, i) => (
|
||||
<Text key={i} color={logoColor(i, lines.length)} bold>{line}</Text>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
@@ -95,31 +102,31 @@ function AnimatedFusionLogo() {
|
||||
|
||||
// ── Splash screen ─────────────────────────────────────────────────────────────
|
||||
|
||||
// Smallest terminal that can fit the full block-letter splash centered.
|
||||
// Below either threshold the block-letter logo gets dropped for a plain
|
||||
// "FUSION" word so nothing wraps awkwardly on small terminals. The splash
|
||||
// always pins to the top-left — never centered — so it doesn't jump around
|
||||
// when the terminal resizes.
|
||||
// Logo width thresholds. Below SPLASH_MIN_COLS we fall back to the plain
|
||||
// "FUSION" word; otherwise we pick the largest variant that fits.
|
||||
const SPLASH_MIN_COLS = 56;
|
||||
const SPLASH_MIN_ROWS = 20;
|
||||
const SPLASH_MIN_ROWS = 12;
|
||||
const LARGE_LOGO_MIN_COLS = 70;
|
||||
const LARGE_LOGO_MIN_ROWS = 16;
|
||||
|
||||
function SplashScreen({ loadingStatus }: { loadingStatus: string }) {
|
||||
const { stdout } = useStdout();
|
||||
const cols = stdout?.columns ?? 80;
|
||||
const rows = stdout?.rows ?? 24;
|
||||
const compact = cols < SPLASH_MIN_COLS || rows < SPLASH_MIN_ROWS;
|
||||
const large = cols >= LARGE_LOGO_MIN_COLS && rows >= LARGE_LOGO_MIN_ROWS;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" paddingX={1} paddingY={1}>
|
||||
{compact ? (
|
||||
<Text bold color="cyanBright">FUSION</Text>
|
||||
<Text bold color="blueBright">FUSION</Text>
|
||||
) : (
|
||||
<AnimatedFusionLogo />
|
||||
<AnimatedFusionLogo lines={large ? FUSION_LOGO_LARGE_LINES : FUSION_LOGO_LINES} />
|
||||
)}
|
||||
<Text color="blueBright" dimColor>{FUSION_TAGLINE}</Text>
|
||||
<Box height={1} />
|
||||
<Box flexDirection="row" gap={1}>
|
||||
<Text color="cyanBright"><Spinner type="dots" /></Text>
|
||||
<Text color="blueBright"><Spinner type="dots" /></Text>
|
||||
<Text color="blueBright" dimColor>{loadingStatus}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -131,7 +138,7 @@ function SplashScreen({ loadingStatus }: { loadingStatus: string }) {
|
||||
function MiniLogo() {
|
||||
return (
|
||||
<Box flexDirection="row" gap={0}>
|
||||
<Text color="cyan" bold>FUSION</Text>
|
||||
<Text color="blueBright" bold>FUSION</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -210,11 +217,6 @@ function SystemPanel({ state, isFocused }: { state: DashboardState; isFocused: b
|
||||
<Text wrap="truncate" color="cyanBright">{info.tokenizedUrl}</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box flexDirection="row" gap={1}>
|
||||
<Text dimColor>Press</Text>
|
||||
<Text color="cyanBright" bold>[Enter]</Text>
|
||||
<Text dimColor>to open in browser</Text>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -222,11 +224,6 @@ function SystemPanel({ state, isFocused }: { state: DashboardState; isFocused: b
|
||||
<Text dimColor>Auth:</Text>
|
||||
<Text color="yellow">no auth</Text>
|
||||
</Box>
|
||||
<Box flexDirection="row" gap={1}>
|
||||
<Text dimColor>Press</Text>
|
||||
<Text color="cyanBright" bold>[Enter]</Text>
|
||||
<Text dimColor>to open in browser</Text>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
<Box flexDirection="row" gap={1}>
|
||||
@@ -756,20 +753,31 @@ function KanbanColumnView({
|
||||
}) {
|
||||
const accent = COLUMN_COLORS[column];
|
||||
const headerColor = isFocused ? "whiteBright" : accent;
|
||||
const cardWidth = Math.max(12, width - 4);
|
||||
const cardWidth = Math.max(16, width - 2);
|
||||
const innerHeaderWidth = Math.max(8, width - 2);
|
||||
const label = `${columnLabel(column).toUpperCase()} (${tasks.length})`;
|
||||
const headerText = ` ${label} `.length > innerHeaderWidth
|
||||
? ` ${label} `.slice(0, innerHeaderWidth)
|
||||
: ` ${label} `.padEnd(innerHeaderWidth, " ");
|
||||
return (
|
||||
<Box flexDirection="column" width={width} flexShrink={0} paddingX={1}>
|
||||
<Box flexDirection="row" gap={1}>
|
||||
<Box
|
||||
flexDirection="column"
|
||||
width={width}
|
||||
flexShrink={1}
|
||||
flexGrow={1}
|
||||
paddingX={1}
|
||||
overflow="hidden"
|
||||
>
|
||||
<Box width={innerHeaderWidth} flexShrink={0}>
|
||||
<Text bold color={headerColor} backgroundColor={isFocused ? accent : undefined}>
|
||||
{` ${columnLabel(column).toUpperCase()} `}
|
||||
{headerText}
|
||||
</Text>
|
||||
<Text dimColor>{tasks.length}</Text>
|
||||
</Box>
|
||||
<Box height={1} />
|
||||
<Box height={1} flexShrink={0} />
|
||||
{tasks.length === 0 ? (
|
||||
<Text dimColor>—</Text>
|
||||
) : (
|
||||
<Box flexDirection="column" gap={0}>
|
||||
<Box flexDirection="column" gap={0} flexShrink={1} overflow="hidden">
|
||||
{tasks.map((task, i) => (
|
||||
<TaskCard
|
||||
key={task.id}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// FUSION block-letter logo using Unicode box-drawing + full blocks.
|
||||
// Font: ANSI Shadow (figlet-style). Hardcoded so we have no runtime dep.
|
||||
// The caller applies a cyan→whiteBright vertical gradient.
|
||||
// FUSION block-letter logos using Unicode box-drawing + full blocks.
|
||||
// The caller picks a size based on terminal dimensions and applies an
|
||||
// all-blue vertical gradient.
|
||||
|
||||
// ANSI Shadow font — 47 cols × 6 rows.
|
||||
export const FUSION_LOGO_LINES = [
|
||||
"███████╗██╗ ██╗███████╗██╗ ██████╗ ███╗ ██╗",
|
||||
"██╔════╝██║ ██║██╔════╝██║██╔═══██╗████╗ ██║",
|
||||
@@ -11,4 +12,16 @@ export const FUSION_LOGO_LINES = [
|
||||
"╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝",
|
||||
];
|
||||
|
||||
// Colossal font — 62 cols × 8 rows. Used when the terminal has room for it.
|
||||
export const FUSION_LOGO_LARGE_LINES = [
|
||||
"8888888888 888 888 .d8888b. 8888888 .d88888b. 888b 888 ",
|
||||
"888 888 888 d88P Y88b 888 d88P\" \"Y88b 8888b 888 ",
|
||||
"888 888 888 Y88b. 888 888 888 88888b 888 ",
|
||||
"8888888 888 888 \"Y888b. 888 888 888 888Y88b 888 ",
|
||||
"888 888 888 \"Y88b. 888 888 888 888 Y88b888 ",
|
||||
"888 888 888 \"888 888 888 888 888 Y88888 ",
|
||||
"888 Y88b. .d88P Y88b d88P 888 Y88b. .d88P 888 Y8888 ",
|
||||
"888 \"Y88888P\" \"Y8888P\" 8888888 \"Y88888P\" 888 Y888 ",
|
||||
];
|
||||
|
||||
export const FUSION_TAGLINE = "AI coding agent dashboard";
|
||||
|
||||
Reference in New Issue
Block a user