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,