From eecf8cf3390070bd2a0fddf857dc7b127f4fab69 Mon Sep 17 00:00:00 2001 From: Martin Dvorak Date: Thu, 24 Sep 2026 07:34:48 +0000 Subject: [PATCH] fix desktop icon grid gap at right edge: stretch column pitch so right-anchored stacks sit flush --- opencode.md | 15 +++++-- src/app/components/useDesktopIcons.ts | 61 +++++++++++++++++++-------- 2 files changed, 56 insertions(+), 20 deletions(-) diff --git a/opencode.md b/opencode.md index 9a7047e..13c302e 100644 --- a/opencode.md +++ b/opencode.md @@ -121,9 +121,13 @@ with icons, it's almost certainly here. `.win-icons-tr` / `.win-icons-br`). They are rendered **hidden** with class `win-measure` purely so the grid can be measured from them. - On mount (a `useLayoutEffect`), `measure()` reads the real rendered boxes of - those stacks: cell size = icon width/height **+ the CSS flex gap** (`GRID_GAP`, - currently 4px), and origin = the left stack's offset. From that it computes - `cols` / `rows` for the whole desktop. + those stacks: cell size = icon width/height, and origin = the left stack's + offset. The row pitch is cell height **+ the CSS flex gap** (`GRID_GAP`, + currently 4px). The column pitch starts at cell width + gap but is **stretched + a hair** so the grid spans from the left margin to the right margin — a fixed + pitch left up to a full cell of slack on the right, which pulled the + right-anchored services/friends stacks away from the border. From that it + computes `cols` / `rows` for the whole desktop. - `defaultCells()` assigns each icon the cell its stack already occupies. So **the grid you see on load is the grid dragging uses** — no re-layout jump. - Icons are then rendered absolutely positioned in `.win-icon-layer`, driven by @@ -183,6 +187,11 @@ list reordering, not free placement. Don't confuse the two. 6. **Friend icons couldn't be dragged.** Native image drag on the `next/image` glyph fired `pointercancel`. Fixed with `draggable={false}` + `-webkit-user-drag: none`. +7. **Right-edge gap.** Columns tiled from the left with a fixed pitch, so the + right-anchored stacks (services/friends) stopped up to a full cell short of + the right border (e.g. ~90px on a 1920px screen), and any remount/resize + re-snapped them. Fixed by stretching the column pitch so the grid spans both + margins; the row pitch is unchanged. --- diff --git a/src/app/components/useDesktopIcons.ts b/src/app/components/useDesktopIcons.ts index 476c505..95a2d59 100644 --- a/src/app/components/useDesktopIcons.ts +++ b/src/app/components/useDesktopIcons.ts @@ -53,11 +53,20 @@ export function useDesktopIcons( const [positions, setPositions] = useState>({}); const [dragId, setDragId] = useState(null); const cellRef = useRef({ w: DEFAULT_CELL_WIDTH, h: DEFAULT_CELL_HEIGHT }); - const gridRef = useRef<{ cols: number; rows: number; originX: number; originY: number }>({ + const gridRef = useRef<{ + cols: number; + rows: number; + originX: number; + originY: number; + pitchX: number; + pitchY: number; + }>({ cols: 1, rows: 1, originX: 10, originY: 12, + pitchX: DEFAULT_CELL_WIDTH + GRID_GAP, + pitchY: DEFAULT_CELL_HEIGHT + GRID_GAP, }); const suppressRef = useRef(false); @@ -71,8 +80,10 @@ export function useDesktopIcons( /** * Work out the grid from where the launcher stacks actually sit. The icon - * width/height (plus the flex gap) is the cell pitch, and the left/top stack - * offsets give column 0 / row 0. Falls back to sensible values before mount. + * width/height is the cell size and the left/top stack offsets give column 0 / + * row 0; the column pitch is stretched a touch so the right-hand stacks stay + * flush with the right margin instead of stopping up to a cell short. Falls + * back to sensible values before mount. */ const measure = useCallback(() => { const root = rootRef.current; @@ -86,11 +97,13 @@ export function useDesktopIcons( // Each stack renders from its own top offset; the left stack defines row 0, // the top-right stack shares it and the bottom-right stack defines the last - // row band. Use the smallest top as the grid origin. + // row band. Use the smallest top as the grid origin. The right-anchored + // stacks also give the true right margin. let cellW = 0; let cellH = 0; let originX = Infinity; let originY = Infinity; + let rightMargin = Infinity; stacks.forEach((stack) => { const stackRect = stack.getBoundingClientRect(); @@ -109,45 +122,60 @@ export function useDesktopIcons( const r = el.getBoundingClientRect(); cellW = Math.max(cellW, r.width); cellH = Math.max(cellH, r.height); + rightMargin = Math.min(rightMargin, rootRect.right - r.right); }); }); if (!Number.isFinite(originX)) originX = 10; if (!Number.isFinite(originY)) originY = 12; + if (!Number.isFinite(rightMargin)) rightMargin = 10; if (cellW <= 0 || cellH <= 0) return; - const pitchX = cellW + GRID_GAP; const pitchY = cellH + GRID_GAP; const width = root.clientWidth || window.innerWidth; const height = (root.clientHeight || window.innerHeight) - TASKBAR_HEIGHT; + // Columns tile from the left, but the services/friends stacks are anchored + // to the right edge. A fixed pitch leaves up to a full cell of slack on the + // right (the remainder of width / pitch), which pulled those icons away from + // the border. Stretch the column pitch a hair so the first and last columns + // land flush on both margins; the extra gap is a few px and stays uniform. + const minPitchX = cellW + GRID_GAP; + const span = width - originX - rightMargin; + const cols = Math.max(1, Math.floor((span + GRID_GAP) / minPitchX)); + let pitchX = minPitchX; + if (cols > 1) { + const stretched = (span - cellW) / (cols - 1); + if (stretched > pitchX) pitchX = stretched; + } + cellRef.current = { w: cellW, h: cellH }; gridRef.current = { - cols: Math.max(1, Math.floor((width - originX) / pitchX)), + cols, rows: Math.max(1, Math.floor((height - originY) / pitchY)), originX, originY, + pitchX, + pitchY, }; }, [rootRef]); const posToCell = useCallback( (p: IconPosition): Cell => { - const { w: cellW, h: cellH } = cellRef.current; const g = gridRef.current; return clampCell( - Math.round((p.x - g.originX) / (cellW + GRID_GAP)), - Math.round((p.y - g.originY) / (cellH + GRID_GAP)) + Math.round((p.x - g.originX) / g.pitchX), + Math.round((p.y - g.originY) / g.pitchY) ); }, [clampCell] ); const cellToPos = useCallback((c: Cell): IconPosition => { - const { w: cellW, h: cellH } = cellRef.current; const g = gridRef.current; return { - x: g.originX + c.col * (cellW + GRID_GAP), - y: g.originY + c.row * (cellH + GRID_GAP), + x: g.originX + c.col * g.pitchX, + y: g.originY + c.row * g.pitchY, }; }, []); @@ -217,7 +245,6 @@ export function useDesktopIcons( const defaultCells = useCallback( (keys: string[]): Record => { const root = rootRef.current; - const { w: cellW, h: cellH } = cellRef.current; const g = gridRef.current; const out: Record = {}; if (!root) return out; @@ -225,8 +252,8 @@ export function useDesktopIcons( const stacks = Array.from( root.querySelectorAll(".win-icons") ).filter((el) => el.querySelector("[data-drag-id]")); - const pitchX = cellW + GRID_GAP; - const pitchY = cellH + GRID_GAP; + const pitchX = g.pitchX; + const pitchY = g.pitchY; const rootRect = root.getBoundingClientRect(); // Natural cell for each icon: the one its stack actually renders it in. @@ -267,8 +294,8 @@ export function useDesktopIcons( const { w: cellW, h: cellH } = cellRef.current; const g = gridRef.current; return clampCell( - Math.floor((x - g.originX + GRID_GAP / 2) / (cellW + GRID_GAP)), - Math.floor((y - g.originY + GRID_GAP / 2) / (cellH + GRID_GAP)) + Math.floor((x - g.originX + (g.pitchX - cellW) / 2) / g.pitchX), + Math.floor((y - g.originY + (g.pitchY - cellH) / 2) / g.pitchY) ); }, [clampCell]