feat/resizable-panels #20

Merged
lerko merged 2 commits from feat/resizable-panels into main 2026-05-17 00:48:39 +00:00
3 changed files with 110 additions and 7 deletions
+79
View File
@@ -1451,6 +1451,14 @@
return; return;
} }
if (ev.key === 'Escape' && $('main').classList.contains('focus-peek')) {
exitFocusPeek();
state.selectedIndex = -1;
renderEntityList();
renderDetailPane();
return;
}
const sel = state.entities[state.selectedIndex]; const sel = state.entities[state.selectedIndex];
switch (ev.key) { switch (ev.key) {
@@ -1534,6 +1542,17 @@
function toggleZen() { function toggleZen() {
const m = $('main'); const m = $('main');
if (m.classList.contains('focus-peek')) {
exitFocusPeek();
return;
}
if (state.selectedIndex >= 0) {
m.classList.add('focus-peek');
return;
}
const isZen = m.classList.contains('hide-rail') && m.classList.contains('hide-peek'); const isZen = m.classList.contains('hide-rail') && m.classList.contains('hide-peek');
if (isZen) { if (isZen) {
m.classList.remove('hide-rail', 'hide-peek'); m.classList.remove('hide-rail', 'hide-peek');
@@ -1546,12 +1565,72 @@
} }
} }
function exitFocusPeek() {
$('main').classList.remove('focus-peek');
}
(function restorePanels() { (function restorePanels() {
const m = $('main'); const m = $('main');
if (localStorage.getItem('nib:hide-rail')) m.classList.add('hide-rail'); if (localStorage.getItem('nib:hide-rail')) m.classList.add('hide-rail');
if (localStorage.getItem('nib:hide-peek')) m.classList.add('hide-peek'); if (localStorage.getItem('nib:hide-peek')) m.classList.add('hide-peek');
const railW = localStorage.getItem('nib:rail-w');
const peekW = localStorage.getItem('nib:peek-w');
if (railW) m.style.setProperty('--rail-w', railW + 'px');
if (peekW) m.style.setProperty('--peek-w', peekW + 'px');
})(); })();
// ========== Resize handles ==========
$$('.resize-handle').forEach(handle => {
let startX, startW, panel;
handle.addEventListener('mousedown', (ev) => {
ev.preventDefault();
panel = handle.dataset.panel;
startX = ev.clientX;
const m = $('main');
m.classList.add('resizing');
handle.classList.add('active');
if (panel === 'rail') {
startW = $('#tag-rail').offsetWidth;
} else {
startW = $('#detail-pane').offsetWidth;
}
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
function onMove(ev) {
const m = $('main');
const dx = ev.clientX - startX;
let newW;
if (panel === 'rail') {
newW = Math.max(120, Math.min(360, startW + dx));
m.style.setProperty('--rail-w', newW + 'px');
} else {
newW = Math.max(250, Math.min(700, startW - dx));
m.style.setProperty('--peek-w', newW + 'px');
}
}
function onUp() {
const m = $('main');
m.classList.remove('resizing');
handle.classList.remove('active');
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
if (panel === 'rail') {
localStorage.setItem('nib:rail-w', $('#tag-rail').offsetWidth);
} else {
localStorage.setItem('nib:peek-w', $('#detail-pane').offsetWidth);
}
}
});
function scrollSelectedIntoView() { function scrollSelectedIntoView() {
const el = $(`.entity-item[data-index="${state.selectedIndex}"], .card-row[data-index="${state.selectedIndex}"]`); const el = $(`.entity-item[data-index="${state.selectedIndex}"], .card-row[data-index="${state.selectedIndex}"]`);
if (el) el.scrollIntoView({ block: 'nearest' }); if (el) el.scrollIntoView({ block: 'nearest' });
+2
View File
@@ -24,11 +24,13 @@
</header> </header>
<main> <main>
<aside id="tag-rail"></aside> <aside id="tag-rail"></aside>
<div class="resize-handle" data-panel="rail"></div>
<section id="entity-panel"> <section id="entity-panel">
<div id="month-nav"></div> <div id="month-nav"></div>
<div id="entity-list"></div> <div id="entity-list"></div>
<div id="capture-bar"></div> <div id="capture-bar"></div>
</section> </section>
<div class="resize-handle" data-panel="peek"></div>
<aside id="detail-pane"> <aside id="detail-pane">
<div class="detail-empty">select an entity</div> <div class="detail-empty">select an entity</div>
</aside> </aside>
+29 -7
View File
@@ -177,17 +177,39 @@ nav { display: flex; gap: 2px; }
/* ── MAIN LAYOUT ────────────────────────────────────── */ /* ── MAIN LAYOUT ────────────────────────────────────── */
main { main {
display: grid; display: grid;
grid-template-columns: 192px 1fr 400px; grid-template-columns: var(--rail-w, 192px) 4px 1fr 4px var(--peek-w, 400px);
overflow: hidden; overflow: hidden;
transition: grid-template-columns var(--t-base);
} }
main.hide-rail { grid-template-columns: 0px 1fr 400px; } main.resizing { transition: none; }
main.hide-peek { grid-template-columns: 192px 1fr 0px; } main:not(.resizing) { transition: grid-template-columns var(--t-base); }
main.hide-rail.hide-peek { grid-template-columns: 0px 1fr 0px; }
main.hide-rail #tag-rail { overflow: hidden; border-right: none; } main.hide-rail { grid-template-columns: 0px 0px 1fr 4px var(--peek-w, 400px); }
main.hide-peek #detail-pane { overflow: hidden; border-left: none; } main.hide-peek { grid-template-columns: var(--rail-w, 192px) 4px 1fr 0px 0px; }
main.hide-rail.hide-peek { grid-template-columns: 0px 0px 1fr 0px 0px; }
main.hide-rail #tag-rail { overflow: hidden; border-right: none; min-width: 0; }
main.hide-peek #detail-pane { overflow: hidden; border-left: none; min-width: 0; }
main.hide-rail .resize-handle[data-panel="rail"] { visibility: hidden; }
main.hide-peek .resize-handle[data-panel="peek"] { visibility: hidden; }
main.focus-peek { grid-template-columns: 0px 0px 0px 0px 1fr; }
main.focus-peek #tag-rail { overflow: hidden; border-right: none; min-width: 0; }
main.focus-peek #entity-panel { overflow: hidden; min-width: 0; }
main.focus-peek .resize-handle { visibility: hidden; }
.resize-handle {
cursor: col-resize;
background: transparent;
transition: background var(--t-fast);
z-index: 10;
}
.resize-handle:hover,
.resize-handle.active {
background: var(--accent);
opacity: .4;
}
/* ── TAG RAIL ───────────────────────────────────────── */ /* ── TAG RAIL ───────────────────────────────────────── */
#tag-rail { #tag-rail {