diff --git a/VERSION b/VERSION index af2dabf..166c9e2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.25.1 +0.25.2 diff --git a/server/pages/templates/base.html b/server/pages/templates/base.html index eec530a..1c74c01 100644 --- a/server/pages/templates/base.html +++ b/server/pages/templates/base.html @@ -101,6 +101,7 @@ + {{if eq .Surface "chat"}}{{template "scripts-chat" .}}{{end}} diff --git a/server/pages/templates/surfaces/admin.html b/server/pages/templates/surfaces/admin.html index 0920a6d..aea6325 100644 --- a/server/pages/templates/surfaces/admin.html +++ b/server/pages/templates/surfaces/admin.html @@ -9,9 +9,9 @@ {{/* Top Bar */}}
- `; + `; body.appendChild(el); // ── Wire listeners ── @@ -833,6 +846,9 @@ function _registerNotesPanel() { if (typeof openNoteGraph === 'function') openNoteGraph(); }); el.querySelector('#notesTodayBtn').addEventListener('click', openDailyNote); + el.querySelector('#notesGraphBackBtn').addEventListener('click', () => { + if (typeof closeNoteGraph === 'function') closeNoteGraph(); + }); el.querySelector('#notesSelectModeBtn').addEventListener('click', () => { if (_notesSelectMode) _exitSelectMode(); else _enterSelectMode(); @@ -890,7 +906,9 @@ function _registerNotesPanel() { element: el, label: 'Notes', onOpen() { - // Loading is handled by openNotes() which calls loadNotesList + // Ensure notes are loaded whenever the panel opens + loadNotesList(); + loadNoteFolders(); }, onClose() { _destroyNoteEditor(); diff --git a/src/js/pane-container.js b/src/js/pane-container.js index 45139a8..10efbf5 100644 --- a/src/js/pane-container.js +++ b/src/js/pane-container.js @@ -345,115 +345,99 @@ function _createHandle(direction, splitEl, index, surfaceId, splitId) { splitEl.appendChild(handle); } - // Drag logic - let startX, startY, leftEl, rightEl, startLeftBasis, startRightBasis; + // Track adjacent panes for dblclick reset + let lastLeftEl = null, lastRightEl = null; - const onMouseDown = (e) => { - e.preventDefault(); - const allPanes = Array.from(splitEl.querySelectorAll(':scope > .pane, :scope > .pane-split')); - // Find the two panes adjacent to this handle - const handleIdx = Array.from(splitEl.children).indexOf(handle); - leftEl = splitEl.children[handleIdx - 1]; - rightEl = splitEl.children[handleIdx + 1]; - if (!leftEl || !rightEl) return; + initDragResize(handle, { + direction, + onStart(_clientPos) { + // Find the two panes adjacent to this handle + const handleIdx = Array.from(splitEl.children).indexOf(handle); + const leftEl = splitEl.children[handleIdx - 1]; + const rightEl = splitEl.children[handleIdx + 1]; + if (!leftEl || !rightEl) return false; - startX = e.clientX; - startY = e.clientY; - // Use current rendered size (matches what flex layout computed) - startLeftBasis = leftEl.getBoundingClientRect().width; - startRightBasis = rightEl.getBoundingClientRect().width; + lastLeftEl = leftEl; + lastRightEl = rightEl; - // Immediately lock the non-flex panes to their current sizes - // so the first move delta doesn't cause a visual snap - if (leftEl.dataset.paneFlex !== '1') { - leftEl.style.flexBasis = startLeftBasis + 'px'; - leftEl.style.flexGrow = '0'; - leftEl.style.flexShrink = '0'; - } - if (rightEl.dataset.paneFlex !== '1') { - rightEl.style.flexBasis = startRightBasis + 'px'; - rightEl.style.flexGrow = '0'; - rightEl.style.flexShrink = '0'; - } + const isHoriz = direction === 'horizontal'; + const dim = isHoriz ? 'width' : 'height'; + const startLeftBasis = leftEl.getBoundingClientRect()[dim]; + const startRightBasis = rightEl.getBoundingClientRect()[dim]; - handle.classList.add('pane-handle--active'); - document.body.style.cursor = direction === 'horizontal' ? 'col-resize' : 'row-resize'; - document.body.style.userSelect = 'none'; + // Lock non-flex panes to current rendered size so the first + // move delta doesn't cause a visual snap + if (leftEl.dataset.paneFlex !== '1') { + leftEl.style.flexBasis = startLeftBasis + 'px'; + leftEl.style.flexGrow = '0'; + leftEl.style.flexShrink = '0'; + } + if (rightEl.dataset.paneFlex !== '1') { + rightEl.style.flexBasis = startRightBasis + 'px'; + rightEl.style.flexGrow = '0'; + rightEl.style.flexShrink = '0'; + } - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mouseup', onMouseUp); - }; + handle.classList.add('pane-handle--active'); - const onMouseMove = (e) => { - const delta = direction === 'horizontal' - ? e.clientX - startX - : e.clientY - startY; + return { leftEl, rightEl, startLeftBasis, startRightBasis }; + }, + onMove(delta, ctx) { + const { leftEl, rightEl, startLeftBasis, startRightBasis } = ctx; + const leftIsFlex = leftEl.dataset.paneFlex === '1'; + const rightIsFlex = rightEl.dataset.paneFlex === '1'; + const isHoriz = direction === 'horizontal'; + const dim = isHoriz ? 'width' : 'height'; + const maxSize = splitEl.getBoundingClientRect()[dim] * 0.6; - const leftIsFlex = leftEl.dataset.paneFlex === '1'; - const rightIsFlex = rightEl.dataset.paneFlex === '1'; - // Cap: no single fixed pane should exceed 60% of container - const maxSize = splitEl.getBoundingClientRect().width * 0.6; - - if (leftIsFlex) { - // Only adjust the right (fixed) pane; left flex absorbs remainder - const newRight = Math.min(maxSize, Math.max(50, startRightBasis - delta)); - rightEl.style.flexBasis = newRight + 'px'; - rightEl.style.flexGrow = '0'; - rightEl.style.flexShrink = '0'; - } else if (rightIsFlex) { - // Only adjust the left (fixed) pane; right flex absorbs remainder - const newLeft = Math.min(maxSize, Math.max(50, startLeftBasis + delta)); - leftEl.style.flexBasis = newLeft + 'px'; - leftEl.style.flexGrow = '0'; - leftEl.style.flexShrink = '0'; - } else { - // Both fixed — adjust both (original behavior) - const newLeft = Math.max(50, startLeftBasis + delta); - const newRight = Math.max(50, startRightBasis - delta); - leftEl.style.flexBasis = newLeft + 'px'; - leftEl.style.flexGrow = '0'; - leftEl.style.flexShrink = '0'; - rightEl.style.flexBasis = newRight + 'px'; - rightEl.style.flexGrow = '0'; - rightEl.style.flexShrink = '0'; - } - }; - - const onMouseUp = () => { - handle.classList.remove('pane-handle--active'); - document.body.style.cursor = ''; - document.body.style.userSelect = ''; - document.removeEventListener('mousemove', onMouseMove); - document.removeEventListener('mouseup', onMouseUp); - - // Persist sizes - _persistSizes(surfaceId, splitEl); - }; - - handle.addEventListener('mousedown', onMouseDown); + if (leftIsFlex) { + const newRight = Math.min(maxSize, Math.max(50, startRightBasis - delta)); + rightEl.style.flexBasis = newRight + 'px'; + rightEl.style.flexGrow = '0'; + rightEl.style.flexShrink = '0'; + } else if (rightIsFlex) { + const newLeft = Math.min(maxSize, Math.max(50, startLeftBasis + delta)); + leftEl.style.flexBasis = newLeft + 'px'; + leftEl.style.flexGrow = '0'; + leftEl.style.flexShrink = '0'; + } else { + const newLeft = Math.max(50, startLeftBasis + delta); + const newRight = Math.max(50, startRightBasis - delta); + leftEl.style.flexBasis = newLeft + 'px'; + leftEl.style.flexGrow = '0'; + leftEl.style.flexShrink = '0'; + rightEl.style.flexBasis = newRight + 'px'; + rightEl.style.flexGrow = '0'; + rightEl.style.flexShrink = '0'; + } + }, + onEnd(_ctx) { + handle.classList.remove('pane-handle--active'); + _persistSizes(surfaceId, splitEl); + }, + }); // Double-click resets to default handle.addEventListener('dblclick', () => { - if (leftEl) { - if (leftEl.dataset.paneFlex === '1') { - leftEl.style.flex = '1'; leftEl.style.flexBasis = ''; leftEl.style.flexGrow = ''; leftEl.style.flexShrink = ''; - } else { - leftEl.style.flexBasis = ''; leftEl.style.flexGrow = ''; leftEl.style.flexShrink = ''; - } - } - if (rightEl) { - if (rightEl.dataset.paneFlex === '1') { - rightEl.style.flex = '1'; rightEl.style.flexBasis = ''; rightEl.style.flexGrow = ''; rightEl.style.flexShrink = ''; - } else { - rightEl.style.flexBasis = ''; rightEl.style.flexGrow = ''; rightEl.style.flexShrink = ''; - } - } + _resetPaneSize(lastLeftEl); + _resetPaneSize(lastRightEl); _persistSizes(surfaceId, splitEl); }); return { handle, splitEl, index, direction }; } +/** Reset a pane element to its default flex sizing. */ +function _resetPaneSize(el) { + if (!el) return; + if (el.dataset.paneFlex === '1') { + el.style.flex = '1'; + } + el.style.flexBasis = ''; + el.style.flexGrow = ''; + el.style.flexShrink = ''; +} + // ── Persistence ───────────────────────────── function _persistSizes(surfaceId, splitEl) { diff --git a/src/js/panels.js b/src/js/panels.js index a89c2e2..c32d29a 100644 --- a/src/js/panels.js +++ b/src/js/panels.js @@ -302,46 +302,37 @@ function toggleSidePanelFullscreen() { // ── Workspace Resize Handle ──────────────── function _initWorkspaceResize() { - let startX, startW; const secondary = document.getElementById('workspaceSecondary'); const handle = document.getElementById('workspaceHandle'); if (!handle || !secondary) return; - const onDown = (e) => { - if (!handle.classList.contains('active')) return; - if (secondary.classList.contains('fullscreen')) return; - e.preventDefault(); - const clientX = e.clientX ?? e.touches?.[0]?.clientX; - startX = clientX; - startW = secondary.getBoundingClientRect().width; - secondary.style.transition = 'none'; - document.body.style.cursor = 'col-resize'; - document.body.style.userSelect = 'none'; - - const onMove = (e) => { - const cx = e.clientX ?? e.touches?.[0]?.clientX ?? startX; - const delta = startX - cx; - const newW = Math.max(280, Math.min(window.innerWidth * 0.7, startW + delta)); + initDragResize(handle, { + direction: 'horizontal', + canDrag() { + return handle.classList.contains('active') && + !secondary.classList.contains('fullscreen'); + }, + onStart(_clientPos) { + // Kill transition first, force reflow, then measure. + // This ensures startW reflects the final CSS value (not mid-animation), + // and the first onMove won't animate since transition is already dead. + // Do NOT set inline width here — getBoundingClientRect includes border + // but style.width is content-box, so it causes a 1px jump on click. + secondary.style.transition = 'none'; + void secondary.offsetWidth; + const startW = secondary.getBoundingClientRect().width; + return { startW }; + }, + onMove(delta, ctx) { + // Handle is left of secondary — dragging left (negative delta) grows it + const newW = Math.max(280, Math.min(window.innerWidth * 0.7, ctx.startW - delta)); secondary.style.width = newW + 'px'; secondary.style.minWidth = newW + 'px'; - }; - const onUp = () => { - document.removeEventListener('mousemove', onMove); - document.removeEventListener('mouseup', onUp); - document.removeEventListener('touchmove', onMove); - document.removeEventListener('touchend', onUp); - document.body.style.cursor = ''; - document.body.style.userSelect = ''; + }, + onEnd(_ctx) { secondary.style.transition = ''; - }; - document.addEventListener('mousemove', onMove); - document.addEventListener('mouseup', onUp); - document.addEventListener('touchmove', onMove, { passive: false }); - document.addEventListener('touchend', onUp); - }; - - handle.addEventListener('mousedown', onDown); - handle.addEventListener('touchstart', onDown, { passive: false }); + }, + }); } // ── Mobile Swipe Navigation ───────────────── diff --git a/src/js/projects-ui.js b/src/js/projects-ui.js index 8d3fabb..c407fa8 100644 --- a/src/js/projects-ui.js +++ b/src/js/projects-ui.js @@ -153,6 +153,11 @@ async function deleteProject(projectId) { App.activeProjectId = null; localStorage.removeItem('cs-active-project'); } + // Close project panel if the deleted project was open in it + if (_projectPanelId === projectId && typeof PanelRegistry !== 'undefined') { + _projectPanelId = null; + PanelRegistry.close('project'); + } UI.renderChatList(); UI.toast('Project deleted', 'success'); } catch (e) {