taskbar handles many open windows: shrink buttons, scroll on overflow, keep active in view
This commit is contained in:
parent
7638fac97b
commit
338146671b
3 changed files with 54 additions and 4 deletions
|
|
@ -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
|
- **Draggable desktop icons** — free placement that snaps to a measured grid, one icon per
|
||||||
cell, with a follow-the-cursor drag ghost.
|
cell, with a follow-the-cursor drag ghost.
|
||||||
- **Multi-window manager** — draggable, resizable, minimizable, maximizable, focus/z-order
|
- **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
|
- **Working terminal app** — command input with history (`↑`/`↓`, `Ctrl+L` to clear) and a
|
||||||
hidden `ssh` easter egg.
|
hidden `ssh` easter egg.
|
||||||
- **Live service status** — each self-hosted service is health-checked through the site's
|
- **Live service status** — each self-hosted service is health-checked through the site's
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useRef, useState, type ReactNode } from "react";
|
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { APPS, APP_BY_ID, createWebsiteApp, type AppDef } from "./apps";
|
import { APPS, APP_BY_ID, createWebsiteApp, type AppDef } from "./apps";
|
||||||
import WinWindow, { type WinState } from "./WinWindow";
|
import WinWindow, { type WinState } from "./WinWindow";
|
||||||
|
|
@ -89,9 +89,25 @@ export default function WindowsDesktop() {
|
||||||
const friendItems = FRIEND_SHORTCUTS;
|
const friendItems = FRIEND_SHORTCUTS;
|
||||||
|
|
||||||
const desktopRef = useRef<HTMLDivElement>(null);
|
const desktopRef = useRef<HTMLDivElement>(null);
|
||||||
|
const tbRef = useRef<HTMLDivElement>(null);
|
||||||
const { positions, dragId, onPointerDown, consumeDrag } =
|
const { positions, dragId, onPointerDown, consumeDrag } =
|
||||||
useDesktopIcons(desktopRef, ICON_KEYS);
|
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 [dynamicApps, setDynamicApps] = useState<Record<string, AppDef>>({});
|
||||||
const getApp = (id: string): AppDef | undefined => APP_BY_ID[id] ?? dynamicApps[id];
|
const getApp = (id: string): AppDef | undefined => APP_BY_ID[id] ?? dynamicApps[id];
|
||||||
|
|
||||||
|
|
@ -452,7 +468,16 @@ export default function WindowsDesktop() {
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</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) => {
|
{wins.map((w) => {
|
||||||
const app = getApp(w.appId) ?? APP_BY_ID.welcome;
|
const app = getApp(w.appId) ?? APP_BY_ID.welcome;
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1106,13 +1106,35 @@ a.win-icon {
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
padding: 4px 4px;
|
padding: 4px 4px;
|
||||||
min-width: 0;
|
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 {
|
.win-tb-btn {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 7px;
|
gap: 7px;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
min-width: 0;
|
||||||
max-width: 172px;
|
max-width: 172px;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
|
|
@ -1182,6 +1204,7 @@ a.win-icon {
|
||||||
}
|
}
|
||||||
.win-tb-btn-label {
|
.win-tb-btn-label {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
|
min-width: 0;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue