diff --git a/README.md b/README.md
index 556629f..1b2eb34 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/opencode.md b/opencode.md
index 982a92e..a96f244 100644
--- a/opencode.md
+++ b/opencode.md
@@ -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 `
`, 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
diff --git a/src/app/components/WindowsDesktop.tsx b/src/app/components/WindowsDesktop.tsx
index d3c3339..87ac9d7 100644
--- a/src/app/components/WindowsDesktop.tsx
+++ b/src/app/components/WindowsDesktop.tsx
@@ -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}
>
-
+
{app.icon}
{app.title}
diff --git a/src/app/skeuo.css b/src/app/skeuo.css
index 22eccd9..89a2a00 100644
--- a/src/app/skeuo.css
+++ b/src/app/skeuo.css
@@ -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 {