clamp window dragging to the screen; drop the coloured per-app tiles in the taskbar

This commit is contained in:
Martin Dvořák 2026-09-24 13:43:26 +00:00
commit 415f48f3f0
4 changed files with 24 additions and 20 deletions

View file

@ -30,7 +30,7 @@ heatmap**, a gym SBD panel and links to friends' sites.
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. Windows are clamped to the viewport so they never open off-screen on a small
monitor.
monitor and can't be dragged off any edge.
- **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

@ -223,11 +223,11 @@ list reordering, not free placement. Don't confuse the two.
`eslint-disable-next-line` with a comment. Don't add more without a reason.
- Watch for `pointercancel`: any custom pointer gesture over an `<img>`, link,
or text selection needs `draggable=false` / `user-select: none`.
- Windows must fit the viewport: `openAppDef` / `resizeWin` clamp `w`/`h` in
`WindowsDesktop.tsx`, a `resize` effect re-clamps on browser resize, and
- Windows must stay on-screen: `openAppDef` / `resizeWin` clamp `w`/`h`,
`moveWin` clamps `x`/`y` (dragging can't push a window off any edge or under
the panel), a `resize` effect re-clamps on browser resize, and
`.win-window.centered` carries `max-width`/`max-height`. Keep these in sync if
you add a new window-open path, or windows will open off-screen on a small
monitor.
you add a new window path, or windows will escape the viewport.
- Exposing the **dev** server over a tunnel needs the tunnel host in
`allowedDevOrigins` (`next.config.ts`). Without it Next dev rejects the
cross-origin HMR WebSocket, so the client never hydrates — the page is SSR

View file

@ -240,19 +240,23 @@ export default function WindowsDesktop() {
prev.map((w) => (w.id === id ? { ...w, maximized: !w.maximized } : w))
);
const moveWin = (id: number, x: number, y: number) => {
const maxX = (typeof window !== "undefined" ? window.innerWidth : 1280) - 120;
const maxY = (typeof window !== "undefined" ? window.innerHeight : 800) - TASKBAR_HEIGHT;
const vw = typeof window !== "undefined" ? window.innerWidth : 1280;
const vh = (typeof window !== "undefined" ? window.innerHeight : 800) - TASKBAR_HEIGHT;
setWins((prev) =>
prev.map((w) =>
w.id === id
? {
...w,
centered: false,
x: Math.max(-w.w + 120, Math.min(maxX, x)),
y: Math.max(0, Math.min(maxY, y)),
}
: w
)
prev.map((w) => {
if (w.id !== id) return w;
// Keep the whole window on-screen: no dragging it off any edge or under
// the panel. (max() guards the case where the window is bigger than the
// viewport, which the open/resize clamps normally prevent.)
const maxX = Math.max(0, vw - w.w);
const maxY = Math.max(0, vh - w.h);
return {
...w,
centered: false,
x: Math.max(0, Math.min(maxX, x)),
y: Math.max(0, Math.min(maxY, y)),
};
})
);
};
const resizeWin = (id: number, x: number, y: number, w: number, h: number) => {
@ -532,7 +536,7 @@ export default function WindowsDesktop() {
onClick={() => taskbarClick(w)}
title={app.title}
>
<span className="win-tb-btn-icon" style={{ background: app.color }}>
<span className="win-tb-btn-icon" aria-hidden="true">
{app.icon}
</span>
<span className="win-tb-btn-label">{app.title}</span>

View file

@ -1441,10 +1441,10 @@ a.win-icon {
.win-tb-btn-icon {
width: 18px;
height: 18px;
border-radius: 4px;
display: grid;
place-items: center;
font-size: 11px;
font-size: 14px;
line-height: 1;
flex-shrink: 0;
}
.win-tb-btn-label {