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 root = null;
|
||||||
let titleEl = null;
|
let titleEl = null;
|
||||||
let bodyEl = null;
|
let bodyEl = null;
|
||||||
|
let drawer = null;
|
||||||
/** Owner key set by `openNamed` (e.g. 'inbox'). `refresh(name, …)`
|
/** Owner key set by `openNamed` (e.g. 'inbox'). `refresh(name, …)`
|
||||||
* is a no-op when the current owner doesn't match, so live
|
* is a no-op when the current owner doesn't match, so live
|
||||||
* updates can re-render an open view without grabbing focus
|
* updates can re-render an open view without grabbing focus
|
||||||
|
|
@ -229,6 +230,7 @@ export const Panel = (() => {
|
||||||
root = $('side-panel');
|
root = $('side-panel');
|
||||||
titleEl = $('side-panel-title');
|
titleEl = $('side-panel-title');
|
||||||
bodyEl = $('side-panel-body');
|
bodyEl = $('side-panel-body');
|
||||||
|
drawer = root && root.querySelector('.side-panel-drawer');
|
||||||
}
|
}
|
||||||
return root != null;
|
return root != null;
|
||||||
}
|
}
|
||||||
|
|
@ -256,6 +258,79 @@ export const Panel = (() => {
|
||||||
root.classList.remove('open');
|
root.classList.remove('open');
|
||||||
root.setAttribute('aria-hidden', 'true');
|
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() {
|
function bind() {
|
||||||
if (!ensure()) return;
|
if (!ensure()) return;
|
||||||
$('side-panel-close').addEventListener('click', close);
|
$('side-panel-close').addEventListener('click', close);
|
||||||
|
|
@ -263,6 +338,8 @@ export const Panel = (() => {
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Escape' && root.classList.contains('open')) close();
|
if (e.key === 'Escape' && root.classList.contains('open')) close();
|
||||||
});
|
});
|
||||||
|
applyStoredWidth();
|
||||||
|
bindResize();
|
||||||
}
|
}
|
||||||
return { open, openNamed, refresh, close, bind };
|
return { open, openNamed, refresh, close, bind };
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -1111,8 +1111,10 @@ summary:hover { color: var(--purple); }
|
||||||
background: var(--bg-elev);
|
background: var(--bg-elev);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
padding: 0.5em 0.8em;
|
padding: 0.5em 0.8em;
|
||||||
max-height: 24em;
|
/* #450: no max-height cap — let the inbox grow to fill the
|
||||||
overflow-y: auto;
|
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 {
|
.inbox li {
|
||||||
padding: 0.25em 0;
|
padding: 0.25em 0;
|
||||||
|
|
@ -1297,7 +1299,19 @@ footer .banner-thin {
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--bg-elev);
|
background: var(--bg-elev);
|
||||||
|
|
@ -1309,6 +1323,36 @@ footer .banner-thin {
|
||||||
.side-panel.open { pointer-events: auto; }
|
.side-panel.open { pointer-events: auto; }
|
||||||
.side-panel.open .side-panel-backdrop { opacity: 1; }
|
.side-panel.open .side-panel-backdrop { opacity: 1; }
|
||||||
.side-panel.open .side-panel-drawer { transform: translateX(0); }
|
.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 {
|
.side-panel-head {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue