Shell 1.6.0: bars that tuck away, and every edge resizes #6
@@ -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
|
||||
|
||||
+13
-54
@@ -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
|
||||
};
|
||||
|
||||
+46
-127
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user