Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
002419ba4d | ||
|
|
f4635cc256 |
2 changed files with 124 additions and 3 deletions
|
|
@ -216,6 +216,7 @@ export const Panel = (() => {
|
|||
let root = null;
|
||||
let titleEl = null;
|
||||
let bodyEl = null;
|
||||
let drawer = null;
|
||||
/** Owner key set by `openNamed` (e.g. 'inbox'). `refresh(name, …)`
|
||||
* is a no-op when the current owner doesn't match, so live
|
||||
* updates can re-render an open view without grabbing focus
|
||||
|
|
@ -229,6 +230,7 @@ export const Panel = (() => {
|
|||
root = $('side-panel');
|
||||
titleEl = $('side-panel-title');
|
||||
bodyEl = $('side-panel-body');
|
||||
drawer = root && root.querySelector('.side-panel-drawer');
|
||||
}
|
||||
return root != null;
|
||||
}
|
||||
|
|
@ -256,6 +258,79 @@ export const Panel = (() => {
|
|||
root.classList.remove('open');
|
||||
root.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
// #451: drag-to-resize the drawer's width. Listens on a thin
|
||||
// hit-strip glued to the drawer's left edge; mousedown captures
|
||||
// pointermove + pointerup on the document so the drag continues
|
||||
// even if the cursor strays outside the 6px handle band. Width
|
||||
// persists to localStorage so it survives page reload. The CSS
|
||||
// clamps the value (min-width: 320px, max-width: 96vw) — drop
|
||||
// unparseable / out-of-range stored values silently.
|
||||
const WIDTH_KEY = 'hyperhive:side-panel-width';
|
||||
const WIDTH_MIN = 320;
|
||||
function clampWidth(w) {
|
||||
const max = Math.floor(window.innerWidth * 0.96);
|
||||
return Math.max(WIDTH_MIN, Math.min(max, w));
|
||||
}
|
||||
function applyStoredWidth() {
|
||||
if (!drawer) return;
|
||||
const raw = (() => {
|
||||
try { return localStorage.getItem(WIDTH_KEY); }
|
||||
catch { return null; }
|
||||
})();
|
||||
if (!raw) return;
|
||||
const parsed = parseInt(raw, 10);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) return;
|
||||
drawer.style.setProperty('--side-panel-w', clampWidth(parsed) + 'px');
|
||||
}
|
||||
function bindResize() {
|
||||
if (!drawer) return;
|
||||
const handle = document.createElement('div');
|
||||
handle.className = 'side-panel-resize';
|
||||
handle.setAttribute('role', 'separator');
|
||||
handle.setAttribute('aria-orientation', 'vertical');
|
||||
handle.setAttribute('aria-label', 'drag to resize side panel');
|
||||
handle.title = 'drag to resize';
|
||||
drawer.prepend(handle);
|
||||
let dragging = false;
|
||||
handle.addEventListener('pointerdown', (e) => {
|
||||
e.preventDefault();
|
||||
dragging = true;
|
||||
document.body.classList.add('side-panel-resizing');
|
||||
// Capture so we keep getting pointermove even when the cursor
|
||||
// outpaces the handle band (drag-fast-then-pause loses the
|
||||
// handle's :hover state otherwise).
|
||||
try { handle.setPointerCapture(e.pointerId); } catch { /* legacy */ }
|
||||
});
|
||||
document.addEventListener('pointermove', (e) => {
|
||||
if (!dragging) return;
|
||||
// Drawer is anchored to the right edge — width = viewport - pointer X.
|
||||
const w = clampWidth(window.innerWidth - e.clientX);
|
||||
drawer.style.setProperty('--side-panel-w', w + 'px');
|
||||
});
|
||||
function stopDrag() {
|
||||
if (!dragging) return;
|
||||
dragging = false;
|
||||
document.body.classList.remove('side-panel-resizing');
|
||||
// Persist the final width. Read the actual rendered width
|
||||
// rather than re-deriving so the stored value matches what
|
||||
// the operator saw at mouseup.
|
||||
const w = drawer.getBoundingClientRect().width;
|
||||
try { localStorage.setItem(WIDTH_KEY, String(Math.round(w))); }
|
||||
catch { /* localStorage unavailable — width is session-only */ }
|
||||
}
|
||||
document.addEventListener('pointerup', stopDrag);
|
||||
document.addEventListener('pointercancel', stopDrag);
|
||||
// Re-clamp on viewport resize so a persisted width that exceeds
|
||||
// 96vw doesn't push the drawer off-screen after a window shrink.
|
||||
window.addEventListener('resize', () => {
|
||||
if (dragging) return;
|
||||
const cur = drawer.getBoundingClientRect().width;
|
||||
const clamped = clampWidth(cur);
|
||||
if (clamped !== Math.round(cur)) {
|
||||
drawer.style.setProperty('--side-panel-w', clamped + 'px');
|
||||
}
|
||||
});
|
||||
}
|
||||
function bind() {
|
||||
if (!ensure()) return;
|
||||
$('side-panel-close').addEventListener('click', close);
|
||||
|
|
@ -263,6 +338,8 @@ export const Panel = (() => {
|
|||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && root.classList.contains('open')) close();
|
||||
});
|
||||
applyStoredWidth();
|
||||
bindResize();
|
||||
}
|
||||
return { open, openNamed, refresh, close, bind };
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -1111,8 +1111,10 @@ summary:hover { color: var(--purple); }
|
|||
background: var(--bg-elev);
|
||||
border: 1px solid var(--border);
|
||||
padding: 0.5em 0.8em;
|
||||
max-height: 24em;
|
||||
overflow-y: auto;
|
||||
/* #450: no max-height cap — let the inbox grow to fill the
|
||||
side-panel-body which already scrolls (`overflow: auto`). The
|
||||
pre-#450 24em cap clamped the list well short of the available
|
||||
panel height even on tall viewports. */
|
||||
}
|
||||
.inbox li {
|
||||
padding: 0.25em 0;
|
||||
|
|
@ -1297,7 +1299,19 @@ footer .banner-thin {
|
|||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: min(760px, 94vw);
|
||||
/* #451: width is a CSS variable so the drag handle (added by
|
||||
Panel.bind) can update it live, and so localStorage-persisted
|
||||
widths apply on first paint. Default min(760px, 94vw) preserves
|
||||
the pre-#451 behaviour for operators who never drag. JS sets
|
||||
`--side-panel-w` via inline style on the drawer; persistence
|
||||
lives in localStorage (key `hyperhive:side-panel-width`),
|
||||
replayed onto the var by `Panel.applyStoredWidth` at bind. */
|
||||
width: var(--side-panel-w, min(760px, 94vw));
|
||||
/* Clamp so a stored width can never push the drawer off-screen
|
||||
or shrink it past readability. min content width matches the
|
||||
panel-head's natural width with the close button visible. */
|
||||
min-width: 320px;
|
||||
max-width: 96vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-elev);
|
||||
|
|
@ -1309,6 +1323,36 @@ footer .banner-thin {
|
|||
.side-panel.open { pointer-events: auto; }
|
||||
.side-panel.open .side-panel-backdrop { opacity: 1; }
|
||||
.side-panel.open .side-panel-drawer { transform: translateX(0); }
|
||||
/* #451: drag-to-resize handle on the drawer's left edge. The handle
|
||||
itself is invisible until hover/drag so it doesn't compete with the
|
||||
2px mauve `border-left` for the visual boundary. Pointer-cursor
|
||||
tells the operator the edge is grabbable; the brighter glow during
|
||||
drag (`body.side-panel-resizing`) is the affordance the eye
|
||||
tracks. */
|
||||
.side-panel-resize {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 6px;
|
||||
margin-left: -3px;
|
||||
cursor: ew-resize;
|
||||
z-index: 1;
|
||||
background: transparent;
|
||||
transition: background 120ms ease;
|
||||
}
|
||||
.side-panel-resize:hover,
|
||||
body.side-panel-resizing .side-panel-resize {
|
||||
background: rgba(203, 166, 247, 0.55);
|
||||
}
|
||||
/* While dragging: kill text selection + force the resize cursor
|
||||
page-wide so the cursor doesn't flip back to default the moment it
|
||||
leaves the 6px handle band. */
|
||||
body.side-panel-resizing {
|
||||
cursor: ew-resize !important;
|
||||
user-select: none;
|
||||
}
|
||||
body.side-panel-resizing * { cursor: ew-resize !important; }
|
||||
.side-panel-head {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
|
|
|
|||
Loading…
Reference in a new issue