diff --git a/README.md b/README.md index bd35014..1faa019 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/app/components/WindowsDesktop.tsx b/src/app/components/WindowsDesktop.tsx index aa60515..0acfabd 100644 --- a/src/app/components/WindowsDesktop.tsx +++ b/src/app/components/WindowsDesktop.tsx @@ -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(null); + const tbRef = useRef(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(".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>({}); const getApp = (id: string): AppDef | undefined => APP_BY_ID[id] ?? dynamicApps[id]; @@ -452,7 +468,16 @@ export default function WindowsDesktop() { -
+
{ + 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 ( diff --git a/src/app/skeuo.css b/src/app/skeuo.css index 1842ece..2c46eca 100644 --- a/src/app/skeuo.css +++ b/src/app/skeuo.css @@ -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;