taskbar handles many open windows: shrink buttons, scroll on overflow, keep active in view

This commit is contained in:
Martin Dvořák 2026-09-24 07:25:09 +00:00
commit 338146671b
3 changed files with 54 additions and 4 deletions

View file

@ -26,7 +26,9 @@ heatmap**, a gym SBD panel and links to friends' sites.
- **Draggable desktop icons** — free placement that snaps to a measured grid, one icon per
cell, with a follow-the-cursor drag ghost.
- **Multi-window manager** — draggable, resizable, minimizable, maximizable, focus/z-order
and animated open/close (with reduced-motion support).
and animated open/close (with reduced-motion support). The taskbar shrinks its window
buttons as more apps open and scrolls when they still don't fit, so every open app stays
reachable.
- **Working terminal app** — command input with history (`↑`/`↓`, `Ctrl+L` to clear) and a
hidden `ssh` easter egg.
- **Live service status** — each self-hosted service is health-checked through the site's

View file

@ -1,6 +1,6 @@
"use client";
import { useRef, useState, type ReactNode } from "react";
import { useEffect, useRef, useState, type ReactNode } from "react";
import Image from "next/image";
import { APPS, APP_BY_ID, createWebsiteApp, type AppDef } from "./apps";
import WinWindow, { type WinState } from "./WinWindow";
@ -89,9 +89,25 @@ export default function WindowsDesktop() {
const friendItems = FRIEND_SHORTCUTS;
const desktopRef = useRef<HTMLDivElement>(null);
const tbRef = useRef<HTMLDivElement>(null);
const { positions, dragId, onPointerDown, consumeDrag } =
useDesktopIcons(desktopRef, ICON_KEYS);
// When the taskbar buttons overflow, keep the focused window's button visible
// so opening an app doesn't leave its button off the (scrollable) right edge.
useEffect(() => {
const bar = tbRef.current;
const active = bar?.querySelector<HTMLElement>(".win-tb-btn.active");
if (!bar || !active) return;
const barRect = bar.getBoundingClientRect();
const btnRect = active.getBoundingClientRect();
if (btnRect.left < barRect.left) {
bar.scrollLeft += btnRect.left - barRect.left;
} else if (btnRect.right > barRect.right) {
bar.scrollLeft += btnRect.right - barRect.right;
}
}, [activeId]);
const [dynamicApps, setDynamicApps] = useState<Record<string, AppDef>>({});
const getApp = (id: string): AppDef | undefined => APP_BY_ID[id] ?? dynamicApps[id];
@ -452,7 +468,16 @@ export default function WindowsDesktop() {
</span>
</button>
<div className="win-tb-buttons">
<div
className="win-tb-buttons"
ref={tbRef}
onWheel={(e) => {
const bar = tbRef.current;
// Translate a vertical mouse wheel into horizontal scrolling while
// the button strip overflows, so every open app stays reachable.
if (bar && bar.scrollWidth > bar.clientWidth) bar.scrollLeft += e.deltaY;
}}
>
{wins.map((w) => {
const app = getApp(w.appId) ?? APP_BY_ID.welcome;
return (

View file

@ -1106,13 +1106,35 @@ a.win-icon {
gap: 2px;
padding: 4px 4px;
min-width: 0;
overflow: hidden;
position: relative;
/* Grow into the free space, then shrink the buttons, then scroll once even
the shrunken buttons no longer fit (see .win-tb-btn min-width). */
flex: 1 1 auto;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.28) transparent;
}
.win-tb-buttons::-webkit-scrollbar {
height: 4px;
}
.win-tb-buttons::-webkit-scrollbar-track {
background: transparent;
}
.win-tb-buttons::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.28);
border-radius: 2px;
}
.win-tb-buttons::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.45);
}
.win-tb-btn {
position: relative;
display: flex;
align-items: center;
gap: 7px;
flex: 0 1 auto;
min-width: 0;
max-width: 172px;
padding: 0 10px;
border: 1px solid transparent;
@ -1182,6 +1204,7 @@ a.win-icon {
}
.win-tb-btn-label {
font-size: 0.75rem;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;