From 68f0ba806dc0df677513c5031290409dd2c65e32 Mon Sep 17 00:00:00 2001 From: Waters Church Date: Thu, 17 Sep 2026 11:52:45 -0400 Subject: [PATCH] Tiles resize from anywhere, so nothing marks the spots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every edge and both bottom corners resize now, so the little marks that used to appear on hover were pointing at the whole rim. They are gone — corners, edges and the seam pill alike — and the cursor over an edge is what says it can be dragged, the way a window's own edges do it. The seam still shows itself while it is actually being dragged, which says what is moving rather than where you may push. With nothing drawn, the grips could be made to cover the rim properly: the top strip now runs corner to corner instead of stopping 24px short, the sides run from it down to the bottom corners, and the bottom strip fills between those. No dead spots left along an edge. The side grips also stopped standing down at a seam. They only did so to avoid stacking three marks in one place, and it cost real ground: a tile taller than its neighbour lost its whole side, not just the part the seam covers. The seam sits above them anyway, so it still wins where it exists and the rest of the edge now works. Co-Authored-By: Claude Opus 5 --- README.md | 21 +++--- public/shell.js | 67 ++++-------------- public/style.css | 173 +++++++++++++---------------------------------- 3 files changed, 70 insertions(+), 191 deletions(-) diff --git a/README.md b/README.md index 9b4d374..5d733fc 100644 --- a/README.md +++ b/README.md @@ -189,16 +189,17 @@ below. Nothing is installed without someone clicking. - **+** — add any enabled module; multiple tiles of the same module are fine (e.g. two transcripts filtered to different channels). -- Drag a tile by its **title bar** to move it; drag either **bottom - corner** to resize; **✕** removes it. Hover a tile and a small mark - appears on each **edge** — drag one to move just that edge, so the tile - grows or shrinks into the space beside it while the opposite edge stays - put. The bottom edge is the exception: when another tile sits directly - below, it moves the shared edge instead, trading rows between the two so - the column keeps its total height. Tiles sit flush — when two share a - vertical edge, hover the seam and a pill appears; drag it to trade width - (macOS split-view style). That seam belongs to the pill, so the two tiles' - own side marks step aside there. +- Drag a tile by its **title bar** to move it; **✕** removes it. +- **Resize from any edge or bottom corner**, the way a window resizes: + nothing is drawn on a tile to mark the spots — the cursor over an edge is + what says it can be dragged. An edge moves only itself, so the tile grows + or shrinks into the space beside it while the opposite edge stays put, + stopping at a neighbour or at the module's own minimum size. Two + exceptions, both of which move a *shared* edge instead: with another tile + directly below, the bottom edge trades rows between the two so the column + keeps its total height; and where two tiles meet side by side, the seam + between them trades width (macOS split-view style), showing itself only + while you drag it. - **Fullscreen** (the ⛶ button) is a viewing mode: the menu bar hides (its green lip at the top-left brings it back, minus the Admin button), the occupied columns stretch to the full width, and every column runs to the diff --git a/public/shell.js b/public/shell.js index 3975d38..ab3deb0 100644 --- a/public/shell.js +++ b/public/shell.js @@ -353,31 +353,23 @@ function buildTile(tile) { const body = document.createElement('div'); body.className = 'tile-body'; - const resize = document.createElement('div'); - resize.className = 'tile-resize'; - resize.title = 'Drag to resize'; - - const resizeLeft = document.createElement('div'); - resizeLeft.className = 'tile-resize-left'; - resizeLeft.title = 'Drag to resize'; - - // pill on the bottom edge: moves that edge (trading rows with tiles below) - const resizeBottom = document.createElement('div'); - resizeBottom.className = 'tile-resize-bottom'; - resizeBottom.title = 'Drag to adjust height'; - - // The other three edges move only themselves: the top edge changes where - // the tile starts, the sides change how wide it is. Each stops where a - // neighbour or the tile's own minimum says it must. - const edgeGrip = (cls, title) => { + /* The rim: both bottom corners resize in two directions, the bottom edge + moves that edge (trading rows with tiles below), and the other three + move only themselves — the top edge changes where the tile starts, the + sides how wide it is — each stopping where a neighbour or the tile's own + minimum says it must. None of them draw anything: the cursor over an + edge is what says it can be dragged, as on a window. */ + const grip = (cls) => { const gripEl = document.createElement('div'); gripEl.className = cls; - gripEl.title = title; return gripEl; }; - const edgeTop = edgeGrip('tile-resize-top', 'Drag to adjust height'); - const edgeRight = edgeGrip('tile-resize-right', 'Drag to adjust width'); - const edgeLeft = edgeGrip('tile-resize-side', 'Drag to adjust width'); + const resize = grip('tile-resize'); + const resizeLeft = grip('tile-resize-left'); + const resizeBottom = grip('tile-resize-bottom'); + const edgeTop = grip('tile-resize-top'); + const edgeRight = grip('tile-resize-right'); + const edgeLeft = grip('tile-resize-side'); // notch on the title bar's bottom edge: hides/shows the bar const notch = document.createElement('button'); @@ -752,29 +744,6 @@ function rebuildDividers() { buildDivider(L, R, top, bottom); } } - markSeamSides(); -} - -/* Where two tiles meet, the seam pill between them already owns that - boundary — and it is the better tool there, since it moves both edges at - once and leaves no gap. A side grip on the same pixels would be a second - control doing something else, so it steps aside and the tile is resized - from that side by the pill. Free sides keep their grip. */ -function markSeamSides() { - const rowsOverlap = (a, b) => Math.min(a.y + a.h, b.y + b.h) > Math.max(a.y, b.y); - for (const t of tiles) { - const el = tileEls.get(t.id)?.el; - if (!el) continue; - let left = false; - let right = false; - for (const o of tiles) { - if (o === t || !rowsOverlap(o, t)) continue; - if (o.x + o.w === t.x) left = true; - if (o.x === t.x + t.w) right = true; - } - el.classList.toggle('seam-left', left); - el.classList.toggle('seam-right', right); - } } /** Every pair of tiles that meets at this column boundary, in any row. */ @@ -794,7 +763,6 @@ function seamPairsAt(boundary) { function buildDivider(L, R, top, bottom) { const el = document.createElement('div'); el.className = 'tile-divider'; - el.title = 'Drag to resize this column'; const place = () => { const m = cellMetrics(); @@ -807,14 +775,6 @@ function buildDivider(L, R, top, bottom) { const boundary = () => L.x + L.w; const groupEls = () => dividers.filter((d) => d.boundary() === boundary()).map((d) => d.el); - const unlink = () => { for (const d of dividers) d.el.classList.remove('linked'); }; - - // Before anything is dragged, show which seams travel with this one. - el.addEventListener('pointerenter', () => { - if (document.body.classList.contains('fullscreen')) return; - for (const other of groupEls()) other.classList.add('linked'); - }); - el.addEventListener('pointerleave', unlink); el.addEventListener('pointerdown', (ev) => { if (ev.button !== 0 && ev.pointerType === 'mouse') return; @@ -870,7 +830,6 @@ function buildDivider(L, R, top, bottom) { el.removeEventListener('pointerup', onUp); el.removeEventListener('pointercancel', onUp); for (const other of group) other.classList.remove('active'); - unlink(); if (changed) saveLayout(); rebuildDividers(); // adjacencies may have changed }; diff --git a/public/style.css b/public/style.css index b679bc6..b3638da 100644 --- a/public/style.css +++ b/public/style.css @@ -464,146 +464,67 @@ body.fullscreen .tile.head-peek .tile-notch { top: 32px; } overflow: hidden; } -.tile-resize, -.tile-resize-left { - position: absolute; - bottom: 0; - width: 20px; - height: 20px; - touch-action: none; - z-index: 5; - color: var(--muted); - opacity: 0; /* corner indicators only show while hovering the tile */ - transition: opacity 0.15s ease; -} -.tile-resize { right: 0; cursor: nwse-resize; } -.tile-resize-left { left: 0; cursor: nesw-resize; } -.tile:hover .tile-resize, -.tile:hover .tile-resize-left { opacity: 0.6; } -.tile .tile-resize:hover, -.tile .tile-resize-left:hover { opacity: 1; color: var(--text); } -.tile-resize::after, -.tile-resize-left::after { - content: ""; - position: absolute; - bottom: 4px; - width: 9px; - height: 9px; - border-bottom: 2px solid currentColor; -} -.tile-resize::after { - right: 4px; - border-right: 2px solid currentColor; - border-radius: 0 0 3px 0; -} -.tile-resize-left::after { - left: 4px; - border-left: 2px solid currentColor; - border-radius: 0 0 0 3px; -} +/* ── resizing: the whole rim of a tile, drawn nowhere ──────────────── + Every edge and both bottom corners resize, so there is nothing to point + at — the grips are invisible and the cursor says what each one does, the + way a window's own edges do. Together they tile the tile's outer 8px + without a gap: the top strip runs corner to corner, the sides run between + it and the bottom corners, and the bottom strip fills between those. */ -/* bottom edge: drag to move the tile's bottom edge (height only). Drawn as - the same 2px line as the corner handles it sits between, on the same - 4px inset, so the three read as one set of marks along the bottom. */ -.tile-resize-bottom { - position: absolute; - left: 24px; - right: 24px; - bottom: 0; - height: 10px; - z-index: 5; - display: flex; - align-items: flex-end; - justify-content: center; - cursor: ns-resize; - touch-action: none; - color: var(--muted); - opacity: 0; - transition: opacity 0.15s ease; -} -.tile-resize-bottom::after { - content: ""; - width: 44px; - max-width: 60%; - height: 0; - margin-bottom: 4px; - border-bottom: 2px solid currentColor; -} -/* the other three edges: the same mark, turned to face its own side. Each - stops short of the corners so the corner grips keep their full 20px. */ +.tile-resize, +.tile-resize-left, +.tile-resize-bottom, .tile-resize-top, .tile-resize-right, .tile-resize-side { position: absolute; - z-index: 6; - display: flex; - align-items: center; - justify-content: center; + z-index: 5; touch-action: none; - color: var(--muted); - opacity: 0; - transition: opacity 0.15s ease; +} + +/* the two bottom corners: both directions at once */ +.tile-resize, +.tile-resize-left { + bottom: 0; + width: 20px; + height: 20px; +} +.tile-resize { right: 0; cursor: nwse-resize; } +.tile-resize-left { left: 0; cursor: nesw-resize; } + +/* top and bottom: height only */ +.tile-resize-top, +.tile-resize-bottom { + height: 8px; + cursor: ns-resize; } .tile-resize-top { top: 0; - left: 24px; - right: 24px; - height: 10px; - align-items: flex-start; - cursor: ns-resize; - /* Above a revealed title bar: the bar slides in under the pointer here, and - this strip has to stay the top edge rather than become part of it. */ + left: 0; + right: 0; + /* Above a revealed title bar: the bar slides in under the pointer here, + and this strip has to stay the top edge rather than become part of it. */ z-index: 8; } +.tile-resize-bottom { bottom: 0; left: 20px; right: 20px; } + +/* left and right: width only, between the top strip and the bottom corners */ .tile-resize-right, .tile-resize-side { - top: 24px; - bottom: 24px; - width: 10px; + top: 8px; + bottom: 20px; + width: 8px; cursor: ew-resize; } -.tile-resize-right { right: 0; justify-content: flex-end; } -.tile-resize-side { left: 0; justify-content: flex-start; } -.tile-resize-top::after, -.tile-resize-right::after, -.tile-resize-side::after { - content: ""; - border-radius: 999px; -} -.tile-resize-top::after { - width: 44px; - max-width: 60%; - height: 0; - margin-top: 4px; - border-top: 2px solid currentColor; -} -.tile-resize-right::after, -.tile-resize-side::after { - width: 0; - height: 44px; - max-height: 60%; -} -.tile-resize-right::after { margin-right: 4px; border-right: 2px solid currentColor; } -.tile-resize-side::after { margin-left: 4px; border-left: 2px solid currentColor; } -/* A side that meets another tile belongs to the seam pill between them. */ -.tile.seam-left .tile-resize-side, -.tile.seam-right .tile-resize-right { display: none; } -.tile:hover .tile-resize-top, -.tile:hover .tile-resize-right, -.tile:hover .tile-resize-side { opacity: 0.6; } -.tile .tile-resize-top:hover, -.tile .tile-resize-right:hover, -.tile .tile-resize-side:hover { opacity: 1; color: var(--text); } - -.tile:hover .tile-resize-bottom { opacity: 0.6; } -.tile .tile-resize-bottom:hover, -.tile.resizing .tile-resize-bottom { opacity: 1; color: var(--text); } +.tile-resize-right { right: 0; } +.tile-resize-side { left: 0; } /* ── seam divider between adjacent tiles ───────────────────────────── */ -/* When two tiles share a vertical edge, an invisible strip sits on the seam; - hovering reveals a small macOS-split-view-style grab pill. Dragging it - trades width between the two tiles. */ +/* When two tiles share a vertical edge, an invisible strip sits on the seam. + Dragging it trades width between the two tiles rather than resizing one, + so it sits above their own edge grips. Like them it draws nothing at rest + — only while it is being dragged, to show which seam is moving. */ .tile-divider { position: absolute; width: 14px; @@ -627,18 +548,16 @@ body.fullscreen .tile.head-peek .tile-notch { top: 32px; } transition: opacity 0.15s ease, background 0.15s ease; } -.tile-divider:hover::after, -.tile-divider.linked::after { opacity: 0.7; } .tile-divider.active::after { opacity: 1; background: var(--text); } -/* Hide the pills while a tile itself is being moved or corner-resized — - they're rebuilt for the new geometry on release. */ +/* Drop the seams while a tile itself is being moved or resized — they're + rebuilt for the new geometry on release. */ #grid.no-dividers .tile-divider { display: none; } /* ── fullscreen mode ───────────────────────────────────────────────── */ /* Fullscreen is a viewing mode: the layout auto-fits the screen, so the - editing affordances (corner handles, seam pills) are parked. */ + editing affordances (the rim grips and the seams) are parked. */ body.fullscreen .tile-resize, body.fullscreen .tile-resize-left, body.fullscreen .tile-resize-bottom,