responsive desktop: shrink icons on cramped viewports so they never collide; clamp windows to the viewport

This commit is contained in:
Martin Dvořák 2026-09-24 12:48:01 +00:00
commit 194501e467
4 changed files with 136 additions and 9 deletions

View file

@ -149,11 +149,15 @@ export default function WindowsDesktop() {
const id = idRef.current++;
const vw = window.innerWidth;
const vh = window.innerHeight - TASKBAR_HEIGHT;
// Fit the window to the viewport so it never opens off-screen or under the
// panel on a narrow/short monitor.
const w = Math.min(app.width, Math.max(260, vw - 20));
const h = Math.min(app.height, Math.max(150, vh - 20));
const cascade = Math.min(wins.length, 8) * 30;
let x = Math.round((vw - app.width) / 2) + cascade;
let y = Math.round((vh - app.height) / 2) + cascade;
x = Math.max(10, Math.min(x, vw - app.width - 10));
y = Math.max(10, Math.min(y, vh - app.height - 10));
let x = Math.round((vw - w) / 2) + cascade;
let y = Math.round((vh - h) / 2) + cascade;
x = Math.max(10, Math.min(x, vw - w - 10));
y = Math.max(10, Math.min(y, vh - h - 10));
setWins((prev) => [
...prev,
{
@ -161,8 +165,8 @@ export default function WindowsDesktop() {
appId: app.id,
x,
y,
w: app.width,
h: app.height,
w,
h,
z: ++zRef.current,
minimized: false,
maximized: false,
@ -252,11 +256,43 @@ export default function WindowsDesktop() {
);
};
const resizeWin = (id: number, x: number, y: number, w: number, h: number) => {
const vw = typeof window !== "undefined" ? window.innerWidth : 1280;
const vh = (typeof window !== "undefined" ? window.innerHeight : 800) - TASKBAR_HEIGHT;
const nw = Math.min(w, Math.max(260, vw - 20));
const nh = Math.min(h, Math.max(150, vh - 20));
setWins((prev) =>
prev.map((win) => (win.id === id ? { ...win, centered: false, x, y, w, h } : win))
prev.map((win) =>
win.id === id ? { ...win, centered: false, x, y, w: nw, h: nh } : win
)
);
};
// Keep windows on-screen when the viewport shrinks (narrow monitor, or the
// user resizing the browser): clamp size to the viewport and pull anything
// that would fall off the edge back into view.
useEffect(() => {
const clampToViewport = () => {
const vw = window.innerWidth;
const vh = window.innerHeight - TASKBAR_HEIGHT;
setWins((prev) =>
prev.map((w) => {
if (w.maximized) return w;
const nw = Math.min(w.w, Math.max(260, vw - 20));
const nh = Math.min(w.h, Math.max(150, vh - 20));
if (w.centered) {
return nw === w.w && nh === w.h ? w : { ...w, w: nw, h: nh };
}
const nx = Math.max(10, Math.min(w.x, vw - nw - 10));
const ny = Math.max(10, Math.min(w.y, vh - nh - 10));
if (nw === w.w && nh === w.h && nx === w.x && ny === w.y) return w;
return { ...w, x: nx, y: ny, w: nw, h: nh };
})
);
};
window.addEventListener("resize", clampToViewport);
return () => window.removeEventListener("resize", clampToViewport);
}, []);
const taskbarClick = (w: WinState) => {
if (w.minimized) {
setWins((prev) =>

View file

@ -163,6 +163,75 @@ a.win-icon {
opacity: 0.92;
}
/* Cramped desktops: shrink the icons so the grid always has a cell for every
icon (so they can never collide) and the launcher stacks stay on screen. The
grid is measured from the rendered icons, so it adapts automatically. */
@media (max-width: 900px), (max-height: 600px) {
.win-icon {
width: 80px;
padding: 5px 4px 7px;
gap: 6px;
}
.win-icon-glyph {
width: 40px;
height: 40px;
font-size: 32px;
}
.win-icon-label {
font-size: 0.68rem;
}
.win-icon-img {
width: 32px;
height: 32px;
}
}
@media (max-width: 720px), (max-height: 440px) {
.win-icon {
width: 66px;
padding: 4px 3px 5px;
gap: 4px;
}
.win-icon-glyph {
width: 32px;
height: 32px;
font-size: 26px;
}
.win-icon-label {
font-size: 0.6rem;
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.win-icon-img {
width: 26px;
height: 26px;
}
}
@media (max-height: 380px) {
.win-icon {
width: 56px;
padding: 3px 3px 4px;
gap: 3px;
}
.win-icon-glyph {
width: 28px;
height: 28px;
font-size: 22px;
}
.win-icon-label {
font-size: 0.54rem;
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.win-icon-img {
width: 22px;
height: 22px;
}
}
/* The three default stacks are laid out once so the grid columns can be read
off them. They stay in the DOM (keeping the same metrics) but must never be
seen or clicked - the visible icons all come from .win-icon-layer. */
@ -246,6 +315,16 @@ a.win-icon {
top: 0;
bottom: 54px;
margin: auto;
/* Never let a centred window (e.g. Welcome) overflow a small viewport. */
max-width: calc(100vw - 20px);
max-height: calc(100vh - 74px);
}
/* On very short viewports the default min-height would win over max-height and
push the window off-screen; let it shrink further so it still fits. */
@media (max-height: 320px) {
.win-window {
min-height: 110px;
}
}
.win-window.closing {
animation: winClose 0.14s ease-in forwards;