two related flyout improvements — both small, single PR. #450 — full-height inbox dropped the .inbox max-height: 24em cap. the side-panel-body already provides overflow: auto, so the cap was just clamping the inbox list short of the available panel height on tall viewports. the inbox now fills as much of the panel as it needs and the panel itself scrolls. #451 — drag-to-resize side panel added a 6px hit-strip on the drawer's left edge. mousedown/move/ up handlers track the drag and update --side-panel-w on the drawer; the CSS variable defaults to min(760px, 94vw) (preserving pre-#451 behaviour) and is clamped to [320px, 96vw] so a bad stored value can never wedge the drawer off-screen. ergonomics - handle is invisible at rest, mauve glow on hover + during drag so the affordance is discoverable but doesn't compete with the 2px mauve border-left for the visual boundary. - body.side-panel-resizing class forces ew-resize cursor + kills user-select page-wide so the cursor doesn't flip back to default the moment it leaves the 6px band during a fast drag. - final width persists to localStorage (key hyperhive:side-panel-width) so it survives reload. window resize re-clamps so a stored width that exceeds the new 96vw shrinks accordingly. - handle is a separator role with aria-orientation: vertical + aria-label for screen readers. files - frontend/packages/dashboard/src/dashboard.css - .inbox: dropped max-height (#450). - .side-panel-drawer: width = var(--side-panel-w, min(760px, 94vw)) + min/max clamp (#451). - new .side-panel-resize + body.side-panel-resizing rules. - frontend/packages/dashboard/src/common.js - Panel.bind now also calls applyStoredWidth + bindResize. - resize handle is prepended to the drawer at bind time so every flyout (inbox, file preview, diff, journal) gets it. validation - npm run build --workspace=@hive/dashboard clean. CSS 40.9kb → 41.3kb. app.js + flow.js grew ~0.5kb each (resize handler). - browser smoke test isn't possible from inside iris's container; the resize math is straightforward (width = innerWidth - clientX, clamped) and the CSS variable + localStorage persistence are standard patterns.
This commit is contained in:
parent
f1896a99b8
commit
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. The body
|
||||
adds `data-spw="<px>"` once persisted; CSS doesn't read it
|
||||
directly — JS sets `--side-panel-w` via inline style on the
|
||||
drawer. */
|
||||
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